Refactor directory structure of components There is no change in functionality. Only moving things around. + Separate html from the js. + Place the unit test for a component within the same folder. + Organize the components in subfolders. Change-Id: I51fdc510db75fc1b33f040ca63decbbdfd4d5513
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html new file mode 100644 index 0000000..caa9674 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -0,0 +1,89 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/gr-change-list-styles.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> +<link rel="import" href="../../shared/gr-change-star/gr-change-star.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> + +<dom-module id="gr-change-list-item"> + <template> + <style> + :host { + display: flex; + border-bottom: 1px solid #eee; + } + :host([selected]) { + background-color: #ebf5fb; + } + :host([needs-review]) { + font-weight: bold; + } + .cell { + flex-shrink: 0; + padding: .3em .5em; + } + a { + color: var(--default-text-color); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + .positionIndicator { + visibility: hidden; + } + :host([selected]) .positionIndicator { + visibility: visible; + } + .u-monospace { + font-family: var(--monospace-font-family); + } + .u-green { + color: #388E3C; + } + .u-red { + color: #D32F2F; + } + </style> + <style include="gr-change-list-styles"></style> + <span class="cell keyboard"> + <span class="positionIndicator">▶</span> + </span> + <span class="cell star" hidden$="[[!showStar]]"> + <gr-change-star change="{{change}}"></gr-change-star> + </span> + <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a> + <span class="cell status">[[_computeChangeStatusString(change)]]</span> + <span class="cell owner"> + <gr-account-link account="[[change.owner]]"></gr-account-link> + </span> + <a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a> + <a class="cell branch" href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a> + <gr-date-formatter class="cell updated" date-str="[[change.updated]]"></gr-date-formatter> + <span class="cell size u-monospace"> + <span class="u-green"><span>+</span>[[change.insertions]]</span>, + <span class="u-red"><span>-</span>[[change.deletions]]</span> + </span> + <template is="dom-repeat" items="[[labelNames]]" as="labelName"> + <span title$="[[_computeLabelTitle(change, labelName)]]" + class$="[[_computeLabelClass(change, labelName)]]">[[_computeLabelValue(change, labelName)]]</span> + </template> + </template> + <script src="gr-change-list-item.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js new file mode 100644 index 0000000..5d03cba --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -0,0 +1,132 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-change-list-item', + + properties: { + selected: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + needsReview: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + labelNames: { + type: Array, + }, + change: Object, + changeURL: { + type: String, + computed: '_computeChangeURL(change._number)', + }, + showStar: { + type: Boolean, + value: false, + }, + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + _computeChangeURL: function(changeNum) { + if (!changeNum) { return ''; } + return '/c/' + changeNum + '/'; + }, + + _computeChangeStatusString: function(change) { + if (change.status == this.ChangeStatus.MERGED) { + return 'Merged'; + } + if (change.mergeable != null && change.mergeable == false) { + return 'Merge Conflict'; + } + if (change.status == this.ChangeStatus.DRAFT) { + return 'Draft'; + } + if (change.status == this.ChangeStatus.ABANDONED) { + return 'Abandoned'; + } + return ''; + }, + + _computeLabelTitle: function(change, labelName) { + var label = change.labels[labelName]; + if (!label) { return labelName; } + var significantLabel = label.rejected || label.approved || + label.disliked || label.recommended; + if (significantLabel && significantLabel.name) { + return labelName + '\nby ' + significantLabel.name; + } + return labelName; + }, + + _computeLabelClass: function(change, labelName) { + var label = change.labels[labelName]; + // Mimic a Set. + var classes = { + 'cell': true, + 'label': true, + }; + if (label) { + if (label.approved) { + classes['u-green'] = true; + } + if (label.value == 1) { + classes['u-monospace'] = true; + classes['u-green'] = true; + } else if (label.value == -1) { + classes['u-monospace'] = true; + classes['u-red'] = true; + } + if (label.rejected) { + classes['u-red'] = true; + } + } + return Object.keys(classes).sort().join(' '); + }, + + _computeLabelValue: function(change, labelName) { + var label = change.labels[labelName]; + if (!label) { return ''; } + if (label.approved) { + return '✓'; + } + if (label.rejected) { + return '✕'; + } + if (label.value > 0) { + return '+' + label.value; + } + if (label.value < 0) { + return label.value; + } + return ''; + }, + + _computeProjectURL: function(project) { + return '/projects/' + project + ',dashboards/default'; + }, + + _computeProjectBranchURL: function(project, branch) { + return '/q/status:open+project:' + project + '+branch:' + branch; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html new file mode 100644 index 0000000..0a4aec4 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -0,0 +1,128 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-change-list-item</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="gr-change-list-item.html"> + +<test-fixture id="basic"> + <template> + <gr-change-list-item></gr-change-list-item> + </template> +</test-fixture> + +<script> + suite('gr-change-list-item tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('computed fields', function() { + assert.equal(element._computeChangeStatusString({mergeable: true}), ''); + assert.equal(element._computeChangeStatusString({mergeable: false}), + 'Merge Conflict'); + assert.equal(element._computeChangeStatusString({status: 'NEW'}), ''); + assert.equal(element._computeChangeStatusString({status: 'MERGED'}), + 'Merged'); + assert.equal(element._computeChangeStatusString({status: 'ABANDONED'}), + 'Abandoned'); + assert.equal(element._computeChangeStatusString({status: 'DRAFT'}), + 'Draft'); + + assert.equal(element._computeLabelClass({labels: {}}), 'cell label'); + assert.equal(element._computeLabelClass( + {labels: {}}, 'Verified'), 'cell label'); + assert.equal(element._computeLabelClass( + {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), + 'cell label u-green u-monospace'); + assert.equal(element._computeLabelClass( + {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'), + 'cell label u-monospace u-red'); + assert.equal(element._computeLabelClass( + {labels: {'Code-Review': {value: 1}}}, 'Code-Review'), + 'cell label u-green u-monospace'); + assert.equal(element._computeLabelClass( + {labels: {'Code-Review': {value: -1}}}, 'Code-Review'), + 'cell label u-monospace u-red'); + + assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'), + 'Verified'); + assert.equal(element._computeLabelTitle( + {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'), + 'Verified\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'), + 'Verified\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}}, + 'Code-Review'), 'Code-Review\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}}, + 'Code-Review'), 'Code-Review\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {recommended: {name: 'Diffy'}, + rejected: {name: 'Admin'}}}}, 'Code-Review'), + 'Code-Review\nby Admin'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {approved: {name: 'Diffy'}, + rejected: {name: 'Admin'}}}}, 'Code-Review'), + 'Code-Review\nby Admin'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {recommended: {name: 'Diffy'}, + disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), + 'Code-Review\nby Admin'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {approved: {name: 'Diffy'}, + disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), + 'Code-Review\nby Diffy'); + + assert.equal(element._computeLabelValue({labels: {}}), ''); + assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), ''); + assert.equal(element._computeLabelValue( + {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {value: 1}}}, 'Verified'), '+1'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {value: -1}}}, 'Verified'), '-1'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {approved: true}}}, 'Verified'), '✓'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {rejected: true}}}, 'Verified'), '✕'); + + assert.equal(element._computeProjectURL('combustible-stuff'), + '/projects/combustible-stuff,dashboards/default'); + + assert.equal(element._computeProjectBranchURL( + 'combustible-stuff', 'lemons'), + '/q/status:open+project:combustible-stuff+branch:lemons'); + + element.change = {_number: 42}; + assert.equal(element.changeURL, '/c/42/'); + element.change = {_number: 43}; + assert.equal(element.changeURL, '/c/43/'); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html new file mode 100644 index 0000000..5b03274 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -0,0 +1,91 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../gr-change-list/gr-change-list.html"> + +<dom-module id="gr-change-list-view"> + <template> + <style> + :host { + background-color: var(--view-background-color); + display: block; + margin: 0 var(--default-horizontal-margin); + } + .loading, + .error { + margin-top: 1em; + background-color: #f1f2f3; + } + .loading { + color: #666; + } + .error { + color: #D32F2F; + } + gr-change-list { + margin-top: 1em; + width: 100%; + } + nav { + margin-bottom: 1em; + padding: .5em 0; + text-align: center; + } + nav a { + display: inline-block; + } + nav a:first-of-type { + margin-right: .5em; + } + @media only screen and (max-width: 50em) { + :host { + margin: 0; + } + .loading, + .error { + padding: 0 var(--default-horizontal-margin); + } + } + </style> + <gr-ajax + auto + url="/changes/" + params="[[_computeQueryParams(_query, _offset)]]" + last-response="{{_changes}}" + last-error="{{_lastError}}" + loading="{{_loading}}"></gr-ajax> + <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div> + <div class="error" hidden$="[[_computeErrorHidden(_loading, _lastError)]]" hidden> + [[_lastError.request.xhr.responseText]] + </div> + <div hidden$="[[_computeListHidden(_loading, _lastError)]]" hidden> + <gr-change-list + changes="{{_changes}}" + selected-index="{{viewState.selectedChangeIndex}}" + show-star="[[loggedIn]]"></gr-change-list> + <nav> + <a href$="[[_computeNavLink(_query, _offset, -1)]]" + hidden$="[[_hidePrevArrow(_offset)]]">← Prev</a> + <a href$="[[_computeNavLink(_query, _offset, 1)]]" + hidden$="[[_hideNextArrow(_changes.length)]]">Next →</a> + </nav> + </div> + </template> + <script src="gr-change-list-view.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js new file mode 100644 index 0000000..d0a97c1d --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -0,0 +1,149 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + var DEFAULT_NUM_CHANGES = 25; + + Polymer({ + is: 'gr-change-list-view', + + /** + * Fired when the title of the page should change. + * + * @event title-change + */ + + properties: { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + + /** + * True when user is logged in. + */ + loggedIn: { + type: Boolean, + value: false, + }, + + /** + * State persisted across restamps of the element. + */ + viewState: { + type: Object, + notify: true, + value: function() { return {}; }, + }, + + /** + * Currently active query. + */ + _query: String, + + /** + * Offset of currently visible query results. + */ + _offset: Number, + + /** + * Change objects loaded from the server. + */ + _changes: Array, + + /** + * Contains error of last request (in case of change loading error). + */ + _lastError: Object, + + /** + * For showing a "loading..." string during ajax requests. + */ + _loading: { + type: Boolean, + value: true, + }, + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + attached: function() { + this.fire('title-change', {title: this._query}); + }, + + _paramsChanged: function(value) { + if (value.view != this.tagName.toLowerCase()) { return; } + + this._query = value.query; + this._offset = value.offset || 0; + if (this.viewState.query != this._query || + this.viewState.offset != this._offset) { + this.set('viewState.selectedChangeIndex', 0); + this.set('viewState.query', this._query); + this.set('viewState.offset', this._offset); + } + + this.fire('title-change', {title: this._query}); + }, + + _computeQueryParams: function(query, offset) { + var options = this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.DETAILED_ACCOUNTS + ); + var obj = { + n: DEFAULT_NUM_CHANGES, // Number of results to return. + O: options, + S: offset || 0, + }; + if (query && query.length > 0) { + obj.q = query; + } + return obj; + }, + + _computeNavLink: function(query, offset, direction) { + // Offset could be a string when passed from the router. + offset = +(offset || 0); + var newOffset = Math.max(0, offset + (25 * direction)); + var href = '/q/' + query; + if (newOffset > 0) { + href += ',' + newOffset; + } + return href; + }, + + _computeErrorHidden: function(loading, lastError) { + return loading || lastError == null; + }, + + _computeListHidden: function(loading, lastError) { + return loading || lastError != null; + }, + + _hidePrevArrow: function(offset) { + return offset == 0; + }, + + _hideNextArrow: function(changesLen) { + return changesLen < DEFAULT_NUM_CHANGES; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html new file mode 100644 index 0000000..8ff66cb --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -0,0 +1,66 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../../styles/gr-change-list-styles.html"> +<link rel="import" href="../gr-change-list-item/gr-change-list-item.html"> + +<dom-module id="gr-change-list"> + <template> + <style> + :host { + display: flex; + flex-direction: column; + } + </style> + <style include="gr-change-list-styles"></style> + <div class="headerRow"> + <span class="topHeader keyboard"></span> <!-- keyboard position indicator --> + <span class="topHeader star" hidden$="[[!showStar]]"></span> + <span class="topHeader subject">Subject</span> + <span class="topHeader status">Status</span> + <span class="topHeader owner">Owner</span> + <span class="topHeader project">Project</span> + <span class="topHeader branch">Branch</span> + <span class="topHeader updated">Updated</span> + <span class="topHeader size">Size</span> + <template is="dom-repeat" items="[[labelNames]]" as="labelName"> + <span class="topHeader label" title$="[[labelName]]"> + [[_computeLabelShortcut(labelName)]] + </span> + </template> + </div> + <template is="dom-repeat" items="{{groups}}" as="changeGroup" index-as="groupIndex"> + <template is="dom-if" if="[[_groupTitle(groupIndex)]]"> + <div class="groupHeader">[[_groupTitle(groupIndex)]]</div> + </template> + <template is="dom-if" if="[[!changeGroup.length]]"> + <div class="noChanges">No changes</div> + </template> + <template is="dom-repeat" items="[[changeGroup]]" as="change"> + <gr-change-list-item + selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]" + needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]" + change="[[change]]" + show-star="[[showStar]]" + label-names="[[labelNames]]"></gr-change-list-item> + </template> + </template> + </template> + <script src="gr-change-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js new file mode 100644 index 0000000..ef71be3 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -0,0 +1,165 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-change-list', + + hostAttributes: { + tabindex: 0, + }, + + properties: { + /** + * The logged-in user's account, or an empty object if no user is logged + * in. + */ + account: { + type: Object, + value: function() { return {}; }, + }, + /** + * An array of ChangeInfo objects to render. + * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info + */ + changes: { + type: Array, + observer: '_changesChanged', + }, + /** + * ChangeInfo objects grouped into arrays. The groups and changes + * properties should not be used together. + */ + groups: { + type: Array, + value: function() { return []; }, + }, + groupTitles: { + type: Array, + value: function() { return []; }, + }, + labelNames: { + type: Array, + computed: '_computeLabelNames(groups)', + }, + selectedIndex: { + type: Number, + notify: true, + }, + showStar: { + type: Boolean, + value: false, + }, + showReviewedState: { + type: Boolean, + value: false, + }, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.RESTClientBehavior, + ], + + _computeLabelNames: function(groups) { + if (!groups) { return []; } + var labels = []; + var nonExistingLabel = function(item) { + return labels.indexOf(item) < 0; + }; + for (var i = 0; i < groups.length; i++) { + var group = groups[i]; + for (var j = 0; j < group.length; j++) { + var change = group[j]; + if (!change.labels) { continue; } + var currentLabels = Object.keys(change.labels); + labels = labels.concat(currentLabels.filter(nonExistingLabel)); + } + } + return labels.sort(); + }, + + _computeLabelShortcut: function(labelName) { + return labelName.replace(/[a-z-]/g, ''); + }, + + _changesChanged: function(changes) { + this.groups = changes ? [changes] : []; + }, + + _groupTitle: function(groupIndex) { + if (groupIndex > this.groupTitles.length - 1) { return null; } + return this.groupTitles[groupIndex]; + }, + + _computeItemSelected: function(index, groupIndex, selectedIndex) { + var idx = 0; + for (var i = 0; i < groupIndex; i++) { + idx += this.groups[i].length; + } + idx += index; + return idx == selectedIndex; + }, + + _computeItemNeedsReview: function(account, change, showReviewedState) { + return showReviewedState && !change.reviewed && + change.status != this.ChangeStatus.MERGED && + account._account_id != change.owner._account_id; + }, + + _handleKey: function(e) { + if (this.shouldSupressKeyboardShortcut(e)) { return; } + + if (this.groups == null) { return; } + var len = 0; + this.groups.forEach(function(group) { + len += group.length; + }); + switch (e.keyCode) { + case 74: // 'j' + e.preventDefault(); + if (this.selectedIndex == len - 1) { return; } + this.selectedIndex += 1; + break; + case 75: // 'k' + e.preventDefault(); + if (this.selectedIndex == 0) { return; } + this.selectedIndex -= 1; + break; + case 79: // 'o' + case 13: // 'enter' + e.preventDefault(); + page.show(this._changeURLForIndex(this.selectedIndex)); + break; + } + }, + + _changeURLForIndex: function(index) { + var changeEls = this._getListItems(); + if (index < changeEls.length && changeEls[index]) { + return changeEls[index].changeURL; + } + return ''; + }, + + _getListItems: function() { + return Polymer.dom(this.root).querySelectorAll('gr-change-list-item'); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html new file mode 100644 index 0000000..0575f85 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -0,0 +1,237 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-change-list</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-change-list.html"> + +<test-fixture id="basic"> + <template> + <gr-change-list></gr-change-list> + </template> +</test-fixture> + +<test-fixture id="grouped"> + <template> + <gr-change-list></gr-change-list> + </template> +</test-fixture> + +<script> + suite('gr-change-list basic tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('computed fields', function() { + assert.equal(element._computeLabelNames( + [[{_number: 0, labels: {}}]]).length, 0); + assert.equal(element._computeLabelNames([[ + {_number: 0, labels: {Verified: {approved: {}}}}, + {_number: 1, labels: { + Verified: {approved: {}}, 'Code-Review': {approved: {}}}}, + {_number: 2, labels: { + Verified: {approved: {}}, 'Library-Compliance': {approved: {}}}}, + ]]).length, 3); + + assert.equal(element._computeLabelShortcut('Code-Review'), 'CR'); + assert.equal(element._computeLabelShortcut('Verified'), 'V'); + assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC'); + assert.equal(element._computeLabelShortcut( + 'Some-Special-Label-7'), 'SSL7'); + }); + + test('keyboard shortcuts', function(done) { + element.selectedIndex = 0; + element.changes = [ + {_number: 0}, + {_number: 1}, + {_number: 2}, + ]; + flushAsynchronousOperations(); + var elementItems = Polymer.dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 3); + + element.async(function() { + assert.isTrue(elementItems[0].selected); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + + var showStub = sinon.stub(page, 'show'); + assert.equal(element.selectedIndex, 2); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert(showStub.lastCall.calledWithExactly('/c/2/'), + 'Should navigate to /c/2/'); + + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert(showStub.lastCall.calledWithExactly('/c/1/'), + 'Should navigate to /c/1/'); + + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + assert.equal(element.selectedIndex, 0); + + showStub.restore(); + done(); + }, 1); + }); + + test('changes needing review', function() { + element.changes = [ + { + _number: 0, + reviewed: true, + owner: { _account_id: 0 }, + }, + { + _number: 1, + owner: { _account_id: 0 }, + }, + { + _number: 2, + status: 'MERGED', + owner: { _account_id: 0 }, + }, + { + _number: 3, + owner: { _account_id: 42 }, + } + ]; + flushAsynchronousOperations(); + var elementItems = Polymer.dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 4); + for (var i = 0; i < elementItems.length; i++) { + assert.isFalse(elementItems[i].hasAttribute('needs-review')); + } + + element.showReviewedState = true; + var elementItems = Polymer.dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 4); + assert.isFalse(elementItems[0].hasAttribute('needs-review')); + assert.isTrue(elementItems[1].hasAttribute('needs-review')); + assert.isFalse(elementItems[2].hasAttribute('needs-review')); + assert.isTrue(elementItems[3].hasAttribute('needs-review')); + + element.account = { _account_id: 42 }; + var elementItems = Polymer.dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 4); + assert.isFalse(elementItems[0].hasAttribute('needs-review')); + assert.isTrue(elementItems[1].hasAttribute('needs-review')); + assert.isFalse(elementItems[2].hasAttribute('needs-review')); + assert.isFalse(elementItems[3].hasAttribute('needs-review')); + }); + + test('no changes', function() { + element.changes = []; + flushAsynchronousOperations(); + var listItems = Polymer.dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(listItems.length, 0); + var noChangesMsg = Polymer.dom(element.root).querySelector('.noChanges'); + assert.ok(noChangesMsg); + }); + + test('empty groups', function() { + element.groups = [[], []]; + flushAsynchronousOperations(); + var listItems = Polymer.dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(listItems.length, 0); + var noChangesMsg = Polymer.dom(element.root).querySelectorAll( + '.noChanges'); + assert.equal(noChangesMsg.length, 2); + }); + }); + + suite('gr-change-list groups', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('keyboard shortcuts', function() { + element.selectedIndex = 0; + element.groups = [ + [ + {_number: 0}, + {_number: 1}, + {_number: 2}, + ], + [ + {_number: 3}, + {_number: 4}, + {_number: 5}, + ], + [ + {_number: 6}, + {_number: 7}, + {_number: 8}, + ] + ]; + element.groupTitles = ['Group 1', 'Group 2', 'Group 3']; + flushAsynchronousOperations(); + var elementItems = Polymer.dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 9); + + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + + var showStub = sinon.stub(page, 'show'); + assert.equal(element.selectedIndex, 2); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert(showStub.lastCall.calledWithExactly('/c/2/'), + 'Should navigate to /c/2/'); + + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert(showStub.lastCall.calledWithExactly('/c/1/'), + 'Should navigate to /c/1/'); + + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + assert.equal(element.selectedIndex, 4); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert(showStub.lastCall.calledWithExactly('/c/4/'), + 'Should navigate to /c/4/'); + showStub.restore(); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html new file mode 100644 index 0000000..e351f44 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -0,0 +1,64 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> + +<dom-module id="gr-dashboard-view"> + <template> + <style> + :host { + background-color: var(--view-background-color); + display: block; + margin: 0 var(--default-horizontal-margin); + } + .loading { + margin-top: 1em; + color: #666; + background-color: #f1f2f3; + } + gr-change-list { + margin-top: 1em; + width: 100%; + } + @media only screen and (max-width: 50em) { + :host { + margin: 0; + } + .loading { + padding: 0 var(--default-horizontal-margin); + } + } + </style> + <gr-ajax + auto + url="/changes/" + params="[[_computeQueryParams()]]" + last-response="{{_results}}" + loading="{{_loading}}"></gr-ajax> + <div class="loading" hidden$="[[!_loading]]">Loading...</div> + <div hidden$="[[_loading]]" hidden> + <gr-change-list + show-star + show-reviewed-state + account="[[account]]" + selected-index="{{viewState.selectedChangeIndex}}" + groups="{{_results}}" + group-titles="[[_groupTitles]]"></gr-change-list> + </div> + </template> + <script src="gr-dashboard-view.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js new file mode 100644 index 0000000..fc6a3ff --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -0,0 +1,76 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-dashboard-view', + + /** + * Fired when the title of the page should change. + * + * @event title-change + */ + + properties: { + account: { + type: Object, + value: function() { return {}; }, + }, + viewState: Object, + + _results: Array, + _groupTitles: { + type: Array, + value: [ + 'Outgoing reviews', + 'Incoming reviews', + 'Recently closed', + ], + }, + + /** + * For showing a "loading..." string during ajax requests. + */ + _loading: { + type: Boolean, + value: true, + }, + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + attached: function() { + this.fire('title-change', {title: 'My Reviews'}); + }, + + _computeQueryParams: function() { + var options = this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.DETAILED_ACCOUNTS, + this.ListChangesOption.REVIEWED + ); + return { + O: options, + q: [ + 'is:open owner:self', + 'is:open reviewer:self -owner:self', + 'is:closed (owner:self OR reviewer:self) -age:4w limit:10', + ], + }; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html new file mode 100644 index 0000000..1593fab --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -0,0 +1,84 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> + +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="../../shared/gr-request/gr-request.html"> + +<link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html"> + +<dom-module id="gr-change-actions"> + <template> + <style> + :host { + display: block; + } + gr-button { + display: block; + margin-bottom: .5em; + } + gr-button:before { + content: attr(data-label); + } + gr-button[loading]:before { + content: attr(data-loading-label); + } + @media screen and (max-width: 50em) { + .confirmDialog { + width: 90vw; + } + } + </style> + <gr-ajax id="actionsXHR" + url="[[_computeRevisionActionsPath(changeNum, patchNum)]]" + last-response="{{_revisionActions}}" + loading="{{_loading}}"></gr-ajax> + <div> + <template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action"> + <gr-button title$="[[action.title]]" + primary$="[[_computePrimary(action.__key)]]" + hidden$="[[!action.enabled]]" + data-action-key$="[[action.__key]]" + data-action-type$="[[action.__type]]" + data-label$="[[action.label]]" + on-tap="_handleActionTap"></gr-button> + </template> + <template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action"> + <gr-button title$="[[action.title]]" + primary$="[[_computePrimary(action.__key)]]" + disabled$="[[!action.enabled]]" + data-action-key$="[[action.__key]]" + data-action-type$="[[action.__type]]" + data-label$="[[action.label]]" + data-loading-label$="[[_computeLoadingLabel(action.__key)]]" + on-tap="_handleActionTap"></gr-button> + </template> + </div> + <gr-overlay id="overlay" with-backdrop> + <gr-confirm-rebase-dialog id="confirmRebase" + class="confirmDialog" + on-confirm="_handleRebaseConfirm" + on-cancel="_handleConfirmDialogCancel" + hidden></gr-confirm-rebase-dialog> + </gr-overlay> + </template> + <script src="gr-change-actions.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js new file mode 100644 index 0000000..a89c0aa --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -0,0 +1,225 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + // TODO(davido): Add the rest of the change actions. + var ChangeActions = { + ABANDON: 'abandon', + DELETE: '/', + RESTORE: 'restore', + }; + + // TODO(andybons): Add the rest of the revision actions. + var RevisionActions = { + DELETE: '/', + PUBLISH: 'publish', + REBASE: 'rebase', + SUBMIT: 'submit', + }; + + Polymer({ + is: 'gr-change-actions', + + /** + * Fired when the change should be reloaded. + * + * @event reload-change + */ + + properties: { + actions: { + type: Object, + }, + changeNum: String, + patchNum: String, + _loading: { + type: Boolean, + value: true, + }, + _revisionActions: Object, + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + observers: [ + '_actionsChanged(actions, _revisionActions)', + ], + + reload: function() { + if (!this.changeNum || !this.patchNum) { + return Promise.resolve(); + } + return this.$.actionsXHR.generateRequest().completes; + }, + + _actionsChanged: function(actions, revisionActions) { + this.hidden = + revisionActions.rebase == null && + revisionActions.submit == null && + revisionActions.publish == null && + actions.abandon == null && + actions.restore == null; + }, + + _computeRevisionActionsPath: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/actions'; + }, + + _getValuesFor: function(obj) { + return Object.keys(obj).map(function(key) { + return obj[key]; + }); + }, + + _computeActionValues: function(actions, type) { + var result = []; + var values = this._getValuesFor( + type == 'change' ? ChangeActions : RevisionActions); + for (var a in actions) { + if (values.indexOf(a) == -1) { continue; } + actions[a].__key = a; + actions[a].__type = type; + result.push(actions[a]); + } + return result; + }, + + _computeLoadingLabel: function(action) { + return { + 'rebase': 'Rebasing...', + 'submit': 'Submitting...', + }[action]; + }, + + _computePrimary: function(actionKey) { + return actionKey == 'submit'; + }, + + _computeButtonClass: function(action) { + if ([RevisionActions.SUBMIT, + RevisionActions.PUBLISH].indexOf(action) != -1) { + return 'primary'; + } + return ''; + }, + + _handleActionTap: function(e) { + e.preventDefault(); + var el = Polymer.dom(e).rootTarget; + var key = el.getAttribute('data-action-key'); + var type = el.getAttribute('data-action-type'); + if (type == 'revision') { + if (key == RevisionActions.REBASE) { + this._showRebaseDialog(); + return; + } + this._fireRevisionAction(this._prependSlash(key), + this._revisionActions[key]); + } else { + this._fireChangeAction(this._prependSlash(key), this.actions[key]); + } + }, + + _prependSlash: function(key) { + return key == '/' ? key : '/' + key; + }, + + _handleConfirmDialogCancel: function() { + var dialogEls = + Polymer.dom(this.root).querySelectorAll('.confirmDialog'); + for (var i = 0; i < dialogEls.length; i++) { + dialogEls[i].hidden = true; + } + this.$.overlay.close(); + }, + + _handleRebaseConfirm: function() { + var payload = {}; + var el = this.$.confirmRebase; + if (el.clearParent) { + // There is a subtle but important difference between setting the base + // to an empty string and omitting it entirely from the payload. An + // empty string implies that the parent should be cleared and the + // change should be rebased on top of the target branch. Leaving out + // the base implies that it should be rebased on top of its current + // parent. + payload.base = ''; + } else if (el.base && el.base.length > 0) { + payload.base = el.base; + } + this.$.overlay.close(); + el.hidden = false; + this._fireRevisionAction('/rebase', this._revisionActions.rebase, + payload); + }, + + _fireChangeAction: function(endpoint, action) { + this._send(action.method, {}, endpoint).then( + function() { + // We can’t reload a change that was deleted. + if (endpoint == ChangeActions.DELETE) { + page.show('/'); + } else { + this.fire('reload-change', null, {bubbles: false}); + } + }.bind(this)).catch(function(err) { + alert('Oops. Something went wrong. Check the console and bug the ' + + 'PolyGerrit team for assistance.'); + throw err; + }); + }, + + _fireRevisionAction: function(endpoint, action, opt_payload) { + var buttonEl = this.$$('[data-action-key="' + action.__key + '"]'); + buttonEl.setAttribute('loading', true); + buttonEl.disabled = true; + function enableButton() { + buttonEl.removeAttribute('loading'); + buttonEl.disabled = false; + } + + this._send(action.method, opt_payload, endpoint, true).then( + function() { + this.fire('reload-change', null, {bubbles: false}); + enableButton(); + }.bind(this)).catch(function(err) { + // TODO(andybons): Handle merge conflict (409 status); + alert('Oops. Something went wrong. Check the console and bug the ' + + 'PolyGerrit team for assistance.'); + enableButton(); + throw err; + }); + }, + + _showRebaseDialog: function() { + this.$.confirmRebase.hidden = false; + this.$.overlay.open(); + }, + + _send: function(method, payload, actionEndpoint, revisionAction) { + var xhr = document.createElement('gr-request'); + this._xhrPromise = xhr.send({ + method: method, + url: this.changeBaseURL(this.changeNum, + revisionAction ? this.patchNum : null) + actionEndpoint, + body: payload, + }); + + return this._xhrPromise; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html new file mode 100644 index 0000000..a89a7a5 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -0,0 +1,155 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-change-actions</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-change-actions.html"> + +<test-fixture id="basic"> + <template> + <gr-change-actions></gr-change-actions> + </template> +</test-fixture> + +<script> + suite('gr-change-actions tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + server = sinon.fakeServer.create(); + + server.respondWith( + 'GET', + '/changes/42/revisions/2/actions', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({ + cherrypick: { + method: 'POST', + label: 'Cherry Pick', + title: 'Cherry pick change to a different branch', + enabled: true + }, + rebase: { + method: 'POST', + label: 'Rebase', + title: 'Rebase onto tip of branch or parent change' + }, + submit: { + method: 'POST', + label: 'Submit', + title: 'Submit patch set 1 into master', + enabled: true + } + }), + ] + ); + + server.respondWith( + 'POST', + '/changes/42/revisions/2/submit', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n{}', // The response is not used by the element. + ] + ); + + server.respondWith( + 'POST', + '/changes/42/revisions/2/rebase', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n{}', // The response is not used by the element. + ] + ); + + element.changeNum = '42'; + element.patchNum = '2'; + element.reload(); + + server.respond(); + }); + + test('submit and rebase buttons show', function(done) { + element.async(function() { + var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button'); + assert.equal(buttonEls.length, 2); + assert.isFalse(element.hidden); + done(); + }, 1); + }); + + test('submit change', function(done) { + element.async(function() { + var submitButton = element.$$('gr-button[data-action-key="submit"]'); + assert.ok(submitButton); + MockInteractions.tap(submitButton); + server.respond(); + + // Upon success it should fire the reload-change event. + element.addEventListener('reload-change', function(e) { + done(); + }); + }, 1); + }); + + test('rebase change', function(done) { + element.async(function() { + var rebaseButton = element.$$('gr-button[data-action-key="rebase"]'); + MockInteractions.tap(rebaseButton); + + element.$.confirmRebase.base = '1234'; + element._handleRebaseConfirm(); + server.respond(); + var lastRequest = server.requests[server.requests.length - 1]; + assert.equal(lastRequest.requestBody, '{"base":"1234"}'); + + element.$.confirmRebase.base = ''; + element._handleRebaseConfirm(); + server.respond(); + lastRequest = server.requests[server.requests.length - 1]; + assert.equal(lastRequest.requestBody, '{}'); + + element.$.confirmRebase.base = 'does not matter'; + element.$.confirmRebase.clearParent = true; + element._handleRebaseConfirm(); + server.respond(); + lastRequest = server.requests[server.requests.length - 1]; + assert.equal(lastRequest.requestBody, '{"base":""}'); + + // Upon each request success it should fire the reload-change event. + var numEvents = 0; + element.addEventListener('reload-change', function(e) { + if (++numEvents == 3) { done(); } + }); + }, 1); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html similarity index 62% rename from polygerrit-ui/app/elements/gr-change-metadata.html rename to polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html index 81b580f..7b1a2f1 100644 --- a/polygerrit-ui/app/elements/gr-change-metadata.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -14,13 +14,13 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-account-link.html"> -<link rel="import" href="gr-date-formatter.html"> -<link rel="import" href="gr-reviewer-list.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> +<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html"> -<script src="../scripts/fake-app.js"></script> +<script src="../../../scripts/fake-app.js"></script> <dom-module id="gr-change-metadata"> <template> @@ -123,69 +123,5 @@ </section> </template> </template> - <script> - (function() { - 'use strict'; - - var SubmitTypeLabel = { - FAST_FORWARD_ONLY: 'Fast Forward Only', - MERGE_IF_NECESSARY: 'Merge if Necessary', - REBASE_IF_NECESSARY: 'Rebase if Necessary', - MERGE_ALWAYS: 'Always Merge', - CHERRY_PICK: 'Cherry Pick', - }; - - Polymer({ - is: 'gr-change-metadata', - - properties: { - change: Object, - mutable: Boolean, - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - _computeHideStrategy: function(change) { - var open = change.status == this.ChangeStatus.NEW || - change.status == this.ChangeStatus.DRAFT; - return !open; - }, - - _computeStrategy: function(change) { - return SubmitTypeLabel[change.submit_type]; - }, - - _computeLabelNames: function(labels) { - return Object.keys(labels).sort(); - }, - - _computeLabelValues: function(labelName, labels) { - var result = []; - var t = labels[labelName]; - if (!t) { return result; } - var approvals = t.all || []; - approvals.forEach(function(label) { - if (label.value && label.value != labels[labelName].default_value) { - var labelClassName; - var labelValPrefix = ''; - if (label.value > 0) { - labelValPrefix = '+'; - labelClassName = 'approved'; - } else if (label.value < 0) { - labelClassName = 'notApproved'; - } - result.push({ - value: labelValPrefix + label.value, - className: labelClassName, - account: label, - }); - } - }); - return result; - }, - }); - })(); - </script> + <script src="gr-change-metadata.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js new file mode 100644 index 0000000..3d2633b --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -0,0 +1,76 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + var SubmitTypeLabel = { + FAST_FORWARD_ONLY: 'Fast Forward Only', + MERGE_IF_NECESSARY: 'Merge if Necessary', + REBASE_IF_NECESSARY: 'Rebase if Necessary', + MERGE_ALWAYS: 'Always Merge', + CHERRY_PICK: 'Cherry Pick', + }; + + Polymer({ + is: 'gr-change-metadata', + + properties: { + change: Object, + mutable: Boolean, + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + _computeHideStrategy: function(change) { + var open = change.status == this.ChangeStatus.NEW || + change.status == this.ChangeStatus.DRAFT; + return !open; + }, + + _computeStrategy: function(change) { + return SubmitTypeLabel[change.submit_type]; + }, + + _computeLabelNames: function(labels) { + return Object.keys(labels).sort(); + }, + + _computeLabelValues: function(labelName, labels) { + var result = []; + var t = labels[labelName]; + if (!t) { return result; } + var approvals = t.all || []; + approvals.forEach(function(label) { + if (label.value && label.value != labels[labelName].default_value) { + var labelClassName; + var labelValPrefix = ''; + if (label.value > 0) { + labelValPrefix = '+'; + labelClassName = 'approved'; + } else if (label.value < 0) { + labelClassName = 'notApproved'; + } + result.push({ + value: labelValPrefix + label.value, + className: labelClassName, + account: label, + }); + } + }); + return result; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html new file mode 100644 index 0000000..6c97b5a --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -0,0 +1,68 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-change-metadata</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-change-metadata.html"> +<script src="../../../scripts/util.js"></script> + +<test-fixture id="basic"> + <template> + <gr-change-metadata></gr-change-metadata> + </template> +</test-fixture> + +<script> + suite('gr-change-metadata tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('computed fields', function() { + assert.isFalse(element._computeHideStrategy({status: 'NEW'})); + assert.isFalse(element._computeHideStrategy({status: 'DRAFT'})); + assert.isTrue(element._computeHideStrategy({status: 'MERGED'})); + assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'})); + assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}), + 'Cherry Pick'); + }); + + test('show strategy for open change', function() { + element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}}; + flushAsynchronousOperations(); + var strategy = element.$$('.strategy'); + assert.ok(strategy); + assert.isFalse(strategy.hasAttribute('hidden')); + assert.equal(strategy.children[1].innerHTML, 'Cherry Pick'); + }); + + test('hide strategy for closed change', function() { + element.change = {status: 'MERGED', labels: {}}; + flushAsynchronousOperations(); + assert.isTrue(element.$$('.strategy').hasAttribute('hidden')); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html new file mode 100644 index 0000000..41cb058 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -0,0 +1,319 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-change-star/gr-change-star.html"> +<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> +<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> + +<link rel="import" href="../gr-change-actions/gr-change-actions.html"> +<link rel="import" href="../gr-change-metadata/gr-change-metadata.html"> +<link rel="import" href="../gr-download-dialog/gr-download-dialog.html"> +<link rel="import" href="../gr-file-list/gr-file-list.html"> +<link rel="import" href="../gr-messages-list/gr-messages-list.html"> +<link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html"> +<link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html"> + +<dom-module id="gr-change-view"> + <template> + <style> + .container { + margin: 1em var(--default-horizontal-margin); + } + .container:not(.loading) { + background-color: var(--view-background-color); + } + .container.loading { + color: #666; + } + .headerContainer { + height: 4.1em; + margin-bottom: .5em; + } + .header { + align-items: center; + background-color: var(--view-background-color); + border-bottom: 1px solid #ddd; + display: flex; + padding: 1em var(--default-horizontal-margin); + z-index: 99; /* Less than gr-overlay's backdrop */ + } + .header.pinned { + border-bottom-color: transparent; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + position: fixed; + top: 0; + transition: box-shadow 250ms linear; + width: calc(100% - (2 * var(--default-horizontal-margin))); + } + .header-title { + flex: 1; + font-size: 1.2em; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + gr-change-star { + margin-right: .25em; + vertical-align: -.425em; + } + .download, + .patchSelectLabel { + margin-left: var(--default-horizontal-margin); + } + .header select { + margin-left: .5em; + } + .header .reply { + margin-left: var(--default-horizontal-margin); + } + gr-reply-dialog { + min-width: 30em; + max-width: 50em; + } + .changeStatus { + color: #999; + text-transform: capitalize; + } + section { + margin: 10px 0; + padding: 10px var(--default-horizontal-margin); + } + /* Strong specificity here is needed due to + https://github.com/Polymer/polymer/issues/2531 */ + .container section.changeInfo { + border-bottom: 1px solid #ddd; + display: flex; + margin-top: 0; + padding-top: 0; + } + .changeInfo-column:not(:last-of-type) { + margin-right: 1em; + padding-right: 1em; + } + .changeMetadata { + border-right: 1px solid #ddd; + font-size: .9em; + } + gr-change-actions { + margin-top: 1em; + } + .commitMessage { + font-family: var(--monospace-font-family); + flex: 0 0 72ch; + margin-right: 2em; + margin-bottom: 1em; + } + .commitMessage h4 { + font-family: var(--font-family); + font-weight: bold; + margin-bottom: .25em; + } + .commitAndRelated { + align-content: flex-start; + display: flex; + flex: 1; + flex-wrap: wrap; + } + gr-file-list { + margin-bottom: 1em; + padding: 0 var(--default-horizontal-margin); + } + @media screen and (max-width: 50em) { + .container { + margin: .5em 0 !important; + } + .container.loading { + margin: 1em var(--default-horizontal-margin) !important; + } + .headerContainer { + height: 5.15em; + } + .header { + align-items: flex-start; + flex-direction: column; + padding: .5em var(--default-horizontal-margin) !important; + } + gr-change-star { + vertical-align: middle; + } + .header-title, + .header-actions, + .header.pinned { + width: 100% !important; + } + .header-title { + font-size: 1.1em; + } + .header-actions { + align-items: center; + display: flex; + justify-content: space-between; + margin-top: .5em; + } + gr-reply-dialog { + min-width: initial; + width: 90vw; + } + .download { + display: none; + } + .patchSelectLabel { + margin-left: 0 !important; + margin-right: .5em; + } + .header select { + margin-left: 0 !important; + margin-right: .5em; + } + .header .reply { + margin-left: 0 !important; + margin-right: .5em; + } + .changeInfo-column:not(:last-of-type) { + margin-right: 0; + padding-right: 0; + } + .changeInfo, + .commitAndRelated { + flex-direction: column; + flex-wrap: nowrap; + } + .changeMetadata { + font-size: 1em; + border-right: none; + margin-bottom: 1em; + margin-top: .25em; + max-width: none; + } + .commitMessage { + flex: initial; + margin-right: 0; + } + } + </style> + <gr-ajax id="detailXHR" + url="[[_computeDetailPath(_changeNum)]]" + params="[[_computeDetailQueryParams()]]" + last-response="{{_change}}" + loading="{{_loading}}"></gr-ajax> + <gr-ajax id="commentsXHR" + url="[[_computeCommentsPath(_changeNum)]]" + last-response="{{_comments}}"></gr-ajax> + <gr-ajax id="commitInfoXHR" + url="[[_computeCommitInfoPath(_changeNum, _patchNum)]]" + last-response="{{_commitInfo}}"></gr-ajax> + <!-- TODO(andybons): Cache the project config. --> + <gr-ajax id="configXHR" + auto + url="[[_computeProjectConfigPath(_change.project)]]" + last-response="{{_projectConfig}}"></gr-ajax> + <div class="container loading" hidden$="{{!_loading}}">Loading...</div> + <div class="container" hidden$="{{_loading}}"> + <div class="headerContainer"> + <div class="header"> + <span class="header-title"> + <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star> + <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span> + <span>[[_change.subject]]</span> + <span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span> + </span> + <span class="header-actions"> + <gr-button class="reply" hidden$="[[!_loggedIn]]" hidden on-tap="_handleReplyTap">Reply</gr-button> + <gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button> + <span> + <label class="patchSelectLabel" for="patchSetSelect">Patch set</label> + <select id="patchSetSelect" on-change="_handlePatchChange"> + <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber"> + <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]"> + <span>[[patchNumber]]</span> + / + <span>[[_computeLatestPatchNum(_change)]]</span> + </option> + </template> + </select> + </span> + </span> + </div> + </div> + <section class="changeInfo"> + <div class="changeInfo-column changeMetadata"> + <gr-change-metadata + change="[[_change]]" + mutable="[[_loggedIn]]"></gr-change-metadata> + <gr-change-actions id="actions" + actions="[[_change.actions]]" + change-num="[[_changeNum]]" + patch-num="[[_patchNum]]" + on-reload-change="_handleReloadChange"></gr-change-actions> + </div> + <div class="changeInfo-column commitAndRelated"> + <div class="commitMessage"> + <h4>Commit message</h4> + <gr-linked-text pre + content="[[_commitInfo.message]]" + config="[[_projectConfig.commentlinks]]"></gr-linked-text> + </div> + <div class="relatedChanges"> + <gr-related-changes-list id="relatedChanges" + change="[[_change]]" + server-config="[[serverConfig]]" + patch-num="[[_patchNum]]"></gr-related-changes-list> + </div> + </div> + </section> + <gr-file-list id="fileList" + change-num="[[_changeNum]]" + patch-num="[[_patchNum]]" + comments="[[_comments]]" + selected-index="{{viewState.selectedFileIndex}}"></gr-file-list> + <gr-messages-list id="messageList" + change-num="[[_changeNum]]" + messages="[[_change.messages]]" + comments="[[_comments]]" + project-config="[[_projectConfig]]" + show-reply-buttons="[[_loggedIn]]" + on-reply="_handleMessageReply"></gr-messages-list> + </div> + <gr-overlay id="downloadOverlay" with-backdrop> + <gr-download-dialog + change="[[_change]]" + patch-num="[[_patchNum]]" + config="[[serverConfig.download]]" + on-close="_handleDownloadDialogClose"></gr-download-dialog> + </gr-overlay> + <gr-overlay id="replyOverlay" + on-iron-overlay-opened="_handleReplyOverlayOpen" + with-backdrop> + <gr-reply-dialog id="replyDialog" + change-num="[[_changeNum]]" + patch-num="[[_patchNum]]" + labels="[[_change.labels]]" + permitted-labels="[[_change.permitted_labels]]" + on-send="_handleReplySent" + on-cancel="_handleReplyCancel" + hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog> + </gr-overlay> + </template> + <script src="gr-change-view.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js new file mode 100644 index 0000000..a42a379 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -0,0 +1,354 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-change-view', + + /** + * Fired when the title of the page should change. + * + * @event title-change + */ + + properties: { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + viewState: { + type: Object, + notify: true, + value: function() { return {}; }, + }, + serverConfig: Object, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, + + _comments: Object, + _change: { + type: Object, + observer: '_changeChanged', + }, + _commitInfo: Object, + _changeNum: String, + _patchNum: String, + _allPatchSets: { + type: Array, + computed: '_computeAllPatchSets(_change)', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _loading: Boolean, + _headerContainerEl: Object, + _headerEl: Object, + _projectConfig: Object, + _boundScrollHandler: { + type: Function, + value: function() { return this._handleBodyScroll.bind(this); }, + }, + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.RESTClientBehavior, + ], + + ready: function() { + app.accountReady.then(function() { + this._loggedIn = app.loggedIn; + }.bind(this)); + this._headerEl = this.$$('.header'); + }, + + attached: function() { + window.addEventListener('scroll', this._boundScrollHandler); + }, + + detached: function() { + window.removeEventListener('scroll', this._boundScrollHandler); + }, + + _handleBodyScroll: function(e) { + var containerEl = this._headerContainerEl || + this.$$('.headerContainer'); + + // Calculate where the header is relative to the window. + var top = containerEl.offsetTop; + for (var offsetParent = containerEl.offsetParent; + offsetParent; + offsetParent = offsetParent.offsetParent) { + top += offsetParent.offsetTop; + } + // The element may not be displayed yet, in which case do nothing. + if (top == 0) { return; } + + this._headerEl.classList.toggle('pinned', window.scrollY >= top); + }, + + _resetHeaderEl: function() { + var el = this._headerEl || this.$$('.header'); + this._headerEl = el; + el.classList.remove('pinned'); + }, + + _handlePatchChange: function(e) { + var patchNum = e.target.value; + var currentPatchNum = + this._change.revisions[this._change.current_revision]._number; + if (patchNum == currentPatchNum) { + page.show(this._computeChangePath(this._changeNum)); + return; + } + page.show(this._computeChangePath(this._changeNum) + '/' + patchNum); + }, + + _handleReplyTap: function(e) { + e.preventDefault(); + this.$.replyOverlay.open(); + }, + + _handleDownloadTap: function(e) { + e.preventDefault(); + this.$.downloadOverlay.open(); + }, + + _handleDownloadDialogClose: function(e) { + this.$.downloadOverlay.close(); + }, + + _handleMessageReply: function(e) { + var msg = e.detail.message.message; + var quoteStr = msg.split('\n').map( + function(line) { return '> ' + line; }).join('\n') + '\n\n'; + this.$.replyDialog.draft += quoteStr; + this.$.replyOverlay.open(); + }, + + _handleReplyOverlayOpen: function(e) { + this.$.replyDialog.reload().then(function() { + this.async(function() { this.$.replyOverlay.center() }, 1); + }.bind(this)); + this.$.replyDialog.focus(); + }, + + _handleReplySent: function(e) { + this.$.replyOverlay.close(); + this._reload(); + }, + + _handleReplyCancel: function(e) { + this.$.replyOverlay.close(); + }, + + _paramsChanged: function(value) { + if (value.view != this.tagName.toLowerCase()) { return; } + + this._changeNum = value.changeNum; + this._patchNum = value.patchNum; + if (this.viewState.changeNum != this._changeNum || + this.viewState.patchNum != this._patchNum) { + this.set('viewState.selectedFileIndex', 0); + this.set('viewState.changeNum', this._changeNum); + this.set('viewState.patchNum', this._patchNum); + } + if (!this._changeNum) { + return; + } + this._reload().then(function() { + this.$.messageList.topMargin = this._headerEl.offsetHeight; + + // Allow the message list to render before scrolling. + this.async(function() { + var msgPrefix = '#message-'; + var hash = window.location.hash; + if (hash.indexOf(msgPrefix) == 0) { + this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length)); + } + }.bind(this), 1); + + app.accountReady.then(function() { + if (!this._loggedIn) { return; } + + if (this.viewState.showReplyDialog) { + this.$.replyOverlay.open(); + this.set('viewState.showReplyDialog', false); + } + }.bind(this)); + }.bind(this)); + }, + + _changeChanged: function(change) { + if (!change) { return; } + this._patchNum = this._patchNum || + change.revisions[change.current_revision]._number; + + var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; + this.fire('title-change', {title: title}); + }, + + _computeChangePath: function(changeNum) { + return '/c/' + changeNum; + }, + + _computeChangePermalink: function(changeNum) { + return '/' + changeNum; + }, + + _computeChangeStatus: function(change, patchNum) { + var status = change.status; + if (status == this.ChangeStatus.NEW) { + var rev = this._getRevisionNumber(change, patchNum); + // TODO(davido): Figure out, why sometimes revision is not there + if (rev == undefined || !rev.draft) { return ''; } + status = this.ChangeStatus.DRAFT; + } + return '(' + status.toLowerCase() + ')'; + }, + + _computeDetailPath: function(changeNum) { + return '/changes/' + changeNum + '/detail'; + }, + + _computeCommitInfoPath: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/commit?links'; + }, + + _computeCommentsPath: function(changeNum) { + return '/changes/' + changeNum + '/comments'; + }, + + _computeProjectConfigPath: function(project) { + return '/projects/' + encodeURIComponent(project) + '/config'; + }, + + _computeDetailQueryParams: function() { + var options = this.listChangesOptionsToHex( + this.ListChangesOption.ALL_REVISIONS, + this.ListChangesOption.CHANGE_ACTIONS, + this.ListChangesOption.DOWNLOAD_COMMANDS + ); + return {O: options}; + }, + + _computeLatestPatchNum: function(change) { + return change.revisions[change.current_revision]._number; + }, + + _computeAllPatchSets: function(change) { + var patchNums = []; + for (var rev in change.revisions) { + patchNums.push(change.revisions[rev]._number); + } + return patchNums.sort(function(a, b) { + return a - b; + }); + }, + + _getRevisionNumber: function(change, patchNum) { + for (var rev in change.revisions) { + if (change.revisions[rev]._number == patchNum) { + return change.revisions[rev]; + } + } + }, + + _computePatchIndexIsSelected: function(index, patchNum) { + return this._allPatchSets[index] == patchNum; + }, + + _computeLabelNames: function(labels) { + return Object.keys(labels).sort(); + }, + + _computeLabelValues: function(labelName, labels) { + var result = []; + var t = labels[labelName]; + if (!t) { return result; } + var approvals = t.all || []; + approvals.forEach(function(label) { + if (label.value && label.value != labels[labelName].default_value) { + var labelClassName; + var labelValPrefix = ''; + if (label.value > 0) { + labelValPrefix = '+'; + labelClassName = 'approved'; + } else if (label.value < 0) { + labelClassName = 'notApproved'; + } + result.push({ + value: labelValPrefix + label.value, + className: labelClassName, + account: label, + }); + } + }); + return result; + }, + + _handleKey: function(e) { + if (this.shouldSupressKeyboardShortcut(e)) { return; } + + switch (e.keyCode) { + case 65: // 'a' + e.preventDefault(); + this.$.replyOverlay.open(); + break; + case 85: // 'u' + e.preventDefault(); + page.show('/'); + break; + } + }, + + _handleReloadChange: function() { + page.show(this._computeChangePath(this._changeNum)); + }, + + _reload: function() { + var detailCompletes = this.$.detailXHR.generateRequest().completes; + this.$.commentsXHR.generateRequest(); + var reloadPatchNumDependentResources = function() { + return Promise.all([ + this.$.commitInfoXHR.generateRequest().completes, + this.$.actions.reload(), + this.$.fileList.reload(), + ]); + }.bind(this); + var reloadDetailDependentResources = function() { + return this.$.relatedChanges.reload(); + }.bind(this); + + this._resetHeaderEl(); + + if (this._patchNum) { + return reloadPatchNumDependentResources().then(function() { + return detailCompletes; + }).then(reloadDetailDependentResources); + } else { + // The patch number is reliant on the change detail request. + return detailCompletes.then(reloadPatchNumDependentResources).then( + reloadDetailDependentResources); + } + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html new file mode 100644 index 0000000..ed9d28d --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -0,0 +1,179 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-change-view</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-change-view.html"> + +<test-fixture id="basic"> + <template> + <gr-change-view></gr-change-view> + </template> +</test-fixture> + +<script> + suite('gr-change-view tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + element.$.configXHR.auto = false; + + server = sinon.fakeServer.create(); + // Eat any requests made by elements in this suite. + server.respondWith( + 'GET', + /\/changes\/(.*)/, + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n{}', + ] + ); + }); + + teardown(function() { + server.restore(); + }); + + test('keyboard shortcuts', function() { + var showStub = sinon.stub(page, 'show'); + + MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + assert(showStub.lastCall.calledWithExactly('/'), + 'Should navigate to /'); + showStub.restore(); + + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + var overlayEl = element.$.replyOverlay; + assert.isTrue(overlayEl.opened); + overlayEl.close(); + assert.isFalse(overlayEl.opened); + }); + + test('patch num change', function(done) { + element._changeNum = '42'; + element._patchNum = 2; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev2: {_number: 2}, + rev1: {_number: 1}, + rev13: {_number: 13}, + rev3: {_number: 3}, + }, + current_revision: 'rev3', + status: 'NEW', + labels: {}, + }; + flushAsynchronousOperations(); + var selectEl = element.$$('.header select'); + assert.ok(selectEl); + var optionEls = + Polymer.dom(element.root).querySelectorAll('.header option'); + assert.equal(optionEls.length, 4); + assert.isFalse( + element.$$('.header option[value="1"]').hasAttribute('selected')); + assert.isTrue( + element.$$('.header option[value="2"]').hasAttribute('selected')); + assert.isFalse( + element.$$('.header option[value="3"]').hasAttribute('selected')); + assert.equal(optionEls[3].value, 13); + + var showStub = sinon.stub(page, 'show'); + + var numEvents = 0; + selectEl.addEventListener('change', function(e) { + numEvents++; + if (numEvents == 1) { + assert(showStub.lastCall.calledWithExactly('/c/42/1'), + 'Should navigate to /c/42/1'); + selectEl.value = '3'; + element.fire('change', {}, {node: selectEl}); + } else if (numEvents == 2) { + assert(showStub.lastCall.calledWithExactly('/c/42'), + 'Should navigate to /c/42'); + showStub.restore(); + done(); + } + }); + selectEl.value = '1'; + element.fire('change', {}, {node: selectEl}); + }); + + test('change status new', function() { + element._changeNum = '1'; + element._patchNum = 1; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + }; + var status = element._computeChangeStatus(element._change, '1'); + assert.equal(status, ''); + }); + + test('change status draft', function() { + element._changeNum = '1'; + element._patchNum = 1; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1}, + }, + current_revision: 'rev1', + status: 'DRAFT', + labels: {}, + }; + var status = element._computeChangeStatus(element._change, '1'); + assert.equal(status, '(draft)'); + }); + + test('revision status draft', function() { + element._changeNum = '1'; + element._patchNum = 2; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1}, + rev2: { + _number: 2, + draft: true, + }, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + }; + var status = element._computeChangeStatus(element._change, '2'); + assert.equal(status, '(draft)'); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html new file mode 100644 index 0000000..263fb28 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -0,0 +1,68 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-comment-list"> + <template> + <style> + :host { + display: block; + } + .file { + border-top: 1px solid #ddd; + font-weight: bold; + margin: 10px 0 3px; + padding: 10px 0 5px; + } + .container { + display: flex; + margin: 5px 0; + } + .lineNum { + margin-right: .35em; + min-width: 7em; + } + .message { + flex: 1; + white-space: pre-wrap; + word-wrap: break-word; + } + </style> + <template is="dom-repeat" items="{{_files}}" as="file"> + <div class="file"> + <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>: + </div> + <template is="dom-repeat" + items="[[_computeCommentsForFile(file)]]" as="comment"> + <div class="container"> + <a class="lineNum" + href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"> + <span hidden$="[[!comment.line]]"> + <span>[[_computePatchDisplayName(comment)]]</span> + Line <span>[[comment.line]]</span>: + </span> + <span hidden$="[[comment.line]]"> + File comment: + </span> + </a> + <div class="message">[[comment.message]]</div> + </div> + </template> + </template> + </template> + <script src="gr-comment-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js new file mode 100644 index 0000000..b40c18e --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -0,0 +1,62 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-comment-list', + + properties: { + changeNum: Number, + comments: { + type: Object, + observer: '_commentsChanged', + }, + patchNum: Number, + + _files: Array, + }, + + _commentsChanged: function(value) { + this._files = Object.keys(value || {}).sort(); + }, + + _computeFileDiffURL: function(file, changeNum, patchNum) { + return '/c/' + changeNum + '/' + patchNum + '/' + file; + }, + + _computeDiffLineURL: function(file, changeNum, patchNum, comment) { + var diffURL = this._computeFileDiffURL(file, changeNum, patchNum); + if (comment.line) { + // TODO(andybons): This is not correct if the comment is on the base. + diffURL += '#' + comment.line; + } + return diffURL; + }, + + _computeCommentsForFile: function(file) { + return this.comments[file]; + }, + + _computePatchDisplayName: function(comment) { + if (comment.side == 'PARENT') { + return 'Base, '; + } + if (comment.patch_set != this.patchNum) { + return 'PS' + comment.patch_set + ', '; + } + return ''; + } + }); +})();
diff --git a/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html similarity index 66% rename from polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html index 96c7188..3896ffa 100644 --- a/polygerrit-ui/app/elements/gr-confirm-rebase-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -14,8 +14,8 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-confirm-dialog.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html"> <dom-module id="gr-confirm-rebase-dialog"> <template> @@ -71,49 +71,5 @@ </div> </gr-confirm-dialog> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-confirm-rebase-dialog', - - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - properties: { - base: String, - clearParent: Boolean, - }, - - _handleConfirmTap: function(e) { - e.preventDefault(); - this.fire('confirm', null, {bubbles: false}); - }, - - _handleCancelTap: function(e) { - e.preventDefault(); - this.fire('cancel', null, {bubbles: false}); - }, - - _handleClearParentTap: function(e) { - var clear = Polymer.dom(e).rootTarget.checked; - if (clear) { - this.base = ''; - } - this.$.parentInput.disabled = clear; - this.clearParent = clear; - }, - }); - })(); - </script> + <script src="gr-confirm-rebase-dialog.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js new file mode 100644 index 0000000..42f2167 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -0,0 +1,56 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-confirm-rebase-dialog', + + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ + + /** + * Fired when the cancel button is pressed. + * + * @event cancel + */ + + properties: { + base: String, + clearParent: Boolean, + }, + + _handleConfirmTap: function(e) { + e.preventDefault(); + this.fire('confirm', null, {bubbles: false}); + }, + + _handleCancelTap: function(e) { + e.preventDefault(); + this.fire('cancel', null, {bubbles: false}); + }, + + _handleClearParentTap: function(e) { + var clear = Polymer.dom(e).rootTarget.checked; + if (clear) { + this.base = ''; + } + this.$.parentInput.disabled = clear; + this.clearParent = clear; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html new file mode 100644 index 0000000..c02e11e --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-confirm-rebase-dialog</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-confirm-rebase-dialog.html"> + +<test-fixture id="basic"> + <template> + <gr-confirm-rebase-dialog></gr-confirm-rebase-dialog> + </template> +</test-fixture> + +<script> + suite('gr-confirm-rebase-dialog tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('controls', function() { + assert.isFalse(element.$.parentInput.hasAttribute('disabled')); + assert.isFalse(element.$.clearParent.checked); + element.base = 'something great'; + MockInteractions.tap(element.$.clearParent); + assert.isTrue(element.$.parentInput.hasAttribute('disabled')); + assert.isTrue(element.$.clearParent.checked); + assert.equal(element.base, ''); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html new file mode 100644 index 0000000..77a262d --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -0,0 +1,144 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> + +<dom-module id="gr-download-dialog"> + <template> + <style> + :host { + display: block; + padding: 1em; + } + ul { + list-style: none; + margin-bottom: .5em; + } + li { + display: inline-block; + margin: 0; + padding: 0; + } + li gr-button { + margin-right: 1em; + } + label, + input { + display: block; + } + label { + font-weight: bold; + } + input { + font-family: var(--monospace-font-family); + font-size: inherit; + margin-bottom: .5em; + width: 60em; + } + li[selected] gr-button { + color: #000; + font-weight: bold; + text-decoration: none; + } + header { + display: flex; + justify-content: space-between; + } + main { + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; + padding: .5em; + } + footer { + display: flex; + justify-content: space-between; + padding-top: .75em; + } + .closeButtonContainer { + display: flex; + flex: 1; + justify-content: flex-end; + } + .patchFiles { + margin-right: 2em; + } + .patchFiles a, + .archives a { + display: inline-block; + margin-right: 1em; + } + .patchFiles a:last-of-type, + .archives a:last-of-type { + margin-right: 0; + } + </style> + <header> + <ul hidden$="[[!_schemes.length]]" hidden> + <template is="dom-repeat" items="[[_schemes]]" as="scheme"> + <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]"> + <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap"> + [[scheme]] + </gr-button> + </li> + </template> + </ul> + <span class="closeButtonContainer"> + <gr-button link on-tap="_handleCloseTap">Close</gr-button> + </span> + </header> + <main hidden$="[[!_schemes.length]]" hidden> + <template is="dom-repeat" + items="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" + as="command"> + <div class="command"> + <label>[[command.title]]</label> + <input is="iron-input" + type="text" + bind-value="[[command.command]]" + on-tap="_handleInputTap" + readonly> + </div> + </template> + </main> + <footer> + <div class="patchFiles"> + <label>Patch file</label> + <div> + <a href$="[[_computeDownloadLink(change, patchNum)]]"> + [[_computeDownloadFilename(change, patchNum)]] + </a> + <a href$="[[_computeZipDownloadLink(change, patchNum)]]"> + [[_computeZipDownloadFilename(change, patchNum)]] + </a> + </div> + </div> + <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden> + <label>Archive</label> + <div class="archives"> + <template is="dom-repeat" items="[[config.archives]]" as="format"> + <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]"> + [[format]] + </a> + </template> + </div> + </div> + </footer> + </template> + <script src="gr-download-dialog.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js new file mode 100644 index 0000000..6677d62 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -0,0 +1,136 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-download-dialog', + + /** + * Fired when the user presses the close button. + * + * @event close + */ + + properties: { + change: Object, + patchNum: String, + config: Object, + + _schemes: { + type: Array, + value: function() { return []; }, + computed: '_computeSchemes(change, patchNum)', + observer: '_schemesChanged', + }, + _selectedScheme: String, + }, + + hostAttributes: { + role: 'dialog', + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + _computeDownloadCommands: function(change, patchNum, _selectedScheme) { + var commandObj; + for (var rev in change.revisions) { + if (change.revisions[rev]._number == patchNum) { + commandObj = change.revisions[rev].fetch[_selectedScheme].commands; + break; + } + } + var commands = []; + for (var title in commandObj) { + commands.push({ + title: title, + command: commandObj[title], + }); + } + return commands; + }, + + _computeZipDownloadLink: function(change, patchNum) { + return this._computeDownloadLink(change, patchNum, true); + }, + + _computeZipDownloadFilename: function(change, patchNum) { + return this._computeDownloadFilename(change, patchNum, true); + }, + + _computeDownloadLink: function(change, patchNum, zip) { + return this.changeBaseURL(change._number, patchNum) + '/patch?' + + (zip ? 'zip' : 'download'); + }, + + _computeDownloadFilename: function(change, patchNum, zip) { + var shortRev; + for (var rev in change.revisions) { + if (change.revisions[rev]._number == patchNum) { + shortRev = rev.substr(0, 7); + break; + } + } + return shortRev + '.diff.' + (zip ? 'zip' : 'base64'); + }, + + _computeArchiveDownloadLink: function(change, patchNum, format) { + return this.changeBaseURL(change._number, patchNum) + + '/archive?format=' + format; + }, + + _computeSchemes: function(change, patchNum) { + for (var rev in change.revisions) { + if (change.revisions[rev]._number == patchNum) { + var fetch = change.revisions[rev].fetch; + if (fetch) { + return Object.keys(fetch).sort(); + } + break; + } + } + return []; + }, + + _computeSchemeSelected: function(scheme, selectedScheme) { + return scheme == selectedScheme; + }, + + _handleSchemeTap: function(e) { + e.preventDefault(); + var el = Polymer.dom(e).rootTarget; + // TODO(andybons): Save as default scheme in preferences. + this._selectedScheme = el.getAttribute('data-scheme'); + }, + + _handleInputTap: function(e) { + e.preventDefault(); + Polymer.dom(e).rootTarget.select(); + }, + + _handleCloseTap: function(e) { + e.preventDefault(); + this.fire('close', null, {bubbles: false}); + }, + + _schemesChanged: function(schemes) { + if (schemes.length == 0) { return; } + if (schemes.indexOf(this._selectedScheme) == -1) { + this._selectedScheme = schemes.sort()[0]; + } + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html new file mode 100644 index 0000000..2480c4a --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -0,0 +1,123 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-download-dialog</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-download-dialog.html"> + +<test-fixture id="basic"> + <template> + <gr-download-dialog></gr-download-dialog> + </template> +</test-fixture> + +<script> + suite('gr-download-dialog tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + element.change = { + current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72', + revisions: { + '34685798fe548b6d17d1e8e5edc43a26d055cc72': { + _number: 1, + fetch: { + repo: { + commands: { + repo: 'repo download test-project 5/1' + } + }, + ssh: { + commands: { + 'Checkout': 'git fetch ssh://andybons@localhost:29418/test-project refs/changes/05/5/1 && git checkout FETCH_HEAD', + 'Cherry Pick': 'git fetch ssh://andybons@localhost:29418/test-project refs/changes/05/5/1 && git cherry-pick FETCH_HEAD', + 'Format Patch': 'git fetch ssh://andybons@localhost:29418/test-project refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD', + 'Pull': 'git pull ssh://andybons@localhost:29418/test-project refs/changes/05/5/1' + } + }, + http: { + commands: { + 'Checkout': 'git fetch http://andybons@localhost:8080/a/test-project refs/changes/05/5/1 && git checkout FETCH_HEAD', + 'Cherry Pick': 'git fetch http://andybons@localhost:8080/a/test-project refs/changes/05/5/1 && git cherry-pick FETCH_HEAD', + 'Format Patch': 'git fetch http://andybons@localhost:8080/a/test-project refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD', + 'Pull': 'git pull http://andybons@localhost:8080/a/test-project refs/changes/05/5/1' + } + } + } + } + } + }; + element.patchNum = 1; + element.config = { + schemes: { + 'anonymous http': {}, + http: {}, + repo: {}, + ssh: {}, + }, + archives: ['tgz', 'tar', 'tbz2', 'txz'], + }; + }); + + test('element visibility', function() { + assert.isFalse(element.$$('ul').hasAttribute('hidden')); + assert.isFalse(element.$$('main').hasAttribute('hidden')); + assert.isFalse(element.$$('.archivesContainer').hasAttribute('hidden')); + + element.set('config.archives', []); + assert.isTrue(element.$$('.archivesContainer').hasAttribute('hidden')); + }); + + test('computed fields', function() { + assert.equal(element._computeArchiveDownloadLink( + {_number: 123}, 2, 'tgz'), + '/changes/123/revisions/2/archive?format=tgz'); + }); + + test('close event', function(done) { + element.addEventListener('close', function() { + done(); + }); + MockInteractions.tap(element.$$('.closeButtonContainer gr-button')); + }); + + test('tab selection', function() { + flushAsynchronousOperations(); + var el = element.$$('[data-scheme="http"]').parentElement; + assert.isTrue(el.hasAttribute('selected')); + ['repo', 'ssh'].forEach(function(scheme) { + var el = element.$$('[data-scheme="' + scheme + '"]').parentElement; + assert.isFalse(el.hasAttribute('selected')); + }); + + MockInteractions.tap(element.$$('[data-scheme="ssh"]')); + el = element.$$('[data-scheme="ssh"]').parentElement; + assert.isTrue(el.hasAttribute('selected')); + ['http', 'repo'].forEach(function(scheme) { + var el = element.$$('[data-scheme="' + scheme + '"]').parentElement; + assert.isFalse(el.hasAttribute('selected')); + }); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html new file mode 100644 index 0000000..e010468 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -0,0 +1,159 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../../shared/gr-request/gr-request.html"> + +<dom-module id="gr-file-list"> + <template> + <style> + :host { + display: block; + } + .row { + display: flex; + padding: .1em .25em; + } + .header { + font-weight: bold; + } + .positionIndicator, + .reviewed, + .status { + align-items: center; + display: inline-flex; + } + .reviewed, + .status { + justify-content: center; + width: 1.5em; + } + .positionIndicator { + justify-content: flex-start; + visibility: hidden; + width: 1.25em; + } + .row[selected] { + background-color: #ebf5fb; + } + .row[selected] .positionIndicator { + visibility: visible; + } + .path { + flex: 1; + overflow: hidden; + padding-left: .35em; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + } + .row:not(.header) .path:hover { + text-decoration: underline; + } + .comments, + .stats { + text-align: right; + } + .comments { + min-width: 10em; + } + .stats { + min-width: 7em; + } + .invisible { + visibility: hidden; + } + .row:not(.header) .stats { + font-family: var(--monospace-font-family); + } + .added { + color: #388E3C; + } + .removed { + color: #D32F2F; + } + .reviewed input[type="checkbox"] { + display: inline-block; + } + .drafts { + color: #C62828; + font-weight: bold; + } + @media screen and (max-width: 50em) { + .row[selected] { + background-color: transparent; + } + .positionIndicator, + .stats { + display: none; + } + .reviewed, + .status { + justify-content: flex-start; + } + .comments { + min-width: initial; + } + } + </style> + <gr-ajax id="filesXHR" + url="[[_computeFilesURL(changeNum, patchNum)]]" + on-response="_handleResponse"></gr-ajax> + <gr-ajax id="draftsXHR" + url="[[_computeDraftsURL(changeNum, patchNum)]]" + last-response="{{_drafts}}"></gr-ajax> + <gr-ajax id="reviewedXHR" + url="[[_computeReviewedURL(changeNum, patchNum)]]" + last-response="{{_reviewed}}"></gr-ajax> + </gr-ajax> + + <div class="row header"> + <div class="positionIndicator"></div> + <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div> + <div class="status"></div> + <div class="path">Path</div> + <div class="comments">Comments</div> + <div class="stats">Stats</div> + </div> + <template is="dom-repeat" items="{{files}}" as="file"> + <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]"> + <div class="positionIndicator">▶</div> + <div class="reviewed" hidden$="[[!_loggedIn]]" hidden> + <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]" + data-path$="[[file.__path]]" on-change="_handleReviewedChange"> + </div> + <div class$="[[_computeClass('status', file.__path)]]"> + [[_computeFileStatus(file.status)]] + </div> + <a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]"> + [[_computeFileDisplayName(file.__path)]] + </a> + <div class="comments"> + <span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span> + [[_computeCommentsString(comments, patchNum, file.__path)]] + </div> + <div class$="[[_computeClass('stats', file.__path)]]"> + <span class="added">+[[file.lines_inserted]]</span> + <span class="removed">-[[file.lines_deleted]]</span> + </div> + </div> + </template> + </template> + <script src="gr-file-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js new file mode 100644 index 0000000..9fe5ca1 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -0,0 +1,205 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; + + Polymer({ + is: 'gr-file-list', + + properties: { + patchNum: String, + changeNum: String, + comments: Object, + files: Array, + selectedIndex: { + type: Number, + notify: true, + }, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, + + _loggedIn: { + type: Boolean, + value: false, + }, + _drafts: Object, + _reviewed: { + type: Array, + value: function() { return []; }, + }, + _xhrPromise: Object, // Used for testing. + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.RESTClientBehavior, + ], + + reload: function() { + if (!this.changeNum || !this.patchNum) { + return Promise.resolve(); + } + return Promise.all([ + this.$.filesXHR.generateRequest().completes, + app.accountReady.then(function() { + this._loggedIn = app.loggedIn; + if (!app.loggedIn) { return; } + this.$.draftsXHR.generateRequest(); + this.$.reviewedXHR.generateRequest(); + }.bind(this)), + ]); + }, + + _computeFilesURL: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/files'; + }, + + _computeCommentsString: function(comments, patchNum, path) { + var patchComments = (comments[path] || []).filter(function(c) { + return c.patch_set == patchNum; + }); + var num = patchComments.length; + if (num == 0) { return ''; } + if (num == 1) { return '1 comment'; } + if (num > 1) { return num + ' comments'; } + }, + + _computeReviewedURL: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed'; + }, + + _computeReviewed: function(file, _reviewed) { + return _reviewed.indexOf(file.__path) != -1; + }, + + _handleReviewedChange: function(e) { + var path = Polymer.dom(e).rootTarget.getAttribute('data-path'); + var index = this._reviewed.indexOf(path); + var reviewed = index != -1; + if (reviewed) { + this.splice('_reviewed', index, 1); + } else { + this.push('_reviewed', path); + } + + var method = reviewed ? 'DELETE' : 'PUT'; + var url = this.changeBaseURL(this.changeNum, this.patchNum) + + '/files/' + encodeURIComponent(path) + '/reviewed'; + this._send(method, url).catch(function(err) { + alert('Couldn’t change file review status. Check the console ' + + 'and contact the PolyGerrit team for assistance.'); + throw err; + }.bind(this)); + }, + + _computeDraftsURL: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/drafts'; + }, + + _computeDraftsString: function(drafts, path) { + var num = (drafts[path] || []).length; + if (num == 0) { return ''; } + if (num == 1) { return '1 draft'; } + if (num > 1) { return num + ' drafts'; } + }, + + _handleResponse: function(e, req) { + var result = e.detail.response; + var paths = Object.keys(result).sort(); + var files = []; + for (var i = 0; i < paths.length; i++) { + var info = result[paths[i]]; + info.__path = paths[i]; + info.lines_inserted = info.lines_inserted || 0; + info.lines_deleted = info.lines_deleted || 0; + files.push(info); + } + this.files = files; + }, + + _handleKey: function(e) { + if (this.shouldSupressKeyboardShortcut(e)) { return; } + + switch (e.keyCode) { + case 74: // 'j' + e.preventDefault(); + this.selectedIndex = + Math.min(this.files.length - 1, this.selectedIndex + 1); + break; + case 75: // 'k' + e.preventDefault(); + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + break; + case 219: // '[' + e.preventDefault(); + this._openSelectedFile(this.files.length - 1); + break; + case 221: // ']' + e.preventDefault(); + this._openSelectedFile(0); + break; + case 13: // <enter> + case 79: // 'o' + e.preventDefault(); + this._openSelectedFile(); + break; + } + }, + + _openSelectedFile: function(opt_index) { + if (opt_index != null) { + this.selectedIndex = opt_index; + } + page.show(this._computeDiffURL(this.changeNum, this.patchNum, + this.files[this.selectedIndex].__path)); + }, + + _computeFileSelected: function(index, selectedIndex) { + return index == selectedIndex; + }, + + _computeFileStatus: function(status) { + return status || 'M'; + }, + + _computeDiffURL: function(changeNum, patchNum, path) { + return '/c/' + changeNum + '/' + patchNum + '/' + path; + }, + + _computeFileDisplayName: function(path) { + return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path; + }, + + _computeClass: function(baseClass, path) { + var classes = [baseClass]; + if (path == COMMIT_MESSAGE_PATH) { + classes.push('invisible'); + } + return classes.join(' '); + }, + + _send: function(method, url) { + var xhr = document.createElement('gr-request'); + this._xhrPromise = xhr.send({ + method: method, + url: url, + }); + return this._xhrPromise; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html new file mode 100644 index 0000000..06a01c6 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -0,0 +1,320 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-file-list</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-file-list.html"> + +<test-fixture id="basic"> + <template> + <gr-file-list></gr-file-list> + </template> +</test-fixture> + +<script> + suite('gr-file-list tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + server = sinon.fakeServer.create(); + server.respondWith( + 'GET', + '/changes/42/revisions/1/files', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({ + '/COMMIT_MSG': { + status: 'A', + lines_inserted: 9, + size_delta: 317, + size: 317 + }, + 'myfile.txt': { + lines_inserted: 35, + size_delta: 1146, + size: 1167 + } + }), + ] + ); + server.respondWith( + 'GET', + '/changes/42/revisions/2/files', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({ + '/COMMIT_MSG': { + status: 'A', + lines_inserted: 9, + size_delta: 317, + size: 317 + }, + 'myfile.txt': { + lines_inserted: 35, + size_delta: 1146, + size: 1167 + }, + 'file_added_in_rev2.txt': { + lines_inserted: 98, + size_delta: 234, + size: 136 + } + }), + ] + ); + server.respondWith( + 'GET', + '/changes/42/revisions/1/drafts', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + '{}', + ] + ); + server.respondWith( + 'GET', + '/changes/42/revisions/2/drafts', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + '{}', + ] + ); + server.respondWith( + 'GET', + '/changes/42/revisions/1/files?reviewed', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + '["/COMMIT_MSG"]', + ] + ); + server.respondWith( + 'GET', + '/changes/42/revisions/2/files?reviewed', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + '["/COMMIT_MSG","myfile.txt"]', + ] + ); + server.respondWith( + 'PUT', + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed', + [ + 201, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + '""', + ] + ); + server.respondWith( + 'DELETE', + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed', + [ + 204, + {'Content-Type': 'application/json'}, + '', + ] + ); + + app.loggedIn = true; + }); + + teardown(function() { + server.restore(); + }); + + test('requests', function(done) { + element.changeNum = '42'; + element.patchNum = '1'; + element.reload(); + server.respond(); + + element.async(function() { + var filenames = element.files.map(function(f) { + return f.__path; + }); + assert.deepEqual(filenames, ['/COMMIT_MSG', 'myfile.txt']); + assert.deepEqual(element._reviewed, ['/COMMIT_MSG']); + + element.patchNum = '2'; + element.reload(); + server.respond(); + element.async(function() { + filenames = element.files.map(function(f) { + return f.__path; + }); + assert.deepEqual(filenames, + ['/COMMIT_MSG', 'file_added_in_rev2.txt', 'myfile.txt']); + assert.deepEqual(element._reviewed, ['/COMMIT_MSG', 'myfile.txt']); + done(); + }, 1); + }, 1); + }); + + test('keyboard shortcuts', function(done) { + element.changeNum = '42'; + element.patchNum = '2'; + element.selectedIndex = 0; + element.reload(); + server.respond(); + + element.async(function() { + var elementItems = Polymer.dom(element.root).querySelectorAll( + '.row:not(.header)'); + assert.equal(elementItems.length, 3); + assert.isTrue(elementItems[0].hasAttribute('selected')); + assert.isFalse(elementItems[1].hasAttribute('selected')); + assert.isFalse(elementItems[2].hasAttribute('selected')); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + + var showStub = sinon.stub(page, 'show'); + assert.equal(element.selectedIndex, 2); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'), + 'Should navigate to /c/42/2/myfile.txt'); + + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 79); // 'o' + assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'), + 'Should navigate to /c/42/2/file_added_in_rev2.txt'); + + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + assert.equal(element.selectedIndex, 0); + + showStub.restore(); + done(); + }, 1); + }); + + test('comment filtering', function() { + var comments = { + '/COMMIT_MSG': [ + {patch_set: 1, message: 'Done'}, + {patch_set: 1, message: 'oh hay'}, + {patch_set: 2, message: 'hello'}, + ], + 'myfile.txt': [ + {patch_set: 1, message: 'good news!'}, + {patch_set: 2, message: 'wat!?'}, + {patch_set: 2, message: 'hi'}, + ], + }; + assert.equal( + element._computeCommentsString(comments, '1', '/COMMIT_MSG'), + '2 comments'); + assert.equal( + element._computeCommentsString(comments, '1', 'myfile.txt'), + '1 comment'); + assert.equal( + element._computeCommentsString(comments, '1', + 'file_added_in_rev2.txt'), + ''); + assert.equal( + element._computeCommentsString(comments, '2', '/COMMIT_MSG'), + '1 comment'); + assert.equal( + element._computeCommentsString(comments, '2', 'myfile.txt'), + '2 comments'); + assert.equal( + element._computeCommentsString(comments, '2', + 'file_added_in_rev2.txt'), + ''); + }); + + test('computed properties', function() { + assert.equal(element._computeFileStatus('A'), 'A'); + assert.equal(element._computeFileStatus(undefined), 'M'); + assert.equal(element._computeFileStatus(null), 'M'); + + assert.equal(element._computeFileDisplayName('/foo/bar/baz'), + '/foo/bar/baz'); + assert.equal(element._computeFileDisplayName('/COMMIT_MSG'), + 'Commit message'); + + assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz'); + assert.equal(element._computeClass('clazz', '/COMMIT_MSG'), + 'clazz invisible'); + }); + + test('file review status', function(done) { + element.changeNum = '42'; + element.patchNum = '2'; + element.reload(); + server.respond(); + + element.async(function() { + var fileRows = + Polymer.dom(element.root).querySelectorAll('.row:not(.header)'); + var commitMsg = fileRows[0].querySelector('input[type="checkbox"]'); + var fileAdded = fileRows[1].querySelector('input[type="checkbox"]'); + var myFile = fileRows[2].querySelector('input[type="checkbox"]'); + + assert.isTrue(commitMsg.checked); + assert.isFalse(fileAdded.checked); + assert.isTrue(myFile.checked); + + assert.equal(element._reviewed.length, 2); + + MockInteractions.tap(commitMsg); + server.respond(); + element._xhrPromise.then(function(req) { + assert.equal(element._reviewed.length, 1); + assert.equal(req.status, 204); + assert.equal(req.url, + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed'); + + MockInteractions.tap(commitMsg); + server.respond(); + }).then(function() { + element._xhrPromise.then(function(req) { + assert.equal(element._reviewed.length, 2); + assert.equal(req.status, 201); + assert.equal(req.url, + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed'); + + done(); + }); + }); + }, 1); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html new file mode 100644 index 0000000..5733acd --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -0,0 +1,125 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> +<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> + +<link rel="import" href="../gr-comment-list/gr-comment-list.html"> + +<dom-module id="gr-message"> + <template> + <style> + :host { + border-top: 1px solid #ddd; + display: block; + position: relative; + } + :host(:not([expanded])) { + cursor: pointer; + } + gr-avatar { + position: absolute; + left: var(--default-horizontal-margin); + } + .collapsed .contentContainer { + color: #777; + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; + } + .showAvatar.expanded .contentContainer { + margin-left: calc(var(--default-horizontal-margin) + 2.5em); + padding: 10px 0; + } + .showAvatar.collapsed .contentContainer { + margin-left: calc(var(--default-horizontal-margin) + 1.75em); + padding: 10px 75px 10px 0; + } + .hideAvatar.collapsed .contentContainer, + .hideAvatar.expanded .contentContainer { + margin-left: 0; + padding: 10px 75px 10px 0; + } + .collapsed gr-avatar { + top: 8px; + height: 1.75em; + width: 1.75em; + } + .expanded gr-avatar { + top: 12px; + height: 2.5em; + width: 2.5em; + } + .name { + font-weight: bold; + } + .content { + font-family: var(--monospace-font-family); + } + .collapsed .name, + .collapsed .content, + .collapsed .message { + display: inline; + } + .collapsed gr-comment-list, + .collapsed .replyContainer { + display: none; + } + .collapsed .name { + color: var(--default-text-color); + } + .expanded .name { + cursor: pointer; + } + .date { + color: #666; + position: absolute; + right: var(--default-horizontal-margin); + top: 10px; + } + .replyContainer { + padding: .5em 0 1em; + } + </style> + <div class$="[[_computeClass(expanded, showAvatar)]]"> + <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar> + <div class="contentContainer"> + <div class="name" on-tap="_handleNameTap">[[message.author.name]]</div> + <div class="content"> + <gr-linked-text class="message" + pre="[[expanded]]" + content="[[message.message]]" + disabled="[[!expanded]]" + config="[[projectConfig.commentlinks]]"></gr-linked-text> + <gr-comment-list + comments="[[comments]]" + change-num="[[changeNum]]" + patch-num="[[message._revision_number]]"></gr-comment-list> + </div> + <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap"> + <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter> + </a> + </div> + <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden> + <gr-button small on-tap="_handleReplyTap">Reply</gr-button> + </div> + </div> + </template> + <script src="gr-message.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js new file mode 100644 index 0000000..1ab5e6c --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -0,0 +1,111 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-message', + + /** + * Fired when this message's permalink is tapped. + * + * @event scroll-to + */ + + /** + * Fired when this message's reply link is tapped. + * + * @event reply + */ + + listeners: { + 'tap': '_handleTap', + }, + + properties: { + changeNum: Number, + message: Object, + comments: { + type: Object, + observer: '_commentsChanged', + }, + expanded: { + type: Boolean, + value: true, + reflectToAttribute: true, + }, + showAvatar: { + type: Boolean, + value: false, + }, + showReplyButton: { + type: Boolean, + value: false, + }, + projectConfig: Object, + }, + + ready: function() { + app.configReady.then(function(cfg) { + this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) && + this.message && this.message.author; + }.bind(this)); + }, + + _commentsChanged: function(value) { + this.expanded = Object.keys(value || {}).length > 0; + }, + + _handleTap: function(e) { + if (this.expanded) { return; } + this.expanded = true; + }, + + _handleNameTap: function(e) { + if (!this.expanded) { return; } + e.stopPropagation(); + this.expanded = false; + }, + + _computeClass: function(expanded, showAvatar) { + var classes = []; + classes.push(expanded ? 'expanded' : 'collapsed'); + classes.push(showAvatar ? 'showAvatar' : 'hideAvatar'); + return classes.join(' '); + }, + + _computeMessageHash: function(message) { + return '#message-' + message.id; + }, + + _handleLinkTap: function(e) { + e.preventDefault(); + + this.fire('scroll-to', {message: this.message}, {bubbles: false}); + + var hash = this._computeMessageHash(this.message); + // Don't add the hash to the window history if it's already there. + // Otherwise you mess up expected back button behavior. + if (window.location.hash == hash) { return; } + // Change the URL but don’t trigger a nav event. Otherwise it will + // reload the page. + page.show(window.location.pathname + hash, null, false); + }, + + _handleReplyTap: function(e) { + e.preventDefault(); + this.fire('reply', {message: this.message}); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html new file mode 100644 index 0000000..0f09b70 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -0,0 +1,64 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-message</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-message.html"> + +<test-fixture id="basic"> + <template> + <gr-message></gr-message> + </template> +</test-fixture> + +<script> + suite('gr-message tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('reply event', function(done) { + element.message = { + 'id': '47c43261_55aa2c41', + 'author': { + '_account_id': 1115495, + 'name': 'Andrew Bonventre', + 'email': 'andybons@chromium.org', + }, + 'date': '2016-01-12 20:24:49.448000000', + 'message': 'Uploaded patch set 1.', + '_revision_number': 1 + }; + + element.addEventListener('reply', function(e) { + assert.deepEqual(e.detail.message, element.message); + done(); + }); + MockInteractions.tap(element.$$('.replyContainer gr-button')); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html new file mode 100644 index 0000000..8a66d03 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -0,0 +1,62 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../gr-message/gr-message.html"> + +<dom-module id="gr-messages-list"> + <template> + <style> + :host { + display: block; + } + .header { + display: flex; + justify-content: space-between; + margin-bottom: .35em; + } + .header, + gr-message { + padding: 0 var(--default-horizontal-margin); + } + .highlighted { + animation: 3s fadeOut; + } + @keyframes fadeOut { + 0% { background-color: #fff9c4; } + 100% { background-color: #fff; } + } + </style> + <div class="header"> + <h3>Messages</h3> + <gr-button link on-tap="_handleExpandCollapseTap"> + [[_computeExpandCollapseMessage(_expanded)]] + </gr-button> + </div> + <template is="dom-repeat" items="[[messages]]" as="message"> + <gr-message + change-num="[[changeNum]]" + message="[[message]]" + comments="[[_computeCommentsForMessage(comments, message, index)]]" + project-config="[[projectConfig]]" + show-reply-button="[[showReplyButtons]]" + on-scroll-to="_handleScrollTo" + data-message-id$="[[message.id]]"></gr-message> + </template> + </template> + <script src="gr-messages-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js new file mode 100644 index 0000000..1b9ce14 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -0,0 +1,111 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-messages-list', + + properties: { + changeNum: Number, + messages: { + type: Array, + value: function() { return []; }, + }, + comments: Object, + projectConfig: Object, + topMargin: Number, + showReplyButtons: { + type: Boolean, + value: false, + }, + + _expanded: { + type: Boolean, + value: false, + }, + }, + + scrollToMessage: function(messageID) { + var el = this.$$('[data-message-id="' + messageID + '"]'); + if (!el) { return; } + + el.expanded = true; + var top = el.offsetTop; + for (var offsetParent = el.offsetParent; + offsetParent; + offsetParent = offsetParent.offsetParent) { + top += offsetParent.offsetTop; + } + window.scrollTo(0, top - this.topMargin); + this._highlightEl(el); + }, + + _highlightEl: function(el) { + var highlightedEls = + Polymer.dom(this.root).querySelectorAll('.highlighted'); + for (var i = 0; i < highlightedEls.length; i++) { + highlightedEls[i].classList.remove('highlighted'); + } + function handleAnimationEnd() { + el.removeEventListener('animationend', handleAnimationEnd); + el.classList.remove('highlighted'); + } + el.addEventListener('animationend', handleAnimationEnd); + el.classList.add('highlighted'); + }, + + _handleExpandCollapseTap: function(e) { + e.preventDefault(); + this._expanded = !this._expanded; + var messageEls = Polymer.dom(this.root).querySelectorAll('gr-message'); + for (var i = 0; i < messageEls.length; i++) { + messageEls[i].expanded = this._expanded; + } + }, + + _handleScrollTo: function(e) { + this.scrollToMessage(e.detail.message.id); + }, + + _computeExpandCollapseMessage: function(expanded) { + return expanded ? 'Collapse all' : 'Expand all'; + }, + + _computeCommentsForMessage: function(comments, message, index) { + comments = comments || {}; + var messages = this.messages || []; + var msgComments = {}; + var mDate = util.parseDate(message.date); + var nextMDate; + if (index < messages.length - 1) { + nextMDate = util.parseDate(messages[index + 1].date); + } + for (var file in comments) { + var fileComments = comments[file]; + for (var i = 0; i < fileComments.length; i++) { + var cDate = util.parseDate(fileComments[i].updated); + if (cDate >= mDate) { + if (nextMDate && cDate >= nextMDate) { + continue; + } + msgComments[file] = msgComments[file] || []; + msgComments[file].push(fileComments[i]); + } + } + } + return msgComments; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html new file mode 100644 index 0000000..5a562ba --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -0,0 +1,133 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-messages-list</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-messages-list.html"> + +<test-fixture id="basic"> + <template> + <gr-messages-list></gr-messages-list> + </template> +</test-fixture> + +<script> + suite('gr-messages-list tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + element.messages = [ + { + 'id': '47c43261_55aa2c41', + 'author': { + '_account_id': 1115495, + 'name': 'Andrew Bonventre', + 'email': 'andybons@chromium.org', + }, + 'date': '2016-01-12 20:24:49.448000000', + 'message': 'Uploaded patch set 1.', + '_revision_number': 1 + }, + { + 'id': '47c43261_9593e420', + 'author': { + '_account_id': 1115495, + 'name': 'Andrew Bonventre', + 'email': 'andybons@chromium.org', + }, + 'date': '2016-01-12 20:28:33.038000000', + 'message': 'Patch Set 1:\n\n(1 comment)', + '_revision_number': 1 + }, + { + 'id': '87b2aaf4_f73260c5', + 'author': { + '_account_id': 1143760, + 'name': 'Mark Mentovai', + 'email': 'mark@chromium.org', + }, + 'date': '2016-01-12 21:17:07.554000000', + 'message': 'Patch Set 1:\n\n(3 comments)', + '_revision_number': 1 + } + ]; + flushAsynchronousOperations(); + }); + + test('expand/collapse all', function() { + var allMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message'); + for (var i = 0; i < allMessageEls.length; i++) { + allMessageEls[i].expanded = false; + } + MockInteractions.tap(allMessageEls[1]); + assert.isTrue(allMessageEls[1].expanded); + + MockInteractions.tap(element.$$('.header gr-button')); + allMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message'); + for (var i = 0; i < allMessageEls.length; i++) { + assert.isTrue(allMessageEls[i].expanded); + } + + MockInteractions.tap(element.$$('.header gr-button')); + allMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message'); + for (var i = 0; i < allMessageEls.length; i++) { + assert.isFalse(allMessageEls[i].expanded); + } + }); + + test('scroll to message', function() { + var allMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message'); + for (var i = 0; i < allMessageEls.length; i++) { + allMessageEls[i].expanded = false; + } + + var scrollToStub = sinon.stub(window, 'scrollTo'); + var highlightStub = sinon.stub(element, '_highlightEl'); + + element.scrollToMessage('invalid'); + + for (var i = 0; i < allMessageEls.length; i++) { + assert.isFalse(allMessageEls[i].expanded, + 'expected gr-message ' + i + ' to not be expanded'); + } + + var messageID = '47c43261_9593e420'; + element.scrollToMessage(messageID); + assert.isTrue( + element.$$('[data-message-id="' + messageID + '"]').expanded); + + assert.isTrue(scrollToStub.calledOnce); + assert.isTrue(highlightStub.calledOnce); + + scrollToStub.restore(); + highlightStub.restore(); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html new file mode 100644 index 0000000..e93d008 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -0,0 +1,132 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> + +<dom-module id="gr-related-changes-list"> + <template> + <style> + :host { + display: block; + } + h3 { + margin: .5em 0 0; + } + section { + margin-bottom: 1em; + } + a { + display: block; + } + .relatedChanges a { + display: inline-block; + } + .strikethrough { + color: #666; + text-decoration: line-through; + } + .status { + color: #666; + font-weight: bold; + } + .notCurrent { + color: #e65100; + } + .indirectAncestor { + color: #33691e; + } + .submittable { + color: #1b5e20; + } + .hidden { + display: none; + } + </style> + <gr-ajax id="relatedXHR" + url="[[_computeRelatedURL(change._number, patchNum)]]" + last-response="{{_relatedResponse}}"></gr-ajax> + <gr-ajax id="submittedTogetherXHR" + url="[[_computeSubmittedTogetherURL(change._number)]]" + last-response="{{_submittedTogether}}"></gr-ajax> + <gr-ajax id="conflictsXHR" + url="/changes/" + params="[[_computeConflictsQueryParams(change._number)]]" + last-response="{{_conflicts}}"></gr-ajax> + <gr-ajax id="cherryPicksXHR" + url="/changes/" + params="[[_computeCherryPicksQueryParams(change.project, change.change_id, change._number)]]" + last-response="{{_cherryPicks}}"></gr-ajax> + <gr-ajax id="sameTopicXHR" + url="/changes/" + params="[[_computeSameTopicQueryParams(change.topic)]]" + last-response="{{_sameTopic}}"></gr-ajax> + + <div hidden$="[[!_loading]]">Loading...</div> + <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden> + <h4>Relation Chain</h4> + <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="change"> + <div> + <a href$="[[_computeChangeURL(change._change_number, change._revision_number)]]" + class$="[[_computeLinkClass(change)]]"> + [[change.commit.subject]] + </a> + <span class$="[[_computeChangeStatusClass(change)]]"> + ([[_computeChangeStatus(change)]]) + </span> + </div> + </template> + </section> + <section hidden$="[[!_submittedTogether.length]]" hidden> + <h4>Submitted together</h4> + <template is="dom-repeat" items="[[_submittedTogether]]" as="change"> + <a href$="[[_computeChangeURL(change._number)]]" + class$="[[_computeLinkClass(change)]]"> + [[change.project]]: [[change.branch]]: [[change.subject]] + </a> + </template> + </section> + <section hidden$="[[!_sameTopic.length]]" hidden> + <h4>Same topic</h4> + <template is="dom-repeat" items="[[_sameTopic]]" as="change"> + <a href$="[[_computeChangeURL(change._number)]]" + class$="[[_computeLinkClass(change)]]"> + [[change.project]]: [[change.branch]]: [[change.subject]] + </a> + </template> + </section> + <section hidden$="[[!_conflicts.length]]" hidden> + <h4>Merge conflicts</h4> + <template is="dom-repeat" items="[[_conflicts]]" as="change"> + <a href$="[[_computeChangeURL(change._number)]]" + class$="[[_computeLinkClass(change)]]"> + [[change.subject]] + </a> + </template> + </section> + <section hidden$="[[!_cherryPicks.length]]" hidden> + <h4>Cherry picks</h4> + <template is="dom-repeat" items="[[_cherryPicks]]" as="change"> + <a href$="[[_computeChangeURL(change._number)]]" + class$="[[_computeLinkClass(change)]]"> + [[change.subject]] + </a> + </template> + </section> + </template> + <script src="gr-related-changes-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js new file mode 100644 index 0000000..f3a298e --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -0,0 +1,242 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-related-changes-list', + + properties: { + change: Object, + patchNum: String, + serverConfig: { + type: Object, + observer: '_serverConfigChanged', + }, + hidden: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + + _loading: Boolean, + _resolveServerConfigReady: Function, + _serverConfigReady: { + type: Object, + value: function() { + return new Promise(function(resolve) { + this._resolveServerConfigReady = resolve; + }.bind(this)); + } + }, + _connectedRevisions: { + type: Array, + computed: '_computeConnectedRevisions(change, patchNum, ' + + '_relatedResponse.changes)', + }, + _relatedResponse: Object, + _submittedTogether: Array, + _conflicts: Array, + _cherryPicks: Array, + _sameTopic: Array, + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + observers: [ + '_resultsChanged(_relatedResponse.changes, _submittedTogether, ' + + '_conflicts, _cherryPicks, _sameTopic)', + ], + + reload: function() { + if (!this.change || !this.patchNum) { + return Promise.resolve(); + } + this._loading = true; + var promises = [ + this.$.relatedXHR.generateRequest().completes, + this.$.submittedTogetherXHR.generateRequest().completes, + this.$.conflictsXHR.generateRequest().completes, + this.$.cherryPicksXHR.generateRequest().completes, + ]; + + return this._serverConfigReady.then(function() { + if (this.change.topic && + !this.serverConfig.change.submit_whole_topic) { + return this.$.sameTopicXHR.generateRequest().completes; + } else { + this._sameTopic = []; + } + return Promise.resolve(); + }.bind(this)).then(Promise.all(promises)).then(function() { + this._loading = false; + }.bind(this)); + }, + + _computeRelatedURL: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/related'; + }, + + _computeSubmittedTogetherURL: function(changeNum) { + return this.changeBaseURL(changeNum) + '/submitted_together'; + }, + + _computeConflictsQueryParams: function(changeNum) { + var options = this.listChangesOptionsToHex( + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT + ); + return { + O: options, + q: 'status:open is:mergeable conflicts:' + changeNum, + }; + }, + + _computeCherryPicksQueryParams: function(project, changeID, changeNum) { + var options = this.listChangesOptionsToHex( + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT + ); + var query = [ + 'project:' + project, + 'change:' + changeID, + '-change:' + changeNum, + '-is:abandoned', + ].join(' '); + return { + O: options, + q: query + } + }, + + _computeSameTopicQueryParams: function(topic) { + var options = this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT, + this.ListChangesOption.DETAILED_LABELS + ); + return { + O: options, + q: 'status:open topic:' + topic, + }; + }, + + _computeChangeURL: function(changeNum, patchNum) { + var urlStr = '/c/' + changeNum; + if (patchNum != null) { + urlStr += '/' + patchNum; + } + return urlStr; + }, + + _computeLinkClass: function(change) { + if (change.status == this.ChangeStatus.ABANDONED) { + return 'strikethrough'; + } + }, + + _computeChangeStatusClass: function(change) { + var classes = ['status']; + if (change._revision_number != change._current_revision_number) { + classes.push('notCurrent'); + } else if (this._isIndirectAncestor(change)) { + classes.push('indirectAncestor'); + } else if (change.submittable) { + classes.push('submittable'); + } else if (change.status == this.ChangeStatus.NEW) { + classes.push('hidden'); + } + return classes.join(' '); + }, + + _computeChangeStatus: function(change) { + switch (change.status) { + case this.ChangeStatus.MERGED: + return 'Merged'; + case this.ChangeStatus.ABANDONED: + return 'Abandoned'; + case this.ChangeStatus.DRAFT: + return 'Draft'; + } + if (change._revision_number != change._current_revision_number) { + return 'Not current'; + } else if (this._isIndirectAncestor(change)) { + return 'Indirect ancestor'; + } else if (change.submittable) { + return 'Submittable'; + } + return '' + }, + + _serverConfigChanged: function(config) { + this._resolveServerConfigReady(config); + }, + + _resultsChanged: function(related, submittedTogether, conflicts, + cherryPicks, sameTopic) { + var results = [ + related, + submittedTogether, + conflicts, + cherryPicks, + sameTopic + ]; + for (var i = 0; i < results.length; i++) { + if (results[i].length > 0) { + this.hidden = false; + return; + } + } + this.hidden = true; + }, + + _isIndirectAncestor: function(change) { + return this._connectedRevisions.indexOf(change.commit.commit) == -1; + }, + + _computeConnectedRevisions: function(change, patchNum, relatedChanges) { + var connected = []; + var changeRevision; + for (var rev in change.revisions) { + if (change.revisions[rev]._number == patchNum) { + changeRevision = rev; + } + } + var commits = relatedChanges.map(function(c) { return c.commit; }); + var pos = commits.length - 1; + + while (pos >= 0) { + var commit = commits[pos].commit; + connected.push(commit); + if (commit == changeRevision) { + break; + } + pos--; + } + while (pos >= 0) { + for (var i = 0; i < commits[pos].parents.length; i++) { + if (connected.indexOf(commits[pos].parents[i].commit) != -1) { + connected.push(commits[pos].commit); + break; + } + } + --pos; + } + return connected; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html new file mode 100644 index 0000000..7e0c236 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -0,0 +1,217 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-related-changes-list</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-related-changes-list.html"> + +<test-fixture id="basic"> + <template> + <gr-related-changes-list></gr-related-changes-list> + </template> +</test-fixture> + +<script> + suite('gr-related-changes-list tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('connected revisions', function() { + var change = { + revisions: { + 'e3c6d60783bfdec9ebae7dcfec4662360433449e': { + _number: 1, + }, + '26e5e4c9c7ae31cbd876271cca281ce22b413997': { + _number: 2, + }, + 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': { + _number: 7, + }, + 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': { + _number: 5, + }, + 'd6bcee67570859ccb684873a85cf50b1f0e96fda': { + _number: 6, + }, + 'cc960918a7f90388f4a9e05753d0f7b90ad44546': { + _number: 3, + }, + '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': { + _number: 4, + } + } + }; + var patchNum = 7; + var relatedChanges = [ + { + commit: { + commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', + parents: [ + { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' + } + ], + }, + }, + { + commit: { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + parents: [ + { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' + } + ], + }, + }, + { + commit: { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + parents: [ + { + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' + } + ], + }, + }, + { + commit: { + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + parents: [ + { + commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907' + } + ], + }, + }, + { + commit: { + commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + parents: [ + { + commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce' + } + ], + }, + }, + { + commit: { + commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce', + parents: [ + { + commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75' + } + ], + }, + } + ]; + + var connectedChanges = + element._computeConnectedRevisions(change, patchNum, relatedChanges); + assert.deepEqual(connectedChanges, [ + '613bc4f81741a559c6667ac08d71dcc3348f73ce', + 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', + ]); + + patchNum = 4; + relatedChanges = [ + { + commit: { + commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', + parents: [ + { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' + } + ], + }, + }, + { + commit: { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + parents: [ + { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' + } + ], + }, + }, + { + commit: { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + parents: [ + { + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' + } + ], + }, + }, + { + commit: { + commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b', + parents: [ + { + commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6' + } + ], + }, + }, + { + commit: { + commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + parents: [ + { + commit: 'af815dac54318826b7f1fa468acc76349ffc588e' + } + ], + }, + }, + { + commit: { + commit: 'af815dac54318826b7f1fa468acc76349ffc588e', + parents: [ + { + commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c' + } + ], + }, + } + ]; + + connectedChanges = + element._computeConnectedRevisions(change, patchNum, relatedChanges); + assert.deepEqual(connectedChanges, [ + 'af815dac54318826b7f1fa468acc76349ffc588e', + '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b', + ]); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html new file mode 100644 index 0000000..ab21e6c --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -0,0 +1,148 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-request/gr-request.html"> + +<dom-module id="gr-reply-dialog"> + <style> + :host { + display: block; + max-height: 90vh; + } + :host([disabled]) { + pointer-events: none; + } + :host([disabled]) .container { + opacity: .5; + } + .container { + display: flex; + flex-direction: column; + max-height: 90vh; + } + section { + border-top: 1px solid #ddd; + padding: .5em .75em; + } + .textareaContainer, + .labelsContainer, + .actionsContainer { + flex-shrink: 0; + } + .textareaContainer { + position: relative; + } + iron-autogrow-textarea { + padding: 0; + font-family: var(--monospace-font-family); + } + .message { + border: none; + width: 100%; + } + .labelContainer:not(:first-of-type) { + margin-top: .5em; + } + .labelName { + display: inline-block; + width: 7em; + margin-right: .5em; + white-space: nowrap; + } + iron-selector { + display: inline-flex; + } + iron-selector > gr-button { + margin-right: .25em; + } + iron-selector > gr-button:first-of-type { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + } + iron-selector > gr-button:last-of-type { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + } + iron-selector > gr-button.iron-selected { + background-color: #ddd; + } + .draftsContainer { + overflow-y: auto; + } + .draftsContainer h3 { + margin-top: .25em; + } + .actionsContainer { + display: flex; + justify-content: space-between; + } + .action:link, + .action:visited { + color: #00e; + } + </style> + <template> + <gr-ajax id="draftsXHR" + url="[[_computeDraftsURL(changeNum)]]" + last-response="{{_drafts}}"></gr-ajax> + <div class="container"> + <section class="textareaContainer"> + <iron-autogrow-textarea + id="textarea" + class="message" + placeholder="Say something..." + disabled="{{disabled}}" + rows="4" + max-rows="15" + bind-value="{{draft}}"></iron-autogrow-textarea> + </section> + <section class="labelsContainer"> + <template is="dom-repeat" + items="[[_computeLabelArray(permittedLabels)]]" as="label"> + <div class="labelContainer"> + <span class="labelName">[[label]]</span> + <iron-selector data-label$="[[label]]" + selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]"> + <template is="dom-repeat" + items="[[_computePermittedLabelValues(permittedLabels, label)]]" + as="value"> + <gr-button data-value$="[[value]]">[[value]]</gr-button> + </template> + </iron-selector> + </div> + </template> + </section> + <section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]"> + <h3>[[_computeDraftsTitle(_drafts)]]</h3> + <gr-comment-list + comments="[[_drafts]]" + change-num="[[changeNum]]" + patch-num="[[patchNum]]"></gr-comment-list> + </section> + <section class="actionsContainer"> + <gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button> + <gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button> + </section> + </div> + </template> + <script src="gr-reply-dialog.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js new file mode 100644 index 0000000..3cd6e12 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -0,0 +1,171 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-reply-dialog', + + /** + * Fired when a reply is successfully sent. + * + * @event send + */ + + /** + * Fired when the user presses the cancel button. + * + * @event cancel + */ + + properties: { + changeNum: String, + patchNum: String, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + draft: { + type: String, + value: '', + }, + labels: Object, + permittedLabels: Object, + + _account: Object, + _drafts: Object, + _xhrPromise: Object, // Used for testing. + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + ready: function() { + app.accountReady.then(function() { + this._account = app.account; + }.bind(this)); + }, + + reload: function() { + return this.$.draftsXHR.generateRequest().completes; + }, + + focus: function() { + this.async(function() { + this.$.textarea.textarea.focus(); + }.bind(this)); + }, + + _computeDraftsURL: function(changeNum) { + return '/changes/' + changeNum + '/drafts'; + }, + + _computeHideDraftList: function(drafts) { + return Object.keys(drafts || {}).length == 0; + }, + + _computeDraftsTitle: function(drafts) { + var total = 0; + for (var file in drafts) { + total += drafts[file].length; + } + if (total == 0) { return ''; } + if (total == 1) { return '1 Draft'; } + if (total > 1) { return total + ' Drafts'; } + }, + + _computeLabelArray: function(labelsObj) { + return Object.keys(labelsObj).sort(); + }, + + _computeIndexOfLabelValue: function( + labels, permittedLabels, labelName, account) { + var t = labels[labelName]; + if (!t) { return null; } + var labelValue = t.default_value; + + // Is there an existing vote for the current user? If so, use that. + var votes = labels[labelName]; + if (votes.all && votes.all.length > 0) { + for (var i = 0; i < votes.all.length; i++) { + if (votes.all[i]._account_id == account._account_id) { + labelValue = votes.all[i].value; + break; + } + } + } + + var len = permittedLabels[labelName] != null ? + permittedLabels[labelName].length : 0; + for (var i = 0; i < len; i++) { + var val = parseInt(permittedLabels[labelName][i], 10); + if (val == labelValue) { + return i; + } + } + return null; + }, + + _computePermittedLabelValues: function(permittedLabels, label) { + return permittedLabels[label]; + }, + + _cancelTapHandler: function(e) { + e.preventDefault(); + this._drafts = null; + this.fire('cancel', null, {bubbles: false}); + }, + + _sendTapHandler: function(e) { + e.preventDefault(); + var obj = { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: {}, + }; + for (var label in this.permittedLabels) { + var selectorEl = this.$$('iron-selector[data-label="' + label + '"]'); + var selectedVal = selectorEl.selectedItem.getAttribute('data-value'); + selectedVal = parseInt(selectedVal, 10); + obj.labels[label] = selectedVal; + } + if (this.draft != null) { + obj.message = this.draft; + } + this.disabled = true; + this._send(obj).then(function(req) { + this.fire('send', null, {bubbles: false}); + this.draft = ''; + this.disabled = false; + this._drafts = null; + }.bind(this)).catch(function(err) { + alert('Oops. Something went wrong. Check the console and bug the ' + + 'PolyGerrit team for assistance.'); + throw err; + }.bind(this)); + }, + + _send: function(payload) { + var xhr = document.createElement('gr-request'); + this._xhrPromise = xhr.send({ + method: 'POST', + url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review', + body: payload, + }); + + return this._xhrPromise; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html new file mode 100644 index 0000000..a6f4671 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -0,0 +1,153 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-reply-dialog</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-reply-dialog.html"> + +<test-fixture id="basic"> + <template> + <gr-reply-dialog></gr-reply-dialog> + </template> +</test-fixture> + +<script> + suite('gr-reply-dialog tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + element.changeNum = 42; + element.patchNum = 1; + element.labels = { + Verified: { + values: { + '-1': 'Fails', + ' 0': 'No score', + '+1': 'Verified' + }, + default_value: 0 + }, + 'Code-Review': { + values: { + '-2': 'Do not submit', + '-1': 'I would prefer that you didn\'t submit this', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved' + }, + default_value: 0 + } + }; + element.permittedLabels = { + 'Code-Review': [ + '-1', + ' 0', + '+1' + ], + Verified: [ + '-1', + ' 0', + '+1' + ] + }; + + server = sinon.fakeServer.create(); + server.respondWith( + 'POST', + '/changes/42/revisions/1/review', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + '{' + + '"labels": {' + + '"Code-Review": -1,' + + '"Verified": -1' + + '}' + + '}' + ] + ); + + // Allow the elements created by dom-repeat to be stamped. + flushAsynchronousOperations(); + }); + + teardown(function() { + server.restore(); + }); + + test('cancel event', function(done) { + element.addEventListener('cancel', function() { done(); }); + MockInteractions.tap(element.$$('.cancel')); + }); + + test('label picker', function(done) { + // Async tick is needed because iron-selector content is distributed and + // distributed content requires an observer to be set up. + element.async(function() { + for (var label in element.permittedLabels) { + assert.ok(element.$$('iron-selector[data-label="' + label + '"]'), + label); + } + element.draft = 'I wholeheartedly disapprove'; + MockInteractions.tap(element.$$( + 'iron-selector[data-label="Code-Review"] > ' + + 'gr-button[data-value="-1"]')); + MockInteractions.tap(element.$$( + 'iron-selector[data-label="Verified"] > ' + + 'gr-button[data-value="-1"]')); + + // This is needed on non-Blink engines most likely due to the ways in + // which the dom-repeat elements are stamped. + element.async(function() { + MockInteractions.tap(element.$$('.send')); + assert.isTrue(element.disabled); + + server.respond(); + + element._xhrPromise.then(function(req) { + assert.isFalse(element.disabled, + 'Element should be enabled when done sending reply.'); + assert.equal(req.status, 200); + assert.equal(req.url, '/changes/42/revisions/1/review'); + var reqObj = JSON.parse(req.xhr.requestBody); + assert.deepEqual(reqObj, { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: { + 'Code-Review': -1, + 'Verified': -1 + }, + message: 'I wholeheartedly disapprove' + }); + assert.equal(req.response.labels['Code-Review'], -1); + assert.equal(req.response.labels.Verified, -1); + done(); + }); + }, 1); + }, 1); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html new file mode 100644 index 0000000..d20fd01 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -0,0 +1,118 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-request/gr-request.html"> + +<dom-module id="gr-reviewer-list"> + <style> + :host { + display: block; + } + :host([disabled]) { + opacity: .8; + pointer-events: none; + } + .autocompleteContainer { + position: relative; + } + .inputContainer { + display: flex; + margin-top: .25em; + } + .inputContainer input { + flex: 1; + font: inherit; + } + .dropdown { + background-color: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, .3); + position: absolute; + left: 0; + top: 100%; + } + .dropdown .reviewer { + cursor: pointer; + padding: .5em .75em; + } + .dropdown .reviewer[selected] { + background-color: #ccc; + } + .remove, + .cancel { + color: #999; + } + .remove { + font-size: .9em; + } + .cancel { + font-size: 2em; + line-height: 1; + padding: 0 .15em; + text-decoration: none; + } + </style> + <template> + <gr-ajax id="autocompleteXHR" + url="[[_computeAutocompleteURL(change)]]" + params="[[_computeAutocompleteParams(_inputVal)]]" + on-response="_handleResponse"></gr-ajax> + + <template is="dom-repeat" items="[[_reviewers]]" as="reviewer"> + <div class="reviewer"> + <gr-account-link account="[[reviewer]]" show-email></gr-account-link> + <gr-button link + class="remove" + data-account-id$="[[reviewer._account_id]]" + on-tap="_handleRemoveTap" + hidden$="[[!_computeCanRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom> + </div> + </template> + <div class="controlsContainer" hidden$="[[!mutable]]"> + <div class="autocompleteContainer" hidden$="[[!_showInput]]"> + <div class="inputContainer"> + <input is="iron-input" id="input" + bind-value="{{_inputVal}}" disabled$="[[disabled]]"> + <gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button> + </div> + <div class="dropdown" hidden$="[[_hideAutocomplete]]"> + <template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer"> + <div class="reviewer" + data-index$="[[index]]" + on-mouseenter="_handleMouseEnterItem" + on-tap="_handleItemTap" + selected$="[[_computeSelected(index, _selectedIndex)]]"> + <template is="dom-if" if="[[reviewer.account]]"> + <gr-account-label + account="[[reviewer.account]]" show-email></gr-account-label> + </template> + <template is="dom-if" if="[[reviewer.group]]"> + <span>[[reviewer.group.name]] (group)</span> + </template> + </div> + </template> + </div> + </div> + <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap" + hidden$="[[_showInput]]">Add reviewer</gr-button> + </div> + </template> + <script src="gr-reviewer-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js new file mode 100644 index 0000000..00fc12e --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -0,0 +1,344 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-reviewer-list', + + properties: { + change: Object, + mutable: { + type: Boolean, + value: false, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + suggestFrom: { + type: Number, + value: 3, + }, + + _reviewers: { + type: Array, + value: function() { return []; }, + }, + _autocompleteData: { + type: Array, + value: function() { return []; }, + observer: '_autocompleteDataChanged', + }, + _inputVal: { + type: String, + value: '', + observer: '_inputValChanged', + }, + _inputRequestHandle: Number, + _inputRequestTimeout: { + type: Number, + value: 250, + }, + _showInput: { + type: Boolean, + value: false, + }, + _hideAutocomplete: { + type: Boolean, + value: true, + observer: '_hideAutocompleteChanged', + }, + _selectedIndex: { + type: Number, + value: 0, + }, + _boundBodyClickHandler: { + type: Function, + value: function() { + return this._handleBodyClick.bind(this); + }, + }, + + // Used for testing. + _lastAutocompleteRequest: Object, + _xhrPromise: Object, + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + observers: [ + '_reviewersChanged(change.reviewers.*, change.owner)', + ], + + detached: function() { + this._clearInputRequestHandle(); + }, + + _clearInputRequestHandle: function() { + if (this._inputRequestHandle != null) { + this.cancelAsync(this._inputRequestHandle); + this._inputRequestHandle = null; + } + }, + + _reviewersChanged: function(changeRecord, owner) { + var result = []; + var reviewers = changeRecord.base; + for (var key in reviewers) { + if (key == 'REVIEWER' || key == 'CC') { + result = result.concat(reviewers[key]); + } + } + this._reviewers = result.filter(function(reviewer) { + return reviewer._account_id != owner._account_id; + }); + }, + + _computeCanRemoveReviewer: function(reviewer, mutable) { + if (!mutable) { return false; } + + for (var i = 0; i < this.change.removable_reviewers.length; i++) { + if (this.change.removable_reviewers[i]._account_id == + reviewer._account_id) { + return true; + } + } + return false; + }, + + _computeAutocompleteURL: function(change) { + return '/changes/' + change._number + '/suggest_reviewers'; + }, + + _computeAutocompleteParams: function(inputVal) { + return { + n: 10, // Return max 10 results + q: inputVal, + }; + }, + + _computeSelected: function(index, selectedIndex) { + return index == selectedIndex; + }, + + _handleResponse: function(e) { + this._autocompleteData = e.detail.response.filter(function(reviewer) { + var account = reviewer.account; + if (!account) { return true; } + for (var i = 0; i < this._reviewers.length; i++) { + if (account._account_id == this.change.owner._account_id || + account._account_id == this._reviewers[i]._account_id) { + return false; + } + } + return true; + }, this); + }, + + _handleBodyClick: function(e) { + var eventPath = Polymer.dom(e).path; + for (var i = 0; i < eventPath.length; i++) { + if (eventPath[i] == this) { + return; + } + } + this._selectedIndex = -1; + this._autocompleteData = []; + }, + + _handleRemoveTap: function(e) { + e.preventDefault(); + var target = Polymer.dom(e).rootTarget; + var accountID = parseInt(target.getAttribute('data-account-id'), 10); + this._send('DELETE', this._restEndpoint(accountID)).then(function(req) { + var reviewers = this.change.reviewers; + ['REVIEWER', 'CC'].forEach(function(type) { + reviewers[type] = reviewers[type] || []; + for (var i = 0; i < reviewers[type].length; i++) { + if (reviewers[type][i]._account_id == accountID) { + this.splice('change.reviewers.' + type, i, 1); + break; + } + } + }, this); + }.bind(this)).catch(function(err) { + alert('Oops. Something went wrong. Check the console and bug the ' + + 'PolyGerrit team for assistance.'); + throw err; + }.bind(this)); + }, + + _handleAddTap: function(e) { + e.preventDefault(); + this._showInput = true; + this.$.input.focus(); + }, + + _handleCancelTap: function(e) { + e.preventDefault(); + this._cancel(); + }, + + _handleMouseEnterItem: function(e) { + this._selectedIndex = + parseInt(Polymer.dom(e).rootTarget.getAttribute('data-index'), 10); + }, + + _handleItemTap: function(e) { + var reviewerEl; + var eventPath = Polymer.dom(e).path; + for (var i = 0; i < eventPath.length; i++) { + var el = eventPath[i]; + if (el.classList && el.classList.contains('reviewer')) { + reviewerEl = el; + break; + } + } + this._selectedIndex = + parseInt(reviewerEl.getAttribute('data-index'), 10); + this._sendAddRequest(); + }, + + _autocompleteDataChanged: function(data) { + this._hideAutocomplete = data.length == 0; + }, + + _hideAutocompleteChanged: function(hidden) { + if (hidden) { + document.body.removeEventListener('click', + this._boundBodyClickHandler); + this._selectedIndex = -1; + } else { + document.body.addEventListener('click', this._boundBodyClickHandler); + this._selectedIndex = 0; + } + }, + + _inputValChanged: function(val) { + var sendRequest = function() { + if (this.disabled || val == null || val.trim().length == 0) { + return; + } + if (val.length < this.suggestFrom) { + this._clearInputRequestHandle(); + this._hideAutocomplete = true; + this._selectedIndex = -1; + return; + } + this._lastAutocompleteRequest = + this.$.autocompleteXHR.generateRequest(); + }.bind(this); + + this._clearInputRequestHandle(); + if (this._inputRequestTimeout == 0) { + sendRequest(); + } else { + this._inputRequestHandle = + this.async(sendRequest, this._inputRequestTimeout); + } + }, + + _handleKey: function(e) { + if (this._hideAutocomplete) { + if (e.keyCode == 27) { // 'esc' + e.preventDefault(); + this._cancel(); + } + return; + } + + switch (e.keyCode) { + case 38: // 'up': + e.preventDefault(); + this._selectedIndex = Math.max(this._selectedIndex - 1, 0); + break; + case 40: // 'down' + e.preventDefault(); + this._selectedIndex = Math.min(this._selectedIndex + 1, + this._autocompleteData.length - 1); + break; + case 27: // 'esc' + e.preventDefault(); + this._hideAutocomplete = true; + break; + case 13: // 'enter' + e.preventDefault(); + this._sendAddRequest(); + break; + } + }, + + _cancel: function() { + this._showInput = false; + this._selectedIndex = 0; + this._inputVal = ''; + this._autocompleteData = []; + this.$.addReviewer.focus(); + }, + + _sendAddRequest: function() { + this._clearInputRequestHandle(); + + var reviewerID; + var reviewer = this._autocompleteData[this._selectedIndex]; + if (reviewer.account) { + reviewerID = reviewer.account._account_id; + } else if (reviewer.group) { + reviewerID = reviewer.group.id; + } + this._autocompleteData = []; + this._send('POST', this._restEndpoint(), reviewerID).then(function(req) { + this.change.reviewers.CC = this.change.reviewers.CC || []; + req.response.reviewers.forEach(function(r) { + this.push('change.removable_reviewers', r); + this.push('change.reviewers.CC', r); + }, this); + this._inputVal = ''; + this.$.input.focus(); + }.bind(this)).catch(function(err) { + // TODO(andybons): Use the message returned by the server. + alert('Unable to add ' + reviewerID + ' as a reviewer.'); + throw err; + }.bind(this)); + }, + + _send: function(method, url, reviewerID) { + this.disabled = true; + var request = document.createElement('gr-request'); + var opts = { + method: method, + url: url, + }; + if (reviewerID) { + opts.body = {reviewer: reviewerID}; + } + this._xhrPromise = request.send(opts); + var enableEl = function() { this.disabled = false; }.bind(this); + this._xhrPromise.then(enableEl).catch(enableEl); + return this._xhrPromise; + }, + + _restEndpoint: function(id) { + var path = '/changes/' + this.change._number + '/reviewers'; + if (id) { + path += '/' + id; + } + return path; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html new file mode 100644 index 0000000..898d328 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -0,0 +1,278 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-reviewer-list</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-reviewer-list.html"> + +<test-fixture id="basic"> + <template> + <gr-reviewer-list></gr-reviewer-list> + </template> +</test-fixture> + +<script> + suite('gr-reviewer-list tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + + server = sinon.fakeServer.create(); + server.respondWith( + 'GET', + /\/changes\/42\/suggest_reviewers\?n=10&q=andy(.*)/, + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify([ + { + account: { + _account_id: 1021482, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + } + }, + { + account: { + _account_id: 1021863, + name: 'Andrew Bonventre', + email: 'andybons@google.com', + } + }, + { + group: { + id: 'c7af6dd375c092ff3b23c0937aa910693dc0c41b', + name: 'andy', + } + } + ]), + ] + ); + server.respondWith( + 'POST', + '/changes/42/reviewers', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({ + reviewers: [{ + _account_id: 1021482, + approvals: { + 'Code-Review': ' 0' + }, + email: 'andybons@chromium.org', + name: 'Andrew Bonventre', + }] + }), + ] + ); + server.respondWith( + 'DELETE', + '/changes/42/reviewers/1021482', + [ + 204, + {'Content-Type': 'application/json'}, + ')]}\'\n{}', + ] + ); + }); + + teardown(function() { + server.restore(); + }); + + test('controls hidden on immutable element', function() { + element.mutable = false; + assert.isTrue(element.$$('.controlsContainer').hasAttribute('hidden')); + element.mutable = true; + assert.isFalse(element.$$('.controlsContainer').hasAttribute('hidden')); + }); + + function getActiveElement() { + return document.activeElement.shadowRoot ? + document.activeElement.shadowRoot.activeElement : + document.activeElement; + } + + test('show/hide input', function() { + element.mutable = true; + assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden')); + assert.isTrue( + element.$$('.autocompleteContainer').hasAttribute('hidden')); + assert.notEqual(getActiveElement().id, 'input'); + MockInteractions.tap(element.$$('.addReviewer')); + assert.isTrue(element.$$('.addReviewer').hasAttribute('hidden')); + assert.isFalse( + element.$$('.autocompleteContainer').hasAttribute('hidden')); + assert.equal(getActiveElement().id, 'input'); + MockInteractions.pressAndReleaseKeyOn(element, 27); // 'esc' + assert.isFalse(element.$$('.addReviewer').hasAttribute('hidden')); + assert.isTrue( + element.$$('.autocompleteContainer').hasAttribute('hidden')); + assert.equal(getActiveElement().id, 'addReviewer'); + }); + + test('only show remove for removable reviewers', function() { + element.mutable = true; + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + 'REVIEWER': [ + { + _account_id: 2, + name: 'Bojack Horseman', + email: 'SecretariatRulez96@hotmail.com', + }, + { + _account_id: 3, + name: 'Pinky Penguin', + }, + ], + 'CC': [ + { + _account_id: 4, + name: 'Diane Nguyen', + email: 'macarthurfellow2B@juno.com', + }, + ] + }, + removable_reviewers: [ + { + _account_id: 3, + name: 'Pinky Penguin', + }, + { + _account_id: 4, + name: 'Diane Nguyen', + email: 'macarthurfellow2B@juno.com', + }, + ] + }; + flushAsynchronousOperations(); + var removeEls = + Polymer.dom(element.root).querySelectorAll('.reviewer > .remove'); + assert.equal(removeEls.length, 3); + Array.from(removeEls).forEach(function(el) { + var accountID = parseInt(el.getAttribute('data-account-id'), 10); + assert.ok(accountID); + if (accountID == 2) { + assert.isTrue(el.hasAttribute('hidden')); + } else { + assert.isFalse(el.hasAttribute('hidden')); + } + }); + }); + + test('autocomplete starts at >= 3 chars', function() { + element._inputRequestTimeout = 0; + element._mutable = true; + var genRequestStub = sinon.stub( + element.$.autocompleteXHR, + 'generateRequest', + function() { + assert(false, 'generateRequest should not be called for input ' + + 'lengths of less than 3 chars'); + } + ); + element._inputVal = 'fo'; + flushAsynchronousOperations(); + genRequestStub.restore(); + }); + + test('add/remove reviewer flow', function(done) { + element.change = { + _number: 42, + reviewers: {}, + removable_reviewers: [], + owner: {_account_id: 0}, + }; + element._inputRequestTimeout = 0; + element._mutable = true; + MockInteractions.tap(element.$$('.addReviewer')); + flushAsynchronousOperations(); + element._inputVal = 'andy'; + server.respond(); + + element._lastAutocompleteRequest.completes.then(function() { + flushAsynchronousOperations(); + assert.isFalse(element.$$('.dropdown').hasAttribute('hidden')); + var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer'); + assert.equal(itemEls.length, 3); + assert.isTrue(itemEls[0].hasAttribute('selected')); + assert.isFalse(itemEls[1].hasAttribute('selected')); + + MockInteractions.pressAndReleaseKeyOn(element, 40); // 'down' + assert.isFalse(itemEls[0].hasAttribute('selected')); + assert.isTrue(itemEls[1].hasAttribute('selected')); + + MockInteractions.pressAndReleaseKeyOn(element, 38); // 'up' + assert.isTrue(itemEls[0].hasAttribute('selected')); + assert.isFalse(itemEls[1].hasAttribute('selected')); + + MockInteractions.pressAndReleaseKeyOn(element, 27); // 'esc' + assert.isTrue(element.$$('.dropdown').hasAttribute('hidden')); + + element._inputVal = 'andyb'; + server.respond(); + + element._lastAutocompleteRequest.completes.then(function() { + assert.isFalse(element.$$('.dropdown').hasAttribute('hidden')); + var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer'); + assert.equal(itemEls.length, 3); + assert.isTrue(itemEls[0].hasAttribute('selected')); + assert.isFalse(itemEls[1].hasAttribute('selected')); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert.isTrue(element.disabled); + server.respond(); + + element._xhrPromise.then(function() { + assert.isFalse(element.disabled); + flushAsynchronousOperations(); + var reviewerEls = + Polymer.dom(element.root).querySelectorAll('.reviewer'); + assert.equal(reviewerEls.length, 1); + MockInteractions.tap(element.$$('.reviewer > .remove')); + flushAsynchronousOperations(); + assert.isTrue(element.disabled); + server.respond(); + + element._xhrPromise.then(function() { + flushAsynchronousOperations(); + assert.isFalse(element.disabled); + var reviewerEls = + Polymer.dom(element.root).querySelectorAll('.reviewer'); + assert.equal(reviewerEls.length, 0); + done(); + }); + }); + }); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html new file mode 100644 index 0000000..ce7faae --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -0,0 +1,54 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-diff-comment/gr-diff-comment.html"> + +<dom-module id="gr-diff-comment-thread"> + <template> + <style> + :host { + display: block; + white-space: normal; + } + gr-diff-comment { + border-left: 1px solid #ddd; + } + gr-diff-comment:first-of-type { + border-top: 1px solid #ddd; + } + gr-diff-comment:last-of-type { + border-bottom: 1px solid #ddd; + } + </style> + <div id="container"> + <template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment"> + <gr-diff-comment + comment="{{comment}}" + change-num="[[changeNum]]" + patch-num="[[patchNum]]" + draft="[[comment.__draft]]" + show-actions="[[showActions]]" + project-config="[[projectConfig]]" + on-height-change="_handleCommentHeightChange" + on-reply="_handleCommentReply" + on-discard="_handleCommentDiscard" + on-done="_handleCommentDone"></gr-diff-comment> + </template> + </div> + </template> + <script src="gr-diff-comment-thread.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js new file mode 100644 index 0000000..32c8313 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -0,0 +1,214 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-diff-comment-thread', + + /** + * Fired when the height of the thread changes. + * + * @event height-change + */ + + /** + * Fired when the thread should be discarded. + * + * @event discard + */ + + properties: { + changeNum: String, + comments: { + type: Array, + value: function() { return []; }, + }, + patchNum: String, + path: String, + showActions: Boolean, + projectConfig: Object, + + _boundWindowResizeHandler: { + type: Function, + value: function() { return this._handleWindowResize.bind(this); } + }, + _lastHeight: Number, + _orderedComments: Array, + }, + + get naturalHeight() { + return this.$.container.offsetHeight; + }, + + observers: [ + '_commentsChanged(comments.splices)', + ], + + attached: function() { + window.addEventListener('resize', this._boundWindowResizeHandler); + }, + + detached: function() { + window.removeEventListener('resize', this._boundWindowResizeHandler); + }, + + _handleWindowResize: function(e) { + this._heightChanged(); + }, + + _commentsChanged: function(changeRecord) { + this._orderedComments = this._sortedComments(this.comments); + }, + + _sortedComments: function(comments) { + comments.sort(function(c1, c2) { + var c1Date = c1.__date || util.parseDate(c1.updated); + var c2Date = c2.__date || util.parseDate(c2.updated); + return c1Date - c2Date; + }); + + var commentIDToReplies = {}; + var topLevelComments = []; + for (var i = 0; i < comments.length; i++) { + var c = comments[i]; + if (c.in_reply_to) { + if (commentIDToReplies[c.in_reply_to] == null) { + commentIDToReplies[c.in_reply_to] = []; + } + commentIDToReplies[c.in_reply_to].push(c); + } else { + topLevelComments.push(c); + } + } + var results = []; + for (var i = 0; i < topLevelComments.length; i++) { + this._visitComment(topLevelComments[i], commentIDToReplies, results); + } + return results; + }, + + _visitComment: function(parent, commentIDToReplies, results) { + results.push(parent); + + var replies = commentIDToReplies[parent.id]; + if (!replies) { return; } + for (var i = 0; i < replies.length; i++) { + this._visitComment(replies[i], commentIDToReplies, results); + } + }, + + _handleCommentHeightChange: function(e) { + e.stopPropagation(); + this._heightChanged(); + }, + + _handleCommentReply: function(e) { + var comment = e.detail.comment; + var quoteStr; + if (e.detail.quote) { + var msg = comment.message; + var quoteStr = msg.split('\n').map( + function(line) { return ' > ' + line; }).join('\n') + '\n\n'; + } + var reply = + this._newReply(comment.id, comment.line, this.path, quoteStr); + this.push('comments', reply); + + // Allow the reply to render in the dom-repeat. + this.async(function() { + var commentEl = this._commentElWithDraftID(reply.__draftID); + commentEl.editing = true; + this.async(this._heightChanged.bind(this), 1); + }.bind(this), 1); + }, + + _handleCommentDone: function(e) { + var comment = e.detail.comment; + var reply = this._newReply(comment.id, comment.line, this.path, 'Done'); + this.push('comments', reply); + + // Allow the reply to render in the dom-repeat. + this.async(function() { + var commentEl = this._commentElWithDraftID(reply.__draftID); + commentEl.save(); + this.async(this._heightChanged.bind(this), 1); + }.bind(this), 1); + }, + + _commentElWithDraftID: function(draftID) { + var commentEls = + Polymer.dom(this.root).querySelectorAll('gr-diff-comment'); + for (var i = 0; i < commentEls.length; i++) { + if (commentEls[i].comment.__draftID == draftID) { + return commentEls[i]; + } + } + return null; + }, + + _newReply: function(inReplyTo, line, path, opt_message) { + var c = { + __draft: true, + __draftID: Math.random().toString(36), + __date: new Date(), + line: line, + path: path, + in_reply_to: inReplyTo, + }; + if (opt_message != null) { + c.message = opt_message; + } + return c; + }, + + _handleCommentDiscard: function(e) { + // TODO(andybons): In Shadow DOM, the event bubbles up, while in Shady + // DOM, it respects the bubbles property. + // https://github.com/Polymer/polymer/issues/3226 + e.stopPropagation(); + var diffCommentEl = Polymer.dom(e).rootTarget; + var idx = this._indexOf(diffCommentEl.comment, this.comments); + if (idx == -1) { + throw Error('Cannot find comment ' + + JSON.stringify(diffCommentEl.comment)); + } + this.splice('comments', idx, 1); + if (this.comments.length == 0) { + this.fire('discard', null, {bubbles: false}); + return; + } + this.async(this._heightChanged.bind(this), 1); + }, + + _heightChanged: function() { + var height = this.$.container.offsetHeight; + if (height == this._lastHeight) { return; } + + this.fire('height-change', {height: height}, {bubbles: false}); + this._lastHeight = height; + }, + + _indexOf: function(comment, arr) { + for (var i = 0; i < arr.length; i++) { + var c = arr[i]; + if ((c.__draftID != null && c.__draftID == comment.__draftID) || + (c.id != null && c.id == comment.id)) { + return i; + } + } + return -1; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html new file mode 100644 index 0000000..52ad066 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -0,0 +1,243 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-diff-comment-thread</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-diff-comment-thread.html"> + +<test-fixture id="basic"> + <template> + <gr-diff-comment-thread></gr-diff-comment-thread> + </template> +</test-fixture> + +<test-fixture id="withComment"> + <template> + <gr-diff-comment-thread></gr-diff-comment-thread> + </template> +</test-fixture> + +<script> + suite('gr-diff-comment-thread tests', function() { + var element; + setup(function() { + element = fixture('basic'); + }); + + test('comments are sorted correctly', function() { + var comments = [ + { + id: 'jacks_reply', + message: 'i like you, too', + in_reply_to: 'sallys_confession', + updated: '2015-12-25 15:00:20.396000000', + }, + { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:20.396000000', + }, + { + id: 'sally_to_dr_finklestein', + message: 'i’m running away', + updated: '2015-10-31 09:00:20.396000000', + }, + { + id: 'sallys_defiance', + in_reply_to: 'sally_to_dr_finklestein', + message: 'i will poison you so i can get away', + updated: '2015-10-31 15:00:20.396000000', + }, + { + id: 'dr_finklesteins_response', + in_reply_to: 'sally_to_dr_finklestein', + message: 'no i will pull a thread and your arm will fall off', + updated: '2015-10-31 11:00:20.396000000' + }, + { + id: 'sallys_mission', + message: 'i have to find santa', + updated: '2015-12-24 21:00:20.396000000' + } + ]; + var results = element._sortedComments(comments); + assert.deepEqual(results, [ + { + id: 'sally_to_dr_finklestein', + message: 'i’m running away', + updated: '2015-10-31 09:00:20.396000000', + }, + { + id: 'dr_finklesteins_response', + in_reply_to: 'sally_to_dr_finklestein', + message: 'no i will pull a thread and your arm will fall off', + updated: '2015-10-31 11:00:20.396000000' + }, + { + id: 'sallys_defiance', + in_reply_to: 'sally_to_dr_finklestein', + message: 'i will poison you so i can get away', + updated: '2015-10-31 15:00:20.396000000', + }, + { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:20.396000000', + }, + { + id: 'jacks_reply', + message: 'i like you, too', + in_reply_to: 'sallys_confession', + updated: '2015-12-25 15:00:20.396000000', + }, + { + id: 'sallys_mission', + message: 'i have to find santa', + updated: '2015-12-24 21:00:20.396000000' + } + ]); + }); + }); + + suite('comment action tests', function() { + var element; + var server; + + setup(function() { + element = fixture('withComment'); + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:33.843000000', + }]; + flushAsynchronousOperations(); + + server = sinon.fakeServer.create(); + // Eat any requests made by elements in this suite. + server.respondWith( + 'PUT', + '/changes/41/1/drafts', + [ + 201, + {'Content-Type': 'application/json'}, + ')]}\'\n' + JSON.stringify({ + id: '7afa4931_de3d65bd', + path: '/path/to/file.txt', + line: 5, + in_reply_to: 'baf0414d_60047215', + updated: '2015-12-21 02:01:10.850000000', + message: 'Done' + }), + ] + ); + + server.respondWith( + 'DELETE', + '/changes/41/1/drafts/baf0414d_60047215', + [ + 204, + {}, + '', + ] + ); + }); + + test('reply', function(done) { + var commentEl = element.$$('gr-diff-comment'); + assert.ok(commentEl); + commentEl.addEventListener('reply', function() { + var drafts = element._orderedComments.filter(function(c) { + return c.__draft == true; + }); + assert.equal(drafts.length, 1); + assert.notOk(drafts[0].message, 'message should be empty'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + done(); + }); + commentEl.fire('reply', {comment: commentEl.comment}, {bubbles: false}); + }); + + test('quote reply', function(done) { + var commentEl = element.$$('gr-diff-comment'); + assert.ok(commentEl); + commentEl.addEventListener('reply', function() { + var drafts = element._orderedComments.filter(function(c) { + return c.__draft == true; + }); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, ' > is this a crossover episode!?\n\n'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + done(); + }); + commentEl.fire('reply', {comment: commentEl.comment, quote: true}, + {bubbles: false}); + }); + + test('done', function(done) { + element.changeNum = '42'; + element.patchNum = '1'; + var commentEl = element.$$('gr-diff-comment'); + assert.ok(commentEl); + commentEl.addEventListener('done', function() { + server.respond(); + var drafts = element._orderedComments.filter(function(c) { + return c.__draft == true; + }); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, 'Done'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + done(); + }); + commentEl.fire('done', {comment: commentEl.comment}, {bubbles: false}); + }); + + test('discard', function(done) { + element.changeNum = '42'; + element.patchNum = '1'; + element.push('comments', element._newReply( + element.comments[0].id, + element.comments[0].line, + element.comments[0].path, + 'it’s pronouced jiff, not giff')); + flushAsynchronousOperations(); + + var draftEl = + Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1]; + assert.ok(draftEl); + draftEl.addEventListener('discard', function() { + server.respond(); + var drafts = element.comments.filter(function(c) { + return c.__draft == true; + }); + assert.equal(drafts.length, 0); + done(); + }); + draftEl.fire('discard', null, {bubbles: false}); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html new file mode 100644 index 0000000..ca6815b --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -0,0 +1,153 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> +<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> +<link rel="import" href="../../shared/gr-request/gr-request.html"> + +<dom-module id="gr-diff-comment"> + <template> + <style> + :host { + background-color: #ffd; + display: block; + --iron-autogrow-textarea: { + padding: 2px; + }; + } + :host([disabled]) { + pointer-events: none; + } + :host([disabled]) .container { + opacity: .5; + } + .header, + .message, + .actions { + padding: .5em .7em; + } + .header { + display: flex; + padding-bottom: 0; + font-family: 'Open Sans', sans-serif; + } + .headerLeft { + flex: 1; + } + .authorName, + .draftLabel { + font-weight: bold; + } + .draftLabel { + color: #999; + display: none; + } + .date { + justify-content: flex-end; + margin-left: 5px; + } + a.date:link, + a.date:visited { + color: #666; + } + .actions { + display: flex; + padding-top: 0; + } + .action { + margin-right: .5em; + } + .danger { + display: flex; + flex: 1; + justify-content: flex-end; + } + .editMessage { + display: none; + margin: .5em .7em; + width: calc(100% - 1.4em - 2px); + } + .danger .action { + margin-right: 0; + } + .container:not(.draft) .actions :not(.reply):not(.quote):not(.done) { + display: none; + } + .draft .reply, + .draft .quote, + .draft .done { + display: none; + } + .draft .draftLabel { + display: inline; + } + .draft:not(.editing) .save, + .draft:not(.editing) .cancel { + display: none; + } + .editing .message, + .editing .reply, + .editing .quote, + .editing .done, + .editing .edit { + display: none; + } + .editing .editMessage { + background-color: #fff; + display: block; + } + </style> + <div class="container" id="container"> + <div class="header" id="header"> + <div class="headerLeft"> + <span class="authorName">[[comment.author.name]]</span> + <span class="draftLabel">DRAFT</span> + </div> + <a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap"> + <gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter> + </a> + </div> + <iron-autogrow-textarea + id="editTextarea" + class="editMessage" + disabled="{{disabled}}" + rows="4" + bind-value="{{_editDraft}}" + on-keyup="_handleTextareaKeyup" + on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea> + <gr-linked-text class="message" + pre + content="[[comment.message]]" + config="[[projectConfig.commentlinks]]"></gr-linked-text> + <div class="actions" hidden$="[[!showActions]]"> + <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button> + <gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button> + <gr-button class="action done" on-tap="_handleDone">Done</gr-button> + <gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button> + <gr-button class="action save" on-tap="_handleSave" + disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button> + <gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button> + <div class="danger"> + <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button> + </div> + </div> + </div> + </template> + <script src="gr-diff-comment.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js new file mode 100644 index 0000000..ca0bedb --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -0,0 +1,247 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-diff-comment', + + /** + * Fired when the height of the comment changes. + * + * @event height-change + */ + + /** + * Fired when the Reply action is triggered. + * + * @event reply + */ + + /** + * Fired when the Done action is triggered. + * + * @event done + */ + + /** + * Fired when this comment is discarded. + * + * @event discard + */ + + properties: { + changeNum: String, + comment: { + type: Object, + notify: true, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + draft: { + type: Boolean, + value: false, + observer: '_draftChanged', + }, + editing: { + type: Boolean, + value: false, + observer: '_editingChanged', + }, + patchNum: String, + showActions: Boolean, + projectConfig: Object, + + _xhrPromise: Object, // Used for testing. + _editDraft: String, + }, + + ready: function() { + this._editDraft = (this.comment && this.comment.message) || ''; + this.editing = this._editDraft.length == 0; + }, + + attached: function() { + this._heightChanged(); + }, + + save: function() { + this.comment.message = this._editDraft; + this.disabled = true; + var endpoint = this._restEndpoint(this.comment.id); + this._send('PUT', endpoint).then(function(req) { + this.disabled = false; + var comment = req.response; + comment.__draft = true; + // Maintain the ephemeral draft ID for identification by other + // elements. + if (this.comment.__draftID) { + comment.__draftID = this.comment.__draftID; + } + this.comment = comment; + this.editing = false; + }.bind(this)).catch(function(err) { + alert('Your draft couldn’t be saved. Check the console and contact ' + + 'the PolyGerrit team for assistance.'); + this.disabled = false; + }.bind(this)); + }, + + _heightChanged: function() { + this.async(function() { + this.fire('height-change', {height: this.offsetHeight}, + {bubbles: false}); + }.bind(this)); + }, + + _draftChanged: function(draft) { + this.$.container.classList.toggle('draft', draft); + }, + + _editingChanged: function(editing) { + this.$.container.classList.toggle('editing', editing); + if (editing) { + var textarea = this.$.editTextarea.textarea; + // Put the cursor at the end always. + textarea.selectionStart = textarea.value.length; + textarea.selectionEnd = textarea.selectionStart; + this.async(function() { + textarea.focus(); + }.bind(this)); + } + if (this.comment && this.comment.id) { + this.$$('.cancel').hidden = !editing; + } + this._heightChanged(); + }, + + _computeLinkToComment: function(comment) { + return '#' + comment.line; + }, + + _computeSaveDisabled: function(draft) { + return draft == null || draft.trim() == ''; + }, + + _handleTextareaKeyup: function(e) { + // TODO(andybons): This isn't always true, but I can't currently think + // of a better metric. + this._heightChanged(); + }, + + _handleTextareaKeydown: function(e) { + if (e.keyCode == 27) { // 'esc' + this._handleCancel(e); + } + }, + + _handleLinkTap: function(e) { + e.preventDefault(); + var hash = this._computeLinkToComment(this.comment); + // Don't add the hash to the window history if it's already there. + // Otherwise you mess up expected back button behavior. + if (window.location.hash == hash) { return; } + // Change the URL but don’t trigger a nav event. Otherwise it will + // reload the page. + page.show(window.location.pathname + hash, null, false); + }, + + _handleReply: function(e) { + this._preventDefaultAndBlur(e); + this.fire('reply', {comment: this.comment}, {bubbles: false}); + }, + + _handleQuote: function(e) { + this._preventDefaultAndBlur(e); + this.fire('reply', {comment: this.comment, quote: true}, + {bubbles: false}); + }, + + _handleDone: function(e) { + this._preventDefaultAndBlur(e); + this.fire('done', {comment: this.comment}, {bubbles: false}); + }, + + _handleEdit: function(e) { + this._preventDefaultAndBlur(e); + this._editDraft = this.comment.message; + this.editing = true; + }, + + _handleSave: function(e) { + this._preventDefaultAndBlur(e); + this.save(); + }, + + _handleCancel: function(e) { + this._preventDefaultAndBlur(e); + if (this.comment.message == null || this.comment.message.length == 0) { + this.fire('discard', null, {bubbles: false}); + return; + } + this._editDraft = this.comment.message; + this.editing = false; + }, + + _handleDiscard: function(e) { + this._preventDefaultAndBlur(e); + if (!this.comment.__draft) { + throw Error('Cannot discard a non-draft comment.'); + } + this.disabled = true; + var commentID = this.comment.id; + if (!commentID) { + this.fire('discard', null, {bubbles: false}); + return; + } + this._send('DELETE', this._restEndpoint(commentID)).then(function(req) { + this.fire('discard', null, {bubbles: false}); + }.bind(this)).catch(function(err) { + alert('Your draft couldn’t be deleted. Check the console and ' + + 'contact the PolyGerrit team for assistance.'); + this.disabled = false; + }.bind(this)); + }, + + _preventDefaultAndBlur: function(e) { + e.preventDefault(); + Polymer.dom(e).rootTarget.blur(); + }, + + _send: function(method, url) { + var xhr = document.createElement('gr-request'); + var opts = { + method: method, + url: url, + }; + if (method == 'PUT' || method == 'POST') { + opts.body = this.comment; + } + this._xhrPromise = xhr.send(opts); + return this._xhrPromise; + }, + + _restEndpoint: function(id) { + var path = '/changes/' + this.changeNum + '/revisions/' + + this.patchNum + '/drafts'; + if (id) { + path += '/' + id; + } + return path; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html new file mode 100644 index 0000000..799dbf2 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -0,0 +1,269 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-diff-comment</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-diff-comment.html"> + +<test-fixture id="basic"> + <template> + <gr-diff-comment></gr-diff-comment> + </template> +</test-fixture> + +<test-fixture id="draft"> + <template> + <gr-diff-comment draft="true"></gr-diff-comment> + </template> +</test-fixture> + +<script> + suite('gr-diff-comment tests', function() { + var element; + setup(function() { + element = fixture('basic'); + element.comment = { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:33.843000000', + }; + }); + + test('proper event fires on reply', function(done) { + element.addEventListener('reply', function(e) { + assert.ok(e.detail.comment); + done(); + }); + MockInteractions.tap(element.$$('.reply')); + }); + + test('proper event fires on quote', function(done) { + element.addEventListener('reply', function(e) { + assert.ok(e.detail.comment); + assert.isTrue(e.detail.quote); + done(); + }); + MockInteractions.tap(element.$$('.quote')); + }); + + test('proper event fires on done', function(done) { + element.addEventListener('done', function(e) { + done(); + }); + MockInteractions.tap(element.$$('.done')); + }); + + test('clicking on date link does not trigger nav', function() { + var showStub = sinon.stub(page, 'show'); + var dateEl = element.$$('.date'); + assert.ok(dateEl); + MockInteractions.tap(dateEl); + var dest = window.location.pathname + '#5'; + assert(showStub.lastCall.calledWithExactly(dest, null, false), + 'Should navigate to ' + dest + ' without triggering nav'); + showStub.restore(); + }); + }); + + suite('gr-diff-comment draft tests', function() { + var element; + var server; + + setup(function() { + element = fixture('draft'); + element.changeNum = 42; + element.patchNum = 1; + element.editing = false; + element.comment = { + __draft: true, + __draftID: 'temp_draft_id', + path: '/path/to/file', + line: 5, + }; + + server = sinon.fakeServer.create(); + server.respondWith( + 'PUT', + '/changes/42/revisions/1/drafts', + [ + 201, + {'Content-Type': 'application/json'}, + ')]}\'\n{' + + '"id": "baf0414d_40572e03",' + + '"path": "/path/to/file",' + + '"line": 5,' + + '"updated": "2015-12-08 21:52:36.177000000",' + + '"message": "created!"' + + '}' + ] + ); + + server.respondWith( + 'PUT', + /\/changes\/42\/revisions\/1\/drafts\/.+/, + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n{' + + '"id": "baf0414d_40572e03",' + + '"path": "/path/to/file",' + + '"line": 5,' + + '"updated": "2015-12-08 21:52:36.177000000",' + + '"message": "saved!"' + + '}' + ] + ); + }); + + teardown(function() { + server.restore(); + }); + + function isVisible(el) { + assert.ok(el); + return getComputedStyle(el).getPropertyValue('display') != 'none'; + } + + test('button visibility states', function() { + element.showActions = false; + assert.isTrue(element.$$('.actions').hasAttribute('hidden')); + element.showActions = true; + assert.isFalse(element.$$('.actions').hasAttribute('hidden')); + + element.draft = true; + assert.isTrue(isVisible(element.$$('.edit')), 'edit is visible'); + assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible'); + assert.isFalse(isVisible(element.$$('.save')), 'save is not visible'); + assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible'); + assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible'); + assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible'); + assert.isFalse(isVisible(element.$$('.done')), 'done is not visible'); + + element.editing = true; + assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible'); + assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible'); + assert.isTrue(isVisible(element.$$('.save')), 'save is visible'); + assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is visible'); + assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible'); + assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible'); + assert.isFalse(isVisible(element.$$('.done')), 'done is not visible'); + + element.draft = false; + element.editing = false; + assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible'); + assert.isFalse(isVisible(element.$$('.discard')), + 'discard is not visible'); + assert.isFalse(isVisible(element.$$('.save')), 'save is not visible'); + assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible'); + assert.isTrue(isVisible(element.$$('.reply')), 'reply is visible'); + assert.isTrue(isVisible(element.$$('.quote')), 'quote is visible'); + assert.isTrue(isVisible(element.$$('.done')), 'done is visible'); + + element.comment.id = 'foo'; + element.draft = true; + element.editing = true; + assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible'); + }); + + test('draft creation/cancelation', function(done) { + assert.isFalse(element.editing); + MockInteractions.tap(element.$$('.edit')); + assert.isTrue(element.editing); + + element._editDraft = ''; + // Save should be disabled on an empty message. + var disabled = element.$$('.save').hasAttribute('disabled'); + assert.isTrue(disabled, 'save button should be disabled.'); + element._editDraft = ' '; + disabled = element.$$('.save').hasAttribute('disabled'); + assert.isTrue(disabled, 'save button should be disabled.'); + + var numDiscardEvents = 0; + element.addEventListener('discard', function(e) { + numDiscardEvents++; + if (numDiscardEvents == 3) { + done(); + } + }); + MockInteractions.tap(element.$$('.cancel')); + MockInteractions.tap(element.$$('.discard')); + MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc + }); + + test('draft saving/editing', function(done) { + element.draft = true; + MockInteractions.tap(element.$$('.edit')); + element._editDraft = 'good news, everyone!'; + MockInteractions.tap(element.$$('.save')); + assert.isTrue(element.disabled, + 'Element should be disabled when creating draft.'); + + server.respond(); + + element._xhrPromise.then(function(req) { + assert.isFalse(element.disabled, + 'Element should be enabled when done creating draft.'); + assert.equal(req.status, 201); + assert.equal(req.url, '/changes/42/revisions/1/drafts'); + assert.equal(req.response.message, 'created!'); + assert.isFalse(element.editing); + }).then(function() { + MockInteractions.tap(element.$$('.edit')); + element._editDraft = 'You’ll be delivering a package to Chapek 9, a ' + + 'world where humans are killed on sight.'; + MockInteractions.tap(element.$$('.save')); + assert.isTrue(element.disabled, + 'Element should be disabled when updating draft.'); + server.respond(); + + element._xhrPromise.then(function(req) { + assert.isFalse(element.disabled, + 'Element should be enabled when done updating draft.'); + assert.equal(req.status, 200); + assert.equal(req.url, + '/changes/42/revisions/1/drafts/baf0414d_40572e03'); + assert.equal(req.response.message, 'saved!'); + assert.isFalse(element.editing); + done(); + }); + }); + }); + + test('clicking on date link does not trigger nav', function() { + var showStub = sinon.stub(page, 'show'); + var dateEl = element.$$('.date'); + assert.ok(dateEl); + MockInteractions.tap(dateEl); + var dest = window.location.pathname + '#5'; + assert(showStub.lastCall.calledWithExactly(dest, null, false), + 'Should navigate to ' + dest + ' without triggering nav'); + showStub.restore(); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html similarity index 66% rename from polygerrit-ui/app/elements/gr-diff-preferences.html rename to polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html index e320620..b945a45 100644 --- a/polygerrit-ui/app/elements/gr-diff-preferences.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -14,9 +14,9 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-input/iron-input.html"> -<link rel="import" href="gr-button.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> <dom-module id="gr-diff-preferences"> <template> @@ -106,65 +106,5 @@ <gr-button on-tap="_handleCancel">Cancel</gr-button> </div> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-diff-preferences', - - /** - * Fired when the user presses the save button. - * - * @event save - */ - - /** - * Fired when the user presses the cancel button. - * - * @event cancel - */ - - properties: { - prefs: { - type: Object, - notify: true, - value: function() { return {}; }, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - }, - - observers: [ - '_prefsChanged(prefs.*)', - ], - - _prefsChanged: function(changeRecord) { - var prefs = changeRecord.base; - this.$.contextSelect.value = prefs.context; - this.$.showTabsInput.checked = prefs.show_tabs; - }, - - _handleContextSelectChange: function(e) { - var selectEl = Polymer.dom(e).rootTarget; - this.set('prefs.context', parseInt(selectEl.value, 10)); - }, - - _handleShowTabsTap: function(e) { - this.set('prefs.show_tabs', Polymer.dom(e).rootTarget.checked); - }, - - _handleSave: function() { - this.fire('save', null, {bubbles: false}); - }, - - _handleCancel: function() { - this.fire('cancel', null, {bubbles: false}); - }, - }); - })(); - </script> + <script src="gr-diff-preferences.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js new file mode 100644 index 0000000..70d176e --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -0,0 +1,72 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-diff-preferences', + + /** + * Fired when the user presses the save button. + * + * @event save + */ + + /** + * Fired when the user presses the cancel button. + * + * @event cancel + */ + + properties: { + prefs: { + type: Object, + notify: true, + value: function() { return {}; }, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + }, + + observers: [ + '_prefsChanged(prefs.*)', + ], + + _prefsChanged: function(changeRecord) { + var prefs = changeRecord.base; + this.$.contextSelect.value = prefs.context; + this.$.showTabsInput.checked = prefs.show_tabs; + }, + + _handleContextSelectChange: function(e) { + var selectEl = Polymer.dom(e).rootTarget; + this.set('prefs.context', parseInt(selectEl.value, 10)); + }, + + _handleShowTabsTap: function(e) { + this.set('prefs.show_tabs', Polymer.dom(e).rootTarget.checked); + }, + + _handleSave: function() { + this.fire('save', null, {bubbles: false}); + }, + + _handleCancel: function() { + this.fire('cancel', null, {bubbles: false}); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html new file mode 100644 index 0000000..2d86a05 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -0,0 +1,75 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-diff-preferences</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-diff-preferences.html"> + +<test-fixture id="basic"> + <template> + <gr-diff-preferences></gr-diff-preferences> + </template> +</test-fixture> + +<script> + suite('gr-diff-preferences tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('model changes', function() { + element.prefs = { + context: 10, + line_length: 100, + show_tabs: true, + tab_size: 8, + }; + + element.$.contextSelect.value = '50'; + element.fire('change', {}, {node: element.$.contextSelect}); + element.$.columnsInput.bindValue = 80; + element.$.tabSizeInput.bindValue = 4; + MockInteractions.tap(element.$.showTabsInput); + + assert.equal(element.prefs.context, 50); + assert.equal(element.prefs.line_length, 80); + assert.equal(element.prefs.tab_size, 4); + assert.isFalse(element.prefs.show_tabs); + }); + + test('events', function(done) { + var savePromise = new Promise(function(resolve) { + element.addEventListener('save', function() { resolve(); }); + }); + var cancelPromise = new Promise(function(resolve) { + element.addEventListener('cancel', function() { resolve(); }); + }); + Promise.all([savePromise, cancelPromise]).then(function() { + done(); + }); + MockInteractions.tap(element.$$('gr-button[primary]')); + MockInteractions.tap(element.$$('gr-button:not([primary])')); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html new file mode 100644 index 0000000..972dc2d --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
@@ -0,0 +1,97 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html"> + +<dom-module id="gr-diff-side"> + <template> + <style> + :host, + .container { + display: flex; + flex: 0 0 auto; + } + .lineNum:before, + .code:before { + /* To ensure the height is non-zero in these elements, a + zero-width space is set as its content. The character + itself doesn't matter. Just that there is something + there. */ + content: '\200B'; + } + .lineNum { + background-color: #eee; + color: #666; + padding: 0 .75em; + text-align: right; + } + .canComment .lineNum { + cursor: pointer; + text-decoration: underline; + } + .canComment .lineNum:hover { + background-color: #ccc; + } + .lightHighlight { + background-color: var(--light-highlight-color); + } + hl, + .darkHighlight { + background-color: var(--dark-highlight-color); + } + .br:after { + /* Line feed */ + content: '\A'; + } + .tab { + display: inline-block; + } + .tab.withIndicator:before { + color: #C62828; + /* >> character */ + content: '\00BB'; + } + .numbers, + .content { + white-space: pre; + } + .numbers .filler { + background-color: #eee; + } + .contextControl { + background-color: #fef; + } + .contextControl a:link, + .contextControl a:visited { + display: block; + text-decoration: none; + } + .numbers .contextControl { + padding: 0 .75em; + text-align: right; + } + .content .contextControl { + text-align: center; + } + </style> + <div class$="[[_computeContainerClass(canComment)]]"> + <div class="numbers" id="numbers"></div> + <div class="content" id="content"></div> + </div> + </template> + <script src="gr-diff-side.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js new file mode 100644 index 0000000..518da3e --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
@@ -0,0 +1,613 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + var CharCode = { + LESS_THAN: '<'.charCodeAt(0), + GREATER_THAN: '>'.charCodeAt(0), + AMPERSAND: '&'.charCodeAt(0), + SEMICOLON: ';'.charCodeAt(0), + }; + + var TAB_REGEX = /\t/g; + + Polymer({ + is: 'gr-diff-side', + + /** + * Fired when an expand context control is clicked. + * + * @event expand-context + */ + + /** + * Fired when a thread's height is changed. + * + * @event thread-height-change + */ + + /** + * Fired when a draft should be added. + * + * @event add-draft + */ + + /** + * Fired when a thread is removed. + * + * @event remove-thread + */ + + properties: { + canComment: { + type: Boolean, + value: false, + }, + content: { + type: Array, + notify: true, + observer: '_contentChanged', + }, + prefs: { + type: Object, + value: function() { return {}; }, + }, + changeNum: String, + patchNum: String, + path: String, + projectConfig: { + type: Object, + observer: '_projectConfigChanged', + }, + + _lineFeedHTML: { + type: String, + value: '<span class="style-scope gr-diff-side br"></span>', + readOnly: true, + }, + _highlightStartTag: { + type: String, + value: '<hl class="style-scope gr-diff-side">', + readOnly: true, + }, + _highlightEndTag: { + type: String, + value: '</hl>', + readOnly: true, + }, + _diffChunkLineNums: { + type: Array, + value: function() { return []; }, + }, + _commentThreadLineNums: { + type: Array, + value: function() { return []; }, + }, + _focusedLineNum: { + type: Number, + value: 1, + }, + }, + + listeners: { + 'tap': '_tapHandler', + }, + + observers: [ + '_prefsChanged(prefs.*)', + ], + + rowInserted: function(index) { + this.renderLineIndexRange(index, index); + this._updateDOMIndices(); + this._updateJumpIndices(); + }, + + rowRemoved: function(index) { + var removedEls = Polymer.dom(this.root).querySelectorAll( + '[data-index="' + index + '"]'); + for (var i = 0; i < removedEls.length; i++) { + removedEls[i].parentNode.removeChild(removedEls[i]); + } + this._updateDOMIndices(); + this._updateJumpIndices(); + }, + + rowUpdated: function(index) { + var removedEls = Polymer.dom(this.root).querySelectorAll( + '[data-index="' + index + '"]'); + for (var i = 0; i < removedEls.length; i++) { + removedEls[i].parentNode.removeChild(removedEls[i]); + } + this.renderLineIndexRange(index, index); + }, + + scrollToLine: function(lineNum) { + if (isNaN(lineNum) || lineNum < 1) { return; } + + var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]'); + if (!el) { return; } + + // Calculate where the line is relative to the window. + var top = el.offsetTop; + for (var offsetParent = el.offsetParent; + offsetParent; + offsetParent = offsetParent.offsetParent) { + top += offsetParent.offsetTop; + } + + // Scroll the element to the middle of the window. Dividing by a third + // instead of half the inner height feels a bit better otherwise the + // element appears to be below the center of the window even when it + // isn't. + window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight); + }, + + scrollToNextDiffChunk: function() { + this._scrollToNextChunkOrThread(this._diffChunkLineNums); + }, + + scrollToPreviousDiffChunk: function() { + this._scrollToPreviousChunkOrThread(this._diffChunkLineNums); + }, + + scrollToNextCommentThread: function() { + this._scrollToNextChunkOrThread(this._commentThreadLineNums); + }, + + scrollToPreviousCommentThread: function() { + this._scrollToPreviousChunkOrThread(this._commentThreadLineNums); + }, + + renderLineIndexRange: function(startIndex, endIndex) { + this._render(this.content, startIndex, endIndex); + }, + + hideElementsWithIndex: function(index) { + var els = Polymer.dom(this.root).querySelectorAll( + '[data-index="' + index + '"]'); + for (var i = 0; i < els.length; i++) { + els[i].setAttribute('hidden', true); + } + }, + + getRowHeight: function(index) { + var row = this.content[index]; + // Filler elements should not be taken into account when determining + // height calculations. + if (row.type == 'FILLER') { + return 0; + } + if (row.height != null) { + return row.height; + } + + var selector = '[data-index="' + index + '"]'; + var els = Polymer.dom(this.root).querySelectorAll(selector); + if (els.length != 2) { + throw Error('Rows should only consist of two elements'); + } + return Math.max(els[0].offsetHeight, els[1].offsetHeight); + }, + + getRowNaturalHeight: function(index) { + var contentEl = this.$$('.content [data-index="' + index + '"]'); + return contentEl.naturalHeight || contentEl.offsetHeight; + }, + + setRowNaturalHeight: function(index) { + var lineEl = this.$$('.numbers [data-index="' + index + '"]'); + var contentEl = this.$$('.content [data-index="' + index + '"]'); + contentEl.style.height = null; + var height = contentEl.offsetHeight; + lineEl.style.height = height + 'px'; + this.content[index].height = height; + return height; + }, + + setRowHeight: function(index, height) { + var selector = '[data-index="' + index + '"]'; + var els = Polymer.dom(this.root).querySelectorAll(selector); + for (var i = 0; i < els.length; i++) { + els[i].style.height = height + 'px'; + } + this.content[index].height = height; + }, + + _scrollToNextChunkOrThread: function(lineNums) { + for (var i = 0; i < lineNums.length; i++) { + if (lineNums[i] > this._focusedLineNum) { + this._focusedLineNum = lineNums[i]; + this.scrollToLine(this._focusedLineNum); + return; + } + } + }, + + _scrollToPreviousChunkOrThread: function(lineNums) { + for (var i = lineNums.length - 1; i >= 0; i--) { + if (this._focusedLineNum > lineNums[i]) { + this._focusedLineNum = lineNums[i]; + this.scrollToLine(this._focusedLineNum); + return; + } + } + }, + + _updateJumpIndices: function() { + this._commentThreadLineNums = []; + this._diffChunkLineNums = []; + var inHighlight = false; + for (var i = 0; i < this.content.length; i++) { + switch (this.content[i].type) { + case 'COMMENT_THREAD': + this._commentThreadLineNums.push( + this.content[i].comments[0].line); + break; + case 'CODE': + // Only grab the first line of the highlighted chunk. + if (!inHighlight && this.content[i].highlight) { + this._diffChunkLineNums.push(this.content[i].lineNum); + inHighlight = true; + } else if (!this.content[i].highlight) { + inHighlight = false; + } + break; + } + } + }, + + _updateDOMIndices: function() { + // There is no way to select elements with a data-index greater than a + // given value. For now, just update all DOM elements. + var lineEls = Polymer.dom(this.root).querySelectorAll( + '.numbers [data-index]'); + var contentEls = Polymer.dom(this.root).querySelectorAll( + '.content [data-index]'); + if (lineEls.length != contentEls.length) { + throw Error( + 'There must be the same number of line and content elements'); + } + var index = 0; + for (var i = 0; i < this.content.length; i++) { + if (this.content[i].hidden) { continue; } + + lineEls[index].setAttribute('data-index', i); + contentEls[index].setAttribute('data-index', i); + index++; + } + }, + + _prefsChanged: function(changeRecord) { + var prefs = changeRecord.base; + this.$.content.style.width = prefs.line_length + 'ch'; + }, + + _projectConfigChanged: function(projectConfig) { + var threadEls = + Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread'); + for (var i = 0; i < threadEls.length; i++) { + threadEls[i].projectConfig = projectConfig; + } + }, + + _contentChanged: function(diff) { + this._clearChildren(this.$.numbers); + this._clearChildren(this.$.content); + this._render(diff, 0, diff.length - 1); + this._updateJumpIndices(); + }, + + _computeContainerClass: function(canComment) { + return 'container' + (canComment ? ' canComment' : ''); + }, + + _tapHandler: function(e) { + var lineEl = Polymer.dom(e).rootTarget; + if (!this.canComment || !lineEl.classList.contains('lineNum')) { + return; + } + + e.preventDefault(); + var index = parseInt(lineEl.getAttribute('data-index'), 10); + var line = parseInt(lineEl.getAttribute('data-line-num'), 10); + this.fire('add-draft', { + index: index, + line: line + }, {bubbles: false}); + }, + + _clearChildren: function(el) { + while (el.firstChild) { + el.removeChild(el.firstChild); + } + }, + + _handleContextControlClick: function(context, e) { + e.preventDefault(); + this.fire('expand-context', {context: context}, {bubbles: false}); + }, + + _render: function(diff, startIndex, endIndex) { + var beforeLineEl; + var beforeContentEl; + if (endIndex != diff.length - 1) { + beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]'); + beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]'); + if (!beforeLineEl && !beforeContentEl) { + // `endIndex` may be present within the model, but not in the DOM. + // Insert it before its successive element. + beforeLineEl = this.$$( + '.numbers [data-index="' + (endIndex + 1) + '"]'); + beforeContentEl = this.$$( + '.content [data-index="' + (endIndex + 1) + '"]'); + } + } + + for (var i = startIndex; i <= endIndex; i++) { + if (diff[i].hidden) { continue; } + + switch (diff[i].type) { + case 'CODE': + this._renderCode(diff[i], i, beforeLineEl, beforeContentEl); + break; + case 'FILLER': + this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl); + break; + case 'CONTEXT_CONTROL': + this._renderContextControl(diff[i], i, beforeLineEl, + beforeContentEl); + break; + case 'COMMENT_THREAD': + this._renderCommentThread(diff[i], i, beforeLineEl, + beforeContentEl); + break; + } + } + }, + + _handleCommentThreadHeightChange: function(e) { + var threadEl = Polymer.dom(e).rootTarget; + var index = parseInt(threadEl.getAttribute('data-index'), 10); + this.content[index].height = e.detail.height; + var lineEl = this.$$('.numbers [data-index="' + index + '"]'); + lineEl.style.height = e.detail.height + 'px'; + this.fire('thread-height-change', { + index: index, + height: e.detail.height, + }, {bubbles: false}); + }, + + _handleCommentThreadDiscard: function(e) { + var threadEl = Polymer.dom(e).rootTarget; + var index = parseInt(threadEl.getAttribute('data-index'), 10); + this.fire('remove-thread', {index: index}, {bubbles: false}); + }, + + _renderCommentThread: function(thread, index, beforeLineEl, + beforeContentEl) { + var lineEl = this._createElement('div', 'commentThread'); + lineEl.classList.add('filler'); + lineEl.setAttribute('data-index', index); + var threadEl = document.createElement('gr-diff-comment-thread'); + threadEl.addEventListener('height-change', + this._handleCommentThreadHeightChange.bind(this)); + threadEl.addEventListener('discard', + this._handleCommentThreadDiscard.bind(this)); + threadEl.setAttribute('data-index', index); + threadEl.changeNum = this.changeNum; + threadEl.patchNum = thread.patchNum || this.patchNum; + threadEl.path = this.path; + threadEl.comments = thread.comments; + threadEl.showActions = this.canComment; + threadEl.projectConfig = this.projectConfig; + + this.$.numbers.insertBefore(lineEl, beforeLineEl); + this.$.content.insertBefore(threadEl, beforeContentEl); + }, + + _renderContextControl: function(control, index, beforeLineEl, + beforeContentEl) { + var lineEl = this._createElement('div', 'contextControl'); + lineEl.setAttribute('data-index', index); + lineEl.textContent = '@@'; + var contentEl = this._createElement('div', 'contextControl'); + contentEl.setAttribute('data-index', index); + var a = this._createElement('a'); + a.href = '#'; + a.textContent = 'Show ' + control.numLines + ' common ' + + (control.numLines == 1 ? 'line' : 'lines') + '...'; + a.addEventListener('click', + this._handleContextControlClick.bind(this, control)); + contentEl.appendChild(a); + + this.$.numbers.insertBefore(lineEl, beforeLineEl); + this.$.content.insertBefore(contentEl, beforeContentEl); + }, + + _renderFiller: function(filler, index, beforeLineEl, beforeContentEl) { + var lineFillerEl = this._createElement('div', 'filler'); + lineFillerEl.setAttribute('data-index', index); + var fillerEl = this._createElement('div', 'filler'); + fillerEl.setAttribute('data-index', index); + var numLines = filler.numLines || 1; + + lineFillerEl.textContent = '\n'.repeat(numLines); + for (var i = 0; i < numLines; i++) { + var newlineEl = this._createElement('span', 'br'); + fillerEl.appendChild(newlineEl); + } + + this.$.numbers.insertBefore(lineFillerEl, beforeLineEl); + this.$.content.insertBefore(fillerEl, beforeContentEl); + }, + + _renderCode: function(code, index, beforeLineEl, beforeContentEl) { + var lineNumEl = this._createElement('div', 'lineNum'); + lineNumEl.setAttribute('data-line-num', code.lineNum); + lineNumEl.setAttribute('data-index', index); + var numLines = code.numLines || 1; + lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines); + + var contentEl = this._createElement('div', 'code'); + contentEl.setAttribute('data-line-num', code.lineNum); + contentEl.setAttribute('data-index', index); + + if (code.highlight) { + contentEl.classList.add(code.intraline.length > 0 ? + 'lightHighlight' : 'darkHighlight'); + } + + var html = util.escapeHTML(code.content); + if (code.highlight && code.intraline.length > 0) { + html = this._addIntralineHighlights(code.content, html, + code.intraline); + } + if (numLines > 1) { + html = this._addNewLines(code.content, html, numLines); + } + html = this._addTabWrappers(code.content, html); + + // If the html is equivalent to the text then it didn't get highlighted + // or escaped. Use textContent which is faster than innerHTML. + if (code.content == html) { + contentEl.textContent = code.content; + } else { + contentEl.innerHTML = html; + } + + this.$.numbers.insertBefore(lineNumEl, beforeLineEl); + this.$.content.insertBefore(contentEl, beforeContentEl); + }, + + // Advance `index` by the appropriate number of characters that would + // represent one source code character and return that index. For + // example, for source code '<span>' the escaped html string is + // '<span>'. Advancing from index 0 on the prior html string would + // return 4, since < maps to one source code character ('<'). + _advanceChar: function(html, index) { + // Any tags don't count as characters + while (index < html.length && + html.charCodeAt(index) == CharCode.LESS_THAN) { + while (index < html.length && + html.charCodeAt(index) != CharCode.GREATER_THAN) { + index++; + } + index++; // skip the ">" itself + } + // An HTML entity (e.g., <) counts as one character. + if (index < html.length && + html.charCodeAt(index) == CharCode.AMPERSAND) { + while (index < html.length && + html.charCodeAt(index) != CharCode.SEMICOLON) { + index++; + } + } + return index + 1; + }, + + _addIntralineHighlights: function(content, html, highlights) { + var startTag = this._highlightStartTag; + var endTag = this._highlightEndTag; + + for (var i = 0; i < highlights.length; i++) { + var hl = highlights[i]; + + var htmlStartIndex = 0; + for (var j = 0; j < hl.startIndex; j++) { + htmlStartIndex = this._advanceChar(html, htmlStartIndex); + } + + var htmlEndIndex = 0; + if (hl.endIndex != null) { + for (var j = 0; j < hl.endIndex; j++) { + htmlEndIndex = this._advanceChar(html, htmlEndIndex); + } + } else { + // If endIndex isn't present, continue to the end of the line. + htmlEndIndex = html.length; + } + // The start and end indices could be the same if a highlight is meant + // to start at the end of a line and continue onto the next one. + // Ignore it. + if (htmlStartIndex != htmlEndIndex) { + html = html.slice(0, htmlStartIndex) + startTag + + html.slice(htmlStartIndex, htmlEndIndex) + endTag + + html.slice(htmlEndIndex); + } + } + return html; + }, + + _addNewLines: function(content, html, numLines) { + var htmlIndex = 0; + var indices = []; + var numChars = 0; + for (var i = 0; i < content.length; i++) { + if (numChars > 0 && numChars % this.prefs.line_length == 0) { + indices.push(htmlIndex); + } + htmlIndex = this._advanceChar(html, htmlIndex); + if (content[i] == '\t') { + numChars += this.prefs.tab_size; + } else { + numChars++; + } + } + var result = html; + var linesLeft = numLines; + // Since the result string is being altered in place, start from the end + // of the string so that the insertion indices are not affected as the + // result string changes. + for (var i = indices.length - 1; i >= 0; i--) { + result = result.slice(0, indices[i]) + this._lineFeedHTML + + result.slice(indices[i]); + linesLeft--; + } + // numLines is the total number of lines this code block should take up. + // Fill in the remaining ones. + for (var i = 0; i < linesLeft; i++) { + result += this._lineFeedHTML; + } + return result; + }, + + _addTabWrappers: function(content, html) { + // TODO(andybons): CSS tab-size is not supported in IE. + // Force this to be a number to prevent arbitrary injection. + var tabSize = +this.prefs.tab_size; + var htmlStr = '<span class="style-scope gr-diff-side tab ' + + (this.prefs.show_tabs ? 'withIndicator" ' : '" ') + + 'style="tab-size:' + tabSize + ';' + + '-moz-tab-size:' + tabSize + ';">\t</span>'; + return html.replace(TAB_REGEX, htmlStr); + }, + + _createElement: function(tagName, className) { + var el = document.createElement(tagName); + // When Shady DOM is being used, these classes are added to account for + // Polymer's polyfill behavior. In order to guarantee sufficient + // specificity within the CSS rules, these are added to every element. + // Since the Polymer DOM utility functions (which would do this + // automatically) are not being used for performance reasons, this is + // done manually. + el.classList.add('style-scope', 'gr-diff-side'); + if (!!className) { + el.classList.add(className); + } + return el; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html new file mode 100644 index 0000000..85a1011 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
@@ -0,0 +1,300 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-diff-side</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-diff-side.html"> + +<test-fixture id="basic"> + <template> + <gr-diff-side></gr-diff-side> + </template> +</test-fixture> + +<script> + suite('gr-diff-side tests', function() { + var element; + + function isVisibleInWindow(el) { + var rect = el.getBoundingClientRect(); + return rect.top >= 0 && rect.left >= 0 && + rect.bottom <= window.innerHeight && rect.right <= window.innerWidth; + } + + setup(function() { + element = fixture('basic'); + }); + + test('comments', function() { + assert.isFalse(element.$$('.container').classList.contains('canComment')); + element.canComment = true; + assert.isTrue(element.$$('.container').classList.contains('canComment')); + // TODO(andybons): Check for comment creation events firing/not firing + // when implemented. + }); + + test('scroll to line', function() { + var content = []; + for (var i = 0; i < 300; i++) { + content.push({ + type: 'CODE', + content: 'All work and no play makes Jack a dull boy', + numLines: 1, + lineNum: i + 1, + highlight: false, + intraline: [], + }); + } + element.content = content; + + window.scrollTo(0, 0); + element.scrollToLine(-12849); + assert.equal(window.scrollY, 0); + element.scrollToLine('sup'); + assert.equal(window.scrollY, 0); + var lineEl = element.$$('.numbers .lineNum[data-line-num="150"]'); + assert.ok(lineEl); + element.scrollToLine(150); + assert.isAbove(window.scrollY, 0); + assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible'); + }); + + test('intraline highlights', function() { + var content = ' <gr-linked-text content="' + + '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>'; + var html = util.escapeHTML(content); + var highlights = [ + {startIndex: 0, endIndex: 33}, + {startIndex: 75}, + ]; + assert.equal( + content.slice(highlights[0].startIndex, highlights[0].endIndex), + ' <gr-linked-text content="'); + assert.equal(content.slice(highlights[1].startIndex), + '"></gr-linked-text>'); + var result = element._addIntralineHighlights(content, html, highlights); + var expected = element._highlightStartTag + + ' <gr-linked-text content="' + + element._highlightEndTag + + '[[_computeCurrentRevisionMessage(change)]]' + + element._highlightStartTag + + '"></gr-linked-text>' + + element._highlightEndTag; + assert.equal(result, expected); + }); + + test('newlines', function() { + element.prefs = { + line_length: 80, + tab_size: 4, + }; + + element.content = [{ + type: 'CODE', + content: 'A'.repeat(50), + numLines: 1, + lineNum: 1, + highlight: false, + intraline: [], + }]; + + var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]'); + assert.ok(lineEl); + var contentEl = element.$$('.content .code[data-line-num="1"]'); + assert.ok(contentEl); + assert.equal(contentEl.innerHTML, 'A'.repeat(50)); + + element.content = [{ + type: 'CODE', + content: 'A'.repeat(100), + numLines: 2, + lineNum: 1, + highlight: false, + intraline: [], + }]; + + lineEl = element.$$('.numbers .lineNum[data-line-num="1"]'); + assert.ok(lineEl); + contentEl = element.$$('.content .code[data-line-num="1"]'); + assert.ok(contentEl); + assert.equal(contentEl.innerHTML, + 'A'.repeat(80) + element._lineFeedHTML + + 'A'.repeat(20) + element._lineFeedHTML); + }); + + test('tabs', function(done) { + element.prefs = { + line_length: 80, + tab_size: 4, + show_tabs: true, + }; + + element.content = [{ + type: 'CODE', + content: 'A'.repeat(50) + '\t' + 'A'.repeat(50), + numLines: 2, + lineNum: 1, + highlight: false, + intraline: [], + }]; + + var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]'); + assert.ok(lineEl); + var contentEl = element.$$('.content .code[data-line-num="1"]'); + assert.ok(contentEl); + var spanEl = contentEl.childNodes[1]; + assert.equal(spanEl.tagName, 'SPAN'); + assert.isTrue(spanEl.classList.contains( + 'style-scope', 'gr-diff-side', 'tab', 'withIndicator')); + + element.prefs.show_tabs = false; + element.content = [{ + type: 'CODE', + content: 'A'.repeat(50) + '\t' + 'A'.repeat(50), + numLines: 2, + lineNum: 1, + highlight: false, + intraline: [], + }]; + contentEl = element.$$('.content .code[data-line-num="1"]'); + assert.ok(contentEl); + spanEl = contentEl.childNodes[1]; + assert.equal(spanEl.tagName, 'SPAN'); + assert.isTrue(spanEl.classList.contains( + 'style-scope', 'gr-diff-side', 'tab')); + + var alertStub = sinon.stub(window, 'alert'); + element.prefs.tab_size = + '"><img src="/" onerror="alert(1);"><span class="'; + element.content = [{ + type: 'CODE', + content: '\t', + numLines: 1, + lineNum: 1, + highlight: false, + intraline: [], + }]; + element.async(function() { + assert.isFalse(alertStub.called); + alertStub.restore(); + done(); + }, 100); // Allow some time for the img error event to fire. + }); + + test('diff context', function() { + var content = [ + {type: 'CODE', hidden: true, content: '<!DOCTYPE html>'}, + {type: 'CODE', hidden: true, content: '<meta charset="utf-8">'}, + {type: 'CODE', hidden: true, content: '<title>My great page</title>'}, + {type: 'CODE', hidden: true, content: '<style>'}, + {type: 'CODE', hidden: true, content: ' *,'}, + {type: 'CODE', hidden: true, content: ' *:before,'}, + {type: 'CODE', hidden: true, content: ' *:after {'}, + {type: 'CODE', hidden: true, content: ' box-sizing: border-box;'}, + {type: 'CONTEXT_CONTROL', numLines: 8, start: 0, end: 8}, + {type: 'CODE', hidden: false, content: ' }'}, + ]; + element.content = content; + + // Only the context elements and the following code line elements should + // be present in the DOM. + var contextEls = + Polymer.dom(element.root).querySelectorAll('.contextControl'); + assert.equal(contextEls.length, 2); + var codeLineEls = + Polymer.dom(element.root).querySelectorAll('.lineNum, .code'); + assert.equal(codeLineEls.length, 2); + + for (var i = 0; i <= 8; i++) { + element.content[i].hidden = false; + } + element.renderLineIndexRange(0, 8); + element.hideElementsWithIndex(8); + + contextEls = + Polymer.dom(element.root).querySelectorAll('.contextControl'); + for (var i = 0; i < contextEls.length; i++) { + assert.isTrue(contextEls[i].hasAttribute('hidden')); + } + + codeLineEls = + Polymer.dom(element.root).querySelectorAll('.lineNum, .code'); + + // Nine lines should now be present in the DOM. + assert.equal(codeLineEls.length, 9 * 2); + }); + + test('tap line to add a draft', function() { + var numAddDraftEvents = 0; + sinon.stub(element, 'fire', function(eventName) { + if (eventName == 'add-draft') { + numAddDraftEvents++; + } + }); + element.content = [{type: 'CODE', content: '<!DOCTYPE html>'}]; + element.canComment = false; + flushAsynchronousOperations(); + + var lineEl = element.$$('.lineNum'); + assert.ok(lineEl); + MockInteractions.tap(lineEl); + assert.equal(numAddDraftEvents, 0); + + element.canComment = true; + MockInteractions.tap(lineEl); + assert.equal(numAddDraftEvents, 1); + }); + + test('jump to diff chunk/thread', function() { + element.content = [ + {type: 'CODE', content: '', intraline: [], lineNum: 1, highlight: true}, + {type: 'CODE', content: '', intraline: [], lineNum: 2, highlight: true}, + {type: 'CODE', content: '', intraline: [], lineNum: 3 }, + {type: 'CODE', content: '', intraline: [], lineNum: 4 }, + {type: 'COMMENT_THREAD', comments: [ { line: 4 }]}, + {type: 'CODE', content: '', intraline: [], lineNum: 5 }, + {type: 'CODE', content: '', intraline: [], lineNum: 6, highlight: true}, + {type: 'CODE', content: '', intraline: [], lineNum: 7, highlight: true}, + {type: 'CODE', content: '', intraline: [], lineNum: 8 }, + {type: 'COMMENT_THREAD', comments: [ { line: 8 }]}, + {type: 'CODE', content: '', intraline: [], lineNum: 9 }, + {type: 'CODE', content: '', intraline: [], lineNum: 10, + highlight: true}, + ]; + + var scrollToLineStub = sinon.stub(element, 'scrollToLine'); + element.scrollToNextDiffChunk(); + assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6)); + element.scrollToPreviousDiffChunk(); + assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(1)); + element.scrollToNextCommentThread(); + assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(4)); + element.scrollToNextCommentThread(); + assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(8)); + element.scrollToPreviousDiffChunk(); + assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6)); + + scrollToLineStub.restore(); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html new file mode 100644 index 0000000..0dc18ae --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -0,0 +1,174 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-request/gr-request.html"> +<link rel="import" href="../gr-diff/gr-diff.html"> + +<dom-module id="gr-diff-view"> + <template> + <style> + :host { + background-color: var(--view-background-color); + display: block; + } + h3 { + margin-top: 1em; + padding: .75em var(--default-horizontal-margin); + } + .reviewed { + display: inline-block; + margin: 0 .25em; + vertical-align: .15em; + } + .jumpToFileContainer { + display: inline-block; + } + .mobileJumpToFileContainer { + display: none; + } + .downArrow { + display: inline-block; + font-size: .6em; + vertical-align: middle; + } + .dropdown-trigger { + color: #00e; + cursor: pointer; + padding: 0; + } + .dropdown-content { + background-color: #fff; + box-shadow: 0 1px 5px rgba(0, 0, 0, .3); + } + .dropdown-content a { + cursor: pointer; + display: block; + font-weight: normal; + padding: .3em .5em; + } + .dropdown-content a:before { + color: #ccc; + content: attr(data-key-nav); + display: inline-block; + margin-right: .5em; + width: .3em; + } + .dropdown-content a:hover { + background-color: #00e; + color: #fff; + } + .dropdown-content a[selected] { + color: #000; + font-weight: bold; + pointer-events: none; + text-decoration: none; + } + .dropdown-content a[selected]:hover { + background-color: #fff; + color: #000; + } + gr-button { + font: inherit; + padding: .3em 0; + text-decoration: none; + } + @media screen and (max-width: 50em) { + .dash { + display: none; + } + .reviewed { + vertical-align: -.1em; + } + .jumpToFileContainer { + display: none; + } + .mobileJumpToFileContainer { + display: block; + width: 100%; + } + } + </style> + <gr-ajax id="changeDetailXHR" + auto + url="[[_computeChangeDetailPath(_changeNum)]]" + params="[[_computeChangeDetailQueryParams()]]" + last-response="{{_change}}"></gr-ajax> + <gr-ajax id="filesXHR" + auto + url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]" + on-response="_handleFilesResponse"></gr-ajax> + <gr-ajax id="configXHR" + auto + url="[[_computeProjectConfigPath(_change.project)]]" + last-response="{{_projectConfig}}"></gr-ajax> + <h3> + <a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]"> + [[_changeNum]]</a><span>:</span> + <span>[[_change.subject]]</span> + <span class="dash">—</span> + <input id="reviewed" + class="reviewed" + type="checkbox" + on-change="_handleReviewedChange" + hidden$="[[!_loggedIn]]" hidden> + <div class="jumpToFileContainer"> + <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler"> + <span>[[_computeFileDisplayName(_path)]]</span> + <span class="downArrow">▼</span> + </gr-button> + <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25"> + <div class="dropdown-content"> + <template is="dom-repeat" items="[[_fileList]]" as="path"> + <a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]" + selected$="[[_computeFileSelected(path, _path)]]" + data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]" + on-tap="_handleFileTap"> + [[_computeFileDisplayName(path)]] + </a> + </template> + </div> + </iron-dropdown> + </div> + <div class="mobileJumpToFileContainer"> + <select on-change="_handleMobileSelectChange"> + <template is="dom-repeat" items="[[_fileList]]" as="path"> + <option + value$="[[path]]" + selected$="[[_computeFileSelected(path, _path)]]"> + [[_computeFileDisplayName(path)]] + </option> + </template> + </select> + </div> + </h3> + <gr-diff id="diff" + change-num="[[_changeNum]]" + prefs="{{prefs}}" + patch-range="[[_patchRange]]" + path="[[_path]]" + project-config="[[_projectConfig]]" + available-patches="[[_computeAvailablePatches(_change.revisions)]]" + on-render="_handleDiffRender"> + </gr-diff> + </template> + <script src="gr-diff-view.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js new file mode 100644 index 0000000..847a641 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -0,0 +1,315 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; + + Polymer({ + is: 'gr-diff-view', + + /** + * Fired when the title of the page should change. + * + * @event title-change + */ + + properties: { + prefs: { + type: Object, + notify: true, + }, + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, + changeViewState: { + type: Object, + notify: true, + value: function() { return {}; }, + }, + + _patchRange: Object, + _change: Object, + _changeNum: String, + _diff: Object, + _fileList: { + type: Array, + value: function() { return []; }, + }, + _path: { + type: String, + observer: '_pathChanged', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _xhrPromise: Object, // Used for testing. + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.RESTClientBehavior, + ], + + ready: function() { + app.accountReady.then(function() { + this._loggedIn = app.loggedIn; + if (this._loggedIn) { + this._setReviewed(true); + } + }.bind(this)); + }, + + attached: function() { + if (this._path) { + this.fire('title-change', + {title: this._computeFileDisplayName(this._path)}); + } + window.addEventListener('resize', this._boundWindowResizeHandler); + }, + + detached: function() { + window.removeEventListener('resize', this._boundWindowResizeHandler); + }, + + _handleReviewedChange: function(e) { + this._setReviewed(Polymer.dom(e).rootTarget.checked); + }, + + _setReviewed: function(reviewed) { + this.$.reviewed.checked = reviewed; + var method = reviewed ? 'PUT' : 'DELETE'; + var url = this.changeBaseURL(this._changeNum, + this._patchRange.patchNum) + '/files/' + + encodeURIComponent(this._path) + '/reviewed'; + this._send(method, url).catch(function(err) { + alert('Couldn’t change file review status. Check the console ' + + 'and contact the PolyGerrit team for assistance.'); + throw err; + }.bind(this)); + }, + + _handleKey: function(e) { + if (this.shouldSupressKeyboardShortcut(e)) { return; } + + switch (e.keyCode) { + case 219: // '[' + e.preventDefault(); + this._navToFile(this._fileList, -1); + break; + case 221: // ']' + e.preventDefault(); + this._navToFile(this._fileList, 1); + break; + case 78: // 'n' + if (e.shiftKey) { + this.$.diff.scrollToNextCommentThread(); + } else { + this.$.diff.scrollToNextDiffChunk(); + } + break; + case 80: // 'p' + if (e.shiftKey) { + this.$.diff.scrollToPreviousCommentThread(); + } else { + this.$.diff.scrollToPreviousDiffChunk(); + } + break; + case 65: // 'a' + if (!this._loggedIn) { return; } + + this.set('changeViewState.showReplyDialog', true); + /* falls through */ // required by JSHint + case 85: // 'u' + if (this._changeNum && this._patchRange.patchNum) { + e.preventDefault(); + page.show(this._computeChangePath( + this._changeNum, + this._patchRange.patchNum, + this._change && this._change.revisions)); + } + break; + case 188: // ',' + this.$.diff.showDiffPreferences(); + break; + } + }, + + _handleDiffRender: function() { + if (window.location.hash.length > 0) { + this.$.diff.scrollToLine( + parseInt(window.location.hash.substring(1), 10)); + } + }, + + _navToFile: function(fileList, direction) { + if (fileList.length == 0) { return; } + + var idx = fileList.indexOf(this._path) + direction; + if (idx < 0 || idx > fileList.length - 1) { + page.show(this._computeChangePath( + this._changeNum, + this._patchRange.patchNum, + this._change && this._change.revisions)); + return; + } + page.show(this._computeDiffURL(this._changeNum, + this._patchRange, + fileList[idx])); + }, + + _paramsChanged: function(value) { + if (value.view != this.tagName.toLowerCase()) { return; } + + this._changeNum = value.changeNum; + this._patchRange = { + patchNum: value.patchNum, + basePatchNum: value.basePatchNum || 'PARENT', + }; + this._path = value.path; + + this.fire('title-change', + {title: this._computeFileDisplayName(this._path)}); + + // When navigating away from the page, there is a possibility that the + // patch number is no longer a part of the URL (say when navigating to + // the top-level change info view) and therefore undefined in `params`. + if (!this._patchRange.patchNum) { + return; + } + + this.$.diff.reload(); + }, + + _pathChanged: function(path) { + if (this._fileList.length == 0) { return; } + + this.set('changeViewState.selectedFileIndex', + this._fileList.indexOf(path)); + + if (this._loggedIn) { + this._setReviewed(true); + } + }, + + _computeDiffURL: function(changeNum, patchRange, path) { + var patchStr = patchRange.patchNum; + if (patchRange.basePatchNum != null && + patchRange.basePatchNum != 'PARENT') { + patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; + } + return '/c/' + changeNum + '/' + patchStr + '/' + path; + }, + + _computeAvailablePatches: function(revisions) { + var patchNums = []; + for (var rev in revisions) { + patchNums.push(revisions[rev]._number); + } + return patchNums.sort(function(a, b) { return a - b; }); + }, + + _computeChangePath: function(changeNum, patchNum, revisions) { + var base = '/c/' + changeNum + '/'; + + // The change may not have loaded yet, making revisions unavailable. + if (!revisions) { + return base + patchNum; + } + + var latestPatchNum = -1; + for (var rev in revisions) { + latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number); + } + if (parseInt(patchNum, 10) != latestPatchNum) { + return base + patchNum; + } + + return base; + }, + + _computeFileDisplayName: function(path) { + return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path; + }, + + _computeChangeDetailPath: function(changeNum) { + return '/changes/' + changeNum + '/detail'; + }, + + _computeChangeDetailQueryParams: function() { + return {O: this.listChangesOptionsToHex( + this.ListChangesOption.ALL_REVISIONS + )}; + }, + + _computeFilesPath: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/files'; + }, + + _computeProjectConfigPath: function(project) { + return '/projects/' + encodeURIComponent(project) + '/config'; + }, + + _computeFileSelected: function(path, currentPath) { + return path == currentPath; + }, + + _computeKeyNav: function(path, selectedPath, fileList) { + var selectedIndex = fileList.indexOf(selectedPath); + if (fileList.indexOf(path) == selectedIndex - 1) { + return '['; + } + if (fileList.indexOf(path) == selectedIndex + 1) { + return ']'; + } + return ''; + }, + + _handleFileTap: function(e) { + this.$.dropdown.close(); + }, + + _handleMobileSelectChange: function(e) { + var path = Polymer.dom(e).rootTarget.value; + page.show( + this._computeDiffURL(this._changeNum, this._patchRange, path)); + }, + + _handleFilesResponse: function(e, req) { + this._fileList = Object.keys(e.detail.response).sort(); + }, + + _showDropdownTapHandler: function(e) { + this.$.dropdown.open(); + }, + + _send: function(method, url) { + var xhr = document.createElement('gr-request'); + this._xhrPromise = xhr.send({ + method: method, + url: url, + }); + return this._xhrPromise; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html new file mode 100644 index 0000000..bfe4906 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -0,0 +1,395 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-diff-view</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-diff-view.html"> + +<test-fixture id="basic"> + <template> + <gr-diff-view></gr-diff-view> + </template> +</test-fixture> + +<script> + suite('gr-diff-view tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + element.$.changeDetailXHR.auto = false; + element.$.filesXHR.auto = false; + element.$.configXHR.auto = false; + element.$.diff.auto = false; + + server = sinon.fakeServer.create(); + server.respondWith( + 'PUT', + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed', + [ + 201, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + '""', + ] + ); + server.respondWith( + 'DELETE', + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed', + [ + 204, + {'Content-Type': 'application/json'}, + '', + ] + ); + }); + + teardown(function() { + server.restore(); + }); + + test('keyboard shortcuts', function() { + element._changeNum = '42'; + element._patchRange = { + patchNum: '10', + }; + element._change = { + revisions: { + a: { _number: 10, }, + }, + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + element.changeViewState.selectedFileIndex = 1; + + var showStub = sinon.stub(page, 'show'); + MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + assert(showStub.lastCall.calledWithExactly('/c/42/'), + 'Should navigate to /c/42/'); + + MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + assert(showStub.lastCall.calledWithExactly('/c/42/10/wheatley.md'), + 'Should navigate to /c/42/10/wheatley.md'); + element._path = 'wheatley.md'; + assert.equal(element.changeViewState.selectedFileIndex, 2); + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/10/glados.txt'), + 'Should navigate to /c/42/10/glados.txt'); + element._path = 'glados.txt'; + assert.equal(element.changeViewState.selectedFileIndex, 1); + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/10/chell.go'), + 'Should navigate to /c/42/10/chell.go'); + element._path = 'chell.go'; + assert.equal(element.changeViewState.selectedFileIndex, 0); + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/'), + 'Should navigate to /c/42/'); + assert.equal(element.changeViewState.selectedFileIndex, 0); + + var showPrefsStub = sinon.stub(element.$.diff, 'showDiffPreferences'); + MockInteractions.pressAndReleaseKeyOn(element, 188); // ',' + assert(showPrefsStub.calledOnce); + + var scrollStub = sinon.stub(element.$.diff, 'scrollToNextDiffChunk'); + MockInteractions.pressAndReleaseKeyOn(element, 78); // 'n' + assert(scrollStub.calledOnce); + scrollStub.restore(); + + scrollStub = sinon.stub(element.$.diff, 'scrollToPreviousDiffChunk'); + MockInteractions.pressAndReleaseKeyOn(element, 80); // 'p' + assert(scrollStub.calledOnce); + scrollStub.restore(); + + scrollStub = sinon.stub(element.$.diff, 'scrollToNextCommentThread'); + MockInteractions.pressAndReleaseKeyOn(element, 78, ['shift']); // 'N' + assert(scrollStub.calledOnce); + scrollStub.restore(); + + scrollStub = sinon.stub(element.$.diff, 'scrollToPreviousCommentThread'); + MockInteractions.pressAndReleaseKeyOn(element, 80, ['shift']); // 'P' + assert(scrollStub.calledOnce); + scrollStub.restore(); + + showPrefsStub.restore(); + showStub.restore(); + }); + + test('keyboard shortcuts with patch range', function() { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '5', + patchNum: '10', + }; + element._change = { + revisions: { + a: { _number: 10, }, + }, + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + + var showStub = sinon.stub(page, 'show'); + + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + + 'only work when the user is logged in.'); + assert.isNull(window.sessionStorage.getItem( + 'changeView.showReplyDialog')); + + element._loggedIn = true; + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + assert.isTrue(element.changeViewState.showReplyDialog); + + assert(showStub.lastCall.calledWithExactly('/c/42/'), + 'Should navigate to /c/42/'); + + MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + assert(showStub.lastCall.calledWithExactly('/c/42/'), + 'Should navigate to /c/42/'); + + MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + assert(showStub.lastCall.calledWithExactly('/c/42/5..10/wheatley.md'), + 'Should navigate to /c/42/5..10/wheatley.md'); + element._path = 'wheatley.md'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/5..10/glados.txt'), + 'Should navigate to /c/42/5..10/glados.txt'); + element._path = 'glados.txt'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/5..10/chell.go'), + 'Should navigate to /c/42/5..10/chell.go'); + element._path = 'chell.go'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/'), + 'Should navigate to /c/42/'); + + showStub.restore(); + }); + + test('keyboard shortcuts with old patch number', function() { + element._changeNum = '42'; + element._patchRange = { + patchNum: '1', + }; + element._change = { + revisions: { + a: { _number: 1, }, + b: { _number: 2, }, + }, + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + + var showStub = sinon.stub(page, 'show'); + + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + + 'only work when the user is logged in.'); + assert.isNull(window.sessionStorage.getItem( + 'changeView.showReplyDialog')); + + element._loggedIn = true; + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + assert.isTrue(element.changeViewState.showReplyDialog); + + assert(showStub.lastCall.calledWithExactly('/c/42/1'), + 'Should navigate to /c/42/1'); + + MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + assert(showStub.lastCall.calledWithExactly('/c/42/1'), + 'Should navigate to /c/42/1'); + + MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'), + 'Should navigate to /c/42/1/wheatley.md'); + element._path = 'wheatley.md'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'), + 'Should navigate to /c/42/1/glados.txt'); + element._path = 'glados.txt'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'), + 'Should navigate to /c/42/1/chell.go'); + element._path = 'chell.go'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/1'), + 'Should navigate to /c/42/1'); + + showStub.restore(); + }); + + test('go up to change via kb without change loaded', function() { + element._changeNum = '42'; + element._patchRange = { + patchNum: '1', + }; + + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + + var showStub = sinon.stub(page, 'show'); + + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + + 'only work when the user is logged in.'); + assert.isNull(window.sessionStorage.getItem( + 'changeView.showReplyDialog')); + + element._loggedIn = true; + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + assert.isTrue(element.changeViewState.showReplyDialog); + + assert(showStub.lastCall.calledWithExactly('/c/42/1'), + 'Should navigate to /c/42/1'); + + MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + assert(showStub.lastCall.calledWithExactly('/c/42/1'), + 'Should navigate to /c/42/1'); + + MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'), + 'Should navigate to /c/42/1/wheatley.md'); + element._path = 'wheatley.md'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'), + 'Should navigate to /c/42/1/glados.txt'); + element._path = 'glados.txt'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'), + 'Should navigate to /c/42/1/chell.go'); + element._path = 'chell.go'; + + MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + assert(showStub.lastCall.calledWithExactly('/c/42/1'), + 'Should navigate to /c/42/1'); + + showStub.restore(); + }); + + test('jump to file dropdown', function() { + element._changeNum = '42'; + element._patchRange = { + patchNum: '10', + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + flushAsynchronousOperations(); + var linkEls = + Polymer.dom(element.root).querySelectorAll('.dropdown-content > a'); + assert.equal(linkEls.length, 3); + assert.isFalse(linkEls[0].hasAttribute('selected')); + assert.isTrue(linkEls[1].hasAttribute('selected')); + assert.isFalse(linkEls[2].hasAttribute('selected')); + assert.equal(linkEls[0].getAttribute('data-key-nav'), '['); + assert.equal(linkEls[1].getAttribute('data-key-nav'), ''); + assert.equal(linkEls[2].getAttribute('data-key-nav'), ']'); + assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/chell.go'); + assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/glados.txt'); + assert.equal(linkEls[2].getAttribute('href'), '/c/42/10/wheatley.md'); + + assert.equal(element._computeFileDisplayName('/foo/bar/baz'), + '/foo/bar/baz'); + assert.equal(element._computeFileDisplayName('/COMMIT_MSG'), + 'Commit message'); + }); + + test('jump to file dropdown with patch range', function() { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '5', + patchNum: '10', + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + flushAsynchronousOperations(); + var linkEls = + Polymer.dom(element.root).querySelectorAll('.dropdown-content > a'); + assert.equal(linkEls.length, 3); + assert.isFalse(linkEls[0].hasAttribute('selected')); + assert.isTrue(linkEls[1].hasAttribute('selected')); + assert.isFalse(linkEls[2].hasAttribute('selected')); + assert.equal(linkEls[0].getAttribute('data-key-nav'), '['); + assert.equal(linkEls[1].getAttribute('data-key-nav'), ''); + assert.equal(linkEls[2].getAttribute('data-key-nav'), ']'); + assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/chell.go'); + assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/glados.txt'); + assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/wheatley.md'); + }); + + test('file review status', function(done) { + element._loggedIn = true; + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '1', + patchNum: '2', + }; + element._fileList = ['/COMMIT_MSG']; + element._path = '/COMMIT_MSG'; + + server.respond(); + + element.async(function() { + var commitMsg = Polymer.dom(element.root).querySelector( + 'input[type="checkbox"]'); + + assert.isTrue(commitMsg.checked); + + MockInteractions.tap(commitMsg); + server.respond(); + element._xhrPromise.then(function(req) { + assert.isFalse(commitMsg.checked); + assert.equal(req.status, 204); + assert.equal(req.url, + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed'); + + MockInteractions.tap(commitMsg); + server.respond(); + }).then(function() { + element._xhrPromise.then(function(req) { + assert.isTrue(commitMsg.checked); + assert.equal(req.status, 201); + assert.equal(req.url, + '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed'); + + done(); + }); + }); + }, 1); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html new file mode 100644 index 0000000..21ee076 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -0,0 +1,123 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> +<link rel="import" href="../../shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="../../shared/gr-request/gr-request.html"> + +<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html"> +<link rel="import" href="../gr-diff-side/gr-diff-side.html"> +<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html"> + +<dom-module id="gr-diff"> + <template> + <style> + .loading { + padding: 0 var(--default-horizontal-margin) 1em; + color: #666; + } + .header { + display: flex; + justify-content: space-between; + margin: 0 var(--default-horizontal-margin) .75em; + } + .prefsButton { + text-align: right; + } + .diffContainer { + border-bottom: 1px solid #eee; + border-top: 1px solid #eee; + display: flex; + font: 12px var(--monospace-font-family); + overflow-x: auto; + } + gr-diff-side:first-of-type { + --light-highlight-color: #fee; + --dark-highlight-color: #ffd4d4; + } + gr-diff-side:last-of-type { + --light-highlight-color: #efe; + --dark-highlight-color: #d4ffd4; + border-right: 1px solid #ddd; + } + </style> + <gr-ajax id="diffXHR" + url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]" + params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]" + last-response="{{_diffResponse}}" + loading="{{_loading}}"></gr-ajax> + <gr-ajax id="baseCommentsXHR" + url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax> + <gr-ajax id="commentsXHR" + url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax> + <gr-ajax id="baseDraftsXHR" + url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax> + <gr-ajax id="draftsXHR" + url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax> + <div class="loading" hidden$="[[!_loading]]">Loading...</div> + <div hidden$="[[_loading]]" hidden> + <div class="header"> + <gr-patch-range-select + path="[[path]]" + change-num="[[changeNum]]" + patch-range="[[patchRange]]" + available-patches="[[availablePatches]]"></gr-patch-range-select> + <gr-button link + class="prefsButton" + on-tap="_handlePrefsTap" + hidden$="[[!prefs]]" + hidden>Diff View Preferences</gr-button> + </div> + <gr-overlay id="prefsOverlay" with-backdrop> + <gr-diff-preferences + prefs="{{prefs}}" + on-save="_handlePrefsSave" + on-cancel="_handlePrefsCancel"></gr-diff-preferences> + </gr-overlay> + + <div class="diffContainer"> + <gr-diff-side id="leftDiff" + change-num="[[changeNum]]" + patch-num="[[patchRange.basePatchNum]]" + path="[[path]]" + content="{{_diff.leftSide}}" + prefs="[[prefs]]" + can-comment="[[_loggedIn]]" + project-config="[[projectConfig]]" + on-expand-context="_handleExpandContext" + on-thread-height-change="_handleThreadHeightChange" + on-add-draft="_handleAddDraft" + on-remove-thread="_handleRemoveThread"></gr-diff-side> + <gr-diff-side id="rightDiff" + change-num="[[changeNum]]" + patch-num="[[patchRange.patchNum]]" + path="[[path]]" + content="{{_diff.rightSide}}" + prefs="[[prefs]]" + can-comment="[[_loggedIn]]" + project-config="[[projectConfig]]" + on-expand-context="_handleExpandContext" + on-thread-height-change="_handleThreadHeightChange" + on-add-draft="_handleAddDraft" + on-remove-thread="_handleRemoveThread"></gr-diff-side> + </div> + </div> + </template> + <script src="gr-diff.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js new file mode 100644 index 0000000..485e2cc --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -0,0 +1,746 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-diff', + + /** + * Fired when the diff is rendered. + * + * @event render + */ + + properties: { + availablePatches: Array, + changeNum: String, + /* + * A single object to encompass basePatchNum and patchNum is used + * so that both can be set at once without incremental observers + * firing after each property changes. + */ + patchRange: Object, + path: String, + prefs: { + type: Object, + notify: true, + }, + projectConfig: Object, + + _prefsReady: { + type: Object, + readOnly: true, + value: function() { + return new Promise(function(resolve) { + this._resolvePrefsReady = resolve; + }.bind(this)); + }, + }, + _baseComments: Array, + _comments: Array, + _drafts: Array, + _baseDrafts: Array, + /** + * Base (left side) comments and drafts grouped by line number. + * Only used for initial rendering. + */ + _groupedBaseComments: { + type: Object, + value: function() { return {}; }, + }, + /** + * Comments and drafts (right side) grouped by line number. + * Only used for initial rendering. + */ + _groupedComments: { + type: Object, + value: function() { return {}; }, + }, + _diffResponse: Object, + _diff: { + type: Object, + value: function() { return {}; }, + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _initialRenderComplete: { + type: Boolean, + value: false, + }, + _loading: { + type: Boolean, + value: true, + }, + _savedPrefs: Object, + + _diffRequestsPromise: Object, // Used for testing. + _diffPreferencesPromise: Object, // Used for testing. + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + observers: [ + '_prefsChanged(prefs.*)', + ], + + ready: function() { + app.accountReady.then(function() { + this._loggedIn = app.loggedIn; + }.bind(this)); + }, + + scrollToLine: function(lineNum) { + // TODO(andybons): Should this always be the right side? + this.$.rightDiff.scrollToLine(lineNum); + }, + + scrollToNextDiffChunk: function() { + this.$.rightDiff.scrollToNextDiffChunk(); + }, + + scrollToPreviousDiffChunk: function() { + this.$.rightDiff.scrollToPreviousDiffChunk(); + }, + + scrollToNextCommentThread: function() { + this.$.rightDiff.scrollToNextCommentThread(); + }, + + scrollToPreviousCommentThread: function() { + this.$.rightDiff.scrollToPreviousCommentThread(); + }, + + reload: function(changeNum, patchRange, path) { + // If a diff takes a considerable amount of time to render, the previous + // diff can end up showing up while the DOM is constructed. Clear the + // content on a reload to prevent this. + this._diff = { + leftSide: [], + rightSide: [], + }; + + var promises = [ + this._prefsReady, + this.$.diffXHR.generateRequest().completes + ]; + + var basePatchNum = this.patchRange.basePatchNum; + + return app.accountReady.then(function() { + promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn)); + this._diffRequestsPromise = Promise.all(promises).then(function() { + this._render(); + }.bind(this)).catch(function(err) { + alert('Oops. Something went wrong. Check the console and bug the ' + + 'PolyGerrit team for assistance.'); + throw err; + }); + }.bind(this)); + }, + + showDiffPreferences: function() { + this.$.prefsOverlay.open(); + }, + + _prefsChanged: function(changeRecord) { + if (this._initialRenderComplete) { + this._render(); + } + this._resolvePrefsReady(changeRecord.base); + }, + + _render: function() { + this._groupCommentsAndDrafts(); + this._processContent(); + + // Allow for the initial rendering to complete before firing the event. + this.async(function() { + this.fire('render', null, {bubbles: false}); + }.bind(this), 1); + + this._initialRenderComplete = true; + }, + + _getCommentsAndDrafts: function(basePatchNum, loggedIn) { + function onlyParent(c) { return c.side == 'PARENT'; } + function withoutParent(c) { return c.side != 'PARENT'; } + + var promises = []; + var commentsPromise = this.$.commentsXHR.generateRequest().completes; + promises.push(commentsPromise.then(function(req) { + var comments = req.response[this.path] || []; + if (basePatchNum == 'PARENT') { + this._baseComments = comments.filter(onlyParent); + } + this._comments = comments.filter(withoutParent); + }.bind(this))); + + if (basePatchNum != 'PARENT') { + commentsPromise = this.$.baseCommentsXHR.generateRequest().completes; + promises.push(commentsPromise.then(function(req) { + this._baseComments = + (req.response[this.path] || []).filter(withoutParent); + }.bind(this))); + } + + if (!loggedIn) { + this._baseDrafts = []; + this._drafts = []; + return Promise.all(promises); + } + + var draftsPromise = this.$.draftsXHR.generateRequest().completes; + promises.push(draftsPromise.then(function(req) { + var drafts = req.response[this.path] || []; + if (basePatchNum == 'PARENT') { + this._baseDrafts = drafts.filter(onlyParent); + } + this._drafts = drafts.filter(withoutParent); + }.bind(this))); + + if (basePatchNum != 'PARENT') { + draftsPromise = this.$.baseDraftsXHR.generateRequest().completes; + promises.push(draftsPromise.then(function(req) { + this._baseDrafts = + (req.response[this.path] || []).filter(withoutParent); + }.bind(this))); + } + + return Promise.all(promises); + }, + + _computeDiffPath: function(changeNum, patchNum, path) { + return this.changeBaseURL(changeNum, patchNum) + '/files/' + + encodeURIComponent(path) + '/diff'; + }, + + _computeCommentsPath: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/comments'; + }, + + _computeDraftsPath: function(changeNum, patchNum) { + return this.changeBaseURL(changeNum, patchNum) + '/drafts'; + }, + + _computeDiffQueryParams: function(basePatchNum) { + var params = { + context: 'ALL', + intraline: null + }; + if (basePatchNum != 'PARENT') { + params.base = basePatchNum; + } + return params; + }, + + _handlePrefsTap: function(e) { + e.preventDefault(); + + // TODO(andybons): This is not supported in IE. Implement a polyfill. + // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds + // an object as a value, it must be marked enumerable. + this._savedPrefs = Object.assign({}, this.prefs); + this.$.prefsOverlay.open(); + }, + + _handlePrefsSave: function(e) { + e.stopPropagation(); + var el = Polymer.dom(e).rootTarget; + el.disabled = true; + app.accountReady.then(function() { + if (!this._loggedIn) { + el.disabled = false; + this.$.prefsOverlay.close(); + return; + } + this._saveDiffPreferences().then(function() { + this.$.prefsOverlay.close(); + el.disabled = false; + }.bind(this)).catch(function(err) { + el.disabled = false; + alert('Oops. Something went wrong. Check the console and bug the ' + + 'PolyGerrit team for assistance.'); + throw err; + }); + }.bind(this)); + }, + + _saveDiffPreferences: function() { + var xhr = document.createElement('gr-request'); + this._diffPreferencesPromise = xhr.send({ + method: 'PUT', + url: '/accounts/self/preferences.diff', + body: this.prefs, + }); + return this._diffPreferencesPromise; + }, + + _handlePrefsCancel: function(e) { + e.stopPropagation(); + this.prefs = this._savedPrefs; + this.$.prefsOverlay.close(); + }, + + _handleExpandContext: function(e) { + var ctx = e.detail.context; + var contextControlIndex = -1; + for (var i = ctx.start; i <= ctx.end; i++) { + this._diff.leftSide[i].hidden = false; + this._diff.rightSide[i].hidden = false; + if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' && + this._diff.rightSide[i].type == 'CONTEXT_CONTROL') { + contextControlIndex = i; + } + } + this._diff.leftSide[contextControlIndex].hidden = true; + this._diff.rightSide[contextControlIndex].hidden = true; + + this.$.leftDiff.hideElementsWithIndex(contextControlIndex); + this.$.rightDiff.hideElementsWithIndex(contextControlIndex); + + this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end); + this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end); + }, + + _handleThreadHeightChange: function(e) { + var index = e.detail.index; + var diffEl = Polymer.dom(e).rootTarget; + var otherSide = diffEl == this.$.leftDiff ? + this.$.rightDiff : this.$.leftDiff; + + var threadHeight = e.detail.height; + var otherSideHeight; + if (otherSide.content[index].type == 'COMMENT_THREAD') { + otherSideHeight = otherSide.getRowNaturalHeight(index); + } else { + otherSideHeight = otherSide.getRowHeight(index); + } + var maxHeight = Math.max(threadHeight, otherSideHeight); + this.$.leftDiff.setRowHeight(index, maxHeight); + this.$.rightDiff.setRowHeight(index, maxHeight); + }, + + _handleAddDraft: function(e) { + var insertIndex = e.detail.index + 1; + var diffEl = Polymer.dom(e).rootTarget; + var content = diffEl.content; + if (content[insertIndex] && + content[insertIndex].type == 'COMMENT_THREAD') { + // A thread is already here. Do nothing. + return; + } + var comment = { + type: 'COMMENT_THREAD', + comments: [{ + __draft: true, + __draftID: Math.random().toString(36), + line: e.detail.line, + path: this.path, + }] + }; + if (diffEl == this.$.leftDiff && + this.patchRange.basePatchNum == 'PARENT') { + comment.comments[0].side = 'PARENT'; + comment.patchNum = this.patchRange.patchNum; + } + + if (content[insertIndex] && + content[insertIndex].type == 'FILLER') { + content[insertIndex] = comment; + diffEl.rowUpdated(insertIndex); + } else { + content.splice(insertIndex, 0, comment); + diffEl.rowInserted(insertIndex); + } + + var otherSide = diffEl == this.$.leftDiff ? + this.$.rightDiff : this.$.leftDiff; + if (otherSide.content[insertIndex] == null || + otherSide.content[insertIndex].type != 'COMMENT_THREAD') { + otherSide.content.splice(insertIndex, 0, { + type: 'FILLER', + }); + otherSide.rowInserted(insertIndex); + } + }, + + _handleRemoveThread: function(e) { + var diffEl = Polymer.dom(e).rootTarget; + var otherSide = diffEl == this.$.leftDiff ? + this.$.rightDiff : this.$.leftDiff; + var index = e.detail.index; + + if (otherSide.content[index].type == 'FILLER') { + otherSide.content.splice(index, 1); + otherSide.rowRemoved(index); + diffEl.content.splice(index, 1); + diffEl.rowRemoved(index); + } else if (otherSide.content[index].type == 'COMMENT_THREAD') { + diffEl.content[index] = {type: 'FILLER'}; + diffEl.rowUpdated(index); + var height = otherSide.setRowNaturalHeight(index); + diffEl.setRowHeight(index, height); + } else { + throw Error('A thread cannot be opposite anything but filler or ' + + 'another thread'); + } + }, + + _processContent: function() { + var leftSide = []; + var rightSide = []; + var initialLineNum = 0 + (this._diffResponse.content.skip || 0); + var ctx = { + hidingLines: false, + lastNumLinesHidden: 0, + left: { + lineNum: initialLineNum, + }, + right: { + lineNum: initialLineNum, + } + }; + var content = this._breakUpCommonChunksWithComments(ctx, + this._diffResponse.content); + var context = this.prefs.context; + if (context == -1) { + // Show the entire file. + context = Infinity; + } + for (var i = 0; i < content.length; i++) { + if (i == 0) { + ctx.skipRange = [0, context]; + } else if (i == content.length - 1) { + ctx.skipRange = [context, 0]; + } else { + ctx.skipRange = [context, context]; + } + ctx.diffChunkIndex = i; + this._addDiffChunk(ctx, content[i], leftSide, rightSide); + } + + this._diff = { + leftSide: leftSide, + rightSide: rightSide, + }; + }, + + // In order to show comments out of the bounds of the selected context, + // treat them as diffs within the model so that the content (and context + // surrounding it) renders correctly. + _breakUpCommonChunksWithComments: function(ctx, content) { + var result = []; + var leftLineNum = ctx.left.lineNum; + var rightLineNum = ctx.right.lineNum; + for (var i = 0; i < content.length; i++) { + if (!content[i].ab) { + result.push(content[i]); + if (content[i].a) { + leftLineNum += content[i].a.length; + } + if (content[i].b) { + rightLineNum += content[i].b.length; + } + continue; + } + var chunk = content[i].ab; + var currentChunk = {ab: []}; + for (var j = 0; j < chunk.length; j++) { + leftLineNum++; + rightLineNum++; + if (this._groupedBaseComments[leftLineNum] == null && + this._groupedComments[rightLineNum] == null) { + currentChunk.ab.push(chunk[j]); + } else { + if (currentChunk.ab && currentChunk.ab.length > 0) { + result.push(currentChunk); + currentChunk = {ab: []}; + } + // Append an annotation to indicate that this line should not be + // highlighted even though it's implied with both `a` and `b` + // defined. This is needed since there may be two lines that + // should be highlighted but are equal (blank lines, for example). + result.push({ + __noHighlight: true, + a: [chunk[j]], + b: [chunk[j]], + }); + } + } + if (currentChunk.ab != null && currentChunk.ab.length > 0) { + result.push(currentChunk); + } + } + return result; + }, + + _groupCommentsAndDrafts: function() { + this._baseDrafts.forEach(function(d) { d.__draft = true; }); + this._drafts.forEach(function(d) { d.__draft = true; }); + var allLeft = this._baseComments.concat(this._baseDrafts); + var allRight = this._comments.concat(this._drafts); + + var leftByLine = {}; + var rightByLine = {}; + var mapFunc = function(byLine) { + return function(c) { + // File comments/drafts are grouped with line 1 for now. + var line = c.line || 1; + if (byLine[line] == null) { + byLine[line] = []; + } + byLine[line].push(c); + }; + }; + allLeft.forEach(mapFunc(leftByLine)); + allRight.forEach(mapFunc(rightByLine)); + + this._groupedBaseComments = leftByLine; + this._groupedComments = rightByLine; + }, + + _addContextControl: function(ctx, leftSide, rightSide) { + var numLinesHidden = ctx.lastNumLinesHidden; + var leftStart = leftSide.length - numLinesHidden; + var leftEnd = leftSide.length; + var rightStart = rightSide.length - numLinesHidden; + var rightEnd = rightSide.length; + if (leftStart != rightStart || leftEnd != rightEnd) { + throw Error( + 'Left and right ranges for context control should be equal:' + + 'Left: [' + leftStart + ', ' + leftEnd + '] ' + + 'Right: [' + rightStart + ', ' + rightEnd + ']'); + } + var obj = { + type: 'CONTEXT_CONTROL', + numLines: numLinesHidden, + start: leftStart, + end: leftEnd, + }; + // NOTE: Be careful, here. This object is meant to be immutable. If the + // object is altered within one side's array it will reflect the + // alterations in another. + leftSide.push(obj); + rightSide.push(obj); + }, + + _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) { + for (var i = 0; i < chunk.ab.length; i++) { + var numLines = Math.ceil( + this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length); + var hidden = i >= ctx.skipRange[0] && + i < chunk.ab.length - ctx.skipRange[1]; + if (ctx.hidingLines && hidden == false) { + // No longer hiding lines. Add a context control. + this._addContextControl(ctx, leftSide, rightSide); + ctx.lastNumLinesHidden = 0; + } + ctx.hidingLines = hidden; + if (hidden) { + ctx.lastNumLinesHidden++; + } + + // Blank lines within a diff content array indicate a newline. + leftSide.push({ + type: 'CODE', + hidden: hidden, + content: chunk.ab[i] || '\n', + numLines: numLines, + lineNum: ++ctx.left.lineNum, + }); + rightSide.push({ + type: 'CODE', + hidden: hidden, + content: chunk.ab[i] || '\n', + numLines: numLines, + lineNum: ++ctx.right.lineNum, + }); + + this._addCommentsIfPresent(ctx, leftSide, rightSide); + } + if (ctx.lastNumLinesHidden > 0) { + this._addContextControl(ctx, leftSide, rightSide); + } + }, + + _addDiffChunk: function(ctx, chunk, leftSide, rightSide) { + if (chunk.ab) { + this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide); + return; + } + + var leftHighlights = []; + if (chunk.edit_a) { + leftHighlights = + this._normalizeIntralineHighlights(chunk.a, chunk.edit_a); + } + var rightHighlights = []; + if (chunk.edit_b) { + rightHighlights = + this._normalizeIntralineHighlights(chunk.b, chunk.edit_b); + } + + var aLen = (chunk.a && chunk.a.length) || 0; + var bLen = (chunk.b && chunk.b.length) || 0; + var maxLen = Math.max(aLen, bLen); + for (var i = 0; i < maxLen; i++) { + var hasLeftContent = chunk.a && i < chunk.a.length; + var hasRightContent = chunk.b && i < chunk.b.length; + var leftContent = hasLeftContent ? chunk.a[i] : ''; + var rightContent = hasRightContent ? chunk.b[i] : ''; + var highlight = !chunk.__noHighlight; + var maxNumLines = this._maxLinesSpanned(leftContent, rightContent); + if (hasLeftContent) { + leftSide.push({ + type: 'CODE', + content: leftContent || '\n', + numLines: maxNumLines, + lineNum: ++ctx.left.lineNum, + highlight: highlight, + intraline: highlight && leftHighlights.filter(function(hl) { + return hl.contentIndex == i; + }), + }); + } else { + leftSide.push({ + type: 'FILLER', + numLines: maxNumLines, + }); + } + if (hasRightContent) { + rightSide.push({ + type: 'CODE', + content: rightContent || '\n', + numLines: maxNumLines, + lineNum: ++ctx.right.lineNum, + highlight: highlight, + intraline: highlight && rightHighlights.filter(function(hl) { + return hl.contentIndex == i; + }), + }); + } else { + rightSide.push({ + type: 'FILLER', + numLines: maxNumLines, + }); + } + this._addCommentsIfPresent(ctx, leftSide, rightSide); + } + }, + + _addCommentsIfPresent: function(ctx, leftSide, rightSide) { + var leftComments = this._groupedBaseComments[ctx.left.lineNum]; + var rightComments = this._groupedComments[ctx.right.lineNum]; + if (leftComments) { + var thread = { + type: 'COMMENT_THREAD', + comments: leftComments, + }; + if (this.patchRange.basePatchNum == 'PARENT') { + thread.patchNum = this.patchRange.patchNum; + } + leftSide.push(thread); + } + if (rightComments) { + rightSide.push({ + type: 'COMMENT_THREAD', + comments: rightComments, + }); + } + if (leftComments && !rightComments) { + rightSide.push({type: 'FILLER'}); + } else if (!leftComments && rightComments) { + leftSide.push({type: 'FILLER'}); + } + this._groupedBaseComments[ctx.left.lineNum] = null; + this._groupedComments[ctx.right.lineNum] = null; + }, + + // The `highlights` array consists of a list of <skip length, mark length> + // pairs, where the skip length is the number of characters between the + // end of the previous edit and the start of this edit, and the mark + // length is the number of edited characters following the skip. The start + // of the edits is from the beginning of the related diff content lines. + // + // Note that the implied newline character at the end of each line is + // included in the length calculation, and thus it is possible for the + // edits to span newlines. + // + // A line highlight object consists of three fields: + // - contentIndex: The index of the diffChunk `content` field (the line + // being referred to). + // - startIndex: Where the highlight should begin. + // - endIndex: (optional) Where the highlight should end. If omitted, the + // highlight is meant to be a continuation onto the next line. + _normalizeIntralineHighlights: function(content, highlights) { + var contentIndex = 0; + var idx = 0; + var normalized = []; + for (var i = 0; i < highlights.length; i++) { + var line = content[contentIndex] + '\n'; + var hl = highlights[i]; + var j = 0; + while (j < hl[0]) { + if (idx == line.length) { + idx = 0; + line = content[++contentIndex] + '\n'; + continue; + } + idx++; + j++; + } + var lineHighlight = { + contentIndex: contentIndex, + startIndex: idx, + }; + + j = 0; + while (line && j < hl[1]) { + if (idx == line.length) { + idx = 0; + line = content[++contentIndex] + '\n'; + normalized.push(lineHighlight); + lineHighlight = { + contentIndex: contentIndex, + startIndex: idx, + }; + continue; + } + idx++; + j++; + } + lineHighlight.endIndex = idx; + normalized.push(lineHighlight); + } + return normalized; + }, + + _visibleLineLength: function(contents) { + // http://jsperf.com/performance-of-match-vs-split + var numTabs = contents.split('\t').length - 1; + return contents.length - numTabs + (this.prefs.tab_size * numTabs); + }, + + _maxLinesSpanned: function(left, right) { + return Math.max( + Math.ceil(this._visibleLineLength(left) / this.prefs.line_length), + Math.ceil(this._visibleLineLength(right) / this.prefs.line_length)); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html new file mode 100644 index 0000000..9a8cb81 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -0,0 +1,574 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-diff</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-diff.html"> + +<test-fixture id="basic"> + <template> + <gr-diff></gr-diff> + </template> +</test-fixture> + +<script> + suite('gr-diff tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + element.changeNum = 42; + element.path = 'sieve.go'; + element.prefs = { + context: 10, + tab_size: 8, + }; + + server = sinon.fakeServer.create(); + server.respondWith( + 'GET', + /\/changes\/42\/revisions\/(1|2)\/files\/sieve\.go\/diff(.*)/, + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({ + change_type: 'MODIFIED', + content: [ + { + ab: [ + '<!DOCTYPE html>', + '<meta charset="utf-8">', + '<title>My great page</title>', + '<style>', + ' *,', + ' *:before,', + ' *:after {', + ' box-sizing: border-box;', + ' }', + '</style>', + '<header>', + ] + }, + { + a: [ + ' Welcome ', + ' to the wooorld of tomorrow!', + ], + b: [ + ' Hello, world!', + ], + }, + { + ab: [ + '</header>', + '<body>', + 'Leela: This is the only place the ship can’t hear us, so ', + 'everyone pretend to shower.', + 'Fry: Same as every day. Got it.', + ] + }, + ] + }), + ] + ); + server.respondWith( + 'GET', + '/changes/42/revisions/1/comments', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({ + '/COMMIT_MSG': [], + 'sieve.go': [ + { + author: { + _account_id: 1000000, + name: 'Andrew Bonventre', + email: 'andybons@gmail.com', + }, + id: '9af53d3f_5f2b8b82', + line: 1, + message: 'this isn’t quite right', + updated: '2015-12-10 02:50:21.627000000', + }, + { + author: { + _account_id: 1000000, + name: 'Andrew Bonventre', + email: 'andybons@gmail.com', + }, + id: '9af53d3f_bf1cd76b', + line: 1, + side: 'PARENT', + message: 'how did this work in the first place?', + updated: '2015-12-10 00:08:42.255000000', + }, + ], + }), + ] + ); + server.respondWith( + 'GET', + '/changes/42/revisions/2/comments', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({ + '/COMMIT_MSG': [], + 'sieve.go': [ + { + author: { + _account_id: 1010008, + name: 'Dave Borowitz', + email: 'dborowitz@google.com', + }, + id: '001a2067_f30f3048', + line: 12, + message: 'What on earth are you thinking, here?', + updated: '2015-12-12 02:51:37.973000000', + }, + { + author: { + _account_id: 1010008, + name: 'Dave Borowitz', + email: 'dborowitz@google.com', + }, + id: '001a2067_f6b1b1c8', + in_reply_to: '9af53d3f_bf1cd76b', + line: 1, + side: 'PARENT', + message: 'Yeah not sure how this worked either?', + updated: '2015-12-12 02:51:37.973000000', + }, + { + author: { + _account_id: 1000000, + name: 'Andrew Bonventre', + email: 'andybons@gmail.com', + }, + id: 'a0407443_30dfe8fb', + in_reply_to: '001a2067_f30f3048', + line: 12, + message: '¯\\_(ツ)_/¯', + updated: '2015-12-12 18:50:21.627000000', + }, + ], + }), + ] + ); + + server.respondWith( + 'PUT', + '/accounts/self/preferences.diff', + [ + 200, + {'Content-Type': 'application/json'}, + ')]}\'\n' + + JSON.stringify({context: 25}), + ] + ); + + }); + + teardown(function() { + server.restore(); + }); + + test('comments with parent', function(done) { + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + + element.reload(); + server.respond(); + + element._diffRequestsPromise.then(function() { + assert.equal(element._baseComments.length, 1); + assert.equal(element._comments.length, 1); + assert.equal(element._baseDrafts.length, 0); + assert.equal(element._drafts.length, 0); + done(); + }); + }); + + test('comments between two patches', function(done) { + element.patchRange = { + basePatchNum: 1, + patchNum: 2, + }; + + element.reload(); + server.respond(); + + element._diffRequestsPromise.then(function() { + assert.equal(element._baseComments.length, 1); + assert.equal(element._comments.length, 2); + assert.equal(element._baseDrafts.length, 0); + assert.equal(element._drafts.length, 0); + done(); + }); + }); + + test('comment rendering', function(done) { + element.prefs.context = -1; + element._loggedIn = true; + element.patchRange = { + basePatchNum: 1, + patchNum: 2, + }; + + element.reload(); + server.respond(); + + // Allow events to fire and the threads to render. + element.async(function() { + var leftThreadEls = + Polymer.dom(element.$.leftDiff.root).querySelectorAll( + 'gr-diff-comment-thread'); + assert.equal(leftThreadEls.length, 1); + assert.equal(leftThreadEls[0].comments.length, 1); + + var rightThreadEls = + Polymer.dom(element.$.rightDiff.root).querySelectorAll( + 'gr-diff-comment-thread'); + assert.equal(rightThreadEls.length, 1); + assert.equal(rightThreadEls[0].comments.length, 2); + + var index = leftThreadEls[0].getAttribute('data-index'); + var leftFillerEls = + Polymer.dom(element.$.leftDiff.root).querySelectorAll( + '.commentThread.filler[data-index="' + index + '"]'); + assert.equal(leftFillerEls.length, 1); + var rightFillerEls = + Polymer.dom(element.$.rightDiff.root).querySelectorAll( + '[data-index="' + index + '"]'); + assert.equal(rightFillerEls.length, 2); + + for (var i = 0; i < rightFillerEls.length; i++) { + assert.isTrue(rightFillerEls[i].classList.contains('filler')); + } + var originalHeight = rightFillerEls[0].offsetHeight; + assert.equal(rightFillerEls[1].offsetHeight, originalHeight); + assert.equal(leftThreadEls[0].offsetHeight, originalHeight); + assert.equal(leftFillerEls[0].offsetHeight, originalHeight); + + // Create a comment on the opposite side of the first comment. + var rightLineEL = element.$.rightDiff.$$( + '.lineNum[data-index="' + (index - 1) + '"]'); + assert.ok(rightLineEL); + MockInteractions.tap(rightLineEL); + element.async(function() { + var newThreadEls = + Polymer.dom(element.$.rightDiff.root).querySelectorAll( + '[data-index="' + index + '"]'); + assert.equal(newThreadEls.length, 2); + for (var i = 0; i < newThreadEls.length; i++) { + assert.isTrue( + newThreadEls[i].classList.contains('commentThread') || + newThreadEls[i].tagName == 'GR-DIFF-COMMENT-THREAD'); + } + var newHeight = newThreadEls[0].offsetHeight; + assert.equal(newThreadEls[1].offsetHeight, newHeight); + assert.equal(leftFillerEls[0].offsetHeight, newHeight); + assert.equal(leftThreadEls[0].offsetHeight, newHeight); + + // The editing mode height of the right comment will be greater than + // the non-editing mode height of the left comment. + assert.isAbove(newHeight, originalHeight); + + // Discard the right thread and ensure the left comment heights are + // back to their original values. + newThreadEls[1].addEventListener('discard', function() { + rightFillerEls = + Polymer.dom(element.$.rightDiff.root).querySelectorAll( + '[data-index="' + index + '"]'); + assert.equal(rightFillerEls.length, 2); + + for (var i = 0; i < rightFillerEls.length; i++) { + assert.isTrue(rightFillerEls[i].classList.contains('filler')); + } + var originalHeight = rightFillerEls[0].offsetHeight; + assert.equal(rightFillerEls[1].offsetHeight, originalHeight); + assert.equal(leftThreadEls[0].offsetHeight, originalHeight); + assert.equal(leftFillerEls[0].offsetHeight, originalHeight); + done(); + }); + var commentEl = newThreadEls[1].$$('gr-diff-comment'); + commentEl.fire('discard', null, {bubbles: false}); + }, 1); + }, 1); + }); + + test('intraline normalization', function() { + // The content and highlights are in the format returned by the Gerrit + // REST API. + var content = [ + ' <section class="summary">', + ' <gr-linked-text content="' + + '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>', + ' </section>', + ]; + var highlights = [ + [31, 34], [42, 26] + ]; + var results = element._normalizeIntralineHighlights(content, highlights); + assert.deepEqual(results, [ + { + contentIndex: 0, + startIndex: 31, + }, + { + contentIndex: 1, + startIndex: 0, + endIndex: 33, + }, + { + contentIndex: 1, + startIndex: 75, + }, + { + contentIndex: 2, + startIndex: 0, + endIndex: 6, + } + ]); + + content = [ + ' this._path = value.path;', + '', + ' // When navigating away from the page, there is a possibility that the', + ' // patch number is no longer a part of the URL (say when navigating to', + ' // the top-level change info view) and therefore undefined in `params`.', + ' if (!this._patchRange.patchNum) {', + ]; + highlights = [ + [14, 17], + [11, 70], + [12, 67], + [12, 67], + [14, 29], + ]; + results = element._normalizeIntralineHighlights(content, highlights); + assert.deepEqual(results, [ + { + contentIndex: 0, + startIndex: 14, + endIndex: 31, + }, + { + contentIndex: 2, + startIndex: 8, + endIndex: 78, + }, + { + contentIndex: 3, + startIndex: 11, + endIndex: 78, + }, + { + contentIndex: 4, + startIndex: 11, + endIndex: 78, + }, + { + contentIndex: 5, + startIndex: 12, + endIndex: 41, + } + ]); + }); + + test('context', function() { + element.prefs.context = 3; + element._diffResponse = { + content: [ + { + ab: [ + '<!DOCTYPE html>', + '<meta charset="utf-8">', + '<title>My great page</title>', + '<style>', + ' *,', + ' *:before,', + ' *:after {', + ' box-sizing: border-box;', + ' }', + '</style>', + '<header>', + ] + }, + { + a: [ + ' Welcome ', + ' to the wooorld of tomorrow!', + ], + b: [ + ' Hello, world!', + ], + }, + { + ab: [ + '</header>', + '<body>', + 'Leela: This is the only place the ship can’t hear us, so ', + 'everyone pretend to shower.', + 'Fry: Same as every day. Got it.', + ] + }, + ] + }; + element._processContent(); + + // First eight lines should be hidden on both sides. + for (var i = 0; i < 8; i++) { + assert.isTrue(element._diff.leftSide[i].hidden); + assert.isTrue(element._diff.rightSide[i].hidden); + } + // A context control should be at index 8 on both sides. + var leftContext = element._diff.leftSide[8]; + var rightContext = element._diff.rightSide[8]; + assert.deepEqual(leftContext, rightContext); + assert.equal(leftContext.numLines, 8); + assert.equal(leftContext.start, 0); + assert.equal(leftContext.end, 8); + + // Line indices 9-16 should be shown. + for (var i = 9; i <= 16; i++) { + // notOk (falsy) because the `hidden` attribute may not be present. + assert.notOk(element._diff.leftSide[i].hidden); + assert.notOk(element._diff.rightSide[i].hidden); + } + + // Lines at indices 17 and 18 should be hidden. + assert.isTrue(element._diff.leftSide[17].hidden); + assert.isTrue(element._diff.rightSide[17].hidden); + assert.isTrue(element._diff.leftSide[18].hidden); + assert.isTrue(element._diff.rightSide[18].hidden); + + // Context control at index 19. + leftContext = element._diff.leftSide[19]; + rightContext = element._diff.rightSide[19]; + assert.deepEqual(leftContext, rightContext); + assert.equal(leftContext.numLines, 2); + assert.equal(leftContext.start, 17); + assert.equal(leftContext.end, 19); + }); + + test('save prefs', function(done) { + element._loggedIn = false; + + element.prefs = { + tab_size: 4, + context: 50, + }; + element.fire('save', {}, {node: element.$$('gr-diff-preferences')}); + assert.isTrue(element._diffPreferencesPromise == null); + + element._loggedIn = true; + element.fire('save', {}, {node: element.$$('gr-diff-preferences')}); + server.respond(); + + element._diffPreferencesPromise.then(function(req) { + assert.equal(req.xhr.requestBody, JSON.stringify(element.prefs)); + done(); + }); + }); + + test('visible line length', function() { + assert.equal(element._visibleLineLength('A'.repeat(5)), 5); + assert.equal( + element._visibleLineLength('A'.repeat(5) + '\t' + 'A'.repeat(5)), 18); + }); + + test('break up common diff chunks', function() { + element._groupedBaseComments = { + 1: {}, + }; + element._groupedComments = { + 10: {}, + }; + var ctx = { + left: {lineNum: 0}, + right: {lineNum: 0}, + }; + var content = [ + { + ab: [ + 'Copyright (C) 2015 The Android Open Source Project', + '', + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.', + 'You may obtain a copy of the License at', + '', + 'http://www.apache.org/licenses/LICENSE-2.0', + '', + 'Unless required by applicable law or agreed to in writing, ', + 'software distributed under the License is distributed on an ', + '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ', + 'either express or implied. See the License for the specific ', + 'language governing permissions and limitations under the License.', + ] + } + ]; + var result = element._breakUpCommonChunksWithComments(ctx, content); + assert.deepEqual(result, [ + { + __noHighlight: true, + a: ['Copyright (C) 2015 The Android Open Source Project'], + b: ['Copyright (C) 2015 The Android Open Source Project'], + }, + { + ab: [ + '', + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.', + 'You may obtain a copy of the License at', + '', + 'http://www.apache.org/licenses/LICENSE-2.0', + '', + 'Unless required by applicable law or agreed to in writing, ', + ] + }, + { + __noHighlight: true, + a: ['software distributed under the License is distributed on an '], + b: ['software distributed under the License is distributed on an '] + }, + { + ab: [ + '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ', + 'either express or implied. See the License for the specific ', + 'language governing permissions and limitations under the License.', + ] + } + ]); + }); + }); + +</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html new file mode 100644 index 0000000..b0ee0b73 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -0,0 +1,53 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-patch-range-select"> + <template> + <style> + :host { + display: block; + } + .patchRange { + display: inline-block; + } + </style> + Patch set: + <span class="patchRange"> + <select id="leftPatchSelect" on-change="_handlePatchChange"> + <option value="PARENT" + selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option> + <template is="dom-repeat" items="{{availablePatches}}" as="patchNum"> + <option value$="[[patchNum]]" + selected$="[[_computeLeftSelected(patchNum, patchRange)]]" + disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option> + </template> + </select> + </span> + → + <span class="patchRange"> + <select id="rightPatchSelect" on-change="_handlePatchChange"> + <template is="dom-repeat" items="{{availablePatches}}" as="patchNum"> + <option value$="[[patchNum]]" + selected$="[[_computeRightSelected(patchNum, patchRange)]]" + disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option> + </template> + </select> + </span> + </template> + <script src="gr-patch-range-select.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js new file mode 100644 index 0000000..3439ecd --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -0,0 +1,54 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-patch-range-select', + + properties: { + availablePatches: Array, + changeNum: String, + patchRange: Object, + path: String, + }, + + _handlePatchChange: function(e) { + var leftPatch = this.$.leftPatchSelect.value; + var rightPatch = this.$.rightPatchSelect.value; + var rangeStr = rightPatch; + if (leftPatch != 'PARENT') { + rangeStr = leftPatch + '..' + rangeStr; + } + page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path); + }, + + _computeLeftSelected: function(patchNum, patchRange) { + return patchNum == patchRange.basePatchNum; + }, + + _computeRightSelected: function(patchNum, patchRange) { + return patchNum == patchRange.patchNum; + }, + + _computeLeftDisabled: function(patchNum, patchRange) { + return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10); + }, + + _computeRightDisabled: function(patchNum, patchRange) { + if (patchRange.basePatchNum == 'PARENT') { return false; } + return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html new file mode 100644 index 0000000..a7d909e --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -0,0 +1,93 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-patch-range-select</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-patch-range-select.html"> + +<test-fixture id="basic"> + <template> + <gr-patch-range-select auto></gr-patch-range-select> + </template> +</test-fixture> + +<script> + suite('gr-patch-range-select tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('enabled/disabled options', function() { + var patchRange = { + basePatchNum: 'PARENT', + patchNum: '3', + }; + ['1', '2', '3'].forEach(function(patchNum) { + assert.isFalse(element._computeRightDisabled(patchNum, patchRange)); + }); + ['PARENT', '1', '2'].forEach(function(patchNum) { + assert.isFalse(element._computeLeftDisabled(patchNum, patchRange)); + }); + assert.isTrue(element._computeLeftDisabled('3', patchRange)); + + patchRange.basePatchNum = '2'; + assert.isTrue(element._computeLeftDisabled('3', patchRange)); + assert.isTrue(element._computeRightDisabled('1', patchRange)); + assert.isTrue(element._computeRightDisabled('2', patchRange)); + assert.isFalse(element._computeRightDisabled('3', patchRange)); + }); + + test('navigation', function(done) { + var showStub = sinon.stub(page, 'show'); + var leftSelectEl = element.$.leftPatchSelect; + var rightSelectEl = element.$.rightPatchSelect; + element.changeNum = '42'; + element.path = 'path/to/file.txt'; + element.availablePatches = ['1', '2', '3']; + flushAsynchronousOperations(); + + var numEvents = 0; + leftSelectEl.addEventListener('change', function(e) { + numEvents++; + if (numEvents == 1) { + assert(showStub.lastCall.calledWithExactly( + '/c/42/3/path/to/file.txt'), + 'Should navigate to /c/42/3/path/to/file.txt'); + leftSelectEl.value = '1'; + element.fire('change', {}, {node: leftSelectEl}); + } else if (numEvents == 2) { + assert(showStub.lastCall.calledWithExactly( + '/c/42/1..3/path/to/file.txt'), + 'Should navigate to /c/42/1..3/path/to/file.txt'); + showStub.restore(); + done(); + } + }); + leftSelectEl.value = 'PARENT'; + rightSelectEl.value = '3'; + element.fire('change', {}, {node: leftSelectEl}); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-ajax.html b/polygerrit-ui/app/elements/gr-ajax.html deleted file mode 100644 index 58b647e..0000000 --- a/polygerrit-ui/app/elements/gr-ajax.html +++ /dev/null
@@ -1,105 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html"> - -<dom-module id="gr-ajax"> - <template> - <iron-ajax id="xhr" - auto="[[auto]]" - url="[[url]]" - params="[[params]]" - json-prefix=")]}'" - last-error="{{lastError}}" - last-response="{{lastResponse}}" - loading="{{loading}}" - on-response="_handleResponse" - on-error="_handleError" - debounce-duration="300"></iron-ajax> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-ajax', - - /** - * Fired when a response is received. - * - * @event response - */ - - /** - * Fired when an error is received. - * - * @event error - */ - - hostAttributes: { - hidden: true - }, - - properties: { - auto: { - type: Boolean, - value: false, - }, - url: String, - params: { - type: Object, - value: function() { - return {}; - }, - }, - lastError: { - type: Object, - notify: true, - }, - lastResponse: { - type: Object, - notify: true, - }, - loading: { - type: Boolean, - notify: true, - }, - }, - - ready: function() { - // Used for debugging which element a request came from. - var headers = this.$.xhr.headers; - headers['x-requesting-element-id'] = this.id || 'gr-ajax (no id)'; - this.$.xhr.headers = headers; - }, - - generateRequest: function() { - return this.$.xhr.generateRequest(); - }, - - _handleResponse: function(e, req) { - this.fire('response', req, {bubbles: false}); - }, - - _handleError: function(e, req) { - this.fire('error', req, {bubbles: false}); - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index 8890983..e7fa31c 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -17,15 +17,17 @@ <link rel="import" href="../bower_components/polymer/polymer.html"> <link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> <link rel="import" href="../styles/app-theme.html"> -<link rel="import" href="gr-account-dropdown.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-change-list-view.html"> -<link rel="import" href="gr-change-view.html"> -<link rel="import" href="gr-dashboard-view.html"> -<link rel="import" href="gr-diff-view.html"> -<link rel="import" href="gr-keyboard-shortcuts-dialog.html"> -<link rel="import" href="gr-overlay.html"> -<link rel="import" href="gr-search-bar.html"> + +<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html"> +<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html"> +<link rel="import" href="./change/gr-change-view/gr-change-view.html"> +<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html"> + +<link rel="import" href="./shared/gr-account-dropdown/gr-account-dropdown.html"> +<link rel="import" href="./shared/gr-ajax/gr-ajax.html"> +<link rel="import" href="./shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html"> +<link rel="import" href="./shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="./shared/gr-search-bar/gr-search-bar.html"> <script src="../bower_components/page/page.js"></script> <script src="../scripts/app.js"></script> @@ -166,164 +168,5 @@ on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog> </gr-overlay> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-app', - - properties: { - account: { - type: Object, - observer: '_accountChanged', - }, - accountReady: { - type: Object, - readOnly: true, - notify: true, - value: function() { - return new Promise(function(resolve) { - this._resolveAccountReady = resolve; - }.bind(this)); - }, - }, - config: { - type: Object, - observer: '_configChanged', - }, - configReady: { - type: Object, - readOnly: true, - notify: true, - value: function() { - return new Promise(function(resolve) { - this._resolveConfigReady = resolve; - }.bind(this)); - }, - }, - version: String, - params: Object, - keyEventTarget: { - type: Object, - value: function() { return document.body; }, - }, - - _diffPreferences: Object, - _showChangeListView: Boolean, - _showDashboardView: Boolean, - _showChangeView: Boolean, - _showDiffView: Boolean, - _viewState: Object, - }, - - listeners: { - 'title-change': '_handleTitleChange', - }, - - observers: [ - '_viewChanged(params.view)', - ], - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - ], - - get loggedIn() { - return !!(this.account && Object.keys(this.account).length > 0); - }, - - ready: function() { - this._viewState = { - changeView: { - changeNum: null, - patchNum: null, - selectedFileIndex: 0, - showReplyDialog: false, - }, - changeListView: { - query: null, - offset: 0, - selectedChangeIndex: 0, - }, - dashboardView: { - selectedChangeIndex: 0, - }, - }; - }, - - _accountChanged: function() { - this._resolveAccountReady(); - this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn); - this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn); - if (this.loggedIn) { - this.$.diffPreferencesXHR.generateRequest(); - } else { - // These defaults should match the defaults in - // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java - // NOTE: There are some settings that don't apply to PolyGerrit - // (Render mode being at least one of them). - this._diffPreferences = { - auto_hide_diff_table_header: true, - context: 10, - cursor_blink_rate: 0, - ignore_whitespace: 'IGNORE_NONE', - intraline_difference: true, - line_length: 100, - show_line_endings: true, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - tab_size: 8, - theme: 'DEFAULT', - }; - } - }, - - _configChanged: function(config) { - this._resolveConfigReady(config); - }, - - _viewChanged: function(view) { - this.set('_showChangeListView', view == 'gr-change-list-view'); - this.set('_showDashboardView', view == 'gr-dashboard-view'); - this.set('_showChangeView', view == 'gr-change-view'); - this.set('_showDiffView', view == 'gr-diff-view'); - }, - - _loginTapHandler: function(e) { - e.preventDefault(); - page.show('/login/' + encodeURIComponent( - window.location.pathname + window.location.hash)); - }, - - _computeLoggedIn: function(account) { // argument used for binding update only - return this.loggedIn; - }, - - _handleTitleChange: function(e) { - if (e.detail.title) { - document.title = e.detail.title + ' · Gerrit Code Review'; - } else { - document.title = ''; - } - }, - - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - switch (e.keyCode) { - case 191: // '/' or '?' with shift key. - // TODO(andybons): Localization using e.key/keypress event. - if (!e.shiftKey) { break; } - this.$.keyboardShortcuts.open(); - } - }, - - _handleKeyboardShortcutDialogClose: function() { - this.$.keyboardShortcuts.close(); - }, - }); - })(); - </script> + <script src="gr-app.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js new file mode 100644 index 0000000..23ee6c6 --- /dev/null +++ b/polygerrit-ui/app/elements/gr-app.js
@@ -0,0 +1,171 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-app', + + properties: { + account: { + type: Object, + observer: '_accountChanged', + }, + accountReady: { + type: Object, + readOnly: true, + notify: true, + value: function() { + return new Promise(function(resolve) { + this._resolveAccountReady = resolve; + }.bind(this)); + }, + }, + config: { + type: Object, + observer: '_configChanged', + }, + configReady: { + type: Object, + readOnly: true, + notify: true, + value: function() { + return new Promise(function(resolve) { + this._resolveConfigReady = resolve; + }.bind(this)); + }, + }, + version: String, + params: Object, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, + + _diffPreferences: Object, + _showChangeListView: Boolean, + _showDashboardView: Boolean, + _showChangeView: Boolean, + _showDiffView: Boolean, + _viewState: Object, + }, + + listeners: { + 'title-change': '_handleTitleChange', + }, + + observers: [ + '_viewChanged(params.view)', + ], + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + get loggedIn() { + return !!(this.account && Object.keys(this.account).length > 0); + }, + + ready: function() { + this._viewState = { + changeView: { + changeNum: null, + patchNum: null, + selectedFileIndex: 0, + showReplyDialog: false, + }, + changeListView: { + query: null, + offset: 0, + selectedChangeIndex: 0, + }, + dashboardView: { + selectedChangeIndex: 0, + }, + }; + }, + + _accountChanged: function() { + this._resolveAccountReady(); + this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn); + this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn); + if (this.loggedIn) { + this.$.diffPreferencesXHR.generateRequest(); + } else { + // These defaults should match the defaults in + // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java + // NOTE: There are some settings that don't apply to PolyGerrit + // (Render mode being at least one of them). + this._diffPreferences = { + auto_hide_diff_table_header: true, + context: 10, + cursor_blink_rate: 0, + ignore_whitespace: 'IGNORE_NONE', + intraline_difference: true, + line_length: 100, + show_line_endings: true, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + tab_size: 8, + theme: 'DEFAULT', + }; + } + }, + + _configChanged: function(config) { + this._resolveConfigReady(config); + }, + + _viewChanged: function(view) { + this.set('_showChangeListView', view == 'gr-change-list-view'); + this.set('_showDashboardView', view == 'gr-dashboard-view'); + this.set('_showChangeView', view == 'gr-change-view'); + this.set('_showDiffView', view == 'gr-diff-view'); + }, + + _loginTapHandler: function(e) { + e.preventDefault(); + page.show('/login/' + encodeURIComponent( + window.location.pathname + window.location.hash)); + }, + + _computeLoggedIn: function(account) { // argument used for binding update only + return this.loggedIn; + }, + + _handleTitleChange: function(e) { + if (e.detail.title) { + document.title = e.detail.title + ' · Gerrit Code Review'; + } else { + document.title = ''; + } + }, + + _handleKey: function(e) { + if (this.shouldSupressKeyboardShortcut(e)) { return; } + + switch (e.keyCode) { + case 191: // '/' or '?' with shift key. + // TODO(andybons): Localization using e.key/keypress event. + if (!e.shiftKey) { break; } + this.$.keyboardShortcuts.open(); + } + }, + + _handleKeyboardShortcutDialogClose: function() { + this.$.keyboardShortcuts.close(); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/gr-avatar.html b/polygerrit-ui/app/elements/gr-avatar.html deleted file mode 100644 index c6b14c7..0000000 --- a/polygerrit-ui/app/elements/gr-avatar.html +++ /dev/null
@@ -1,83 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> - -<dom-module id="gr-avatar"> - <template> - <style> - :host { - display: inline-block; - border-radius: 50%; - background-size: cover; - background-color: var(--background-color, #f1f2f3); - } - </style> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-avatar', - - properties: { - account: { - type: Object, - observer: '_accountChanged', - }, - imageSize: { - type: Number, - value: 16, - }, - }, - - created: function() { - this.hidden = true; - }, - - ready: function() { - app.configReady.then(function(cfg) { - var hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); - if (hasAvatars) { - this.hidden = false; - this._updateAvatarURL(this.account); // src needs to be set if avatar becomes visible - } - }.bind(this)); - }, - - _accountChanged: function(account) { - this._updateAvatarURL(account); - }, - - _updateAvatarURL: function(account) { - if (!this.hidden && account) { - var url = this._buildAvatarURL(this.account); - if (url) { - this.style.backgroundImage = 'url("' + url + '")'; - } - } - }, - - _buildAvatarURL: function(account) { - if (!account) { return ''; } - return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-actions.html b/polygerrit-ui/app/elements/gr-change-actions.html deleted file mode 100644 index 2381ce4..0000000 --- a/polygerrit-ui/app/elements/gr-change-actions.html +++ /dev/null
@@ -1,295 +0,0 @@ -<!-- -Copyright (C) 2016 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-input/iron-input.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-confirm-rebase-dialog.html"> -<link rel="import" href="gr-overlay.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-change-actions"> - <template> - <style> - :host { - display: block; - } - gr-button { - display: block; - margin-bottom: .5em; - } - gr-button:before { - content: attr(data-label); - } - gr-button[loading]:before { - content: attr(data-loading-label); - } - @media screen and (max-width: 50em) { - .confirmDialog { - width: 90vw; - } - } - </style> - <gr-ajax id="actionsXHR" - url="[[_computeRevisionActionsPath(changeNum, patchNum)]]" - last-response="{{_revisionActions}}" - loading="{{_loading}}"></gr-ajax> - <div> - <template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action"> - <gr-button title$="[[action.title]]" - primary$="[[_computePrimary(action.__key)]]" - hidden$="[[!action.enabled]]" - data-action-key$="[[action.__key]]" - data-action-type$="[[action.__type]]" - data-label$="[[action.label]]" - on-tap="_handleActionTap"></gr-button> - </template> - <template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action"> - <gr-button title$="[[action.title]]" - primary$="[[_computePrimary(action.__key)]]" - disabled$="[[!action.enabled]]" - data-action-key$="[[action.__key]]" - data-action-type$="[[action.__type]]" - data-label$="[[action.label]]" - data-loading-label$="[[_computeLoadingLabel(action.__key)]]" - on-tap="_handleActionTap"></gr-button> - </template> - </div> - <gr-overlay id="overlay" with-backdrop> - <gr-confirm-rebase-dialog id="confirmRebase" - class="confirmDialog" - on-confirm="_handleRebaseConfirm" - on-cancel="_handleConfirmDialogCancel" - hidden></gr-confirm-rebase-dialog> - </gr-overlay> - </template> - <script> - (function() { - 'use strict'; - - // TODO(davido): Add the rest of the change actions. - var ChangeActions = { - ABANDON: 'abandon', - DELETE: '/', - RESTORE: 'restore', - }; - - // TODO(andybons): Add the rest of the revision actions. - var RevisionActions = { - DELETE: '/', - PUBLISH: 'publish', - REBASE: 'rebase', - SUBMIT: 'submit', - }; - - Polymer({ - is: 'gr-change-actions', - - /** - * Fired when the change should be reloaded. - * - * @event reload-change - */ - - properties: { - actions: { - type: Object, - }, - changeNum: String, - patchNum: String, - _loading: { - type: Boolean, - value: true, - }, - _revisionActions: Object, - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - observers: [ - '_actionsChanged(actions, _revisionActions)', - ], - - reload: function() { - if (!this.changeNum || !this.patchNum) { - return Promise.resolve(); - } - return this.$.actionsXHR.generateRequest().completes; - }, - - _actionsChanged: function(actions, revisionActions) { - this.hidden = - revisionActions.rebase == null && - revisionActions.submit == null && - revisionActions.publish == null && - actions.abandon == null && - actions.restore == null; - }, - - _computeRevisionActionsPath: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/actions'; - }, - - _getValuesFor: function(obj) { - return Object.keys(obj).map(function(key) { - return obj[key]; - }); - }, - - _computeActionValues: function(actions, type) { - var result = []; - var values = this._getValuesFor( - type == 'change' ? ChangeActions : RevisionActions); - for (var a in actions) { - if (values.indexOf(a) == -1) { continue; } - actions[a].__key = a; - actions[a].__type = type; - result.push(actions[a]); - } - return result; - }, - - _computeLoadingLabel: function(action) { - return { - 'rebase': 'Rebasing...', - 'submit': 'Submitting...', - }[action]; - }, - - _computePrimary: function(actionKey) { - return actionKey == 'submit'; - }, - - _computeButtonClass: function(action) { - if ([RevisionActions.SUBMIT, - RevisionActions.PUBLISH].indexOf(action) != -1) { - return 'primary'; - } - return ''; - }, - - _handleActionTap: function(e) { - e.preventDefault(); - var el = Polymer.dom(e).rootTarget; - var key = el.getAttribute('data-action-key'); - var type = el.getAttribute('data-action-type'); - if (type == 'revision') { - if (key == RevisionActions.REBASE) { - this._showRebaseDialog(); - return; - } - this._fireRevisionAction(this._prependSlash(key), - this._revisionActions[key]); - } else { - this._fireChangeAction(this._prependSlash(key), this.actions[key]); - } - }, - - _prependSlash: function(key) { - return key == '/' ? key : '/' + key; - }, - - _handleConfirmDialogCancel: function() { - var dialogEls = - Polymer.dom(this.root).querySelectorAll('.confirmDialog'); - for (var i = 0; i < dialogEls.length; i++) { - dialogEls[i].hidden = true; - } - this.$.overlay.close(); - }, - - _handleRebaseConfirm: function() { - var payload = {}; - var el = this.$.confirmRebase; - if (el.clearParent) { - // There is a subtle but important difference between setting the base - // to an empty string and omitting it entirely from the payload. An - // empty string implies that the parent should be cleared and the - // change should be rebased on top of the target branch. Leaving out - // the base implies that it should be rebased on top of its current - // parent. - payload.base = ''; - } else if (el.base && el.base.length > 0) { - payload.base = el.base; - } - this.$.overlay.close(); - el.hidden = false; - this._fireRevisionAction('/rebase', this._revisionActions.rebase, - payload); - }, - - _fireChangeAction: function(endpoint, action) { - this._send(action.method, {}, endpoint).then( - function() { - // We can’t reload a change that was deleted. - if (endpoint == ChangeActions.DELETE) { - page.show('/'); - } else { - this.fire('reload-change', null, {bubbles: false}); - } - }.bind(this)).catch(function(err) { - alert('Oops. Something went wrong. Check the console and bug the ' + - 'PolyGerrit team for assistance.'); - throw err; - }); - }, - - _fireRevisionAction: function(endpoint, action, opt_payload) { - var buttonEl = this.$$('[data-action-key="' + action.__key + '"]'); - buttonEl.setAttribute('loading', true); - buttonEl.disabled = true; - function enableButton() { - buttonEl.removeAttribute('loading'); - buttonEl.disabled = false; - } - - this._send(action.method, opt_payload, endpoint, true).then( - function() { - this.fire('reload-change', null, {bubbles: false}); - enableButton(); - }.bind(this)).catch(function(err) { - // TODO(andybons): Handle merge conflict (409 status); - alert('Oops. Something went wrong. Check the console and bug the ' + - 'PolyGerrit team for assistance.'); - enableButton(); - throw err; - }); - }, - - _showRebaseDialog: function() { - this.$.confirmRebase.hidden = false; - this.$.overlay.open(); - }, - - _send: function(method, payload, actionEndpoint, revisionAction) { - var xhr = document.createElement('gr-request'); - this._xhrPromise = xhr.send({ - method: method, - url: this.changeBaseURL(this.changeNum, - revisionAction ? this.patchNum : null) + actionEndpoint, - body: payload, - }); - - return this._xhrPromise; - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-item.html b/polygerrit-ui/app/elements/gr-change-list-item.html deleted file mode 100644 index 1e57e09..0000000 --- a/polygerrit-ui/app/elements/gr-change-list-item.html +++ /dev/null
@@ -1,210 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../styles/gr-change-list-styles.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-account-link.html"> -<link rel="import" href="gr-change-star.html"> -<link rel="import" href="gr-date-formatter.html"> - -<dom-module id="gr-change-list-item"> - <template> - <style> - :host { - display: flex; - border-bottom: 1px solid #eee; - } - :host([selected]) { - background-color: #ebf5fb; - } - :host([needs-review]) { - font-weight: bold; - } - .cell { - flex-shrink: 0; - padding: .3em .5em; - } - a { - color: var(--default-text-color); - text-decoration: none; - } - a:hover { - text-decoration: underline; - } - .positionIndicator { - visibility: hidden; - } - :host([selected]) .positionIndicator { - visibility: visible; - } - .u-monospace { - font-family: var(--monospace-font-family); - } - .u-green { - color: #388E3C; - } - .u-red { - color: #D32F2F; - } - </style> - <style include="gr-change-list-styles"></style> - <span class="cell keyboard"> - <span class="positionIndicator">▶</span> - </span> - <span class="cell star" hidden$="[[!showStar]]"> - <gr-change-star change="{{change}}"></gr-change-star> - </span> - <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a> - <span class="cell status">[[_computeChangeStatusString(change)]]</span> - <span class="cell owner"> - <gr-account-link account="[[change.owner]]"></gr-account-link> - </span> - <a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a> - <a class="cell branch" href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a> - <gr-date-formatter class="cell updated" date-str="[[change.updated]]"></gr-date-formatter> - <span class="cell size u-monospace"> - <span class="u-green"><span>+</span>[[change.insertions]]</span>, - <span class="u-red"><span>-</span>[[change.deletions]]</span> - </span> - <template is="dom-repeat" items="[[labelNames]]" as="labelName"> - <span title$="[[_computeLabelTitle(change, labelName)]]" - class$="[[_computeLabelClass(change, labelName)]]">[[_computeLabelValue(change, labelName)]]</span> - </template> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-change-list-item', - - properties: { - selected: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - needsReview: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - labelNames: { - type: Array, - }, - change: Object, - changeURL: { - type: String, - computed: '_computeChangeURL(change._number)', - }, - showStar: { - type: Boolean, - value: false, - }, - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - _computeChangeURL: function(changeNum) { - if (!changeNum) { return ''; } - return '/c/' + changeNum + '/'; - }, - - _computeChangeStatusString: function(change) { - if (change.status == this.ChangeStatus.MERGED) { - return 'Merged'; - } - if (change.mergeable != null && change.mergeable == false) { - return 'Merge Conflict'; - } - if (change.status == this.ChangeStatus.DRAFT) { - return 'Draft'; - } - if (change.status == this.ChangeStatus.ABANDONED) { - return 'Abandoned'; - } - return ''; - }, - - _computeLabelTitle: function(change, labelName) { - var label = change.labels[labelName]; - if (!label) { return labelName; } - var significantLabel = label.rejected || label.approved || - label.disliked || label.recommended; - if (significantLabel && significantLabel.name) { - return labelName + '\nby ' + significantLabel.name; - } - return labelName; - }, - - _computeLabelClass: function(change, labelName) { - var label = change.labels[labelName]; - // Mimic a Set. - var classes = { - 'cell': true, - 'label': true, - }; - if (label) { - if (label.approved) { - classes['u-green'] = true; - } - if (label.value == 1) { - classes['u-monospace'] = true; - classes['u-green'] = true; - } else if (label.value == -1) { - classes['u-monospace'] = true; - classes['u-red'] = true; - } - if (label.rejected) { - classes['u-red'] = true; - } - } - return Object.keys(classes).sort().join(' '); - }, - - _computeLabelValue: function(change, labelName) { - var label = change.labels[labelName]; - if (!label) { return ''; } - if (label.approved) { - return '✓'; - } - if (label.rejected) { - return '✕'; - } - if (label.value > 0) { - return '+' + label.value; - } - if (label.value < 0) { - return label.value; - } - return ''; - }, - - _computeProjectURL: function(project) { - return '/projects/' + project + ',dashboards/default'; - }, - - _computeProjectBranchURL: function(project, branch) { - return '/q/status:open+project:' + project + '+branch:' + branch; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-view.html b/polygerrit-ui/app/elements/gr-change-list-view.html deleted file mode 100644 index d6f7649..0000000 --- a/polygerrit-ui/app/elements/gr-change-list-view.html +++ /dev/null
@@ -1,229 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-change-list.html"> - -<dom-module id="gr-change-list-view"> - <template> - <style> - :host { - background-color: var(--view-background-color); - display: block; - margin: 0 var(--default-horizontal-margin); - } - .loading, - .error { - margin-top: 1em; - background-color: #f1f2f3; - } - .loading { - color: #666; - } - .error { - color: #D32F2F; - } - gr-change-list { - margin-top: 1em; - width: 100%; - } - nav { - margin-bottom: 1em; - padding: .5em 0; - text-align: center; - } - nav a { - display: inline-block; - } - nav a:first-of-type { - margin-right: .5em; - } - @media only screen and (max-width: 50em) { - :host { - margin: 0; - } - .loading, - .error { - padding: 0 var(--default-horizontal-margin); - } - } - </style> - <gr-ajax - auto - url="/changes/" - params="[[_computeQueryParams(_query, _offset)]]" - last-response="{{_changes}}" - last-error="{{_lastError}}" - loading="{{_loading}}"></gr-ajax> - <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div> - <div class="error" hidden$="[[_computeErrorHidden(_loading, _lastError)]]" hidden> - [[_lastError.request.xhr.responseText]] - </div> - <div hidden$="[[_computeListHidden(_loading, _lastError)]]" hidden> - <gr-change-list - changes="{{_changes}}" - selected-index="{{viewState.selectedChangeIndex}}" - show-star="[[loggedIn]]"></gr-change-list> - <nav> - <a href$="[[_computeNavLink(_query, _offset, -1)]]" - hidden$="[[_hidePrevArrow(_offset)]]">← Prev</a> - <a href$="[[_computeNavLink(_query, _offset, 1)]]" - hidden$="[[_hideNextArrow(_changes.length)]]">Next →</a> - </nav> - </div> - </template> - <script> - (function() { - 'use strict'; - - var DEFAULT_NUM_CHANGES = 25; - - Polymer({ - is: 'gr-change-list-view', - - /** - * Fired when the title of the page should change. - * - * @event title-change - */ - - properties: { - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, - - /** - * True when user is logged in. - */ - loggedIn: { - type: Boolean, - value: false, - }, - - /** - * State persisted across restamps of the element. - */ - viewState: { - type: Object, - notify: true, - value: function() { return {}; }, - }, - - /** - * Currently active query. - */ - _query: String, - - /** - * Offset of currently visible query results. - */ - _offset: Number, - - /** - * Change objects loaded from the server. - */ - _changes: Array, - - /** - * Contains error of last request (in case of change loading error). - */ - _lastError: Object, - - /** - * For showing a "loading..." string during ajax requests. - */ - _loading: { - type: Boolean, - value: true, - }, - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - attached: function() { - this.fire('title-change', {title: this._query}); - }, - - _paramsChanged: function(value) { - if (value.view != this.tagName.toLowerCase()) { return; } - - this._query = value.query; - this._offset = value.offset || 0; - if (this.viewState.query != this._query || - this.viewState.offset != this._offset) { - this.set('viewState.selectedChangeIndex', 0); - this.set('viewState.query', this._query); - this.set('viewState.offset', this._offset); - } - - this.fire('title-change', {title: this._query}); - }, - - _computeQueryParams: function(query, offset) { - var options = this.listChangesOptionsToHex( - this.ListChangesOption.LABELS, - this.ListChangesOption.DETAILED_ACCOUNTS - ); - var obj = { - n: DEFAULT_NUM_CHANGES, // Number of results to return. - O: options, - S: offset || 0, - }; - if (query && query.length > 0) { - obj.q = query; - } - return obj; - }, - - _computeNavLink: function(query, offset, direction) { - // Offset could be a string when passed from the router. - offset = +(offset || 0); - var newOffset = Math.max(0, offset + (25 * direction)); - var href = '/q/' + query; - if (newOffset > 0) { - href += ',' + newOffset; - } - return href; - }, - - _computeErrorHidden: function(loading, lastError) { - return loading || lastError == null; - }, - - _computeListHidden: function(loading, lastError) { - return loading || lastError != null; - }, - - _hidePrevArrow: function(offset) { - return offset == 0; - }, - - _hideNextArrow: function(changesLen) { - return changesLen < DEFAULT_NUM_CHANGES; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list.html b/polygerrit-ui/app/elements/gr-change-list.html deleted file mode 100644 index 1a0341a..0000000 --- a/polygerrit-ui/app/elements/gr-change-list.html +++ /dev/null
@@ -1,221 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="../styles/gr-change-list-styles.html"> -<link rel="import" href="gr-change-list-item.html"> - -<dom-module id="gr-change-list"> - <template> - <style> - :host { - display: flex; - flex-direction: column; - } - </style> - <style include="gr-change-list-styles"></style> - <div class="headerRow"> - <span class="topHeader keyboard"></span> <!-- keyboard position indicator --> - <span class="topHeader star" hidden$="[[!showStar]]"></span> - <span class="topHeader subject">Subject</span> - <span class="topHeader status">Status</span> - <span class="topHeader owner">Owner</span> - <span class="topHeader project">Project</span> - <span class="topHeader branch">Branch</span> - <span class="topHeader updated">Updated</span> - <span class="topHeader size">Size</span> - <template is="dom-repeat" items="[[labelNames]]" as="labelName"> - <span class="topHeader label" title$="[[labelName]]"> - [[_computeLabelShortcut(labelName)]] - </span> - </template> - </div> - <template is="dom-repeat" items="{{groups}}" as="changeGroup" index-as="groupIndex"> - <template is="dom-if" if="[[_groupTitle(groupIndex)]]"> - <div class="groupHeader">[[_groupTitle(groupIndex)]]</div> - </template> - <template is="dom-if" if="[[!changeGroup.length]]"> - <div class="noChanges">No changes</div> - </template> - <template is="dom-repeat" items="[[changeGroup]]" as="change"> - <gr-change-list-item - selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]" - needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]" - change="[[change]]" - show-star="[[showStar]]" - label-names="[[labelNames]]"></gr-change-list-item> - </template> - </template> - </template> - - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-change-list', - - hostAttributes: { - tabindex: 0, - }, - - properties: { - /** - * The logged-in user's account, or an empty object if no user is logged - * in. - */ - account: { - type: Object, - value: function() { return {}; }, - }, - /** - * An array of ChangeInfo objects to render. - * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info - */ - changes: { - type: Array, - observer: '_changesChanged', - }, - /** - * ChangeInfo objects grouped into arrays. The groups and changes - * properties should not be used together. - */ - groups: { - type: Array, - value: function() { return []; }, - }, - groupTitles: { - type: Array, - value: function() { return []; }, - }, - labelNames: { - type: Array, - computed: '_computeLabelNames(groups)', - }, - selectedIndex: { - type: Number, - notify: true, - }, - showStar: { - type: Boolean, - value: false, - }, - showReviewedState: { - type: Boolean, - value: false, - }, - keyEventTarget: { - type: Object, - value: function() { return document.body; }, - }, - }, - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.RESTClientBehavior, - ], - - _computeLabelNames: function(groups) { - if (!groups) { return []; } - var labels = []; - var nonExistingLabel = function(item) { - return labels.indexOf(item) < 0; - }; - for (var i = 0; i < groups.length; i++) { - var group = groups[i]; - for (var j = 0; j < group.length; j++) { - var change = group[j]; - if (!change.labels) { continue; } - var currentLabels = Object.keys(change.labels); - labels = labels.concat(currentLabels.filter(nonExistingLabel)); - } - } - return labels.sort(); - }, - - _computeLabelShortcut: function(labelName) { - return labelName.replace(/[a-z-]/g, ''); - }, - - _changesChanged: function(changes) { - this.groups = changes ? [changes] : []; - }, - - _groupTitle: function(groupIndex) { - if (groupIndex > this.groupTitles.length - 1) { return null; } - return this.groupTitles[groupIndex]; - }, - - _computeItemSelected: function(index, groupIndex, selectedIndex) { - var idx = 0; - for (var i = 0; i < groupIndex; i++) { - idx += this.groups[i].length; - } - idx += index; - return idx == selectedIndex; - }, - - _computeItemNeedsReview: function(account, change, showReviewedState) { - return showReviewedState && !change.reviewed && - change.status != this.ChangeStatus.MERGED && - account._account_id != change.owner._account_id; - }, - - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - if (this.groups == null) { return; } - var len = 0; - this.groups.forEach(function(group) { - len += group.length; - }); - switch (e.keyCode) { - case 74: // 'j' - e.preventDefault(); - if (this.selectedIndex == len - 1) { return; } - this.selectedIndex += 1; - break; - case 75: // 'k' - e.preventDefault(); - if (this.selectedIndex == 0) { return; } - this.selectedIndex -= 1; - break; - case 79: // 'o' - case 13: // 'enter' - e.preventDefault(); - page.show(this._changeURLForIndex(this.selectedIndex)); - break; - } - }, - - _changeURLForIndex: function(index) { - var changeEls = this._getListItems(); - if (index < changeEls.length && changeEls[index]) { - return changeEls[index].changeURL; - } - return ''; - }, - - _getListItems: function() { - return Polymer.dom(this.root).querySelectorAll('gr-change-list-item'); - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-star.html b/polygerrit-ui/app/elements/gr-change-star.html deleted file mode 100644 index ad377ef..0000000 --- a/polygerrit-ui/app/elements/gr-change-star.html +++ /dev/null
@@ -1,101 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-change-star"> - <template> - <style> - :host { - display: inline-block; - overflow: hidden; - } - .starButton { - background-color: transparent; - border-color: transparent; - cursor: pointer; - font-size: 1.1em; - width: 1.2em; - height: 1.2em; - outline: none; - } - .starButton svg { - fill: #ccc; - width: 1em; - height: 1em; - } - .starButton-active svg { - fill: #ffac33; - } - </style> - <button class$="[[_computeStarClass(change.starred)]]" on-tap="_handleStarTap"> - <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 --> - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657 l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657 L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg> - </button> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-change-star', - - properties: { - change: { - type: Object, - notify: true, - }, - - _xhrPromise: Object, // Used for testing. - }, - - _computeStarClass: function(starred) { - var classes = ['starButton']; - if (starred) { - classes.push('starButton-active'); - } - return classes.join(' '); - }, - - _handleStarTap: function() { - var method = this.change.starred ? 'DELETE' : 'PUT'; - this.set('change.starred', !this.change.starred); - this._send(method, this._restEndpoint()).catch(function(err) { - this.set('change.starred', !this.change.starred); - alert('Change couldn’t be starred. Check the console and contact ' + - 'the PolyGerrit team for assistance.'); - throw err; - }.bind(this)); - }, - - _send: function(method, url) { - var xhr = document.createElement('gr-request'); - this._xhrPromise = xhr.send({ - method: method, - url: url, - }); - return this._xhrPromise; - }, - - _restEndpoint: function() { - return '/accounts/self/starred.changes/' + this.change._number; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html deleted file mode 100644 index 05eb709..0000000 --- a/polygerrit-ui/app/elements/gr-change-view.html +++ /dev/null
@@ -1,661 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-account-link.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-change-actions.html"> -<link rel="import" href="gr-change-metadata.html"> -<link rel="import" href="gr-change-star.html"> -<link rel="import" href="gr-date-formatter.html"> -<link rel="import" href="gr-download-dialog.html"> -<link rel="import" href="gr-file-list.html"> -<link rel="import" href="gr-linked-text.html"> -<link rel="import" href="gr-messages-list.html"> -<link rel="import" href="gr-overlay.html"> -<link rel="import" href="gr-related-changes-list.html"> -<link rel="import" href="gr-reply-dialog.html"> - -<dom-module id="gr-change-view"> - <template> - <style> - .container { - margin: 1em var(--default-horizontal-margin); - } - .container:not(.loading) { - background-color: var(--view-background-color); - } - .container.loading { - color: #666; - } - .headerContainer { - height: 4.1em; - margin-bottom: .5em; - } - .header { - align-items: center; - background-color: var(--view-background-color); - border-bottom: 1px solid #ddd; - display: flex; - padding: 1em var(--default-horizontal-margin); - z-index: 99; /* Less than gr-overlay's backdrop */ - } - .header.pinned { - border-bottom-color: transparent; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - position: fixed; - top: 0; - transition: box-shadow 250ms linear; - width: calc(100% - (2 * var(--default-horizontal-margin))); - } - .header-title { - flex: 1; - font-size: 1.2em; - font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - gr-change-star { - margin-right: .25em; - vertical-align: -.425em; - } - .download, - .patchSelectLabel { - margin-left: var(--default-horizontal-margin); - } - .header select { - margin-left: .5em; - } - .header .reply { - margin-left: var(--default-horizontal-margin); - } - gr-reply-dialog { - min-width: 30em; - max-width: 50em; - } - .changeStatus { - color: #999; - text-transform: capitalize; - } - section { - margin: 10px 0; - padding: 10px var(--default-horizontal-margin); - } - /* Strong specificity here is needed due to - https://github.com/Polymer/polymer/issues/2531 */ - .container section.changeInfo { - border-bottom: 1px solid #ddd; - display: flex; - margin-top: 0; - padding-top: 0; - } - .changeInfo-column:not(:last-of-type) { - margin-right: 1em; - padding-right: 1em; - } - .changeMetadata { - border-right: 1px solid #ddd; - font-size: .9em; - } - gr-change-actions { - margin-top: 1em; - } - .commitMessage { - font-family: var(--monospace-font-family); - flex: 0 0 72ch; - margin-right: 2em; - margin-bottom: 1em; - } - .commitMessage h4 { - font-family: var(--font-family); - font-weight: bold; - margin-bottom: .25em; - } - .commitAndRelated { - align-content: flex-start; - display: flex; - flex: 1; - flex-wrap: wrap; - } - gr-file-list { - margin-bottom: 1em; - padding: 0 var(--default-horizontal-margin); - } - @media screen and (max-width: 50em) { - .container { - margin: .5em 0 !important; - } - .container.loading { - margin: 1em var(--default-horizontal-margin) !important; - } - .headerContainer { - height: 5.15em; - } - .header { - align-items: flex-start; - flex-direction: column; - padding: .5em var(--default-horizontal-margin) !important; - } - gr-change-star { - vertical-align: middle; - } - .header-title, - .header-actions, - .header.pinned { - width: 100% !important; - } - .header-title { - font-size: 1.1em; - } - .header-actions { - align-items: center; - display: flex; - justify-content: space-between; - margin-top: .5em; - } - gr-reply-dialog { - min-width: initial; - width: 90vw; - } - .download { - display: none; - } - .patchSelectLabel { - margin-left: 0 !important; - margin-right: .5em; - } - .header select { - margin-left: 0 !important; - margin-right: .5em; - } - .header .reply { - margin-left: 0 !important; - margin-right: .5em; - } - .changeInfo-column:not(:last-of-type) { - margin-right: 0; - padding-right: 0; - } - .changeInfo, - .commitAndRelated { - flex-direction: column; - flex-wrap: nowrap; - } - .changeMetadata { - font-size: 1em; - border-right: none; - margin-bottom: 1em; - margin-top: .25em; - max-width: none; - } - .commitMessage { - flex: initial; - margin-right: 0; - } - } - </style> - <gr-ajax id="detailXHR" - url="[[_computeDetailPath(_changeNum)]]" - params="[[_computeDetailQueryParams()]]" - last-response="{{_change}}" - loading="{{_loading}}"></gr-ajax> - <gr-ajax id="commentsXHR" - url="[[_computeCommentsPath(_changeNum)]]" - last-response="{{_comments}}"></gr-ajax> - <gr-ajax id="commitInfoXHR" - url="[[_computeCommitInfoPath(_changeNum, _patchNum)]]" - last-response="{{_commitInfo}}"></gr-ajax> - <!-- TODO(andybons): Cache the project config. --> - <gr-ajax id="configXHR" - auto - url="[[_computeProjectConfigPath(_change.project)]]" - last-response="{{_projectConfig}}"></gr-ajax> - <div class="container loading" hidden$="{{!_loading}}">Loading...</div> - <div class="container" hidden$="{{_loading}}"> - <div class="headerContainer"> - <div class="header"> - <span class="header-title"> - <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star> - <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span> - <span>[[_change.subject]]</span> - <span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span> - </span> - <span class="header-actions"> - <gr-button class="reply" hidden$="[[!_loggedIn]]" hidden on-tap="_handleReplyTap">Reply</gr-button> - <gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button> - <span> - <label class="patchSelectLabel" for="patchSetSelect">Patch set</label> - <select id="patchSetSelect" on-change="_handlePatchChange"> - <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber"> - <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]"> - <span>[[patchNumber]]</span> - / - <span>[[_computeLatestPatchNum(_change)]]</span> - </option> - </template> - </select> - </span> - </span> - </div> - </div> - <section class="changeInfo"> - <div class="changeInfo-column changeMetadata"> - <gr-change-metadata - change="[[_change]]" - mutable="[[_loggedIn]]"></gr-change-metadata> - <gr-change-actions id="actions" - actions="[[_change.actions]]" - change-num="[[_changeNum]]" - patch-num="[[_patchNum]]" - on-reload-change="_handleReloadChange"></gr-change-actions> - </div> - <div class="changeInfo-column commitAndRelated"> - <div class="commitMessage"> - <h4>Commit message</h4> - <gr-linked-text pre - content="[[_commitInfo.message]]" - config="[[_projectConfig.commentlinks]]"></gr-linked-text> - </div> - <div class="relatedChanges"> - <gr-related-changes-list id="relatedChanges" - change="[[_change]]" - server-config="[[serverConfig]]" - patch-num="[[_patchNum]]"></gr-related-changes-list> - </div> - </div> - </section> - <gr-file-list id="fileList" - change-num="[[_changeNum]]" - patch-num="[[_patchNum]]" - comments="[[_comments]]" - selected-index="{{viewState.selectedFileIndex}}"></gr-file-list> - <gr-messages-list id="messageList" - change-num="[[_changeNum]]" - messages="[[_change.messages]]" - comments="[[_comments]]" - project-config="[[_projectConfig]]" - show-reply-buttons="[[_loggedIn]]" - on-reply="_handleMessageReply"></gr-messages-list> - </div> - <gr-overlay id="downloadOverlay" with-backdrop> - <gr-download-dialog - change="[[_change]]" - patch-num="[[_patchNum]]" - config="[[serverConfig.download]]" - on-close="_handleDownloadDialogClose"></gr-download-dialog> - </gr-overlay> - <gr-overlay id="replyOverlay" - on-iron-overlay-opened="_handleReplyOverlayOpen" - with-backdrop> - <gr-reply-dialog id="replyDialog" - change-num="[[_changeNum]]" - patch-num="[[_patchNum]]" - labels="[[_change.labels]]" - permitted-labels="[[_change.permitted_labels]]" - on-send="_handleReplySent" - on-cancel="_handleReplyCancel" - hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog> - </gr-overlay> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-change-view', - - /** - * Fired when the title of the page should change. - * - * @event title-change - */ - - properties: { - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, - viewState: { - type: Object, - notify: true, - value: function() { return {}; }, - }, - serverConfig: Object, - keyEventTarget: { - type: Object, - value: function() { return document.body; }, - }, - - _comments: Object, - _change: { - type: Object, - observer: '_changeChanged', - }, - _commitInfo: Object, - _changeNum: String, - _patchNum: String, - _allPatchSets: { - type: Array, - computed: '_computeAllPatchSets(_change)', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _loading: Boolean, - _headerContainerEl: Object, - _headerEl: Object, - _projectConfig: Object, - _boundScrollHandler: { - type: Function, - value: function() { return this._handleBodyScroll.bind(this); }, - }, - }, - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.RESTClientBehavior, - ], - - ready: function() { - app.accountReady.then(function() { - this._loggedIn = app.loggedIn; - }.bind(this)); - this._headerEl = this.$$('.header'); - }, - - attached: function() { - window.addEventListener('scroll', this._boundScrollHandler); - }, - - detached: function() { - window.removeEventListener('scroll', this._boundScrollHandler); - }, - - _handleBodyScroll: function(e) { - var containerEl = this._headerContainerEl || - this.$$('.headerContainer'); - - // Calculate where the header is relative to the window. - var top = containerEl.offsetTop; - for (var offsetParent = containerEl.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { - top += offsetParent.offsetTop; - } - // The element may not be displayed yet, in which case do nothing. - if (top == 0) { return; } - - this._headerEl.classList.toggle('pinned', window.scrollY >= top); - }, - - _resetHeaderEl: function() { - var el = this._headerEl || this.$$('.header'); - this._headerEl = el; - el.classList.remove('pinned'); - }, - - _handlePatchChange: function(e) { - var patchNum = e.target.value; - var currentPatchNum = - this._change.revisions[this._change.current_revision]._number; - if (patchNum == currentPatchNum) { - page.show(this._computeChangePath(this._changeNum)); - return; - } - page.show(this._computeChangePath(this._changeNum) + '/' + patchNum); - }, - - _handleReplyTap: function(e) { - e.preventDefault(); - this.$.replyOverlay.open(); - }, - - _handleDownloadTap: function(e) { - e.preventDefault(); - this.$.downloadOverlay.open(); - }, - - _handleDownloadDialogClose: function(e) { - this.$.downloadOverlay.close(); - }, - - _handleMessageReply: function(e) { - var msg = e.detail.message.message; - var quoteStr = msg.split('\n').map( - function(line) { return '> ' + line; }).join('\n') + '\n\n'; - this.$.replyDialog.draft += quoteStr; - this.$.replyOverlay.open(); - }, - - _handleReplyOverlayOpen: function(e) { - this.$.replyDialog.reload().then(function() { - this.async(function() { this.$.replyOverlay.center() }, 1); - }.bind(this)); - this.$.replyDialog.focus(); - }, - - _handleReplySent: function(e) { - this.$.replyOverlay.close(); - this._reload(); - }, - - _handleReplyCancel: function(e) { - this.$.replyOverlay.close(); - }, - - _paramsChanged: function(value) { - if (value.view != this.tagName.toLowerCase()) { return; } - - this._changeNum = value.changeNum; - this._patchNum = value.patchNum; - if (this.viewState.changeNum != this._changeNum || - this.viewState.patchNum != this._patchNum) { - this.set('viewState.selectedFileIndex', 0); - this.set('viewState.changeNum', this._changeNum); - this.set('viewState.patchNum', this._patchNum); - } - if (!this._changeNum) { - return; - } - this._reload().then(function() { - this.$.messageList.topMargin = this._headerEl.offsetHeight; - - // Allow the message list to render before scrolling. - this.async(function() { - var msgPrefix = '#message-'; - var hash = window.location.hash; - if (hash.indexOf(msgPrefix) == 0) { - this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length)); - } - }.bind(this), 1); - - app.accountReady.then(function() { - if (!this._loggedIn) { return; } - - if (this.viewState.showReplyDialog) { - this.$.replyOverlay.open(); - this.set('viewState.showReplyDialog', false); - } - }.bind(this)); - }.bind(this)); - }, - - _changeChanged: function(change) { - if (!change) { return; } - this._patchNum = this._patchNum || - change.revisions[change.current_revision]._number; - - var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; - this.fire('title-change', {title: title}); - }, - - _computeChangePath: function(changeNum) { - return '/c/' + changeNum; - }, - - _computeChangePermalink: function(changeNum) { - return '/' + changeNum; - }, - - _computeChangeStatus: function(change, patchNum) { - var status = change.status; - if (status == this.ChangeStatus.NEW) { - var rev = this._getRevisionNumber(change, patchNum); - // TODO(davido): Figure out, why sometimes revision is not there - if (rev == undefined || !rev.draft) { return ''; } - status = this.ChangeStatus.DRAFT; - } - return '(' + status.toLowerCase() + ')'; - }, - - _computeDetailPath: function(changeNum) { - return '/changes/' + changeNum + '/detail'; - }, - - _computeCommitInfoPath: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/commit?links'; - }, - - _computeCommentsPath: function(changeNum) { - return '/changes/' + changeNum + '/comments'; - }, - - _computeProjectConfigPath: function(project) { - return '/projects/' + encodeURIComponent(project) + '/config'; - }, - - _computeDetailQueryParams: function() { - var options = this.listChangesOptionsToHex( - this.ListChangesOption.ALL_REVISIONS, - this.ListChangesOption.CHANGE_ACTIONS, - this.ListChangesOption.DOWNLOAD_COMMANDS - ); - return {O: options}; - }, - - _computeLatestPatchNum: function(change) { - return change.revisions[change.current_revision]._number; - }, - - _computeAllPatchSets: function(change) { - var patchNums = []; - for (var rev in change.revisions) { - patchNums.push(change.revisions[rev]._number); - } - return patchNums.sort(function(a, b) { - return a - b; - }); - }, - - _getRevisionNumber: function(change, patchNum) { - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { - return change.revisions[rev]; - } - } - }, - - _computePatchIndexIsSelected: function(index, patchNum) { - return this._allPatchSets[index] == patchNum; - }, - - _computeLabelNames: function(labels) { - return Object.keys(labels).sort(); - }, - - _computeLabelValues: function(labelName, labels) { - var result = []; - var t = labels[labelName]; - if (!t) { return result; } - var approvals = t.all || []; - approvals.forEach(function(label) { - if (label.value && label.value != labels[labelName].default_value) { - var labelClassName; - var labelValPrefix = ''; - if (label.value > 0) { - labelValPrefix = '+'; - labelClassName = 'approved'; - } else if (label.value < 0) { - labelClassName = 'notApproved'; - } - result.push({ - value: labelValPrefix + label.value, - className: labelClassName, - account: label, - }); - } - }); - return result; - }, - - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - switch (e.keyCode) { - case 65: // 'a' - e.preventDefault(); - this.$.replyOverlay.open(); - break; - case 85: // 'u' - e.preventDefault(); - page.show('/'); - break; - } - }, - - _handleReloadChange: function() { - page.show(this._computeChangePath(this._changeNum)); - }, - - _reload: function() { - var detailCompletes = this.$.detailXHR.generateRequest().completes; - this.$.commentsXHR.generateRequest(); - var reloadPatchNumDependentResources = function() { - return Promise.all([ - this.$.commitInfoXHR.generateRequest().completes, - this.$.actions.reload(), - this.$.fileList.reload(), - ]); - }.bind(this); - var reloadDetailDependentResources = function() { - return this.$.relatedChanges.reload(); - }.bind(this); - - this._resetHeaderEl(); - - if (this._patchNum) { - return reloadPatchNumDependentResources().then(function() { - return detailCompletes; - }).then(reloadDetailDependentResources); - } else { - // The patch number is reliant on the change detail request. - return detailCompletes.then(reloadPatchNumDependentResources).then( - reloadDetailDependentResources); - } - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-comment-list.html b/polygerrit-ui/app/elements/gr-comment-list.html deleted file mode 100644 index 6fb5dae..0000000 --- a/polygerrit-ui/app/elements/gr-comment-list.html +++ /dev/null
@@ -1,118 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> - -<dom-module id="gr-comment-list"> - <template> - <style> - :host { - display: block; - } - .file { - border-top: 1px solid #ddd; - font-weight: bold; - margin: 10px 0 3px; - padding: 10px 0 5px; - } - .container { - display: flex; - margin: 5px 0; - } - .lineNum { - margin-right: .35em; - min-width: 7em; - } - .message { - flex: 1; - white-space: pre-wrap; - word-wrap: break-word; - } - </style> - <template is="dom-repeat" items="{{_files}}" as="file"> - <div class="file"> - <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>: - </div> - <template is="dom-repeat" - items="[[_computeCommentsForFile(file)]]" as="comment"> - <div class="container"> - <a class="lineNum" - href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"> - <span hidden$="[[!comment.line]]"> - <span>[[_computePatchDisplayName(comment)]]</span> - Line <span>[[comment.line]]</span>: - </span> - <span hidden$="[[comment.line]]"> - File comment: - </span> - </a> - <div class="message">[[comment.message]]</div> - </div> - </template> - </template> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-comment-list', - - properties: { - changeNum: Number, - comments: { - type: Object, - observer: '_commentsChanged', - }, - patchNum: Number, - - _files: Array, - }, - - _commentsChanged: function(value) { - this._files = Object.keys(value || {}).sort(); - }, - - _computeFileDiffURL: function(file, changeNum, patchNum) { - return '/c/' + changeNum + '/' + patchNum + '/' + file; - }, - - _computeDiffLineURL: function(file, changeNum, patchNum, comment) { - var diffURL = this._computeFileDiffURL(file, changeNum, patchNum); - if (comment.line) { - // TODO(andybons): This is not correct if the comment is on the base. - diffURL += '#' + comment.line; - } - return diffURL; - }, - - _computeCommentsForFile: function(file) { - return this.comments[file]; - }, - - _computePatchDisplayName: function(comment) { - if (comment.side == 'PARENT') { - return 'Base, '; - } - if (comment.patch_set != this.patchNum) { - return 'PS' + comment.patch_set + ', '; - } - return ''; - } - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-confirm-dialog.html b/polygerrit-ui/app/elements/gr-confirm-dialog.html deleted file mode 100644 index a82e31c..0000000 --- a/polygerrit-ui/app/elements/gr-confirm-dialog.html +++ /dev/null
@@ -1,89 +0,0 @@ -<!-- -Copyright (C) 2016 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-button.html"> - -<dom-module id="gr-confirm-dialog"> - <template> - <style> - :host { - display: block; - } - header { - border-bottom: 1px solid #ddd; - font-weight: bold; - } - header, - main, - footer { - padding: .5em .65em; - } - footer { - display: flex; - justify-content: space-between; - } - </style> - <header><content select=".header"></content></header> - <main><content select=".main"></content></main> - <footer> - <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button> - <gr-button on-tap="_handleCancelTap">Cancel</gr-button> - </footer> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-confirm-dialog', - - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - properties: { - confirmLabel: { - type: String, - value: 'Confirm', - } - }, - - hostAttributes: { - role: 'dialog', - }, - - _handleConfirmTap: function(e) { - e.preventDefault(); - this.fire('confirm', null, {bubbles: false}); - }, - - _handleCancelTap: function(e) { - e.preventDefault(); - this.fire('cancel', null, {bubbles: false}); - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-dashboard-view.html b/polygerrit-ui/app/elements/gr-dashboard-view.html deleted file mode 100644 index cdd4b1a..0000000 --- a/polygerrit-ui/app/elements/gr-dashboard-view.html +++ /dev/null
@@ -1,129 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> - -<dom-module id="gr-dashboard-view"> - <template> - <style> - :host { - background-color: var(--view-background-color); - display: block; - margin: 0 var(--default-horizontal-margin); - } - .loading { - margin-top: 1em; - color: #666; - background-color: #f1f2f3; - } - gr-change-list { - margin-top: 1em; - width: 100%; - } - @media only screen and (max-width: 50em) { - :host { - margin: 0; - } - .loading { - padding: 0 var(--default-horizontal-margin); - } - } - </style> - <gr-ajax - auto - url="/changes/" - params="[[_computeQueryParams()]]" - last-response="{{_results}}" - loading="{{_loading}}"></gr-ajax> - <div class="loading" hidden$="[[!_loading]]">Loading...</div> - <div hidden$="[[_loading]]" hidden> - <gr-change-list - show-star - show-reviewed-state - account="[[account]]" - selected-index="{{viewState.selectedChangeIndex}}" - groups="{{_results}}" - group-titles="[[_groupTitles]]"></gr-change-list> - </div> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-dashboard-view', - - /** - * Fired when the title of the page should change. - * - * @event title-change - */ - - properties: { - account: { - type: Object, - value: function() { return {}; }, - }, - viewState: Object, - - _results: Array, - _groupTitles: { - type: Array, - value: [ - 'Outgoing reviews', - 'Incoming reviews', - 'Recently closed', - ], - }, - - /** - * For showing a "loading..." string during ajax requests. - */ - _loading: { - type: Boolean, - value: true, - }, - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - attached: function() { - this.fire('title-change', {title: 'My Reviews'}); - }, - - _computeQueryParams: function() { - var options = this.listChangesOptionsToHex( - this.ListChangesOption.LABELS, - this.ListChangesOption.DETAILED_ACCOUNTS, - this.ListChangesOption.REVIEWED - ); - return { - O: options, - q: [ - 'is:open owner:self', - 'is:open reviewer:self -owner:self', - 'is:closed (owner:self OR reviewer:self) -age:4w limit:10', - ], - }; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-date-formatter.html b/polygerrit-ui/app/elements/gr-date-formatter.html deleted file mode 100644 index a13dbb8..0000000 --- a/polygerrit-ui/app/elements/gr-date-formatter.html +++ /dev/null
@@ -1,94 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> - -<dom-module id="gr-date-formatter"> - <template> - <style> - :host { - display: inline; - } - </style> - <span>[[_computeDateStr(dateStr)]]</span> - </template> - <script> - (function() { - 'use strict'; - - var Duration = { - HOUR: 1000 * 60 * 60, - DAY: 1000 * 60 * 60 * 24, - }; - - var ShortMonthNames = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', - 'Nov', 'Dec' - ]; - - Polymer({ - is: 'gr-date-formatter', - - properties: { - dateStr: { - type: String, - value: null, - notify: true - } - }, - - _computeDateStr: function(dateStr) { - return this._dateStr(this._parseDateStr(dateStr), new Date()); - }, - - _parseDateStr: function(dateStr) { - if (!dateStr) { return null; } - return util.parseDate(dateStr); - }, - - _dateStr: function(t, now) { - if (!t) { return ''; } - var diff = now.getTime() - t.getTime(); - if (diff < Duration.DAY && t.getDay() == now.getDay()) { - // Within 24 hours and on the same day: - // '2:14 AM' - var pm = t.getHours() >= 12; - var hours = t.getHours(); - if (hours == 0) { - hours = 12; - } else if (hours > 12) { - hours = t.getHours() - 12; - } - var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() : - t.getMinutes(); - return hours + ':' + minutes + (pm ? ' PM' : ' AM'); - } else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) && - diff < 180 * Duration.DAY) { - // From one to six months: - // 'Aug 29' - return ShortMonthNames[t.getMonth()] + ' ' + t.getDate(); - } else if (diff >= 180 * Duration.DAY) { - // More than six months: - // 'Aug 29, 1997' - return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' + - t.getFullYear(); - } - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/gr-diff-comment-thread.html deleted file mode 100644 index 178b417..0000000 --- a/polygerrit-ui/app/elements/gr-diff-comment-thread.html +++ /dev/null
@@ -1,257 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-diff-comment.html"> - -<dom-module id="gr-diff-comment-thread"> - <template> - <style> - :host { - display: block; - white-space: normal; - } - gr-diff-comment { - border-left: 1px solid #ddd; - } - gr-diff-comment:first-of-type { - border-top: 1px solid #ddd; - } - gr-diff-comment:last-of-type { - border-bottom: 1px solid #ddd; - } - </style> - <div id="container"> - <template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment"> - <gr-diff-comment - comment="{{comment}}" - change-num="[[changeNum]]" - patch-num="[[patchNum]]" - draft="[[comment.__draft]]" - show-actions="[[showActions]]" - project-config="[[projectConfig]]" - on-height-change="_handleCommentHeightChange" - on-reply="_handleCommentReply" - on-discard="_handleCommentDiscard" - on-done="_handleCommentDone"></gr-diff-comment> - </template> - </div> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-diff-comment-thread', - - /** - * Fired when the height of the thread changes. - * - * @event height-change - */ - - /** - * Fired when the thread should be discarded. - * - * @event discard - */ - - properties: { - changeNum: String, - comments: { - type: Array, - value: function() { return []; }, - }, - patchNum: String, - path: String, - showActions: Boolean, - projectConfig: Object, - - _boundWindowResizeHandler: { - type: Function, - value: function() { return this._handleWindowResize.bind(this); } - }, - _lastHeight: Number, - _orderedComments: Array, - }, - - get naturalHeight() { - return this.$.container.offsetHeight; - }, - - observers: [ - '_commentsChanged(comments.splices)', - ], - - attached: function() { - window.addEventListener('resize', this._boundWindowResizeHandler); - }, - - detached: function() { - window.removeEventListener('resize', this._boundWindowResizeHandler); - }, - - _handleWindowResize: function(e) { - this._heightChanged(); - }, - - _commentsChanged: function(changeRecord) { - this._orderedComments = this._sortedComments(this.comments); - }, - - _sortedComments: function(comments) { - comments.sort(function(c1, c2) { - var c1Date = c1.__date || util.parseDate(c1.updated); - var c2Date = c2.__date || util.parseDate(c2.updated); - return c1Date - c2Date; - }); - - var commentIDToReplies = {}; - var topLevelComments = []; - for (var i = 0; i < comments.length; i++) { - var c = comments[i]; - if (c.in_reply_to) { - if (commentIDToReplies[c.in_reply_to] == null) { - commentIDToReplies[c.in_reply_to] = []; - } - commentIDToReplies[c.in_reply_to].push(c); - } else { - topLevelComments.push(c); - } - } - var results = []; - for (var i = 0; i < topLevelComments.length; i++) { - this._visitComment(topLevelComments[i], commentIDToReplies, results); - } - return results; - }, - - _visitComment: function(parent, commentIDToReplies, results) { - results.push(parent); - - var replies = commentIDToReplies[parent.id]; - if (!replies) { return; } - for (var i = 0; i < replies.length; i++) { - this._visitComment(replies[i], commentIDToReplies, results); - } - }, - - _handleCommentHeightChange: function(e) { - e.stopPropagation(); - this._heightChanged(); - }, - - _handleCommentReply: function(e) { - var comment = e.detail.comment; - var quoteStr; - if (e.detail.quote) { - var msg = comment.message; - var quoteStr = msg.split('\n').map( - function(line) { return ' > ' + line; }).join('\n') + '\n\n'; - } - var reply = - this._newReply(comment.id, comment.line, this.path, quoteStr); - this.push('comments', reply); - - // Allow the reply to render in the dom-repeat. - this.async(function() { - var commentEl = this._commentElWithDraftID(reply.__draftID); - commentEl.editing = true; - this.async(this._heightChanged.bind(this), 1); - }.bind(this), 1); - }, - - _handleCommentDone: function(e) { - var comment = e.detail.comment; - var reply = this._newReply(comment.id, comment.line, this.path, 'Done'); - this.push('comments', reply); - - // Allow the reply to render in the dom-repeat. - this.async(function() { - var commentEl = this._commentElWithDraftID(reply.__draftID); - commentEl.save(); - this.async(this._heightChanged.bind(this), 1); - }.bind(this), 1); - }, - - _commentElWithDraftID: function(draftID) { - var commentEls = - Polymer.dom(this.root).querySelectorAll('gr-diff-comment'); - for (var i = 0; i < commentEls.length; i++) { - if (commentEls[i].comment.__draftID == draftID) { - return commentEls[i]; - } - } - return null; - }, - - _newReply: function(inReplyTo, line, path, opt_message) { - var c = { - __draft: true, - __draftID: Math.random().toString(36), - __date: new Date(), - line: line, - path: path, - in_reply_to: inReplyTo, - }; - if (opt_message != null) { - c.message = opt_message; - } - return c; - }, - - _handleCommentDiscard: function(e) { - // TODO(andybons): In Shadow DOM, the event bubbles up, while in Shady - // DOM, it respects the bubbles property. - // https://github.com/Polymer/polymer/issues/3226 - e.stopPropagation(); - var diffCommentEl = Polymer.dom(e).rootTarget; - var idx = this._indexOf(diffCommentEl.comment, this.comments); - if (idx == -1) { - throw Error('Cannot find comment ' + - JSON.stringify(diffCommentEl.comment)); - } - this.splice('comments', idx, 1); - if (this.comments.length == 0) { - this.fire('discard', null, {bubbles: false}); - return; - } - this.async(this._heightChanged.bind(this), 1); - }, - - _heightChanged: function() { - var height = this.$.container.offsetHeight; - if (height == this._lastHeight) { return; } - - this.fire('height-change', {height: height}, {bubbles: false}); - this._lastHeight = height; - }, - - _indexOf: function(comment, arr) { - for (var i = 0; i < arr.length; i++) { - var c = arr[i]; - if ((c.__draftID != null && c.__draftID == comment.__draftID) || - (c.id != null && c.id == comment.id)) { - return i; - } - } - return -1; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment.html b/polygerrit-ui/app/elements/gr-diff-comment.html deleted file mode 100644 index c544a8d..0000000 --- a/polygerrit-ui/app/elements/gr-diff-comment.html +++ /dev/null
@@ -1,389 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-date-formatter.html"> -<link rel="import" href="gr-linked-text.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-diff-comment"> - <template> - <style> - :host { - background-color: #ffd; - display: block; - --iron-autogrow-textarea: { - padding: 2px; - }; - } - :host([disabled]) { - pointer-events: none; - } - :host([disabled]) .container { - opacity: .5; - } - .header, - .message, - .actions { - padding: .5em .7em; - } - .header { - display: flex; - padding-bottom: 0; - font-family: 'Open Sans', sans-serif; - } - .headerLeft { - flex: 1; - } - .authorName, - .draftLabel { - font-weight: bold; - } - .draftLabel { - color: #999; - display: none; - } - .date { - justify-content: flex-end; - margin-left: 5px; - } - a.date:link, - a.date:visited { - color: #666; - } - .actions { - display: flex; - padding-top: 0; - } - .action { - margin-right: .5em; - } - .danger { - display: flex; - flex: 1; - justify-content: flex-end; - } - .editMessage { - display: none; - margin: .5em .7em; - width: calc(100% - 1.4em - 2px); - } - .danger .action { - margin-right: 0; - } - .container:not(.draft) .actions :not(.reply):not(.quote):not(.done) { - display: none; - } - .draft .reply, - .draft .quote, - .draft .done { - display: none; - } - .draft .draftLabel { - display: inline; - } - .draft:not(.editing) .save, - .draft:not(.editing) .cancel { - display: none; - } - .editing .message, - .editing .reply, - .editing .quote, - .editing .done, - .editing .edit { - display: none; - } - .editing .editMessage { - background-color: #fff; - display: block; - } - </style> - <div class="container" id="container"> - <div class="header" id="header"> - <div class="headerLeft"> - <span class="authorName">[[comment.author.name]]</span> - <span class="draftLabel">DRAFT</span> - </div> - <a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap"> - <gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter> - </a> - </div> - <iron-autogrow-textarea - id="editTextarea" - class="editMessage" - disabled="{{disabled}}" - rows="4" - bind-value="{{_editDraft}}" - on-keyup="_handleTextareaKeyup" - on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea> - <gr-linked-text class="message" - pre - content="[[comment.message]]" - config="[[projectConfig.commentlinks]]"></gr-linked-text> - <div class="actions" hidden$="[[!showActions]]"> - <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button> - <gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button> - <gr-button class="action done" on-tap="_handleDone">Done</gr-button> - <gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button> - <gr-button class="action save" on-tap="_handleSave" - disabled$="[[_computeSaveDisabled(_editDraft)]]">Save</gr-button> - <gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button> - <div class="danger"> - <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button> - </div> - </div> - </div> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-diff-comment', - - /** - * Fired when the height of the comment changes. - * - * @event height-change - */ - - /** - * Fired when the Reply action is triggered. - * - * @event reply - */ - - /** - * Fired when the Done action is triggered. - * - * @event done - */ - - /** - * Fired when this comment is discarded. - * - * @event discard - */ - - properties: { - changeNum: String, - comment: { - type: Object, - notify: true, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - draft: { - type: Boolean, - value: false, - observer: '_draftChanged', - }, - editing: { - type: Boolean, - value: false, - observer: '_editingChanged', - }, - patchNum: String, - showActions: Boolean, - projectConfig: Object, - - _xhrPromise: Object, // Used for testing. - _editDraft: String, - }, - - ready: function() { - this._editDraft = (this.comment && this.comment.message) || ''; - this.editing = this._editDraft.length == 0; - }, - - attached: function() { - this._heightChanged(); - }, - - save: function() { - this.comment.message = this._editDraft; - this.disabled = true; - var endpoint = this._restEndpoint(this.comment.id); - this._send('PUT', endpoint).then(function(req) { - this.disabled = false; - var comment = req.response; - comment.__draft = true; - // Maintain the ephemeral draft ID for identification by other - // elements. - if (this.comment.__draftID) { - comment.__draftID = this.comment.__draftID; - } - this.comment = comment; - this.editing = false; - }.bind(this)).catch(function(err) { - alert('Your draft couldn’t be saved. Check the console and contact ' + - 'the PolyGerrit team for assistance.'); - this.disabled = false; - }.bind(this)); - }, - - _heightChanged: function() { - this.async(function() { - this.fire('height-change', {height: this.offsetHeight}, - {bubbles: false}); - }.bind(this)); - }, - - _draftChanged: function(draft) { - this.$.container.classList.toggle('draft', draft); - }, - - _editingChanged: function(editing) { - this.$.container.classList.toggle('editing', editing); - if (editing) { - var textarea = this.$.editTextarea.textarea; - // Put the cursor at the end always. - textarea.selectionStart = textarea.value.length; - textarea.selectionEnd = textarea.selectionStart; - this.async(function() { - textarea.focus(); - }.bind(this)); - } - if (this.comment && this.comment.id) { - this.$$('.cancel').hidden = !editing; - } - this._heightChanged(); - }, - - _computeLinkToComment: function(comment) { - return '#' + comment.line; - }, - - _computeSaveDisabled: function(draft) { - return draft == null || draft.trim() == ''; - }, - - _handleTextareaKeyup: function(e) { - // TODO(andybons): This isn't always true, but I can't currently think - // of a better metric. - this._heightChanged(); - }, - - _handleTextareaKeydown: function(e) { - if (e.keyCode == 27) { // 'esc' - this._handleCancel(e); - } - }, - - _handleLinkTap: function(e) { - e.preventDefault(); - var hash = this._computeLinkToComment(this.comment); - // Don't add the hash to the window history if it's already there. - // Otherwise you mess up expected back button behavior. - if (window.location.hash == hash) { return; } - // Change the URL but don’t trigger a nav event. Otherwise it will - // reload the page. - page.show(window.location.pathname + hash, null, false); - }, - - _handleReply: function(e) { - this._preventDefaultAndBlur(e); - this.fire('reply', {comment: this.comment}, {bubbles: false}); - }, - - _handleQuote: function(e) { - this._preventDefaultAndBlur(e); - this.fire('reply', {comment: this.comment, quote: true}, - {bubbles: false}); - }, - - _handleDone: function(e) { - this._preventDefaultAndBlur(e); - this.fire('done', {comment: this.comment}, {bubbles: false}); - }, - - _handleEdit: function(e) { - this._preventDefaultAndBlur(e); - this._editDraft = this.comment.message; - this.editing = true; - }, - - _handleSave: function(e) { - this._preventDefaultAndBlur(e); - this.save(); - }, - - _handleCancel: function(e) { - this._preventDefaultAndBlur(e); - if (this.comment.message == null || this.comment.message.length == 0) { - this.fire('discard', null, {bubbles: false}); - return; - } - this._editDraft = this.comment.message; - this.editing = false; - }, - - _handleDiscard: function(e) { - this._preventDefaultAndBlur(e); - if (!this.comment.__draft) { - throw Error('Cannot discard a non-draft comment.'); - } - this.disabled = true; - var commentID = this.comment.id; - if (!commentID) { - this.fire('discard', null, {bubbles: false}); - return; - } - this._send('DELETE', this._restEndpoint(commentID)).then(function(req) { - this.fire('discard', null, {bubbles: false}); - }.bind(this)).catch(function(err) { - alert('Your draft couldn’t be deleted. Check the console and ' + - 'contact the PolyGerrit team for assistance.'); - this.disabled = false; - }.bind(this)); - }, - - _preventDefaultAndBlur: function(e) { - e.preventDefault(); - Polymer.dom(e).rootTarget.blur(); - }, - - _send: function(method, url) { - var xhr = document.createElement('gr-request'); - var opts = { - method: method, - url: url, - }; - if (method == 'PUT' || method == 'POST') { - opts.body = this.comment; - } - this._xhrPromise = xhr.send(opts); - return this._xhrPromise; - }, - - _restEndpoint: function(id) { - var path = '/changes/' + this.changeNum + '/revisions/' + - this.patchNum + '/drafts'; - if (id) { - path += '/' + id; - } - return path; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-side.html b/polygerrit-ui/app/elements/gr-diff-side.html deleted file mode 100644 index 09ed5f8..0000000 --- a/polygerrit-ui/app/elements/gr-diff-side.html +++ /dev/null
@@ -1,698 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-diff-comment-thread.html"> - -<dom-module id="gr-diff-side"> - <template> - <style> - :host, - .container { - display: flex; - flex: 0 0 auto; - } - .lineNum:before, - .code:before { - /* To ensure the height is non-zero in these elements, a - zero-width space is set as its content. The character - itself doesn't matter. Just that there is something - there. */ - content: '\200B'; - } - .lineNum { - background-color: #eee; - color: #666; - padding: 0 .75em; - text-align: right; - } - .canComment .lineNum { - cursor: pointer; - text-decoration: underline; - } - .canComment .lineNum:hover { - background-color: #ccc; - } - .lightHighlight { - background-color: var(--light-highlight-color); - } - hl, - .darkHighlight { - background-color: var(--dark-highlight-color); - } - .br:after { - /* Line feed */ - content: '\A'; - } - .tab { - display: inline-block; - } - .tab.withIndicator:before { - color: #C62828; - /* >> character */ - content: '\00BB'; - } - .numbers, - .content { - white-space: pre; - } - .numbers .filler { - background-color: #eee; - } - .contextControl { - background-color: #fef; - } - .contextControl a:link, - .contextControl a:visited { - display: block; - text-decoration: none; - } - .numbers .contextControl { - padding: 0 .75em; - text-align: right; - } - .content .contextControl { - text-align: center; - } - </style> - <div class$="[[_computeContainerClass(canComment)]]"> - <div class="numbers" id="numbers"></div> - <div class="content" id="content"></div> - </div> - </template> - <script> - (function() { - 'use strict'; - - var CharCode = { - LESS_THAN: '<'.charCodeAt(0), - GREATER_THAN: '>'.charCodeAt(0), - AMPERSAND: '&'.charCodeAt(0), - SEMICOLON: ';'.charCodeAt(0), - }; - - var TAB_REGEX = /\t/g; - - Polymer({ - is: 'gr-diff-side', - - /** - * Fired when an expand context control is clicked. - * - * @event expand-context - */ - - /** - * Fired when a thread's height is changed. - * - * @event thread-height-change - */ - - /** - * Fired when a draft should be added. - * - * @event add-draft - */ - - /** - * Fired when a thread is removed. - * - * @event remove-thread - */ - - properties: { - canComment: { - type: Boolean, - value: false, - }, - content: { - type: Array, - notify: true, - observer: '_contentChanged', - }, - prefs: { - type: Object, - value: function() { return {}; }, - }, - changeNum: String, - patchNum: String, - path: String, - projectConfig: { - type: Object, - observer: '_projectConfigChanged', - }, - - _lineFeedHTML: { - type: String, - value: '<span class="style-scope gr-diff-side br"></span>', - readOnly: true, - }, - _highlightStartTag: { - type: String, - value: '<hl class="style-scope gr-diff-side">', - readOnly: true, - }, - _highlightEndTag: { - type: String, - value: '</hl>', - readOnly: true, - }, - _diffChunkLineNums: { - type: Array, - value: function() { return []; }, - }, - _commentThreadLineNums: { - type: Array, - value: function() { return []; }, - }, - _focusedLineNum: { - type: Number, - value: 1, - }, - }, - - listeners: { - 'tap': '_tapHandler', - }, - - observers: [ - '_prefsChanged(prefs.*)', - ], - - rowInserted: function(index) { - this.renderLineIndexRange(index, index); - this._updateDOMIndices(); - this._updateJumpIndices(); - }, - - rowRemoved: function(index) { - var removedEls = Polymer.dom(this.root).querySelectorAll( - '[data-index="' + index + '"]'); - for (var i = 0; i < removedEls.length; i++) { - removedEls[i].parentNode.removeChild(removedEls[i]); - } - this._updateDOMIndices(); - this._updateJumpIndices(); - }, - - rowUpdated: function(index) { - var removedEls = Polymer.dom(this.root).querySelectorAll( - '[data-index="' + index + '"]'); - for (var i = 0; i < removedEls.length; i++) { - removedEls[i].parentNode.removeChild(removedEls[i]); - } - this.renderLineIndexRange(index, index); - }, - - scrollToLine: function(lineNum) { - if (isNaN(lineNum) || lineNum < 1) { return; } - - var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]'); - if (!el) { return; } - - // Calculate where the line is relative to the window. - var top = el.offsetTop; - for (var offsetParent = el.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { - top += offsetParent.offsetTop; - } - - // Scroll the element to the middle of the window. Dividing by a third - // instead of half the inner height feels a bit better otherwise the - // element appears to be below the center of the window even when it - // isn't. - window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight); - }, - - scrollToNextDiffChunk: function() { - this._scrollToNextChunkOrThread(this._diffChunkLineNums); - }, - - scrollToPreviousDiffChunk: function() { - this._scrollToPreviousChunkOrThread(this._diffChunkLineNums); - }, - - scrollToNextCommentThread: function() { - this._scrollToNextChunkOrThread(this._commentThreadLineNums); - }, - - scrollToPreviousCommentThread: function() { - this._scrollToPreviousChunkOrThread(this._commentThreadLineNums); - }, - - renderLineIndexRange: function(startIndex, endIndex) { - this._render(this.content, startIndex, endIndex); - }, - - hideElementsWithIndex: function(index) { - var els = Polymer.dom(this.root).querySelectorAll( - '[data-index="' + index + '"]'); - for (var i = 0; i < els.length; i++) { - els[i].setAttribute('hidden', true); - } - }, - - getRowHeight: function(index) { - var row = this.content[index]; - // Filler elements should not be taken into account when determining - // height calculations. - if (row.type == 'FILLER') { - return 0; - } - if (row.height != null) { - return row.height; - } - - var selector = '[data-index="' + index + '"]'; - var els = Polymer.dom(this.root).querySelectorAll(selector); - if (els.length != 2) { - throw Error('Rows should only consist of two elements'); - } - return Math.max(els[0].offsetHeight, els[1].offsetHeight); - }, - - getRowNaturalHeight: function(index) { - var contentEl = this.$$('.content [data-index="' + index + '"]'); - return contentEl.naturalHeight || contentEl.offsetHeight; - }, - - setRowNaturalHeight: function(index) { - var lineEl = this.$$('.numbers [data-index="' + index + '"]'); - var contentEl = this.$$('.content [data-index="' + index + '"]'); - contentEl.style.height = null; - var height = contentEl.offsetHeight; - lineEl.style.height = height + 'px'; - this.content[index].height = height; - return height; - }, - - setRowHeight: function(index, height) { - var selector = '[data-index="' + index + '"]'; - var els = Polymer.dom(this.root).querySelectorAll(selector); - for (var i = 0; i < els.length; i++) { - els[i].style.height = height + 'px'; - } - this.content[index].height = height; - }, - - _scrollToNextChunkOrThread: function(lineNums) { - for (var i = 0; i < lineNums.length; i++) { - if (lineNums[i] > this._focusedLineNum) { - this._focusedLineNum = lineNums[i]; - this.scrollToLine(this._focusedLineNum); - return; - } - } - }, - - _scrollToPreviousChunkOrThread: function(lineNums) { - for (var i = lineNums.length - 1; i >= 0; i--) { - if (this._focusedLineNum > lineNums[i]) { - this._focusedLineNum = lineNums[i]; - this.scrollToLine(this._focusedLineNum); - return; - } - } - }, - - _updateJumpIndices: function() { - this._commentThreadLineNums = []; - this._diffChunkLineNums = []; - var inHighlight = false; - for (var i = 0; i < this.content.length; i++) { - switch (this.content[i].type) { - case 'COMMENT_THREAD': - this._commentThreadLineNums.push( - this.content[i].comments[0].line); - break; - case 'CODE': - // Only grab the first line of the highlighted chunk. - if (!inHighlight && this.content[i].highlight) { - this._diffChunkLineNums.push(this.content[i].lineNum); - inHighlight = true; - } else if (!this.content[i].highlight) { - inHighlight = false; - } - break; - } - } - }, - - _updateDOMIndices: function() { - // There is no way to select elements with a data-index greater than a - // given value. For now, just update all DOM elements. - var lineEls = Polymer.dom(this.root).querySelectorAll( - '.numbers [data-index]'); - var contentEls = Polymer.dom(this.root).querySelectorAll( - '.content [data-index]'); - if (lineEls.length != contentEls.length) { - throw Error( - 'There must be the same number of line and content elements'); - } - var index = 0; - for (var i = 0; i < this.content.length; i++) { - if (this.content[i].hidden) { continue; } - - lineEls[index].setAttribute('data-index', i); - contentEls[index].setAttribute('data-index', i); - index++; - } - }, - - _prefsChanged: function(changeRecord) { - var prefs = changeRecord.base; - this.$.content.style.width = prefs.line_length + 'ch'; - }, - - _projectConfigChanged: function(projectConfig) { - var threadEls = - Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread'); - for (var i = 0; i < threadEls.length; i++) { - threadEls[i].projectConfig = projectConfig; - } - }, - - _contentChanged: function(diff) { - this._clearChildren(this.$.numbers); - this._clearChildren(this.$.content); - this._render(diff, 0, diff.length - 1); - this._updateJumpIndices(); - }, - - _computeContainerClass: function(canComment) { - return 'container' + (canComment ? ' canComment' : ''); - }, - - _tapHandler: function(e) { - var lineEl = Polymer.dom(e).rootTarget; - if (!this.canComment || !lineEl.classList.contains('lineNum')) { - return; - } - - e.preventDefault(); - var index = parseInt(lineEl.getAttribute('data-index'), 10); - var line = parseInt(lineEl.getAttribute('data-line-num'), 10); - this.fire('add-draft', { - index: index, - line: line - }, {bubbles: false}); - }, - - _clearChildren: function(el) { - while (el.firstChild) { - el.removeChild(el.firstChild); - } - }, - - _handleContextControlClick: function(context, e) { - e.preventDefault(); - this.fire('expand-context', {context: context}, {bubbles: false}); - }, - - _render: function(diff, startIndex, endIndex) { - var beforeLineEl; - var beforeContentEl; - if (endIndex != diff.length - 1) { - beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]'); - beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]'); - if (!beforeLineEl && !beforeContentEl) { - // `endIndex` may be present within the model, but not in the DOM. - // Insert it before its successive element. - beforeLineEl = this.$$( - '.numbers [data-index="' + (endIndex + 1) + '"]'); - beforeContentEl = this.$$( - '.content [data-index="' + (endIndex + 1) + '"]'); - } - } - - for (var i = startIndex; i <= endIndex; i++) { - if (diff[i].hidden) { continue; } - - switch (diff[i].type) { - case 'CODE': - this._renderCode(diff[i], i, beforeLineEl, beforeContentEl); - break; - case 'FILLER': - this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl); - break; - case 'CONTEXT_CONTROL': - this._renderContextControl(diff[i], i, beforeLineEl, - beforeContentEl); - break; - case 'COMMENT_THREAD': - this._renderCommentThread(diff[i], i, beforeLineEl, - beforeContentEl); - break; - } - } - }, - - _handleCommentThreadHeightChange: function(e) { - var threadEl = Polymer.dom(e).rootTarget; - var index = parseInt(threadEl.getAttribute('data-index'), 10); - this.content[index].height = e.detail.height; - var lineEl = this.$$('.numbers [data-index="' + index + '"]'); - lineEl.style.height = e.detail.height + 'px'; - this.fire('thread-height-change', { - index: index, - height: e.detail.height, - }, {bubbles: false}); - }, - - _handleCommentThreadDiscard: function(e) { - var threadEl = Polymer.dom(e).rootTarget; - var index = parseInt(threadEl.getAttribute('data-index'), 10); - this.fire('remove-thread', {index: index}, {bubbles: false}); - }, - - _renderCommentThread: function(thread, index, beforeLineEl, - beforeContentEl) { - var lineEl = this._createElement('div', 'commentThread'); - lineEl.classList.add('filler'); - lineEl.setAttribute('data-index', index); - var threadEl = document.createElement('gr-diff-comment-thread'); - threadEl.addEventListener('height-change', - this._handleCommentThreadHeightChange.bind(this)); - threadEl.addEventListener('discard', - this._handleCommentThreadDiscard.bind(this)); - threadEl.setAttribute('data-index', index); - threadEl.changeNum = this.changeNum; - threadEl.patchNum = thread.patchNum || this.patchNum; - threadEl.path = this.path; - threadEl.comments = thread.comments; - threadEl.showActions = this.canComment; - threadEl.projectConfig = this.projectConfig; - - this.$.numbers.insertBefore(lineEl, beforeLineEl); - this.$.content.insertBefore(threadEl, beforeContentEl); - }, - - _renderContextControl: function(control, index, beforeLineEl, - beforeContentEl) { - var lineEl = this._createElement('div', 'contextControl'); - lineEl.setAttribute('data-index', index); - lineEl.textContent = '@@'; - var contentEl = this._createElement('div', 'contextControl'); - contentEl.setAttribute('data-index', index); - var a = this._createElement('a'); - a.href = '#'; - a.textContent = 'Show ' + control.numLines + ' common ' + - (control.numLines == 1 ? 'line' : 'lines') + '...'; - a.addEventListener('click', - this._handleContextControlClick.bind(this, control)); - contentEl.appendChild(a); - - this.$.numbers.insertBefore(lineEl, beforeLineEl); - this.$.content.insertBefore(contentEl, beforeContentEl); - }, - - _renderFiller: function(filler, index, beforeLineEl, beforeContentEl) { - var lineFillerEl = this._createElement('div', 'filler'); - lineFillerEl.setAttribute('data-index', index); - var fillerEl = this._createElement('div', 'filler'); - fillerEl.setAttribute('data-index', index); - var numLines = filler.numLines || 1; - - lineFillerEl.textContent = '\n'.repeat(numLines); - for (var i = 0; i < numLines; i++) { - var newlineEl = this._createElement('span', 'br'); - fillerEl.appendChild(newlineEl); - } - - this.$.numbers.insertBefore(lineFillerEl, beforeLineEl); - this.$.content.insertBefore(fillerEl, beforeContentEl); - }, - - _renderCode: function(code, index, beforeLineEl, beforeContentEl) { - var lineNumEl = this._createElement('div', 'lineNum'); - lineNumEl.setAttribute('data-line-num', code.lineNum); - lineNumEl.setAttribute('data-index', index); - var numLines = code.numLines || 1; - lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines); - - var contentEl = this._createElement('div', 'code'); - contentEl.setAttribute('data-line-num', code.lineNum); - contentEl.setAttribute('data-index', index); - - if (code.highlight) { - contentEl.classList.add(code.intraline.length > 0 ? - 'lightHighlight' : 'darkHighlight'); - } - - var html = util.escapeHTML(code.content); - if (code.highlight && code.intraline.length > 0) { - html = this._addIntralineHighlights(code.content, html, - code.intraline); - } - if (numLines > 1) { - html = this._addNewLines(code.content, html, numLines); - } - html = this._addTabWrappers(code.content, html); - - // If the html is equivalent to the text then it didn't get highlighted - // or escaped. Use textContent which is faster than innerHTML. - if (code.content == html) { - contentEl.textContent = code.content; - } else { - contentEl.innerHTML = html; - } - - this.$.numbers.insertBefore(lineNumEl, beforeLineEl); - this.$.content.insertBefore(contentEl, beforeContentEl); - }, - - // Advance `index` by the appropriate number of characters that would - // represent one source code character and return that index. For - // example, for source code '<span>' the escaped html string is - // '<span>'. Advancing from index 0 on the prior html string would - // return 4, since < maps to one source code character ('<'). - _advanceChar: function(html, index) { - // Any tags don't count as characters - while (index < html.length && - html.charCodeAt(index) == CharCode.LESS_THAN) { - while (index < html.length && - html.charCodeAt(index) != CharCode.GREATER_THAN) { - index++; - } - index++; // skip the ">" itself - } - // An HTML entity (e.g., <) counts as one character. - if (index < html.length && - html.charCodeAt(index) == CharCode.AMPERSAND) { - while (index < html.length && - html.charCodeAt(index) != CharCode.SEMICOLON) { - index++; - } - } - return index + 1; - }, - - _addIntralineHighlights: function(content, html, highlights) { - var startTag = this._highlightStartTag; - var endTag = this._highlightEndTag; - - for (var i = 0; i < highlights.length; i++) { - var hl = highlights[i]; - - var htmlStartIndex = 0; - for (var j = 0; j < hl.startIndex; j++) { - htmlStartIndex = this._advanceChar(html, htmlStartIndex); - } - - var htmlEndIndex = 0; - if (hl.endIndex != null) { - for (var j = 0; j < hl.endIndex; j++) { - htmlEndIndex = this._advanceChar(html, htmlEndIndex); - } - } else { - // If endIndex isn't present, continue to the end of the line. - htmlEndIndex = html.length; - } - // The start and end indices could be the same if a highlight is meant - // to start at the end of a line and continue onto the next one. - // Ignore it. - if (htmlStartIndex != htmlEndIndex) { - html = html.slice(0, htmlStartIndex) + startTag + - html.slice(htmlStartIndex, htmlEndIndex) + endTag + - html.slice(htmlEndIndex); - } - } - return html; - }, - - _addNewLines: function(content, html, numLines) { - var htmlIndex = 0; - var indices = []; - var numChars = 0; - for (var i = 0; i < content.length; i++) { - if (numChars > 0 && numChars % this.prefs.line_length == 0) { - indices.push(htmlIndex); - } - htmlIndex = this._advanceChar(html, htmlIndex); - if (content[i] == '\t') { - numChars += this.prefs.tab_size; - } else { - numChars++; - } - } - var result = html; - var linesLeft = numLines; - // Since the result string is being altered in place, start from the end - // of the string so that the insertion indices are not affected as the - // result string changes. - for (var i = indices.length - 1; i >= 0; i--) { - result = result.slice(0, indices[i]) + this._lineFeedHTML + - result.slice(indices[i]); - linesLeft--; - } - // numLines is the total number of lines this code block should take up. - // Fill in the remaining ones. - for (var i = 0; i < linesLeft; i++) { - result += this._lineFeedHTML; - } - return result; - }, - - _addTabWrappers: function(content, html) { - // TODO(andybons): CSS tab-size is not supported in IE. - // Force this to be a number to prevent arbitrary injection. - var tabSize = +this.prefs.tab_size; - var htmlStr = '<span class="style-scope gr-diff-side tab ' + - (this.prefs.show_tabs ? 'withIndicator" ' : '" ') + - 'style="tab-size:' + tabSize + ';' + - '-moz-tab-size:' + tabSize + ';">\t</span>'; - return html.replace(TAB_REGEX, htmlStr); - }, - - _createElement: function(tagName, className) { - var el = document.createElement(tagName); - // When Shady DOM is being used, these classes are added to account for - // Polymer's polyfill behavior. In order to guarantee sufficient - // specificity within the CSS rules, these are added to every element. - // Since the Polymer DOM utility functions (which would do this - // automatically) are not being used for performance reasons, this is - // done manually. - el.classList.add('style-scope', 'gr-diff-side'); - if (!!className) { - el.classList.add(className); - } - return el; - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html deleted file mode 100644 index 761dd71..0000000 --- a/polygerrit-ui/app/elements/gr-diff-view.html +++ /dev/null
@@ -1,477 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-diff.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-diff-view"> - <template> - <style> - :host { - background-color: var(--view-background-color); - display: block; - } - h3 { - margin-top: 1em; - padding: .75em var(--default-horizontal-margin); - } - .reviewed { - display: inline-block; - margin: 0 .25em; - vertical-align: .15em; - } - .jumpToFileContainer { - display: inline-block; - } - .mobileJumpToFileContainer { - display: none; - } - .downArrow { - display: inline-block; - font-size: .6em; - vertical-align: middle; - } - .dropdown-trigger { - color: #00e; - cursor: pointer; - padding: 0; - } - .dropdown-content { - background-color: #fff; - box-shadow: 0 1px 5px rgba(0, 0, 0, .3); - } - .dropdown-content a { - cursor: pointer; - display: block; - font-weight: normal; - padding: .3em .5em; - } - .dropdown-content a:before { - color: #ccc; - content: attr(data-key-nav); - display: inline-block; - margin-right: .5em; - width: .3em; - } - .dropdown-content a:hover { - background-color: #00e; - color: #fff; - } - .dropdown-content a[selected] { - color: #000; - font-weight: bold; - pointer-events: none; - text-decoration: none; - } - .dropdown-content a[selected]:hover { - background-color: #fff; - color: #000; - } - gr-button { - font: inherit; - padding: .3em 0; - text-decoration: none; - } - @media screen and (max-width: 50em) { - .dash { - display: none; - } - .reviewed { - vertical-align: -.1em; - } - .jumpToFileContainer { - display: none; - } - .mobileJumpToFileContainer { - display: block; - width: 100%; - } - } - </style> - <gr-ajax id="changeDetailXHR" - auto - url="[[_computeChangeDetailPath(_changeNum)]]" - params="[[_computeChangeDetailQueryParams()]]" - last-response="{{_change}}"></gr-ajax> - <gr-ajax id="filesXHR" - auto - url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]" - on-response="_handleFilesResponse"></gr-ajax> - <gr-ajax id="configXHR" - auto - url="[[_computeProjectConfigPath(_change.project)]]" - last-response="{{_projectConfig}}"></gr-ajax> - <h3> - <a href$="[[_computeChangePath(_changeNum, _patchRange.patchNum, _change.revisions)]]"> - [[_changeNum]]</a><span>:</span> - <span>[[_change.subject]]</span> - <span class="dash">—</span> - <input id="reviewed" - class="reviewed" - type="checkbox" - on-change="_handleReviewedChange" - hidden$="[[!_loggedIn]]" hidden> - <div class="jumpToFileContainer"> - <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler"> - <span>[[_computeFileDisplayName(_path)]]</span> - <span class="downArrow">▼</span> - </gr-button> - <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25"> - <div class="dropdown-content"> - <template is="dom-repeat" items="[[_fileList]]" as="path"> - <a href$="[[_computeDiffURL(_changeNum, _patchRange, path)]]" - selected$="[[_computeFileSelected(path, _path)]]" - data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]" - on-tap="_handleFileTap"> - [[_computeFileDisplayName(path)]] - </a> - </template> - </div> - </iron-dropdown> - </div> - <div class="mobileJumpToFileContainer"> - <select on-change="_handleMobileSelectChange"> - <template is="dom-repeat" items="[[_fileList]]" as="path"> - <option - value$="[[path]]" - selected$="[[_computeFileSelected(path, _path)]]"> - [[_computeFileDisplayName(path)]] - </option> - </template> - </select> - </div> - </h3> - <gr-diff id="diff" - change-num="[[_changeNum]]" - prefs="{{prefs}}" - patch-range="[[_patchRange]]" - path="[[_path]]" - project-config="[[_projectConfig]]" - available-patches="[[_computeAvailablePatches(_change.revisions)]]" - on-render="_handleDiffRender"> - </gr-diff> - </template> - <script> - (function() { - 'use strict'; - - var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; - - Polymer({ - is: 'gr-diff-view', - - /** - * Fired when the title of the page should change. - * - * @event title-change - */ - - properties: { - prefs: { - type: Object, - notify: true, - }, - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, - keyEventTarget: { - type: Object, - value: function() { return document.body; }, - }, - changeViewState: { - type: Object, - notify: true, - value: function() { return {}; }, - }, - - _patchRange: Object, - _change: Object, - _changeNum: String, - _diff: Object, - _fileList: { - type: Array, - value: function() { return []; }, - }, - _path: { - type: String, - observer: '_pathChanged', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _xhrPromise: Object, // Used for testing. - }, - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.RESTClientBehavior, - ], - - ready: function() { - app.accountReady.then(function() { - this._loggedIn = app.loggedIn; - if (this._loggedIn) { - this._setReviewed(true); - } - }.bind(this)); - }, - - attached: function() { - if (this._path) { - this.fire('title-change', - {title: this._computeFileDisplayName(this._path)}); - } - window.addEventListener('resize', this._boundWindowResizeHandler); - }, - - detached: function() { - window.removeEventListener('resize', this._boundWindowResizeHandler); - }, - - _handleReviewedChange: function(e) { - this._setReviewed(Polymer.dom(e).rootTarget.checked); - }, - - _setReviewed: function(reviewed) { - this.$.reviewed.checked = reviewed; - var method = reviewed ? 'PUT' : 'DELETE'; - var url = this.changeBaseURL(this._changeNum, - this._patchRange.patchNum) + '/files/' + - encodeURIComponent(this._path) + '/reviewed'; - this._send(method, url).catch(function(err) { - alert('Couldn’t change file review status. Check the console ' + - 'and contact the PolyGerrit team for assistance.'); - throw err; - }.bind(this)); - }, - - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - switch (e.keyCode) { - case 219: // '[' - e.preventDefault(); - this._navToFile(this._fileList, -1); - break; - case 221: // ']' - e.preventDefault(); - this._navToFile(this._fileList, 1); - break; - case 78: // 'n' - if (e.shiftKey) { - this.$.diff.scrollToNextCommentThread(); - } else { - this.$.diff.scrollToNextDiffChunk(); - } - break; - case 80: // 'p' - if (e.shiftKey) { - this.$.diff.scrollToPreviousCommentThread(); - } else { - this.$.diff.scrollToPreviousDiffChunk(); - } - break; - case 65: // 'a' - if (!this._loggedIn) { return; } - - this.set('changeViewState.showReplyDialog', true); - /* falls through */ // required by JSHint - case 85: // 'u' - if (this._changeNum && this._patchRange.patchNum) { - e.preventDefault(); - page.show(this._computeChangePath( - this._changeNum, - this._patchRange.patchNum, - this._change && this._change.revisions)); - } - break; - case 188: // ',' - this.$.diff.showDiffPreferences(); - break; - } - }, - - _handleDiffRender: function() { - if (window.location.hash.length > 0) { - this.$.diff.scrollToLine( - parseInt(window.location.hash.substring(1), 10)); - } - }, - - _navToFile: function(fileList, direction) { - if (fileList.length == 0) { return; } - - var idx = fileList.indexOf(this._path) + direction; - if (idx < 0 || idx > fileList.length - 1) { - page.show(this._computeChangePath( - this._changeNum, - this._patchRange.patchNum, - this._change && this._change.revisions)); - return; - } - page.show(this._computeDiffURL(this._changeNum, - this._patchRange, - fileList[idx])); - }, - - _paramsChanged: function(value) { - if (value.view != this.tagName.toLowerCase()) { return; } - - this._changeNum = value.changeNum; - this._patchRange = { - patchNum: value.patchNum, - basePatchNum: value.basePatchNum || 'PARENT', - }; - this._path = value.path; - - this.fire('title-change', - {title: this._computeFileDisplayName(this._path)}); - - // When navigating away from the page, there is a possibility that the - // patch number is no longer a part of the URL (say when navigating to - // the top-level change info view) and therefore undefined in `params`. - if (!this._patchRange.patchNum) { - return; - } - - this.$.diff.reload(); - }, - - _pathChanged: function(path) { - if (this._fileList.length == 0) { return; } - - this.set('changeViewState.selectedFileIndex', - this._fileList.indexOf(path)); - - if (this._loggedIn) { - this._setReviewed(true); - } - }, - - _computeDiffURL: function(changeNum, patchRange, path) { - var patchStr = patchRange.patchNum; - if (patchRange.basePatchNum != null && - patchRange.basePatchNum != 'PARENT') { - patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; - } - return '/c/' + changeNum + '/' + patchStr + '/' + path; - }, - - _computeAvailablePatches: function(revisions) { - var patchNums = []; - for (var rev in revisions) { - patchNums.push(revisions[rev]._number); - } - return patchNums.sort(function(a, b) { return a - b; }); - }, - - _computeChangePath: function(changeNum, patchNum, revisions) { - var base = '/c/' + changeNum + '/'; - - // The change may not have loaded yet, making revisions unavailable. - if (!revisions) { - return base + patchNum; - } - - var latestPatchNum = -1; - for (var rev in revisions) { - latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number); - } - if (parseInt(patchNum, 10) != latestPatchNum) { - return base + patchNum; - } - - return base; - }, - - _computeFileDisplayName: function(path) { - return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path; - }, - - _computeChangeDetailPath: function(changeNum) { - return '/changes/' + changeNum + '/detail'; - }, - - _computeChangeDetailQueryParams: function() { - return {O: this.listChangesOptionsToHex( - this.ListChangesOption.ALL_REVISIONS - )}; - }, - - _computeFilesPath: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/files'; - }, - - _computeProjectConfigPath: function(project) { - return '/projects/' + encodeURIComponent(project) + '/config'; - }, - - _computeFileSelected: function(path, currentPath) { - return path == currentPath; - }, - - _computeKeyNav: function(path, selectedPath, fileList) { - var selectedIndex = fileList.indexOf(selectedPath); - if (fileList.indexOf(path) == selectedIndex - 1) { - return '['; - } - if (fileList.indexOf(path) == selectedIndex + 1) { - return ']'; - } - return ''; - }, - - _handleFileTap: function(e) { - this.$.dropdown.close(); - }, - - _handleMobileSelectChange: function(e) { - var path = Polymer.dom(e).rootTarget.value; - page.show( - this._computeDiffURL(this._changeNum, this._patchRange, path)); - }, - - _handleFilesResponse: function(e, req) { - this._fileList = Object.keys(e.detail.response).sort(); - }, - - _showDropdownTapHandler: function(e) { - this.$.dropdown.open(); - }, - - _send: function(method, url) { - var xhr = document.createElement('gr-request'); - this._xhrPromise = xhr.send({ - method: method, - url: url, - }); - return this._xhrPromise; - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff.html b/polygerrit-ui/app/elements/gr-diff.html deleted file mode 100644 index 39b1c9c..0000000 --- a/polygerrit-ui/app/elements/gr-diff.html +++ /dev/null
@@ -1,857 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-diff-preferences.html"> -<link rel="import" href="gr-diff-side.html"> -<link rel="import" href="gr-overlay.html"> -<link rel="import" href="gr-patch-range-select.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-diff"> - <template> - <style> - .loading { - padding: 0 var(--default-horizontal-margin) 1em; - color: #666; - } - .header { - display: flex; - justify-content: space-between; - margin: 0 var(--default-horizontal-margin) .75em; - } - .prefsButton { - text-align: right; - } - .diffContainer { - border-bottom: 1px solid #eee; - border-top: 1px solid #eee; - display: flex; - font: 12px var(--monospace-font-family); - overflow-x: auto; - } - gr-diff-side:first-of-type { - --light-highlight-color: #fee; - --dark-highlight-color: #ffd4d4; - } - gr-diff-side:last-of-type { - --light-highlight-color: #efe; - --dark-highlight-color: #d4ffd4; - border-right: 1px solid #ddd; - } - </style> - <gr-ajax id="diffXHR" - url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]" - params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]" - last-response="{{_diffResponse}}" - loading="{{_loading}}"></gr-ajax> - <gr-ajax id="baseCommentsXHR" - url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax> - <gr-ajax id="commentsXHR" - url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax> - <gr-ajax id="baseDraftsXHR" - url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax> - <gr-ajax id="draftsXHR" - url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax> - <div class="loading" hidden$="[[!_loading]]">Loading...</div> - <div hidden$="[[_loading]]" hidden> - <div class="header"> - <gr-patch-range-select - path="[[path]]" - change-num="[[changeNum]]" - patch-range="[[patchRange]]" - available-patches="[[availablePatches]]"></gr-patch-range-select> - <gr-button link - class="prefsButton" - on-tap="_handlePrefsTap" - hidden$="[[!prefs]]" - hidden>Diff View Preferences</gr-button> - </div> - <gr-overlay id="prefsOverlay" with-backdrop> - <gr-diff-preferences - prefs="{{prefs}}" - on-save="_handlePrefsSave" - on-cancel="_handlePrefsCancel"></gr-diff-preferences> - </gr-overlay> - - <div class="diffContainer"> - <gr-diff-side id="leftDiff" - change-num="[[changeNum]]" - patch-num="[[patchRange.basePatchNum]]" - path="[[path]]" - content="{{_diff.leftSide}}" - prefs="[[prefs]]" - can-comment="[[_loggedIn]]" - project-config="[[projectConfig]]" - on-expand-context="_handleExpandContext" - on-thread-height-change="_handleThreadHeightChange" - on-add-draft="_handleAddDraft" - on-remove-thread="_handleRemoveThread"></gr-diff-side> - <gr-diff-side id="rightDiff" - change-num="[[changeNum]]" - patch-num="[[patchRange.patchNum]]" - path="[[path]]" - content="{{_diff.rightSide}}" - prefs="[[prefs]]" - can-comment="[[_loggedIn]]" - project-config="[[projectConfig]]" - on-expand-context="_handleExpandContext" - on-thread-height-change="_handleThreadHeightChange" - on-add-draft="_handleAddDraft" - on-remove-thread="_handleRemoveThread"></gr-diff-side> - </div> - </div> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-diff', - - /** - * Fired when the diff is rendered. - * - * @event render - */ - - properties: { - availablePatches: Array, - changeNum: String, - /* - * A single object to encompass basePatchNum and patchNum is used - * so that both can be set at once without incremental observers - * firing after each property changes. - */ - patchRange: Object, - path: String, - prefs: { - type: Object, - notify: true, - }, - projectConfig: Object, - - _prefsReady: { - type: Object, - readOnly: true, - value: function() { - return new Promise(function(resolve) { - this._resolvePrefsReady = resolve; - }.bind(this)); - }, - }, - _baseComments: Array, - _comments: Array, - _drafts: Array, - _baseDrafts: Array, - /** - * Base (left side) comments and drafts grouped by line number. - * Only used for initial rendering. - */ - _groupedBaseComments: { - type: Object, - value: function() { return {}; }, - }, - /** - * Comments and drafts (right side) grouped by line number. - * Only used for initial rendering. - */ - _groupedComments: { - type: Object, - value: function() { return {}; }, - }, - _diffResponse: Object, - _diff: { - type: Object, - value: function() { return {}; }, - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _initialRenderComplete: { - type: Boolean, - value: false, - }, - _loading: { - type: Boolean, - value: true, - }, - _savedPrefs: Object, - - _diffRequestsPromise: Object, // Used for testing. - _diffPreferencesPromise: Object, // Used for testing. - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - observers: [ - '_prefsChanged(prefs.*)', - ], - - ready: function() { - app.accountReady.then(function() { - this._loggedIn = app.loggedIn; - }.bind(this)); - }, - - scrollToLine: function(lineNum) { - // TODO(andybons): Should this always be the right side? - this.$.rightDiff.scrollToLine(lineNum); - }, - - scrollToNextDiffChunk: function() { - this.$.rightDiff.scrollToNextDiffChunk(); - }, - - scrollToPreviousDiffChunk: function() { - this.$.rightDiff.scrollToPreviousDiffChunk(); - }, - - scrollToNextCommentThread: function() { - this.$.rightDiff.scrollToNextCommentThread(); - }, - - scrollToPreviousCommentThread: function() { - this.$.rightDiff.scrollToPreviousCommentThread(); - }, - - reload: function(changeNum, patchRange, path) { - // If a diff takes a considerable amount of time to render, the previous - // diff can end up showing up while the DOM is constructed. Clear the - // content on a reload to prevent this. - this._diff = { - leftSide: [], - rightSide: [], - }; - - var promises = [ - this._prefsReady, - this.$.diffXHR.generateRequest().completes - ]; - - var basePatchNum = this.patchRange.basePatchNum; - - return app.accountReady.then(function() { - promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn)); - this._diffRequestsPromise = Promise.all(promises).then(function() { - this._render(); - }.bind(this)).catch(function(err) { - alert('Oops. Something went wrong. Check the console and bug the ' + - 'PolyGerrit team for assistance.'); - throw err; - }); - }.bind(this)); - }, - - showDiffPreferences: function() { - this.$.prefsOverlay.open(); - }, - - _prefsChanged: function(changeRecord) { - if (this._initialRenderComplete) { - this._render(); - } - this._resolvePrefsReady(changeRecord.base); - }, - - _render: function() { - this._groupCommentsAndDrafts(); - this._processContent(); - - // Allow for the initial rendering to complete before firing the event. - this.async(function() { - this.fire('render', null, {bubbles: false}); - }.bind(this), 1); - - this._initialRenderComplete = true; - }, - - _getCommentsAndDrafts: function(basePatchNum, loggedIn) { - function onlyParent(c) { return c.side == 'PARENT'; } - function withoutParent(c) { return c.side != 'PARENT'; } - - var promises = []; - var commentsPromise = this.$.commentsXHR.generateRequest().completes; - promises.push(commentsPromise.then(function(req) { - var comments = req.response[this.path] || []; - if (basePatchNum == 'PARENT') { - this._baseComments = comments.filter(onlyParent); - } - this._comments = comments.filter(withoutParent); - }.bind(this))); - - if (basePatchNum != 'PARENT') { - commentsPromise = this.$.baseCommentsXHR.generateRequest().completes; - promises.push(commentsPromise.then(function(req) { - this._baseComments = - (req.response[this.path] || []).filter(withoutParent); - }.bind(this))); - } - - if (!loggedIn) { - this._baseDrafts = []; - this._drafts = []; - return Promise.all(promises); - } - - var draftsPromise = this.$.draftsXHR.generateRequest().completes; - promises.push(draftsPromise.then(function(req) { - var drafts = req.response[this.path] || []; - if (basePatchNum == 'PARENT') { - this._baseDrafts = drafts.filter(onlyParent); - } - this._drafts = drafts.filter(withoutParent); - }.bind(this))); - - if (basePatchNum != 'PARENT') { - draftsPromise = this.$.baseDraftsXHR.generateRequest().completes; - promises.push(draftsPromise.then(function(req) { - this._baseDrafts = - (req.response[this.path] || []).filter(withoutParent); - }.bind(this))); - } - - return Promise.all(promises); - }, - - _computeDiffPath: function(changeNum, patchNum, path) { - return this.changeBaseURL(changeNum, patchNum) + '/files/' + - encodeURIComponent(path) + '/diff'; - }, - - _computeCommentsPath: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/comments'; - }, - - _computeDraftsPath: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/drafts'; - }, - - _computeDiffQueryParams: function(basePatchNum) { - var params = { - context: 'ALL', - intraline: null - }; - if (basePatchNum != 'PARENT') { - params.base = basePatchNum; - } - return params; - }, - - _handlePrefsTap: function(e) { - e.preventDefault(); - - // TODO(andybons): This is not supported in IE. Implement a polyfill. - // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds - // an object as a value, it must be marked enumerable. - this._savedPrefs = Object.assign({}, this.prefs); - this.$.prefsOverlay.open(); - }, - - _handlePrefsSave: function(e) { - e.stopPropagation(); - var el = Polymer.dom(e).rootTarget; - el.disabled = true; - app.accountReady.then(function() { - if (!this._loggedIn) { - el.disabled = false; - this.$.prefsOverlay.close(); - return; - } - this._saveDiffPreferences().then(function() { - this.$.prefsOverlay.close(); - el.disabled = false; - }.bind(this)).catch(function(err) { - el.disabled = false; - alert('Oops. Something went wrong. Check the console and bug the ' + - 'PolyGerrit team for assistance.'); - throw err; - }); - }.bind(this)); - }, - - _saveDiffPreferences: function() { - var xhr = document.createElement('gr-request'); - this._diffPreferencesPromise = xhr.send({ - method: 'PUT', - url: '/accounts/self/preferences.diff', - body: this.prefs, - }); - return this._diffPreferencesPromise; - }, - - _handlePrefsCancel: function(e) { - e.stopPropagation(); - this.prefs = this._savedPrefs; - this.$.prefsOverlay.close(); - }, - - _handleExpandContext: function(e) { - var ctx = e.detail.context; - var contextControlIndex = -1; - for (var i = ctx.start; i <= ctx.end; i++) { - this._diff.leftSide[i].hidden = false; - this._diff.rightSide[i].hidden = false; - if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' && - this._diff.rightSide[i].type == 'CONTEXT_CONTROL') { - contextControlIndex = i; - } - } - this._diff.leftSide[contextControlIndex].hidden = true; - this._diff.rightSide[contextControlIndex].hidden = true; - - this.$.leftDiff.hideElementsWithIndex(contextControlIndex); - this.$.rightDiff.hideElementsWithIndex(contextControlIndex); - - this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end); - this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end); - }, - - _handleThreadHeightChange: function(e) { - var index = e.detail.index; - var diffEl = Polymer.dom(e).rootTarget; - var otherSide = diffEl == this.$.leftDiff ? - this.$.rightDiff : this.$.leftDiff; - - var threadHeight = e.detail.height; - var otherSideHeight; - if (otherSide.content[index].type == 'COMMENT_THREAD') { - otherSideHeight = otherSide.getRowNaturalHeight(index); - } else { - otherSideHeight = otherSide.getRowHeight(index); - } - var maxHeight = Math.max(threadHeight, otherSideHeight); - this.$.leftDiff.setRowHeight(index, maxHeight); - this.$.rightDiff.setRowHeight(index, maxHeight); - }, - - _handleAddDraft: function(e) { - var insertIndex = e.detail.index + 1; - var diffEl = Polymer.dom(e).rootTarget; - var content = diffEl.content; - if (content[insertIndex] && - content[insertIndex].type == 'COMMENT_THREAD') { - // A thread is already here. Do nothing. - return; - } - var comment = { - type: 'COMMENT_THREAD', - comments: [{ - __draft: true, - __draftID: Math.random().toString(36), - line: e.detail.line, - path: this.path, - }] - }; - if (diffEl == this.$.leftDiff && - this.patchRange.basePatchNum == 'PARENT') { - comment.comments[0].side = 'PARENT'; - comment.patchNum = this.patchRange.patchNum; - } - - if (content[insertIndex] && - content[insertIndex].type == 'FILLER') { - content[insertIndex] = comment; - diffEl.rowUpdated(insertIndex); - } else { - content.splice(insertIndex, 0, comment); - diffEl.rowInserted(insertIndex); - } - - var otherSide = diffEl == this.$.leftDiff ? - this.$.rightDiff : this.$.leftDiff; - if (otherSide.content[insertIndex] == null || - otherSide.content[insertIndex].type != 'COMMENT_THREAD') { - otherSide.content.splice(insertIndex, 0, { - type: 'FILLER', - }); - otherSide.rowInserted(insertIndex); - } - }, - - _handleRemoveThread: function(e) { - var diffEl = Polymer.dom(e).rootTarget; - var otherSide = diffEl == this.$.leftDiff ? - this.$.rightDiff : this.$.leftDiff; - var index = e.detail.index; - - if (otherSide.content[index].type == 'FILLER') { - otherSide.content.splice(index, 1); - otherSide.rowRemoved(index); - diffEl.content.splice(index, 1); - diffEl.rowRemoved(index); - } else if (otherSide.content[index].type == 'COMMENT_THREAD') { - diffEl.content[index] = {type: 'FILLER'}; - diffEl.rowUpdated(index); - var height = otherSide.setRowNaturalHeight(index); - diffEl.setRowHeight(index, height); - } else { - throw Error('A thread cannot be opposite anything but filler or ' + - 'another thread'); - } - }, - - _processContent: function() { - var leftSide = []; - var rightSide = []; - var initialLineNum = 0 + (this._diffResponse.content.skip || 0); - var ctx = { - hidingLines: false, - lastNumLinesHidden: 0, - left: { - lineNum: initialLineNum, - }, - right: { - lineNum: initialLineNum, - } - }; - var content = this._breakUpCommonChunksWithComments(ctx, - this._diffResponse.content); - var context = this.prefs.context; - if (context == -1) { - // Show the entire file. - context = Infinity; - } - for (var i = 0; i < content.length; i++) { - if (i == 0) { - ctx.skipRange = [0, context]; - } else if (i == content.length - 1) { - ctx.skipRange = [context, 0]; - } else { - ctx.skipRange = [context, context]; - } - ctx.diffChunkIndex = i; - this._addDiffChunk(ctx, content[i], leftSide, rightSide); - } - - this._diff = { - leftSide: leftSide, - rightSide: rightSide, - }; - }, - - // In order to show comments out of the bounds of the selected context, - // treat them as diffs within the model so that the content (and context - // surrounding it) renders correctly. - _breakUpCommonChunksWithComments: function(ctx, content) { - var result = []; - var leftLineNum = ctx.left.lineNum; - var rightLineNum = ctx.right.lineNum; - for (var i = 0; i < content.length; i++) { - if (!content[i].ab) { - result.push(content[i]); - if (content[i].a) { - leftLineNum += content[i].a.length; - } - if (content[i].b) { - rightLineNum += content[i].b.length; - } - continue; - } - var chunk = content[i].ab; - var currentChunk = {ab: []}; - for (var j = 0; j < chunk.length; j++) { - leftLineNum++; - rightLineNum++; - if (this._groupedBaseComments[leftLineNum] == null && - this._groupedComments[rightLineNum] == null) { - currentChunk.ab.push(chunk[j]); - } else { - if (currentChunk.ab && currentChunk.ab.length > 0) { - result.push(currentChunk); - currentChunk = {ab: []}; - } - // Append an annotation to indicate that this line should not be - // highlighted even though it's implied with both `a` and `b` - // defined. This is needed since there may be two lines that - // should be highlighted but are equal (blank lines, for example). - result.push({ - __noHighlight: true, - a: [chunk[j]], - b: [chunk[j]], - }); - } - } - if (currentChunk.ab != null && currentChunk.ab.length > 0) { - result.push(currentChunk); - } - } - return result; - }, - - _groupCommentsAndDrafts: function() { - this._baseDrafts.forEach(function(d) { d.__draft = true; }); - this._drafts.forEach(function(d) { d.__draft = true; }); - var allLeft = this._baseComments.concat(this._baseDrafts); - var allRight = this._comments.concat(this._drafts); - - var leftByLine = {}; - var rightByLine = {}; - var mapFunc = function(byLine) { - return function(c) { - // File comments/drafts are grouped with line 1 for now. - var line = c.line || 1; - if (byLine[line] == null) { - byLine[line] = []; - } - byLine[line].push(c); - }; - }; - allLeft.forEach(mapFunc(leftByLine)); - allRight.forEach(mapFunc(rightByLine)); - - this._groupedBaseComments = leftByLine; - this._groupedComments = rightByLine; - }, - - _addContextControl: function(ctx, leftSide, rightSide) { - var numLinesHidden = ctx.lastNumLinesHidden; - var leftStart = leftSide.length - numLinesHidden; - var leftEnd = leftSide.length; - var rightStart = rightSide.length - numLinesHidden; - var rightEnd = rightSide.length; - if (leftStart != rightStart || leftEnd != rightEnd) { - throw Error( - 'Left and right ranges for context control should be equal:' + - 'Left: [' + leftStart + ', ' + leftEnd + '] ' + - 'Right: [' + rightStart + ', ' + rightEnd + ']'); - } - var obj = { - type: 'CONTEXT_CONTROL', - numLines: numLinesHidden, - start: leftStart, - end: leftEnd, - }; - // NOTE: Be careful, here. This object is meant to be immutable. If the - // object is altered within one side's array it will reflect the - // alterations in another. - leftSide.push(obj); - rightSide.push(obj); - }, - - _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) { - for (var i = 0; i < chunk.ab.length; i++) { - var numLines = Math.ceil( - this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length); - var hidden = i >= ctx.skipRange[0] && - i < chunk.ab.length - ctx.skipRange[1]; - if (ctx.hidingLines && hidden == false) { - // No longer hiding lines. Add a context control. - this._addContextControl(ctx, leftSide, rightSide); - ctx.lastNumLinesHidden = 0; - } - ctx.hidingLines = hidden; - if (hidden) { - ctx.lastNumLinesHidden++; - } - - // Blank lines within a diff content array indicate a newline. - leftSide.push({ - type: 'CODE', - hidden: hidden, - content: chunk.ab[i] || '\n', - numLines: numLines, - lineNum: ++ctx.left.lineNum, - }); - rightSide.push({ - type: 'CODE', - hidden: hidden, - content: chunk.ab[i] || '\n', - numLines: numLines, - lineNum: ++ctx.right.lineNum, - }); - - this._addCommentsIfPresent(ctx, leftSide, rightSide); - } - if (ctx.lastNumLinesHidden > 0) { - this._addContextControl(ctx, leftSide, rightSide); - } - }, - - _addDiffChunk: function(ctx, chunk, leftSide, rightSide) { - if (chunk.ab) { - this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide); - return; - } - - var leftHighlights = []; - if (chunk.edit_a) { - leftHighlights = - this._normalizeIntralineHighlights(chunk.a, chunk.edit_a); - } - var rightHighlights = []; - if (chunk.edit_b) { - rightHighlights = - this._normalizeIntralineHighlights(chunk.b, chunk.edit_b); - } - - var aLen = (chunk.a && chunk.a.length) || 0; - var bLen = (chunk.b && chunk.b.length) || 0; - var maxLen = Math.max(aLen, bLen); - for (var i = 0; i < maxLen; i++) { - var hasLeftContent = chunk.a && i < chunk.a.length; - var hasRightContent = chunk.b && i < chunk.b.length; - var leftContent = hasLeftContent ? chunk.a[i] : ''; - var rightContent = hasRightContent ? chunk.b[i] : ''; - var highlight = !chunk.__noHighlight; - var maxNumLines = this._maxLinesSpanned(leftContent, rightContent); - if (hasLeftContent) { - leftSide.push({ - type: 'CODE', - content: leftContent || '\n', - numLines: maxNumLines, - lineNum: ++ctx.left.lineNum, - highlight: highlight, - intraline: highlight && leftHighlights.filter(function(hl) { - return hl.contentIndex == i; - }), - }); - } else { - leftSide.push({ - type: 'FILLER', - numLines: maxNumLines, - }); - } - if (hasRightContent) { - rightSide.push({ - type: 'CODE', - content: rightContent || '\n', - numLines: maxNumLines, - lineNum: ++ctx.right.lineNum, - highlight: highlight, - intraline: highlight && rightHighlights.filter(function(hl) { - return hl.contentIndex == i; - }), - }); - } else { - rightSide.push({ - type: 'FILLER', - numLines: maxNumLines, - }); - } - this._addCommentsIfPresent(ctx, leftSide, rightSide); - } - }, - - _addCommentsIfPresent: function(ctx, leftSide, rightSide) { - var leftComments = this._groupedBaseComments[ctx.left.lineNum]; - var rightComments = this._groupedComments[ctx.right.lineNum]; - if (leftComments) { - var thread = { - type: 'COMMENT_THREAD', - comments: leftComments, - }; - if (this.patchRange.basePatchNum == 'PARENT') { - thread.patchNum = this.patchRange.patchNum; - } - leftSide.push(thread); - } - if (rightComments) { - rightSide.push({ - type: 'COMMENT_THREAD', - comments: rightComments, - }); - } - if (leftComments && !rightComments) { - rightSide.push({type: 'FILLER'}); - } else if (!leftComments && rightComments) { - leftSide.push({type: 'FILLER'}); - } - this._groupedBaseComments[ctx.left.lineNum] = null; - this._groupedComments[ctx.right.lineNum] = null; - }, - - // The `highlights` array consists of a list of <skip length, mark length> - // pairs, where the skip length is the number of characters between the - // end of the previous edit and the start of this edit, and the mark - // length is the number of edited characters following the skip. The start - // of the edits is from the beginning of the related diff content lines. - // - // Note that the implied newline character at the end of each line is - // included in the length calculation, and thus it is possible for the - // edits to span newlines. - // - // A line highlight object consists of three fields: - // - contentIndex: The index of the diffChunk `content` field (the line - // being referred to). - // - startIndex: Where the highlight should begin. - // - endIndex: (optional) Where the highlight should end. If omitted, the - // highlight is meant to be a continuation onto the next line. - _normalizeIntralineHighlights: function(content, highlights) { - var contentIndex = 0; - var idx = 0; - var normalized = []; - for (var i = 0; i < highlights.length; i++) { - var line = content[contentIndex] + '\n'; - var hl = highlights[i]; - var j = 0; - while (j < hl[0]) { - if (idx == line.length) { - idx = 0; - line = content[++contentIndex] + '\n'; - continue; - } - idx++; - j++; - } - var lineHighlight = { - contentIndex: contentIndex, - startIndex: idx, - }; - - j = 0; - while (line && j < hl[1]) { - if (idx == line.length) { - idx = 0; - line = content[++contentIndex] + '\n'; - normalized.push(lineHighlight); - lineHighlight = { - contentIndex: contentIndex, - startIndex: idx, - }; - continue; - } - idx++; - j++; - } - lineHighlight.endIndex = idx; - normalized.push(lineHighlight); - } - return normalized; - }, - - _visibleLineLength: function(contents) { - // http://jsperf.com/performance-of-match-vs-split - var numTabs = contents.split('\t').length - 1; - return contents.length - numTabs + (this.prefs.tab_size * numTabs); - }, - - _maxLinesSpanned: function(left, right) { - return Math.max( - Math.ceil(this._visibleLineLength(left) / this.prefs.line_length), - Math.ceil(this._visibleLineLength(right) / this.prefs.line_length)); - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-download-dialog.html b/polygerrit-ui/app/elements/gr-download-dialog.html deleted file mode 100644 index 3212a0f..0000000 --- a/polygerrit-ui/app/elements/gr-download-dialog.html +++ /dev/null
@@ -1,269 +0,0 @@ -<!-- -Copyright (C) 2016 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-input/iron-input.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-button.html"> - -<dom-module id="gr-download-dialog"> - <template> - <style> - :host { - display: block; - padding: 1em; - } - ul { - list-style: none; - margin-bottom: .5em; - } - li { - display: inline-block; - margin: 0; - padding: 0; - } - li gr-button { - margin-right: 1em; - } - label, - input { - display: block; - } - label { - font-weight: bold; - } - input { - font-family: var(--monospace-font-family); - font-size: inherit; - margin-bottom: .5em; - width: 60em; - } - li[selected] gr-button { - color: #000; - font-weight: bold; - text-decoration: none; - } - header { - display: flex; - justify-content: space-between; - } - main { - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; - padding: .5em; - } - footer { - display: flex; - justify-content: space-between; - padding-top: .75em; - } - .closeButtonContainer { - display: flex; - flex: 1; - justify-content: flex-end; - } - .patchFiles { - margin-right: 2em; - } - .patchFiles a, - .archives a { - display: inline-block; - margin-right: 1em; - } - .patchFiles a:last-of-type, - .archives a:last-of-type { - margin-right: 0; - } - </style> - <header> - <ul hidden$="[[!_schemes.length]]" hidden> - <template is="dom-repeat" items="[[_schemes]]" as="scheme"> - <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]"> - <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap"> - [[scheme]] - </gr-button> - </li> - </template> - </ul> - <span class="closeButtonContainer"> - <gr-button link on-tap="_handleCloseTap">Close</gr-button> - </span> - </header> - <main hidden$="[[!_schemes.length]]" hidden> - <template is="dom-repeat" - items="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" - as="command"> - <div class="command"> - <label>[[command.title]]</label> - <input is="iron-input" - type="text" - bind-value="[[command.command]]" - on-tap="_handleInputTap" - readonly> - </div> - </template> - </main> - <footer> - <div class="patchFiles"> - <label>Patch file</label> - <div> - <a href$="[[_computeDownloadLink(change, patchNum)]]"> - [[_computeDownloadFilename(change, patchNum)]] - </a> - <a href$="[[_computeZipDownloadLink(change, patchNum)]]"> - [[_computeZipDownloadFilename(change, patchNum)]] - </a> - </div> - </div> - <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden> - <label>Archive</label> - <div class="archives"> - <template is="dom-repeat" items="[[config.archives]]" as="format"> - <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]"> - [[format]] - </a> - </template> - </div> - </div> - </footer> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-download-dialog', - - /** - * Fired when the user presses the close button. - * - * @event close - */ - - properties: { - change: Object, - patchNum: String, - config: Object, - - _schemes: { - type: Array, - value: function() { return []; }, - computed: '_computeSchemes(change, patchNum)', - observer: '_schemesChanged', - }, - _selectedScheme: String, - }, - - hostAttributes: { - role: 'dialog', - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - _computeDownloadCommands: function(change, patchNum, _selectedScheme) { - var commandObj; - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { - commandObj = change.revisions[rev].fetch[_selectedScheme].commands; - break; - } - } - var commands = []; - for (var title in commandObj) { - commands.push({ - title: title, - command: commandObj[title], - }); - } - return commands; - }, - - _computeZipDownloadLink: function(change, patchNum) { - return this._computeDownloadLink(change, patchNum, true); - }, - - _computeZipDownloadFilename: function(change, patchNum) { - return this._computeDownloadFilename(change, patchNum, true); - }, - - _computeDownloadLink: function(change, patchNum, zip) { - return this.changeBaseURL(change._number, patchNum) + '/patch?' + - (zip ? 'zip' : 'download'); - }, - - _computeDownloadFilename: function(change, patchNum, zip) { - var shortRev; - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { - shortRev = rev.substr(0, 7); - break; - } - } - return shortRev + '.diff.' + (zip ? 'zip' : 'base64'); - }, - - _computeArchiveDownloadLink: function(change, patchNum, format) { - return this.changeBaseURL(change._number, patchNum) + - '/archive?format=' + format; - }, - - _computeSchemes: function(change, patchNum) { - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { - var fetch = change.revisions[rev].fetch; - if (fetch) { - return Object.keys(fetch).sort(); - } - break; - } - } - return []; - }, - - _computeSchemeSelected: function(scheme, selectedScheme) { - return scheme == selectedScheme; - }, - - _handleSchemeTap: function(e) { - e.preventDefault(); - var el = Polymer.dom(e).rootTarget; - // TODO(andybons): Save as default scheme in preferences. - this._selectedScheme = el.getAttribute('data-scheme'); - }, - - _handleInputTap: function(e) { - e.preventDefault(); - Polymer.dom(e).rootTarget.select(); - }, - - _handleCloseTap: function(e) { - e.preventDefault(); - this.fire('close', null, {bubbles: false}); - }, - - _schemesChanged: function(schemes) { - if (schemes.length == 0) { return; } - if (schemes.indexOf(this._selectedScheme) == -1) { - this._selectedScheme = schemes.sort()[0]; - } - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-file-list.html b/polygerrit-ui/app/elements/gr-file-list.html deleted file mode 100644 index bcf3e05..0000000 --- a/polygerrit-ui/app/elements/gr-file-list.html +++ /dev/null
@@ -1,352 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-file-list"> - <template> - <style> - :host { - display: block; - } - .row { - display: flex; - padding: .1em .25em; - } - .header { - font-weight: bold; - } - .positionIndicator, - .reviewed, - .status { - align-items: center; - display: inline-flex; - } - .reviewed, - .status { - justify-content: center; - width: 1.5em; - } - .positionIndicator { - justify-content: flex-start; - visibility: hidden; - width: 1.25em; - } - .row[selected] { - background-color: #ebf5fb; - } - .row[selected] .positionIndicator { - visibility: visible; - } - .path { - flex: 1; - overflow: hidden; - padding-left: .35em; - text-decoration: none; - text-overflow: ellipsis; - white-space: nowrap; - } - .row:not(.header) .path:hover { - text-decoration: underline; - } - .comments, - .stats { - text-align: right; - } - .comments { - min-width: 10em; - } - .stats { - min-width: 7em; - } - .invisible { - visibility: hidden; - } - .row:not(.header) .stats { - font-family: var(--monospace-font-family); - } - .added { - color: #388E3C; - } - .removed { - color: #D32F2F; - } - .reviewed input[type="checkbox"] { - display: inline-block; - } - .drafts { - color: #C62828; - font-weight: bold; - } - @media screen and (max-width: 50em) { - .row[selected] { - background-color: transparent; - } - .positionIndicator, - .stats { - display: none; - } - .reviewed, - .status { - justify-content: flex-start; - } - .comments { - min-width: initial; - } - } - </style> - <gr-ajax id="filesXHR" - url="[[_computeFilesURL(changeNum, patchNum)]]" - on-response="_handleResponse"></gr-ajax> - <gr-ajax id="draftsXHR" - url="[[_computeDraftsURL(changeNum, patchNum)]]" - last-response="{{_drafts}}"></gr-ajax> - <gr-ajax id="reviewedXHR" - url="[[_computeReviewedURL(changeNum, patchNum)]]" - last-response="{{_reviewed}}"></gr-ajax> - </gr-ajax> - - <div class="row header"> - <div class="positionIndicator"></div> - <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div> - <div class="status"></div> - <div class="path">Path</div> - <div class="comments">Comments</div> - <div class="stats">Stats</div> - </div> - <template is="dom-repeat" items="{{files}}" as="file"> - <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]"> - <div class="positionIndicator">▶</div> - <div class="reviewed" hidden$="[[!_loggedIn]]" hidden> - <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]" - data-path$="[[file.__path]]" on-change="_handleReviewedChange"> - </div> - <div class$="[[_computeClass('status', file.__path)]]"> - [[_computeFileStatus(file.status)]] - </div> - <a class="path" href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]"> - [[_computeFileDisplayName(file.__path)]] - </a> - <div class="comments"> - <span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span> - [[_computeCommentsString(comments, patchNum, file.__path)]] - </div> - <div class$="[[_computeClass('stats', file.__path)]]"> - <span class="added">+[[file.lines_inserted]]</span> - <span class="removed">-[[file.lines_deleted]]</span> - </div> - </div> - </template> - </template> - <script> - (function() { - 'use strict'; - - var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; - - Polymer({ - is: 'gr-file-list', - - properties: { - patchNum: String, - changeNum: String, - comments: Object, - files: Array, - selectedIndex: { - type: Number, - notify: true, - }, - keyEventTarget: { - type: Object, - value: function() { return document.body; }, - }, - - _loggedIn: { - type: Boolean, - value: false, - }, - _drafts: Object, - _reviewed: { - type: Array, - value: function() { return []; }, - }, - _xhrPromise: Object, // Used for testing. - }, - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.RESTClientBehavior, - ], - - reload: function() { - if (!this.changeNum || !this.patchNum) { - return Promise.resolve(); - } - return Promise.all([ - this.$.filesXHR.generateRequest().completes, - app.accountReady.then(function() { - this._loggedIn = app.loggedIn; - if (!app.loggedIn) { return; } - this.$.draftsXHR.generateRequest(); - this.$.reviewedXHR.generateRequest(); - }.bind(this)), - ]); - }, - - _computeFilesURL: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/files'; - }, - - _computeCommentsString: function(comments, patchNum, path) { - var patchComments = (comments[path] || []).filter(function(c) { - return c.patch_set == patchNum; - }); - var num = patchComments.length; - if (num == 0) { return ''; } - if (num == 1) { return '1 comment'; } - if (num > 1) { return num + ' comments'; } - }, - - _computeReviewedURL: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed'; - }, - - _computeReviewed: function(file, _reviewed) { - return _reviewed.indexOf(file.__path) != -1; - }, - - _handleReviewedChange: function(e) { - var path = Polymer.dom(e).rootTarget.getAttribute('data-path'); - var index = this._reviewed.indexOf(path); - var reviewed = index != -1; - if (reviewed) { - this.splice('_reviewed', index, 1); - } else { - this.push('_reviewed', path); - } - - var method = reviewed ? 'DELETE' : 'PUT'; - var url = this.changeBaseURL(this.changeNum, this.patchNum) + - '/files/' + encodeURIComponent(path) + '/reviewed'; - this._send(method, url).catch(function(err) { - alert('Couldn’t change file review status. Check the console ' + - 'and contact the PolyGerrit team for assistance.'); - throw err; - }.bind(this)); - }, - - _computeDraftsURL: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/drafts'; - }, - - _computeDraftsString: function(drafts, path) { - var num = (drafts[path] || []).length; - if (num == 0) { return ''; } - if (num == 1) { return '1 draft'; } - if (num > 1) { return num + ' drafts'; } - }, - - _handleResponse: function(e, req) { - var result = e.detail.response; - var paths = Object.keys(result).sort(); - var files = []; - for (var i = 0; i < paths.length; i++) { - var info = result[paths[i]]; - info.__path = paths[i]; - info.lines_inserted = info.lines_inserted || 0; - info.lines_deleted = info.lines_deleted || 0; - files.push(info); - } - this.files = files; - }, - - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - switch (e.keyCode) { - case 74: // 'j' - e.preventDefault(); - this.selectedIndex = - Math.min(this.files.length - 1, this.selectedIndex + 1); - break; - case 75: // 'k' - e.preventDefault(); - this.selectedIndex = Math.max(0, this.selectedIndex - 1); - break; - case 219: // '[' - e.preventDefault(); - this._openSelectedFile(this.files.length - 1); - break; - case 221: // ']' - e.preventDefault(); - this._openSelectedFile(0); - break; - case 13: // <enter> - case 79: // 'o' - e.preventDefault(); - this._openSelectedFile(); - break; - } - }, - - _openSelectedFile: function(opt_index) { - if (opt_index != null) { - this.selectedIndex = opt_index; - } - page.show(this._computeDiffURL(this.changeNum, this.patchNum, - this.files[this.selectedIndex].__path)); - }, - - _computeFileSelected: function(index, selectedIndex) { - return index == selectedIndex; - }, - - _computeFileStatus: function(status) { - return status || 'M'; - }, - - _computeDiffURL: function(changeNum, patchNum, path) { - return '/c/' + changeNum + '/' + patchNum + '/' + path; - }, - - _computeFileDisplayName: function(path) { - return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path; - }, - - _computeClass: function(baseClass, path) { - var classes = [baseClass]; - if (path == COMMIT_MESSAGE_PATH) { - classes.push('invisible'); - } - return classes.join(' '); - }, - - _send: function(method, url) { - var xhr = document.createElement('gr-request'); - this._xhrPromise = xhr.send({ - method: method, - url: url, - }); - return this._xhrPromise; - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-linked-text.html b/polygerrit-ui/app/elements/gr-linked-text.html deleted file mode 100644 index 5c2d9a5..0000000 --- a/polygerrit-ui/app/elements/gr-linked-text.html +++ /dev/null
@@ -1,101 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<script src="../scripts/ba-linkify.js"></script> -<script src="../scripts/link-text-parser.js"></script> -<dom-module id="gr-linked-text"> - <template> - <style> - :host { - display: block; - } - :host([pre]) span { - white-space: pre-wrap; - word-wrap: break-word; - } - :host([disabled]) a { - color: inherit; - text-decoration: none; - pointer-events: none; - } - </style> - <span id="output"></span> - </template> - <script> - 'use strict'; - - Polymer({ - is: 'gr-linked-text', - - properties: { - content: { - type: String, - observer: '_contentChanged', - }, - pre: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - config: Object, - }, - - observers: [ - '_contentOrConfigChanged(content, config)', - ], - - _contentChanged: function(content) { - // In the case where the config may not be set (perhaps due to the - // request for it still being in flight), set the content anyway to - // prevent waiting on the config to display the text. - if (this.config != null) { return; } - this.$.output.textContent = content; - }, - - _contentOrConfigChanged: function(content, config) { - var output = Polymer.dom(this.$.output); - output.textContent = ''; - var parser = new GrLinkTextParser(config, function(text, href, html) { - if (href) { - var a = document.createElement('a'); - a.href = href; - a.textContent = text; - a.target = '_blank'; - output.appendChild(a); - } else if (html) { - var fragment = document.createDocumentFragment(); - // Create temporary div to hold the nodes in. - var div = document.createElement('div'); - div.innerHTML = html; - while (div.firstChild) { - fragment.appendChild(div.firstChild); - } - output.appendChild(fragment); - } else { - output.appendChild(document.createTextNode(text)); - } - }); - parser.parse(content); - } - }); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-message.html b/polygerrit-ui/app/elements/gr-message.html deleted file mode 100644 index fef8968..0000000 --- a/polygerrit-ui/app/elements/gr-message.html +++ /dev/null
@@ -1,223 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-account-link.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-comment-list.html"> -<link rel="import" href="gr-date-formatter.html"> -<link rel="import" href="gr-linked-text.html"> - -<dom-module id="gr-message"> - <template> - <style> - :host { - border-top: 1px solid #ddd; - display: block; - position: relative; - } - :host(:not([expanded])) { - cursor: pointer; - } - gr-avatar { - position: absolute; - left: var(--default-horizontal-margin); - } - .collapsed .contentContainer { - color: #777; - white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; - } - .showAvatar.expanded .contentContainer { - margin-left: calc(var(--default-horizontal-margin) + 2.5em); - padding: 10px 0; - } - .showAvatar.collapsed .contentContainer { - margin-left: calc(var(--default-horizontal-margin) + 1.75em); - padding: 10px 75px 10px 0; - } - .hideAvatar.collapsed .contentContainer, - .hideAvatar.expanded .contentContainer { - margin-left: 0; - padding: 10px 75px 10px 0; - } - .collapsed gr-avatar { - top: 8px; - height: 1.75em; - width: 1.75em; - } - .expanded gr-avatar { - top: 12px; - height: 2.5em; - width: 2.5em; - } - .name { - font-weight: bold; - } - .content { - font-family: var(--monospace-font-family); - } - .collapsed .name, - .collapsed .content, - .collapsed .message { - display: inline; - } - .collapsed gr-comment-list, - .collapsed .replyContainer { - display: none; - } - .collapsed .name { - color: var(--default-text-color); - } - .expanded .name { - cursor: pointer; - } - .date { - color: #666; - position: absolute; - right: var(--default-horizontal-margin); - top: 10px; - } - .replyContainer { - padding: .5em 0 1em; - } - </style> - <div class$="[[_computeClass(expanded, showAvatar)]]"> - <gr-avatar account="[[message.author]]" image-size="100"></gr-avatar> - <div class="contentContainer"> - <div class="name" on-tap="_handleNameTap">[[message.author.name]]</div> - <div class="content"> - <gr-linked-text class="message" - pre="[[expanded]]" - content="[[message.message]]" - disabled="[[!expanded]]" - config="[[projectConfig.commentlinks]]"></gr-linked-text> - <gr-comment-list - comments="[[comments]]" - change-num="[[changeNum]]" - patch-num="[[message._revision_number]]"></gr-comment-list> - </div> - <a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap"> - <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter> - </a> - </div> - <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden> - <gr-button small on-tap="_handleReplyTap">Reply</gr-button> - </div> - </div> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-message', - - /** - * Fired when this message's permalink is tapped. - * - * @event scroll-to - */ - - /** - * Fired when this message's reply link is tapped. - * - * @event reply - */ - - listeners: { - 'tap': '_handleTap', - }, - - properties: { - changeNum: Number, - message: Object, - comments: { - type: Object, - observer: '_commentsChanged', - }, - expanded: { - type: Boolean, - value: true, - reflectToAttribute: true, - }, - showAvatar: { - type: Boolean, - value: false, - }, - showReplyButton: { - type: Boolean, - value: false, - }, - projectConfig: Object, - }, - - ready: function() { - app.configReady.then(function(cfg) { - this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) && - this.message && this.message.author; - }.bind(this)); - }, - - _commentsChanged: function(value) { - this.expanded = Object.keys(value || {}).length > 0; - }, - - _handleTap: function(e) { - if (this.expanded) { return; } - this.expanded = true; - }, - - _handleNameTap: function(e) { - if (!this.expanded) { return; } - e.stopPropagation(); - this.expanded = false; - }, - - _computeClass: function(expanded, showAvatar) { - var classes = []; - classes.push(expanded ? 'expanded' : 'collapsed'); - classes.push(showAvatar ? 'showAvatar' : 'hideAvatar'); - return classes.join(' '); - }, - - _computeMessageHash: function(message) { - return '#message-' + message.id; - }, - - _handleLinkTap: function(e) { - e.preventDefault(); - - this.fire('scroll-to', {message: this.message}, {bubbles: false}); - - var hash = this._computeMessageHash(this.message); - // Don't add the hash to the window history if it's already there. - // Otherwise you mess up expected back button behavior. - if (window.location.hash == hash) { return; } - // Change the URL but don’t trigger a nav event. Otherwise it will - // reload the page. - page.show(window.location.pathname + hash, null, false); - }, - - _handleReplyTap: function(e) { - e.preventDefault(); - this.fire('reply', {message: this.message}); - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-messages-list.html b/polygerrit-ui/app/elements/gr-messages-list.html deleted file mode 100644 index 38bec1a..0000000 --- a/polygerrit-ui/app/elements/gr-messages-list.html +++ /dev/null
@@ -1,162 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-message.html"> - -<dom-module id="gr-messages-list"> - <template> - <style> - :host { - display: block; - } - .header { - display: flex; - justify-content: space-between; - margin-bottom: .35em; - } - .header, - gr-message { - padding: 0 var(--default-horizontal-margin); - } - .highlighted { - animation: 3s fadeOut; - } - @keyframes fadeOut { - 0% { background-color: #fff9c4; } - 100% { background-color: #fff; } - } - </style> - <div class="header"> - <h3>Messages</h3> - <gr-button link on-tap="_handleExpandCollapseTap"> - [[_computeExpandCollapseMessage(_expanded)]] - </gr-button> - </div> - <template is="dom-repeat" items="[[messages]]" as="message"> - <gr-message - change-num="[[changeNum]]" - message="[[message]]" - comments="[[_computeCommentsForMessage(comments, message, index)]]" - project-config="[[projectConfig]]" - show-reply-button="[[showReplyButtons]]" - on-scroll-to="_handleScrollTo" - data-message-id$="[[message.id]]"></gr-message> - </template> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-messages-list', - - properties: { - changeNum: Number, - messages: { - type: Array, - value: function() { return []; }, - }, - comments: Object, - projectConfig: Object, - topMargin: Number, - showReplyButtons: { - type: Boolean, - value: false, - }, - - _expanded: { - type: Boolean, - value: false, - }, - }, - - scrollToMessage: function(messageID) { - var el = this.$$('[data-message-id="' + messageID + '"]'); - if (!el) { return; } - - el.expanded = true; - var top = el.offsetTop; - for (var offsetParent = el.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { - top += offsetParent.offsetTop; - } - window.scrollTo(0, top - this.topMargin); - this._highlightEl(el); - }, - - _highlightEl: function(el) { - var highlightedEls = - Polymer.dom(this.root).querySelectorAll('.highlighted'); - for (var i = 0; i < highlightedEls.length; i++) { - highlightedEls[i].classList.remove('highlighted'); - } - function handleAnimationEnd() { - el.removeEventListener('animationend', handleAnimationEnd); - el.classList.remove('highlighted'); - } - el.addEventListener('animationend', handleAnimationEnd); - el.classList.add('highlighted'); - }, - - _handleExpandCollapseTap: function(e) { - e.preventDefault(); - this._expanded = !this._expanded; - var messageEls = Polymer.dom(this.root).querySelectorAll('gr-message'); - for (var i = 0; i < messageEls.length; i++) { - messageEls[i].expanded = this._expanded; - } - }, - - _handleScrollTo: function(e) { - this.scrollToMessage(e.detail.message.id); - }, - - _computeExpandCollapseMessage: function(expanded) { - return expanded ? 'Collapse all' : 'Expand all'; - }, - - _computeCommentsForMessage: function(comments, message, index) { - comments = comments || {}; - var messages = this.messages || []; - var msgComments = {}; - var mDate = util.parseDate(message.date); - var nextMDate; - if (index < messages.length - 1) { - nextMDate = util.parseDate(messages[index + 1].date); - } - for (var file in comments) { - var fileComments = comments[file]; - for (var i = 0; i < fileComments.length; i++) { - var cDate = util.parseDate(fileComments[i].updated); - if (cDate >= mDate) { - if (nextMDate && cDate >= nextMDate) { - continue; - } - msgComments[file] = msgComments[file] || []; - msgComments[file].push(fileComments[i]); - } - } - } - return msgComments; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-overlay.html b/polygerrit-ui/app/elements/gr-overlay.html deleted file mode 100644 index 3119747..0000000 --- a/polygerrit-ui/app/elements/gr-overlay.html +++ /dev/null
@@ -1,65 +0,0 @@ -<!-- -Copyright (C) 2016 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-overlay-behavior/iron-overlay-behavior.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> - -<dom-module id="gr-overlay"> - <template> - <style> - :host { - background: #fff; - box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px; - } - </style> - <content></content> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-overlay', - - behaviors: [ - Polymer.IronOverlayBehavior, - ], - - detached: function() { - // For good measure. - Gerrit.KeyboardShortcutBehavior.enabled = true; - }, - - open: function() { - Gerrit.KeyboardShortcutBehavior.enabled = false; - Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments); - }, - - close: function() { - Gerrit.KeyboardShortcutBehavior.enabled = true; - Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments); - }, - - cancel: function() { - Gerrit.KeyboardShortcutBehavior.enabled = true; - Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments); - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-patch-range-select.html b/polygerrit-ui/app/elements/gr-patch-range-select.html deleted file mode 100644 index a2a5dc4..0000000 --- a/polygerrit-ui/app/elements/gr-patch-range-select.html +++ /dev/null
@@ -1,95 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> - -<dom-module id="gr-patch-range-select"> - <template> - <style> - :host { - display: block; - } - .patchRange { - display: inline-block; - } - </style> - Patch set: - <span class="patchRange"> - <select id="leftPatchSelect" on-change="_handlePatchChange"> - <option value="PARENT" - selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option> - <template is="dom-repeat" items="{{availablePatches}}" as="patchNum"> - <option value$="[[patchNum]]" - selected$="[[_computeLeftSelected(patchNum, patchRange)]]" - disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option> - </template> - </select> - </span> - → - <span class="patchRange"> - <select id="rightPatchSelect" on-change="_handlePatchChange"> - <template is="dom-repeat" items="{{availablePatches}}" as="patchNum"> - <option value$="[[patchNum]]" - selected$="[[_computeRightSelected(patchNum, patchRange)]]" - disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option> - </template> - </select> - </span> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-patch-range-select', - - properties: { - availablePatches: Array, - changeNum: String, - patchRange: Object, - path: String, - }, - - _handlePatchChange: function(e) { - var leftPatch = this.$.leftPatchSelect.value; - var rightPatch = this.$.rightPatchSelect.value; - var rangeStr = rightPatch; - if (leftPatch != 'PARENT') { - rangeStr = leftPatch + '..' + rangeStr; - } - page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path); - }, - - _computeLeftSelected: function(patchNum, patchRange) { - return patchNum == patchRange.basePatchNum; - }, - - _computeRightSelected: function(patchNum, patchRange) { - return patchNum == patchRange.patchNum; - }, - - _computeLeftDisabled: function(patchNum, patchRange) { - return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10); - }, - - _computeRightDisabled: function(patchNum, patchRange) { - if (patchRange.basePatchNum == 'PARENT') { return false; } - return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10); - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-related-changes-list.html b/polygerrit-ui/app/elements/gr-related-changes-list.html deleted file mode 100644 index e3a7d2e..0000000 --- a/polygerrit-ui/app/elements/gr-related-changes-list.html +++ /dev/null
@@ -1,363 +0,0 @@ -<!-- -Copyright (C) 2016 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-ajax.html"> - -<dom-module id="gr-related-changes-list"> - <template> - <style> - :host { - display: block; - } - h3 { - margin: .5em 0 0; - } - section { - margin-bottom: 1em; - } - a { - display: block; - } - .relatedChanges a { - display: inline-block; - } - .strikethrough { - color: #666; - text-decoration: line-through; - } - .status { - color: #666; - font-weight: bold; - } - .notCurrent { - color: #e65100; - } - .indirectAncestor { - color: #33691e; - } - .submittable { - color: #1b5e20; - } - .hidden { - display: none; - } - </style> - <gr-ajax id="relatedXHR" - url="[[_computeRelatedURL(change._number, patchNum)]]" - last-response="{{_relatedResponse}}"></gr-ajax> - <gr-ajax id="submittedTogetherXHR" - url="[[_computeSubmittedTogetherURL(change._number)]]" - last-response="{{_submittedTogether}}"></gr-ajax> - <gr-ajax id="conflictsXHR" - url="/changes/" - params="[[_computeConflictsQueryParams(change._number)]]" - last-response="{{_conflicts}}"></gr-ajax> - <gr-ajax id="cherryPicksXHR" - url="/changes/" - params="[[_computeCherryPicksQueryParams(change.project, change.change_id, change._number)]]" - last-response="{{_cherryPicks}}"></gr-ajax> - <gr-ajax id="sameTopicXHR" - url="/changes/" - params="[[_computeSameTopicQueryParams(change.topic)]]" - last-response="{{_sameTopic}}"></gr-ajax> - - <div hidden$="[[!_loading]]">Loading...</div> - <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden> - <h4>Relation Chain</h4> - <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="change"> - <div> - <a href$="[[_computeChangeURL(change._change_number, change._revision_number)]]" - class$="[[_computeLinkClass(change)]]"> - [[change.commit.subject]] - </a> - <span class$="[[_computeChangeStatusClass(change)]]"> - ([[_computeChangeStatus(change)]]) - </span> - </div> - </template> - </section> - <section hidden$="[[!_submittedTogether.length]]" hidden> - <h4>Submitted together</h4> - <template is="dom-repeat" items="[[_submittedTogether]]" as="change"> - <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> - [[change.project]]: [[change.branch]]: [[change.subject]] - </a> - </template> - </section> - <section hidden$="[[!_sameTopic.length]]" hidden> - <h4>Same topic</h4> - <template is="dom-repeat" items="[[_sameTopic]]" as="change"> - <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> - [[change.project]]: [[change.branch]]: [[change.subject]] - </a> - </template> - </section> - <section hidden$="[[!_conflicts.length]]" hidden> - <h4>Merge conflicts</h4> - <template is="dom-repeat" items="[[_conflicts]]" as="change"> - <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> - [[change.subject]] - </a> - </template> - </section> - <section hidden$="[[!_cherryPicks.length]]" hidden> - <h4>Cherry picks</h4> - <template is="dom-repeat" items="[[_cherryPicks]]" as="change"> - <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> - [[change.subject]] - </a> - </template> - </section> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-related-changes-list', - - properties: { - change: Object, - patchNum: String, - serverConfig: { - type: Object, - observer: '_serverConfigChanged', - }, - hidden: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - - _loading: Boolean, - _resolveServerConfigReady: Function, - _serverConfigReady: { - type: Object, - value: function() { - return new Promise(function(resolve) { - this._resolveServerConfigReady = resolve; - }.bind(this)); - } - }, - _connectedRevisions: { - type: Array, - computed: '_computeConnectedRevisions(change, patchNum, ' + - '_relatedResponse.changes)', - }, - _relatedResponse: Object, - _submittedTogether: Array, - _conflicts: Array, - _cherryPicks: Array, - _sameTopic: Array, - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - observers: [ - '_resultsChanged(_relatedResponse.changes, _submittedTogether, ' + - '_conflicts, _cherryPicks, _sameTopic)', - ], - - reload: function() { - if (!this.change || !this.patchNum) { - return Promise.resolve(); - } - this._loading = true; - var promises = [ - this.$.relatedXHR.generateRequest().completes, - this.$.submittedTogetherXHR.generateRequest().completes, - this.$.conflictsXHR.generateRequest().completes, - this.$.cherryPicksXHR.generateRequest().completes, - ]; - - return this._serverConfigReady.then(function() { - if (this.change.topic && - !this.serverConfig.change.submit_whole_topic) { - return this.$.sameTopicXHR.generateRequest().completes; - } else { - this._sameTopic = []; - } - return Promise.resolve(); - }.bind(this)).then(Promise.all(promises)).then(function() { - this._loading = false; - }.bind(this)); - }, - - _computeRelatedURL: function(changeNum, patchNum) { - return this.changeBaseURL(changeNum, patchNum) + '/related'; - }, - - _computeSubmittedTogetherURL: function(changeNum) { - return this.changeBaseURL(changeNum) + '/submitted_together'; - }, - - _computeConflictsQueryParams: function(changeNum) { - var options = this.listChangesOptionsToHex( - this.ListChangesOption.CURRENT_REVISION, - this.ListChangesOption.CURRENT_COMMIT - ); - return { - O: options, - q: 'status:open is:mergeable conflicts:' + changeNum, - }; - }, - - _computeCherryPicksQueryParams: function(project, changeID, changeNum) { - var options = this.listChangesOptionsToHex( - this.ListChangesOption.CURRENT_REVISION, - this.ListChangesOption.CURRENT_COMMIT - ); - var query = [ - 'project:' + project, - 'change:' + changeID, - '-change:' + changeNum, - '-is:abandoned', - ].join(' '); - return { - O: options, - q: query - } - }, - - _computeSameTopicQueryParams: function(topic) { - var options = this.listChangesOptionsToHex( - this.ListChangesOption.LABELS, - this.ListChangesOption.CURRENT_REVISION, - this.ListChangesOption.CURRENT_COMMIT, - this.ListChangesOption.DETAILED_LABELS - ); - return { - O: options, - q: 'status:open topic:' + topic, - }; - }, - - _computeChangeURL: function(changeNum, patchNum) { - var urlStr = '/c/' + changeNum; - if (patchNum != null) { - urlStr += '/' + patchNum; - } - return urlStr; - }, - - _computeLinkClass: function(change) { - if (change.status == this.ChangeStatus.ABANDONED) { - return 'strikethrough'; - } - }, - - _computeChangeStatusClass: function(change) { - var classes = ['status']; - if (change._revision_number != change._current_revision_number) { - classes.push('notCurrent'); - } else if (this._isIndirectAncestor(change)) { - classes.push('indirectAncestor'); - } else if (change.submittable) { - classes.push('submittable'); - } else if (change.status == this.ChangeStatus.NEW) { - classes.push('hidden'); - } - return classes.join(' '); - }, - - _computeChangeStatus: function(change) { - switch (change.status) { - case this.ChangeStatus.MERGED: - return 'Merged'; - case this.ChangeStatus.ABANDONED: - return 'Abandoned'; - case this.ChangeStatus.DRAFT: - return 'Draft'; - } - if (change._revision_number != change._current_revision_number) { - return 'Not current'; - } else if (this._isIndirectAncestor(change)) { - return 'Indirect ancestor'; - } else if (change.submittable) { - return 'Submittable'; - } - return '' - }, - - _serverConfigChanged: function(config) { - this._resolveServerConfigReady(config); - }, - - _resultsChanged: function(related, submittedTogether, conflicts, - cherryPicks, sameTopic) { - var results = [ - related, - submittedTogether, - conflicts, - cherryPicks, - sameTopic - ]; - for (var i = 0; i < results.length; i++) { - if (results[i].length > 0) { - this.hidden = false; - return; - } - } - this.hidden = true; - }, - - _isIndirectAncestor: function(change) { - return this._connectedRevisions.indexOf(change.commit.commit) == -1; - }, - - _computeConnectedRevisions: function(change, patchNum, relatedChanges) { - var connected = []; - var changeRevision; - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { - changeRevision = rev; - } - } - var commits = relatedChanges.map(function(c) { return c.commit; }); - var pos = commits.length - 1; - - while (pos >= 0) { - var commit = commits[pos].commit; - connected.push(commit); - if (commit == changeRevision) { - break; - } - pos--; - } - while (pos >= 0) { - for (var i = 0; i < commits[pos].parents.length; i++) { - if (connected.indexOf(commits[pos].parents[i].commit) != -1) { - connected.push(commits[pos].commit); - break; - } - } - --pos; - } - return connected; - }, - - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-reply-dialog.html b/polygerrit-ui/app/elements/gr-reply-dialog.html deleted file mode 100644 index ead001a..0000000 --- a/polygerrit-ui/app/elements/gr-reply-dialog.html +++ /dev/null
@@ -1,307 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../bower_components/iron-selector/iron-selector.html"> -<link rel="import" href="../behaviors/rest-client-behavior.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-reply-dialog"> - <style> - :host { - display: block; - max-height: 90vh; - } - :host([disabled]) { - pointer-events: none; - } - :host([disabled]) .container { - opacity: .5; - } - .container { - display: flex; - flex-direction: column; - max-height: 90vh; - } - section { - border-top: 1px solid #ddd; - padding: .5em .75em; - } - .textareaContainer, - .labelsContainer, - .actionsContainer { - flex-shrink: 0; - } - .textareaContainer { - position: relative; - } - iron-autogrow-textarea { - padding: 0; - font-family: var(--monospace-font-family); - } - .message { - border: none; - width: 100%; - } - .labelContainer:not(:first-of-type) { - margin-top: .5em; - } - .labelName { - display: inline-block; - width: 7em; - margin-right: .5em; - white-space: nowrap; - } - iron-selector { - display: inline-flex; - } - iron-selector > gr-button { - margin-right: .25em; - } - iron-selector > gr-button:first-of-type { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - } - iron-selector > gr-button:last-of-type { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - } - iron-selector > gr-button.iron-selected { - background-color: #ddd; - } - .draftsContainer { - overflow-y: auto; - } - .draftsContainer h3 { - margin-top: .25em; - } - .actionsContainer { - display: flex; - justify-content: space-between; - } - .action:link, - .action:visited { - color: #00e; - } - </style> - <template> - <gr-ajax id="draftsXHR" - url="[[_computeDraftsURL(changeNum)]]" - last-response="{{_drafts}}"></gr-ajax> - <div class="container"> - <section class="textareaContainer"> - <iron-autogrow-textarea - id="textarea" - class="message" - placeholder="Say something..." - disabled="{{disabled}}" - rows="4" - max-rows="15" - bind-value="{{draft}}"></iron-autogrow-textarea> - </section> - <section class="labelsContainer"> - <template is="dom-repeat" - items="[[_computeLabelArray(permittedLabels)]]" as="label"> - <div class="labelContainer"> - <span class="labelName">[[label]]</span> - <iron-selector data-label$="[[label]]" - selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]"> - <template is="dom-repeat" - items="[[_computePermittedLabelValues(permittedLabels, label)]]" - as="value"> - <gr-button data-value$="[[value]]">[[value]]</gr-button> - </template> - </iron-selector> - </div> - </template> - </section> - <section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]"> - <h3>[[_computeDraftsTitle(_drafts)]]</h3> - <gr-comment-list - comments="[[_drafts]]" - change-num="[[changeNum]]" - patch-num="[[patchNum]]"></gr-comment-list> - </section> - <section class="actionsContainer"> - <gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button> - <gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button> - </section> - </div> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-reply-dialog', - - /** - * Fired when a reply is successfully sent. - * - * @event send - */ - - /** - * Fired when the user presses the cancel button. - * - * @event cancel - */ - - properties: { - changeNum: String, - patchNum: String, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - draft: { - type: String, - value: '', - }, - labels: Object, - permittedLabels: Object, - - _account: Object, - _drafts: Object, - _xhrPromise: Object, // Used for testing. - }, - - behaviors: [ - Gerrit.RESTClientBehavior, - ], - - ready: function() { - app.accountReady.then(function() { - this._account = app.account; - }.bind(this)); - }, - - reload: function() { - return this.$.draftsXHR.generateRequest().completes; - }, - - focus: function() { - this.async(function() { - this.$.textarea.textarea.focus(); - }.bind(this)); - }, - - _computeDraftsURL: function(changeNum) { - return '/changes/' + changeNum + '/drafts'; - }, - - _computeHideDraftList: function(drafts) { - return Object.keys(drafts || {}).length == 0; - }, - - _computeDraftsTitle: function(drafts) { - var total = 0; - for (var file in drafts) { - total += drafts[file].length; - } - if (total == 0) { return ''; } - if (total == 1) { return '1 Draft'; } - if (total > 1) { return total + ' Drafts'; } - }, - - _computeLabelArray: function(labelsObj) { - return Object.keys(labelsObj).sort(); - }, - - _computeIndexOfLabelValue: function( - labels, permittedLabels, labelName, account) { - var t = labels[labelName]; - if (!t) { return null; } - var labelValue = t.default_value; - - // Is there an existing vote for the current user? If so, use that. - var votes = labels[labelName]; - if (votes.all && votes.all.length > 0) { - for (var i = 0; i < votes.all.length; i++) { - if (votes.all[i]._account_id == account._account_id) { - labelValue = votes.all[i].value; - break; - } - } - } - - var len = permittedLabels[labelName] != null ? - permittedLabels[labelName].length : 0; - for (var i = 0; i < len; i++) { - var val = parseInt(permittedLabels[labelName][i], 10); - if (val == labelValue) { - return i; - } - } - return null; - }, - - _computePermittedLabelValues: function(permittedLabels, label) { - return permittedLabels[label]; - }, - - _cancelTapHandler: function(e) { - e.preventDefault(); - this._drafts = null; - this.fire('cancel', null, {bubbles: false}); - }, - - _sendTapHandler: function(e) { - e.preventDefault(); - var obj = { - drafts: 'PUBLISH_ALL_REVISIONS', - labels: {}, - }; - for (var label in this.permittedLabels) { - var selectorEl = this.$$('iron-selector[data-label="' + label + '"]'); - var selectedVal = selectorEl.selectedItem.getAttribute('data-value'); - selectedVal = parseInt(selectedVal, 10); - obj.labels[label] = selectedVal; - } - if (this.draft != null) { - obj.message = this.draft; - } - this.disabled = true; - this._send(obj).then(function(req) { - this.fire('send', null, {bubbles: false}); - this.draft = ''; - this.disabled = false; - this._drafts = null; - }.bind(this)).catch(function(err) { - alert('Oops. Something went wrong. Check the console and bug the ' + - 'PolyGerrit team for assistance.'); - throw err; - }.bind(this)); - }, - - _send: function(payload) { - var xhr = document.createElement('gr-request'); - this._xhrPromise = xhr.send({ - method: 'POST', - url: this.changeBaseURL(this.changeNum, this.patchNum) + '/review', - body: payload, - }); - - return this._xhrPromise; - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-request.html b/polygerrit-ui/app/elements/gr-request.html deleted file mode 100644 index d6d3a95..0000000 --- a/polygerrit-ui/app/elements/gr-request.html +++ /dev/null
@@ -1,49 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-ajax/iron-request.html"> - -<dom-module id="gr-request"> - <template> - <iron-request id="xhr"></iron-request> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-request', - - hostAttributes: { - hidden: true - }, - - send: function(options) { - options.headers = options.headers || {}; - if (options.body != null) { - options.headers['content-type'] = - options.headers['content-type'] || 'application/json'; - } - options.headers['x-gerrit-auth'] = options.headers['x-gerrit-auth'] || - util.getCookie('XSRF_TOKEN'); - options.jsonPrefix = options.jsonPrefix || ')]}\''; - return this.$.xhr.send(options); - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-reviewer-list.html b/polygerrit-ui/app/elements/gr-reviewer-list.html deleted file mode 100644 index 2b7f0c5..0000000 --- a/polygerrit-ui/app/elements/gr-reviewer-list.html +++ /dev/null
@@ -1,450 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-input/iron-input.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> -<link rel="import" href="gr-ajax.html"> -<link rel="import" href="gr-button.html"> -<link rel="import" href="gr-request.html"> - -<dom-module id="gr-reviewer-list"> - <style> - :host { - display: block; - } - :host([disabled]) { - opacity: .8; - pointer-events: none; - } - .autocompleteContainer { - position: relative; - } - .inputContainer { - display: flex; - margin-top: .25em; - } - .inputContainer input { - flex: 1; - font: inherit; - } - .dropdown { - background-color: #fff; - box-shadow: 0 1px 3px rgba(0, 0, 0, .3); - position: absolute; - left: 0; - top: 100%; - } - .dropdown .reviewer { - cursor: pointer; - padding: .5em .75em; - } - .dropdown .reviewer[selected] { - background-color: #ccc; - } - .remove, - .cancel { - color: #999; - } - .remove { - font-size: .9em; - } - .cancel { - font-size: 2em; - line-height: 1; - padding: 0 .15em; - text-decoration: none; - } - </style> - <template> - <gr-ajax id="autocompleteXHR" - url="[[_computeAutocompleteURL(change)]]" - params="[[_computeAutocompleteParams(_inputVal)]]" - on-response="_handleResponse"></gr-ajax> - - <template is="dom-repeat" items="[[_reviewers]]" as="reviewer"> - <div class="reviewer"> - <gr-account-link account="[[reviewer]]" show-email></gr-account-link> - <gr-button link - class="remove" - data-account-id$="[[reviewer._account_id]]" - on-tap="_handleRemoveTap" - hidden$="[[!_computeCanRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom> - </div> - </template> - <div class="controlsContainer" hidden$="[[!mutable]]"> - <div class="autocompleteContainer" hidden$="[[!_showInput]]"> - <div class="inputContainer"> - <input is="iron-input" id="input" - bind-value="{{_inputVal}}" disabled$="[[disabled]]"> - <gr-button link class="cancel" on-tap="_handleCancelTap">×</gr-button> - </div> - <div class="dropdown" hidden$="[[_hideAutocomplete]]"> - <template is="dom-repeat" items="[[_autocompleteData]]" as="reviewer"> - <div class="reviewer" - data-index$="[[index]]" - on-mouseenter="_handleMouseEnterItem" - on-tap="_handleItemTap" - selected$="[[_computeSelected(index, _selectedIndex)]]"> - <template is="dom-if" if="[[reviewer.account]]"> - <gr-account-label - account="[[reviewer.account]]" show-email></gr-account-label> - </template> - <template is="dom-if" if="[[reviewer.group]]"> - <span>[[reviewer.group.name]] (group)</span> - </template> - </div> - </template> - </div> - </div> - <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap" - hidden$="[[_showInput]]">Add reviewer</gr-button> - </div> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-reviewer-list', - - properties: { - change: Object, - mutable: { - type: Boolean, - value: false, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - suggestFrom: { - type: Number, - value: 3, - }, - - _reviewers: { - type: Array, - value: function() { return []; }, - }, - _autocompleteData: { - type: Array, - value: function() { return []; }, - observer: '_autocompleteDataChanged', - }, - _inputVal: { - type: String, - value: '', - observer: '_inputValChanged', - }, - _inputRequestHandle: Number, - _inputRequestTimeout: { - type: Number, - value: 250, - }, - _showInput: { - type: Boolean, - value: false, - }, - _hideAutocomplete: { - type: Boolean, - value: true, - observer: '_hideAutocompleteChanged', - }, - _selectedIndex: { - type: Number, - value: 0, - }, - _boundBodyClickHandler: { - type: Function, - value: function() { - return this._handleBodyClick.bind(this); - }, - }, - - // Used for testing. - _lastAutocompleteRequest: Object, - _xhrPromise: Object, - }, - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - ], - - observers: [ - '_reviewersChanged(change.reviewers.*, change.owner)', - ], - - detached: function() { - this._clearInputRequestHandle(); - }, - - _clearInputRequestHandle: function() { - if (this._inputRequestHandle != null) { - this.cancelAsync(this._inputRequestHandle); - this._inputRequestHandle = null; - } - }, - - _reviewersChanged: function(changeRecord, owner) { - var result = []; - var reviewers = changeRecord.base; - for (var key in reviewers) { - if (key == 'REVIEWER' || key == 'CC') { - result = result.concat(reviewers[key]); - } - } - this._reviewers = result.filter(function(reviewer) { - return reviewer._account_id != owner._account_id; - }); - }, - - _computeCanRemoveReviewer: function(reviewer, mutable) { - if (!mutable) { return false; } - - for (var i = 0; i < this.change.removable_reviewers.length; i++) { - if (this.change.removable_reviewers[i]._account_id == - reviewer._account_id) { - return true; - } - } - return false; - }, - - _computeAutocompleteURL: function(change) { - return '/changes/' + change._number + '/suggest_reviewers'; - }, - - _computeAutocompleteParams: function(inputVal) { - return { - n: 10, // Return max 10 results - q: inputVal, - }; - }, - - _computeSelected: function(index, selectedIndex) { - return index == selectedIndex; - }, - - _handleResponse: function(e) { - this._autocompleteData = e.detail.response.filter(function(reviewer) { - var account = reviewer.account; - if (!account) { return true; } - for (var i = 0; i < this._reviewers.length; i++) { - if (account._account_id == this.change.owner._account_id || - account._account_id == this._reviewers[i]._account_id) { - return false; - } - } - return true; - }, this); - }, - - _handleBodyClick: function(e) { - var eventPath = Polymer.dom(e).path; - for (var i = 0; i < eventPath.length; i++) { - if (eventPath[i] == this) { - return; - } - } - this._selectedIndex = -1; - this._autocompleteData = []; - }, - - _handleRemoveTap: function(e) { - e.preventDefault(); - var target = Polymer.dom(e).rootTarget; - var accountID = parseInt(target.getAttribute('data-account-id'), 10); - this._send('DELETE', this._restEndpoint(accountID)).then(function(req) { - var reviewers = this.change.reviewers; - ['REVIEWER', 'CC'].forEach(function(type) { - reviewers[type] = reviewers[type] || []; - for (var i = 0; i < reviewers[type].length; i++) { - if (reviewers[type][i]._account_id == accountID) { - this.splice('change.reviewers.' + type, i, 1); - break; - } - } - }, this); - }.bind(this)).catch(function(err) { - alert('Oops. Something went wrong. Check the console and bug the ' + - 'PolyGerrit team for assistance.'); - throw err; - }.bind(this)); - }, - - _handleAddTap: function(e) { - e.preventDefault(); - this._showInput = true; - this.$.input.focus(); - }, - - _handleCancelTap: function(e) { - e.preventDefault(); - this._cancel(); - }, - - _handleMouseEnterItem: function(e) { - this._selectedIndex = - parseInt(Polymer.dom(e).rootTarget.getAttribute('data-index'), 10); - }, - - _handleItemTap: function(e) { - var reviewerEl; - var eventPath = Polymer.dom(e).path; - for (var i = 0; i < eventPath.length; i++) { - var el = eventPath[i]; - if (el.classList && el.classList.contains('reviewer')) { - reviewerEl = el; - break; - } - } - this._selectedIndex = - parseInt(reviewerEl.getAttribute('data-index'), 10); - this._sendAddRequest(); - }, - - _autocompleteDataChanged: function(data) { - this._hideAutocomplete = data.length == 0; - }, - - _hideAutocompleteChanged: function(hidden) { - if (hidden) { - document.body.removeEventListener('click', - this._boundBodyClickHandler); - this._selectedIndex = -1; - } else { - document.body.addEventListener('click', this._boundBodyClickHandler); - this._selectedIndex = 0; - } - }, - - _inputValChanged: function(val) { - var sendRequest = function() { - if (this.disabled || val == null || val.trim().length == 0) { - return; - } - if (val.length < this.suggestFrom) { - this._clearInputRequestHandle(); - this._hideAutocomplete = true; - this._selectedIndex = -1; - return; - } - this._lastAutocompleteRequest = - this.$.autocompleteXHR.generateRequest(); - }.bind(this); - - this._clearInputRequestHandle(); - if (this._inputRequestTimeout == 0) { - sendRequest(); - } else { - this._inputRequestHandle = - this.async(sendRequest, this._inputRequestTimeout); - } - }, - - _handleKey: function(e) { - if (this._hideAutocomplete) { - if (e.keyCode == 27) { // 'esc' - e.preventDefault(); - this._cancel(); - } - return; - } - - switch (e.keyCode) { - case 38: // 'up': - e.preventDefault(); - this._selectedIndex = Math.max(this._selectedIndex - 1, 0); - break; - case 40: // 'down' - e.preventDefault(); - this._selectedIndex = Math.min(this._selectedIndex + 1, - this._autocompleteData.length - 1); - break; - case 27: // 'esc' - e.preventDefault(); - this._hideAutocomplete = true; - break; - case 13: // 'enter' - e.preventDefault(); - this._sendAddRequest(); - break; - } - }, - - _cancel: function() { - this._showInput = false; - this._selectedIndex = 0; - this._inputVal = ''; - this._autocompleteData = []; - this.$.addReviewer.focus(); - }, - - _sendAddRequest: function() { - this._clearInputRequestHandle(); - - var reviewerID; - var reviewer = this._autocompleteData[this._selectedIndex]; - if (reviewer.account) { - reviewerID = reviewer.account._account_id; - } else if (reviewer.group) { - reviewerID = reviewer.group.id; - } - this._autocompleteData = []; - this._send('POST', this._restEndpoint(), reviewerID).then(function(req) { - this.change.reviewers.CC = this.change.reviewers.CC || []; - req.response.reviewers.forEach(function(r) { - this.push('change.removable_reviewers', r); - this.push('change.reviewers.CC', r); - }, this); - this._inputVal = ''; - this.$.input.focus(); - }.bind(this)).catch(function(err) { - // TODO(andybons): Use the message returned by the server. - alert('Unable to add ' + reviewerID + ' as a reviewer.'); - throw err; - }.bind(this)); - }, - - _send: function(method, url, reviewerID) { - this.disabled = true; - var request = document.createElement('gr-request'); - var opts = { - method: method, - url: url, - }; - if (reviewerID) { - opts.body = {reviewer: reviewerID}; - } - this._xhrPromise = request.send(opts); - var enableEl = function() { this.disabled = false; }.bind(this); - this._xhrPromise.then(enableEl).catch(enableEl); - return this._xhrPromise; - }, - - _restEndpoint: function(id) { - var path = '/changes/' + this.change._number + '/reviewers'; - if (id) { - path += '/' + id; - } - return path; - }, - }); - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-search-bar.html b/polygerrit-ui/app/elements/gr-search-bar.html deleted file mode 100644 index 293474c..0000000 --- a/polygerrit-ui/app/elements/gr-search-bar.html +++ /dev/null
@@ -1,115 +0,0 @@ -<!-- -Copyright (C) 2015 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-input/iron-input.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> -<link rel="import" href="gr-button.html"> - -<dom-module id="gr-search-bar"> - <template> - <style> - :host { - display: inline-block; - } - form { - display: flex; - } - input { - border: 1px solid #d1d2d3; - outline: none; - } - input { - flex: 1; - font: inherit; - border-radius: 2px 0 0 2px; - } - gr-button { - background-color: #f1f2f3; - border-radius: 0 2px 2px 0; - border-left-width: 0; - } - </style> - <form> - <input is="iron-input" id="searchInput" bind-value="{{_inputVal}}"> - <gr-button id="searchButton">Search</gr-button> - </form> - </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-search-bar', - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - ], - - listeners: { - 'searchInput.keydown': '_inputKeyDownHandler', - 'searchButton.tap': '_preventDefaultAndNavigateToInputVal', - }, - - properties: { - value: { - type: String, - value: '', - notify: true, - observer: '_valueChanged', - }, - keyEventTarget: { - type: Object, - value: function() { return document.body; }, - }, - - _inputVal: String, - }, - - _valueChanged: function(value) { - this._inputVal = value; - }, - - _inputKeyDownHandler: function(e) { - if (e.keyCode == 13) { // Enter key - this._preventDefaultAndNavigateToInputVal(e); - } - }, - - _preventDefaultAndNavigateToInputVal: function(e) { - e.preventDefault(); - Polymer.dom(e).rootTarget.blur(); - page.show('/q/' + this._inputVal); - }, - - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - switch (e.keyCode) { - case 191: // '/' or '?' with shift key. - // TODO(andybons): Localization using e.key/keypress event. - if (e.shiftKey) { break; } - e.preventDefault(); - var s = this.$.searchInput; - s.focus(); - s.setSelectionRange(0, s.value.length); - break; - } - }, - }); - - })(); - </script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-account-dropdown.html b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html similarity index 81% rename from polygerrit-ui/app/elements/gr-account-dropdown.html rename to polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html index 99850f6..7615d15 100644 --- a/polygerrit-ui/app/elements/gr-account-dropdown.html +++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.html
@@ -14,9 +14,9 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="gr-button.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html"> +<link rel="import" href="../gr-button/gr-button.html"> <dom-module id="gr-account-dropdown"> <style> @@ -78,21 +78,5 @@ </div> </iron-dropdown> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-account-dropdown', - - properties: { - account: Object, - }, - - _showDropdownTapHandler: function(e) { - this.$.dropdown.open(); - }, - }); - })(); - </script> + <script src="gr-account-dropdown.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js new file mode 100644 index 0000000..09de6c1 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown.js
@@ -0,0 +1,28 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-account-dropdown', + + properties: { + account: Object, + }, + + _showDropdownTapHandler: function(e) { + this.$.dropdown.open(); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html new file mode 100644 index 0000000..3ae3b14 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-account-dropdown/gr-account-dropdown_test.html
@@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-account-dropdown</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-account-dropdown.html"> + +<test-fixture id="basic"> + <template> + <gr-account-dropdown></gr-account-dropdown> + </template> +</test-fixture> + +<script> + suite('gr-account-dropdown tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('tap on trigger opens menu', function() { + assert.isFalse(element.$.dropdown.opened); + MockInteractions.tap(element.$.trigger); + assert.isTrue(element.$.dropdown.opened); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html similarity index 60% rename from polygerrit-ui/app/elements/gr-account-label.html rename to polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html index 7d35698..b7f9715 100644 --- a/polygerrit-ui/app/elements/gr-account-label.html +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -14,8 +14,8 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-avatar.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-avatar/gr-avatar.html"> <dom-module id="gr-account-label"> <template> @@ -44,39 +44,5 @@ </span> </span> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-account-label', - - properties: { - account: Object, - avatarImageSize: { - type: Number, - value: 32, - }, - showEmail: { - type: Boolean, - value: false, - }, - }, - - _computeAccountTitle: function(account) { - if (!account || !account.name) { return; } - var result = util.escapeHTML(account.name); - if (account.email) { - result += ' <' + util.escapeHTML(account.email) + '>'; - } - return result; - }, - - _computeShowEmail: function(showEmail, account) { - return !!(showEmail && account && account.email); - }, - - }); - })(); - </script> + <script src="gr-account-label.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js new file mode 100644 index 0000000..98871cb --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -0,0 +1,45 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-account-label', + + properties: { + account: Object, + avatarImageSize: { + type: Number, + value: 32, + }, + showEmail: { + type: Boolean, + value: false, + }, + }, + + _computeAccountTitle: function(account) { + if (!account || !account.name) { return; } + var result = util.escapeHTML(account.name); + if (account.email) { + result += ' <' + util.escapeHTML(account.email) + '>'; + } + return result; + }, + + _computeShowEmail: function(showEmail, account) { + return !!(showEmail && account && account.email); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html new file mode 100644 index 0000000..c39f288 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -0,0 +1,74 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-account-label</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="gr-account-label.html"> + +<test-fixture id="basic"> + <template> + <gr-account-label></gr-account-label> + </template> +</test-fixture> + +<script> + suite('gr-account-label tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('computed fields', function() { + assert.equal(element._computeAccountTitle( + { + name: 'Andrew Bonventre', + email: 'andybons+gerrit@gmail.com' + }), + 'Andrew Bonventre <andybons+gerrit@gmail.com>'); + + assert.equal(element._computeAccountTitle( + {name: 'Andrew Bonventre'}), + 'Andrew Bonventre'); + + assert.equal(element._computeShowEmail(true, + { + name: 'Andrew Bonventre', + email: 'andybons+gerrit@gmail.com' + }), true); + + assert.equal(element._computeShowEmail(true, + {name: 'Andrew Bonventre'}), false); + + assert.equal(element._computeShowEmail(false, + {name: 'Andrew Bonventre'}), false); + + assert.equal(element._computeShowEmail( + true, undefined), false); + + assert.equal(element._computeShowEmail( + false, undefined), false); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html similarity index 65% rename from polygerrit-ui/app/elements/gr-account-link.html rename to polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html index 8b0b726..d3585ef 100644 --- a/polygerrit-ui/app/elements/gr-account-link.html +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -14,8 +14,8 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-account-label.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-account-label/gr-account-label.html"> <dom-module id="gr-account-link"> <template> @@ -40,28 +40,5 @@ </a> </span> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-account-link', - - properties: { - account: Object, - avatarImageSize: { - type: Number, - value: 32, - }, - }, - - _computeOwnerLink: function(account) { - if (!account) { return; } - var accountID = account.email || account._account_id; - return '/q/owner:' + encodeURIComponent(accountID) + '+status:open'; - }, - - }); - })(); - </script> + <script src="gr-account-link.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js new file mode 100644 index 0000000..058b27d --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -0,0 +1,34 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-account-link', + + properties: { + account: Object, + avatarImageSize: { + type: Number, + value: 32, + }, + }, + + _computeOwnerLink: function(account) { + if (!account) { return; } + var accountID = account.email || account._account_id; + return '/q/owner:' + encodeURIComponent(accountID) + '+status:open'; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html new file mode 100644 index 0000000..e1ef862 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -0,0 +1,55 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-account-link</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="gr-account-link.html"> + +<test-fixture id="basic"> + <template> + <gr-account-link></gr-account-link> + </template> +</test-fixture> + +<script> + suite('gr-account-link tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('computed fields', function() { + assert.equal(element._computeOwnerLink( + { + _account_id: 123, + email: 'andybons+gerrit@gmail.com' + }), + '/q/owner:andybons%2Bgerrit%40gmail.com+status:open'); + + assert.equal(element._computeOwnerLink({_account_id: 42}), + '/q/owner:42+status:open'); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html new file mode 100644 index 0000000..9a93426 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.html
@@ -0,0 +1,35 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-ajax/iron-ajax.html"> + +<dom-module id="gr-ajax"> + <template> + <iron-ajax id="xhr" + auto="[[auto]]" + url="[[url]]" + params="[[params]]" + json-prefix=")]}'" + last-error="{{lastError}}" + last-response="{{lastResponse}}" + loading="{{loading}}" + on-response="_handleResponse" + on-error="_handleError" + debounce-duration="300"></iron-ajax> + </template> + <script src="gr-ajax.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js new file mode 100644 index 0000000..7fec507 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-ajax/gr-ajax.js
@@ -0,0 +1,81 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-ajax', + + /** + * Fired when a response is received. + * + * @event response + */ + + /** + * Fired when an error is received. + * + * @event error + */ + + hostAttributes: { + hidden: true + }, + + properties: { + auto: { + type: Boolean, + value: false, + }, + url: String, + params: { + type: Object, + value: function() { + return {}; + }, + }, + lastError: { + type: Object, + notify: true, + }, + lastResponse: { + type: Object, + notify: true, + }, + loading: { + type: Boolean, + notify: true, + }, + }, + + ready: function() { + // Used for debugging which element a request came from. + var headers = this.$.xhr.headers; + headers['x-requesting-element-id'] = this.id || 'gr-ajax (no id)'; + this.$.xhr.headers = headers; + }, + + generateRequest: function() { + return this.$.xhr.generateRequest(); + }, + + _handleResponse: function(e, req) { + this.fire('response', req, {bubbles: false}); + }, + + _handleError: function(e, req) { + this.fire('error', req, {bubbles: false}); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html new file mode 100644 index 0000000..3491443 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -0,0 +1,31 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-avatar"> + <template> + <style> + :host { + display: inline-block; + border-radius: 50%; + background-size: cover; + background-color: var(--background-color, #f1f2f3); + } + </style> + </template> + <script src="gr-avatar.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js new file mode 100644 index 0000000..8f289ca --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -0,0 +1,63 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-avatar', + + properties: { + account: { + type: Object, + observer: '_accountChanged', + }, + imageSize: { + type: Number, + value: 16, + }, + }, + + created: function() { + this.hidden = true; + }, + + ready: function() { + app.configReady.then(function(cfg) { + var hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); + if (hasAvatars) { + this.hidden = false; + this._updateAvatarURL(this.account); // src needs to be set if avatar becomes visible + } + }.bind(this)); + }, + + _accountChanged: function(account) { + this._updateAvatarURL(account); + }, + + _updateAvatarURL: function(account) { + if (!this.hidden && account) { + var url = this._buildAvatarURL(this.account); + if (url) { + this.style.backgroundImage = 'url("' + url + '")'; + } + } + }, + + _buildAvatarURL: function(account) { + if (!account) { return ''; } + return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html new file mode 100644 index 0000000..7e3c25c --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-avatar</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/fake-app.js"></script> + +<link rel="import" href="gr-avatar.html"> + +<test-fixture id="basic"> + <template> + <gr-avatar></gr-avatar> + </template> +</test-fixture> + +<script> + suite('gr-avatar tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('methods', function() { + assert.equal(element._buildAvatarURL( + { + _account_id: 123 + }), + '/accounts/123/avatar?s=16'); + }); + + test('dom for existing account', function() { + assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially'); + element.hidden = false; + element.imageSize = 64; + element.account = { + _account_id: 123 + }; + assert.isFalse(element.hasAttribute('hidden'), 'element hidden'); + assert.isTrue(element.style.backgroundImage.indexOf('/accounts/123/avatar?s=64') > -1); + }); + + test('dom for non available account', function() { + assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially'); + element.account = undefined; + assert.isTrue(element.hasAttribute('hidden'), 'element not hidden'); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html similarity index 70% rename from polygerrit-ui/app/elements/gr-button.html rename to polygerrit-ui/app/elements/shared/gr-button/gr-button.html index 33b7871..1df98fd 100644 --- a/polygerrit-ui/app/elements/gr-button.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -14,8 +14,8 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> <dom-module id="gr-button"> <template strip-whitespace> @@ -102,50 +102,5 @@ </style> <content></content> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-button', - - properties: { - disabled: { - type: Boolean, - observer: '_disabledChanged', - reflectToAttribute: true, - }, - _enabledTabindex: { - type: String, - value: '0', - }, - }, - - behaviors: [ - Gerrit.KeyboardShortcutBehavior, - ], - - hostAttributes: { - role: 'button', - tabindex: '0', - }, - - _disabledChanged: function(disabled) { - if (disabled) { - this._enabledTabindex = this.getAttribute('tabindex'); - } - this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex); - }, - - _handleKey: function(e) { - switch (e.keyCode) { - case 32: // 'spacebar' - case 13: // 'enter' - e.preventDefault(); - this.click(); - } - }, - }); - })(); - </script> + <script src="gr-button.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js new file mode 100644 index 0000000..772fccc --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -0,0 +1,57 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-button', + + properties: { + disabled: { + type: Boolean, + observer: '_disabledChanged', + reflectToAttribute: true, + }, + _enabledTabindex: { + type: String, + value: '0', + }, + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + hostAttributes: { + role: 'button', + tabindex: '0', + }, + + _disabledChanged: function(disabled) { + if (disabled) { + this._enabledTabindex = this.getAttribute('tabindex'); + } + this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex); + }, + + _handleKey: function(e) { + switch (e.keyCode) { + case 32: // 'spacebar' + case 13: // 'enter' + e.preventDefault(); + this.click(); + } + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html new file mode 100644 index 0000000..62a9d2d --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -0,0 +1,51 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-request/gr-request.html"> + +<dom-module id="gr-change-star"> + <template> + <style> + :host { + display: inline-block; + overflow: hidden; + } + .starButton { + background-color: transparent; + border-color: transparent; + cursor: pointer; + font-size: 1.1em; + width: 1.2em; + height: 1.2em; + outline: none; + } + .starButton svg { + fill: #ccc; + width: 1em; + height: 1em; + } + .starButton-active svg { + fill: #ffac33; + } + </style> + <button class$="[[_computeStarClass(change.starred)]]" on-tap="_handleStarTap"> + <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 --> + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657 l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657 L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg> + </button> + </template> + <script src="gr-change-star.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js new file mode 100644 index 0000000..26680b6 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -0,0 +1,61 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-change-star', + + properties: { + change: { + type: Object, + notify: true, + }, + + _xhrPromise: Object, // Used for testing. + }, + + _computeStarClass: function(starred) { + var classes = ['starButton']; + if (starred) { + classes.push('starButton-active'); + } + return classes.join(' '); + }, + + _handleStarTap: function() { + var method = this.change.starred ? 'DELETE' : 'PUT'; + this.set('change.starred', !this.change.starred); + this._send(method, this._restEndpoint()).catch(function(err) { + this.set('change.starred', !this.change.starred); + alert('Change couldn’t be starred. Check the console and contact ' + + 'the PolyGerrit team for assistance.'); + throw err; + }.bind(this)); + }, + + _send: function(method, url) { + var xhr = document.createElement('gr-request'); + this._xhrPromise = xhr.send({ + method: method, + url: url, + }); + return this._xhrPromise; + }, + + _restEndpoint: function() { + return '/accounts/self/starred.changes/' + this.change._number; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html new file mode 100644 index 0000000..86ee947 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -0,0 +1,114 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-change-star</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../scripts/fake-app.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-change-star.html"> + +<test-fixture id="basic"> + <template> + <gr-change-star></gr-change-star> + </template> +</test-fixture> + +<script> + suite('gr-change-star tests', function() { + var element; + var server; + + setup(function() { + element = fixture('basic'); + element.change = { + _number: 2, + starred: true, + }; + + server = sinon.fakeServer.create(); + server.respondWith( + 'PUT', + '/accounts/self/starred.changes/2', + [ + 204, + {'Content-Type': 'application/json'}, + '' + ] + ); + + server.respondWith( + 'DELETE', + '/accounts/self/starred.changes/2', + [ + 204, + {'Content-Type': 'application/json'}, + '' + ] + ); + }); + + teardown(function() { + server.restore(); + }); + + test('star visibility states', function() { + element.set('change.starred', true); + assert.isTrue(element.$$('button').classList.contains('starButton')); + assert.isTrue( + element.$$('button').classList.contains('starButton-active')); + + element.set('change.starred', false); + assert.isTrue(element.$$('button').classList.contains('starButton')); + assert.isFalse( + element.$$('button').classList.contains('starButton-active')); + }); + + test('starring', function(done) { + element.set('change.starred', false); + MockInteractions.tap(element.$$('button')); + + server.respond(); + + element._xhrPromise.then(function(req) { + assert.equal(req.status, 204); + assert.equal(req.url, '/accounts/self/starred.changes/2'); + assert.equal(element.change.starred, true); + done(); + }); + }); + + test('unstarring', function(done) { + element.set('change.starred', true); + MockInteractions.tap(element.$$('button')); + + server.respond(); + + element._xhrPromise.then(function(req) { + assert.equal(req.status, 204); + assert.equal(req.url, '/accounts/self/starred.changes/2'); + assert.equal(element.change.starred, false); + done(); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html new file mode 100644 index 0000000..d8fc1df --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -0,0 +1,48 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-button/gr-button.html"> + +<dom-module id="gr-confirm-dialog"> + <template> + <style> + :host { + display: block; + } + header { + border-bottom: 1px solid #ddd; + font-weight: bold; + } + header, + main, + footer { + padding: .5em .65em; + } + footer { + display: flex; + justify-content: space-between; + } + </style> + <header><content select=".header"></content></header> + <main><content select=".main"></content></main> + <footer> + <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button> + <gr-button on-tap="_handleCancelTap">Cancel</gr-button> + </footer> + </template> + <script src="gr-confirm-dialog.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js new file mode 100644 index 0000000..0f20e0a --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
@@ -0,0 +1,53 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-confirm-dialog', + + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ + + /** + * Fired when the cancel button is pressed. + * + * @event cancel + */ + + properties: { + confirmLabel: { + type: String, + value: 'Confirm', + } + }, + + hostAttributes: { + role: 'dialog', + }, + + _handleConfirmTap: function(e) { + e.preventDefault(); + this.fire('confirm', null, {bubbles: false}); + }, + + _handleCancelTap: function(e) { + e.preventDefault(); + this.fire('cancel', null, {bubbles: false}); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html new file mode 100644 index 0000000..812f32a --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
@@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-confirm-dialog</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-confirm-dialog.html"> + +<test-fixture id="basic"> + <template> + <gr-confirm-dialog></gr-confirm-dialog> + </template> +</test-fixture> + +<script> + suite('gr-confirm-dialog tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('events', function(done) { + var numEvents = 0; + function handler() { if (++numEvents == 2) { done(); } } + + element.addEventListener('confirm', handler); + element.addEventListener('cancel', handler); + + MockInteractions.tap(element.$$('gr-button[primary]')); + MockInteractions.tap(element.$$('gr-button:not([primary])')); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html similarity index 60% copy from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html copy to polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html index 6abf8c2..6d4b2ea 100644 --- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -1,5 +1,5 @@ <!-- -Copyright (C) 2016 The Android Open Source Project +Copyright (C) 2015 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,11 +14,16 @@ limitations under the License. --> -<link rel="import" href="../../bower_components/polymer/polymer.html"> -<script src="../../bower_components/fetch/fetch.js"></script> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> -<dom-module id="gr-rest-api-interface"> - <template></template> - <script src="gr-rest-api-interface.js"></script> +<dom-module id="gr-date-formatter"> + <template> + <style> + :host { + display: inline; + } + </style> + <span>[[_computeDateStr(dateStr)]]</span> + </template> + <script src="gr-date-formatter.js"></script> </dom-module> -
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js new file mode 100644 index 0000000..cb77cc1 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -0,0 +1,76 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + var Duration = { + HOUR: 1000 * 60 * 60, + DAY: 1000 * 60 * 60 * 24, + }; + + var ShortMonthNames = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', + 'Nov', 'Dec' + ]; + + Polymer({ + is: 'gr-date-formatter', + + properties: { + dateStr: { + type: String, + value: null, + notify: true + } + }, + + _computeDateStr: function(dateStr) { + return this._dateStr(this._parseDateStr(dateStr), new Date()); + }, + + _parseDateStr: function(dateStr) { + if (!dateStr) { return null; } + return util.parseDate(dateStr); + }, + + _dateStr: function(t, now) { + if (!t) { return ''; } + var diff = now.getTime() - t.getTime(); + if (diff < Duration.DAY && t.getDay() == now.getDay()) { + // Within 24 hours and on the same day: + // '2:14 AM' + var pm = t.getHours() >= 12; + var hours = t.getHours(); + if (hours == 0) { + hours = 12; + } else if (hours > 12) { + hours = t.getHours() - 12; + } + var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() : + t.getMinutes(); + return hours + ':' + minutes + (pm ? ' PM' : ' AM'); + } else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) && + diff < 180 * Duration.DAY) { + // From one to six months: + // 'Aug 29' + return ShortMonthNames[t.getMonth()] + ' ' + t.getDate(); + } else if (diff >= 180 * Duration.DAY) { + // More than six months: + // 'Aug 29, 1997' + return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' + + t.getFullYear(); + } + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html new file mode 100644 index 0000000..9bba517 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -0,0 +1,84 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-date-formatter</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="gr-date-formatter.html"> + +<test-fixture id="basic"> + <template> + <gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter> + </template> +</test-fixture> + +<script> + suite('gr-date-formatter tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('date is parsed correctly', function() { + assert.notOk((new Date('foo')).valueOf()); + var d = element._parseDateStr(element.getAttribute('date-str')); + assert.isAbove(d.valueOf(), 0); + }); + + function normalizedDate(dateStr) { + var d = new Date(dateStr); + d.setMinutes(d.getMinutes() + d.getTimezoneOffset()); + return d; + } + + function testDates(nowStr, dateStr, expected) { + var now = normalizedDate(nowStr); + var t = normalizedDate(dateStr); + assert.equal(element._dateStr(t, now), expected); + } + + test('dates strings are correct', function() { + // Within 24 hours on same day. + testDates('2015-07-29T20:34:00.000Z', + '2015-07-29T15:34:00.000Z', + '3:34 PM'); + testDates('2016-01-27T17:41:00.000Z', + '2016-01-27T12:41:00.000Z', + '12:41 PM'); + + // Within 24 hours on different days. + testDates('2015-07-29T03:34:00.000Z', + '2015-07-28T20:25:00.000Z', + 'Jul 28'); + + // More than 24 hours. Less than six months. + testDates('2015-07-29T20:34:00.000Z', + '2015-06-15T03:25:00.000Z', + 'Jun 15'); + + // More than six months. + testDates('2015-09-15T20:34:00.000Z', + '2015-01-15T03:25:00.000Z', + 'Jan 15, 2015'); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html similarity index 86% rename from polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html rename to polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html index 7e80baa..9988c28 100644 --- a/polygerrit-ui/app/elements/gr-keyboard-shortcuts-dialog.html +++ b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -14,8 +14,8 @@ limitations under the License. --> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="gr-button.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-button/gr-button.html"> <dom-module id="gr-keyboard-shortcuts-dialog"> <template> @@ -199,42 +199,5 @@ </main> <footer></footer> </template> - <script> - (function() { - 'use strict'; - - Polymer({ - is: 'gr-keyboard-shortcuts-dialog', - - /** - * Fired when the user presses the close button. - * - * @event close - */ - - properties: { - view: String, - }, - - hostAttributes: { - role: 'dialog', - }, - - _computeInView: function(currentView, view) { - return view == currentView; - }, - - _computeInChangeListView: function(currentView) { - return currentView == 'gr-change-list-view' || - currentView == 'gr-dashboard-view'; - }, - - _handleCloseTap: function(e) { - e.preventDefault(); - this.fire('close', null, {bubbles: false}); - }, - - }); - })(); - </script> + <script src="gr-keyboard-shortcuts-dialog.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js new file mode 100644 index 0000000..7ed5012 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -0,0 +1,48 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-keyboard-shortcuts-dialog', + + /** + * Fired when the user presses the close button. + * + * @event close + */ + + properties: { + view: String, + }, + + hostAttributes: { + role: 'dialog', + }, + + _computeInView: function(currentView, view) { + return view == currentView; + }, + + _computeInChangeListView: function(currentView) { + return currentView == 'gr-change-list-view' || + currentView == 'gr-dashboard-view'; + }, + + _handleCloseTap: function(e) { + e.preventDefault(); + this.fire('close', null, {bubbles: false}); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js b/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js new file mode 100644 index 0000000..26dacd6 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/ba-linkify.js
@@ -0,0 +1,191 @@ +/*! + * JavaScript Linkify - v0.3 - 6/27/2009 + * http://benalman.com/projects/javascript-linkify/ + * + * Copyright (c) 2009 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + * + * Some regexps adapted from http://userscripts.org/scripts/review/7122 + */ + +// Script: JavaScript Linkify: Process links in text! +// +// *Version: 0.3, Last updated: 6/27/2009* +// +// Project Home - http://benalman.com/projects/javascript-linkify/ +// GitHub - http://github.com/cowboy/javascript-linkify/ +// Source - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.js +// (Minified) - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.min.js (2.8kb) +// +// About: License +// +// Copyright (c) 2009 "Cowboy" Ben Alman, +// Dual licensed under the MIT and GPL licenses. +// http://benalman.com/about/license/ +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +window.linkify = (function(){ + var + SCHEME = "[a-z\\d.-]+://", + IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])", + HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+", + TLD = "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)", + HOST_OR_IP = "(?:" + HOSTNAME + TLD + "|" + IPV4 + ")", + PATH = "(?:[;/][^#?<>\\s]*)?", + QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?", + URI1 = "\\b" + SCHEME + "[^<>\\s]+", + URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)", + + MAILTO = "mailto:", + EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)", + + URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ), + SCHEME_RE = new RegExp( "^" + SCHEME, "i" ), + + quotes = { + "'": "`", + '>': '<', + ')': '(', + ']': '[', + '}': '{', + '»': '«', + '›': '‹' + }, + + default_options = { + callback: function( text, href ) { + return href ? '<a href="' + href + '" title="' + href + '">' + text + '</a>' : text; + }, + punct_regexp: /(?:[!?.,:;'"]|(?:&|&)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/ + }; + + return function( txt, options ) { + options = options || {}; + + // Temp variables. + var arr, + i, + link, + href, + + // Output HTML. + html = '', + + // Store text / link parts, in order, for re-combination. + parts = [], + + // Used for keeping track of indices in the text. + idx_prev, + idx_last, + idx, + link_last, + + // Used for trimming trailing punctuation and quotes from links. + matches_begin, + matches_end, + quote_begin, + quote_end; + + // Initialize options. + for ( i in default_options ) { + if ( options[ i ] === undefined ) { + options[ i ] = default_options[ i ]; + } + } + + // Find links. + while ( arr = URI_RE.exec( txt ) ) { + + link = arr[0]; + idx_last = URI_RE.lastIndex; + idx = idx_last - link.length; + + // Not a link if preceded by certain characters. + if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) { + continue; + } + + // Trim trailing punctuation. + do { + // If no changes are made, we don't want to loop forever! + link_last = link; + + quote_end = link.substr( -1 ) + quote_begin = quotes[ quote_end ]; + + // Ending quote character? + if ( quote_begin ) { + matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) ); + matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) ); + + // If quotes are unbalanced, remove trailing quote character. + if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) { + link = link.substr( 0, link.length - 1 ); + idx_last--; + } + } + + // Ending non-quote punctuation character? + if ( options.punct_regexp ) { + link = link.replace( options.punct_regexp, function(a){ + idx_last -= a.length; + return ''; + }); + } + } while ( link.length && link !== link_last ); + + href = link; + + // Add appropriate protocol to naked links. + if ( !SCHEME_RE.test( href ) ) { + href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO ) + : !href.indexOf( 'irc.' ) ? 'irc://' + : !href.indexOf( 'ftp.' ) ? 'ftp://' + : 'http://' ) + + href; + } + + // Push preceding non-link text onto the array. + if ( idx_prev != idx ) { + parts.push([ txt.slice( idx_prev, idx ) ]); + idx_prev = idx_last; + } + + // Push massaged link onto the array + parts.push([ link, href ]); + }; + + // Push remaining non-link text onto the array. + parts.push([ txt.substr( idx_prev ) ]); + + // Process the array items. + for ( i = 0; i < parts.length; i++ ) { + html += options.callback.apply( window, parts[i] ); + } + + // In case of catastrophic failure, return the original text; + return html || txt; + }; + +})(); \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html new file mode 100644 index 0000000..68a98e8 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -0,0 +1,39 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<script src="ba-linkify.js"></script> +<script src="link-text-parser.js"></script> +<dom-module id="gr-linked-text"> + <template> + <style> + :host { + display: block; + } + :host([pre]) span { + white-space: pre-wrap; + word-wrap: break-word; + } + :host([disabled]) a { + color: inherit; + text-decoration: none; + pointer-events: none; + } + </style> + <span id="output"></span> + </template> + <script src="gr-linked-text.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js new file mode 100644 index 0000000..cb852fd --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -0,0 +1,76 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-linked-text', + + properties: { + content: { + type: String, + observer: '_contentChanged', + }, + pre: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + config: Object, + }, + + observers: [ + '_contentOrConfigChanged(content, config)', + ], + + _contentChanged: function(content) { + // In the case where the config may not be set (perhaps due to the + // request for it still being in flight), set the content anyway to + // prevent waiting on the config to display the text. + if (this.config != null) { return; } + this.$.output.textContent = content; + }, + + _contentOrConfigChanged: function(content, config) { + var output = Polymer.dom(this.$.output); + output.textContent = ''; + var parser = new GrLinkTextParser(config, function(text, href, html) { + if (href) { + var a = document.createElement('a'); + a.href = href; + a.textContent = text; + a.target = '_blank'; + output.appendChild(a); + } else if (html) { + var fragment = document.createDocumentFragment(); + // Create temporary div to hold the nodes in. + var div = document.createElement('div'); + div.innerHTML = html; + while (div.firstChild) { + fragment.appendChild(div.firstChild); + } + output.appendChild(fragment); + } else { + output.appendChild(document.createTextNode(text)); + } + }); + parser.parse(content); + } + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html new file mode 100644 index 0000000..08ee24b --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -0,0 +1,147 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-linked-text</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../scripts/util.js"></script> + +<link rel="import" href="gr-linked-text.html"> + +<test-fixture id="basic"> + <template> + <gr-linked-text> + <div id="output"></div> + </gr-linked-text> + </template> +</test-fixture> + +<script> + suite('gr-linked-text tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + element.config = { + ph: { + match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)', + link: 'https://code.google.com/p/gerrit/issues/detail?id=$2' + }, + changeid: { + match: '(I[0-9a-f]{8,40})', + link: '#/q/$1' + }, + googlesearch: { + match: 'google:(.+)', + link: 'https://bing.com/search?q=$1', // html should supercede link. + html: '<a href="https://google.com/search?q=$1">$1</a>', + }, + hashedhtml: { + match: 'hash:(.+)', + html: '<a href="#/awesomesauce">$1</a>', + }, + disabledconfig: { + match: 'foo:(.+)', + link: 'https://google.com/search?q=$1', + enabled: false, + }, + }; + }); + + test('URL pattern was parsed and linked.', function() { + // Reguar inline link. + var url = 'https://code.google.com/p/gerrit/issues/detail?id=3650'; + element.content = url; + var linkEl = element.$.output.childNodes[0]; + assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.href, url); + assert.equal(linkEl.textContent, url); + }); + + test('Bug pattern was parsed and linked', function() { + // "Issue/Bug" pattern. + element.content = 'Issue 3650'; + + var linkEl = element.$.output.childNodes[0]; + var url = 'https://code.google.com/p/gerrit/issues/detail?id=3650'; + assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.href, url); + assert.equal(linkEl.textContent, 'Issue 3650'); + + element.content = 'Bug 3650'; + linkEl = element.$.output.childNodes[0]; + assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.href, url); + assert.equal(linkEl.textContent, 'Bug 3650'); + }); + + test('Change-Id pattern was parsed and linked', function() { + // "Change-Id:" pattern. + var changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; + var prefix = 'Change-Id: '; + element.content = prefix + changeID; + + var textNode = element.$.output.childNodes[0]; + var linkEl = element.$.output.childNodes[1]; + assert.equal(textNode.textContent, prefix); + var url = '/q/' + changeID; + assert.equal(linkEl.target, '_blank'); + // Since url is a path, the host is added automatically. + assert.isTrue(linkEl.href.endsWith(url)); + assert.equal(linkEl.textContent, changeID); + }); + + test('Multiple matches', function() { + element.content = 'Issue 3650\nIssue 3450'; + var linkEl1 = element.$.output.childNodes[0]; + var linkEl2 = element.$.output.childNodes[2]; + + assert.equal(linkEl1.target, '_blank'); + assert.equal(linkEl1.href, + 'https://code.google.com/p/gerrit/issues/detail?id=3650'); + assert.equal(linkEl1.textContent, 'Issue 3650'); + + assert.equal(linkEl2.target, '_blank'); + assert.equal(linkEl2.href, + 'https://code.google.com/p/gerrit/issues/detail?id=3450'); + assert.equal(linkEl2.textContent, 'Issue 3450'); + }); + + test('html field in link config', function() { + element.content = 'google:do a barrel roll'; + var linkEl = element.$.output.childNodes[0]; + assert.equal(linkEl.href, + 'https://google.com/search?q=do%20a%20barrel%20roll'); + assert.equal(linkEl.textContent, 'do a barrel roll'); + }); + + test('removing hash from links', function() { + element.content = 'hash:foo'; + var linkEl = element.$.output.childNodes[0]; + assert.isTrue(linkEl.href.endsWith('/awesomesauce')); + assert.equal(linkEl.textContent, 'foo'); + }); + + test('disabled config', function() { + element.content = 'foo:baz'; + assert.equal(element.$.output.innerHTML, 'foo:baz'); + }); + + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js new file mode 100644 index 0000000..b4b1678 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -0,0 +1,87 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +function GrLinkTextParser(linkConfig, callback) { + this.linkConfig = linkConfig; + this.callback = callback; + Object.preventExtensions(this); +} + +GrLinkTextParser.prototype.addText = function(text, href) { + if (!text) { + return; + } + this.callback(text, href); +}; + +GrLinkTextParser.prototype.addHTML = function(html) { + this.callback(null, null, html); +}; + +GrLinkTextParser.prototype.parse = function(text) { + linkify(text, { + callback: this.parseChunk.bind(this) + }); +}; + +GrLinkTextParser.prototype.parseChunk = function(text, href) { + if (href) { + this.addText(text, href); + } else { + this.parseLinks(text, this.linkConfig); + } +}; + +GrLinkTextParser.prototype.parseLinks = function(text, patterns) { + for (var p in patterns) { + if (patterns[p].enabled != null && patterns[p].enabled == false) { + continue; + } + // PolyGerrit doesn't use hash-based navigation like GWT. + // Account for this. + // TODO(andybons): Support Gerrit being served from a base other than /, + // e.g. https://git.eclipse.org/r/ + if (patterns[p].html) { + patterns[p].html = + patterns[p].html.replace(/<a href=\"#\//g, '<a href="/'); + } else if (patterns[p].link) { + if (patterns[p].link[0] == '#') { + patterns[p].link = patterns[p].link.substr(1); + } + } + + var pattern = new RegExp(patterns[p].match, 'g'); + + var match; + while ((match = pattern.exec(text)) != null) { + var before = text.substr(0, match.index); + this.addText(before); + text = text.substr(match.index + match[0].length); + var result = match[0].replace(pattern, + patterns[p].html || patterns[p].link); + + if (patterns[p].html) { + this.addHTML(result); + } else if (patterns[p].link) { + this.addText(match[0], result); + } else { + throw Error('linkconfig entry ' + p + + ' doesn’t contain a link or html attribute.'); + } + } + } + this.addText(text); +};
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html new file mode 100644 index 0000000..817d8c5 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -0,0 +1,32 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-overlay-behavior/iron-overlay-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> + +<dom-module id="gr-overlay"> + <template> + <style> + :host { + background: #fff; + box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px; + } + </style> + <content></content> + </template> + <script src="gr-overlay.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js new file mode 100644 index 0000000..5fa33ea --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -0,0 +1,44 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-overlay', + + behaviors: [ + Polymer.IronOverlayBehavior, + ], + + detached: function() { + // For good measure. + Gerrit.KeyboardShortcutBehavior.enabled = true; + }, + + open: function() { + Gerrit.KeyboardShortcutBehavior.enabled = false; + Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments); + }, + + close: function() { + Gerrit.KeyboardShortcutBehavior.enabled = true; + Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments); + }, + + cancel: function() { + Gerrit.KeyboardShortcutBehavior.enabled = true; + Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-request/gr-request.html similarity index 60% copy from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html copy to polygerrit-ui/app/elements/shared/gr-request/gr-request.html index 6abf8c2..df9eddc 100644 --- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-request/gr-request.html
@@ -1,5 +1,5 @@ <!-- -Copyright (C) 2016 The Android Open Source Project +Copyright (C) 2015 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,11 +14,12 @@ limitations under the License. --> -<link rel="import" href="../../bower_components/polymer/polymer.html"> -<script src="../../bower_components/fetch/fetch.js"></script> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-ajax/iron-request.html"> -<dom-module id="gr-rest-api-interface"> - <template></template> - <script src="gr-rest-api-interface.js"></script> +<dom-module id="gr-request"> + <template> + <iron-request id="xhr"></iron-request> + </template> + <script src="gr-request.js"></script> </dom-module> -
diff --git a/polygerrit-ui/app/elements/shared/gr-request/gr-request.js b/polygerrit-ui/app/elements/shared/gr-request/gr-request.js new file mode 100644 index 0000000..be24344 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-request/gr-request.js
@@ -0,0 +1,36 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-request', + + hostAttributes: { + hidden: true + }, + + send: function(options) { + options.headers = options.headers || {}; + if (options.body != null) { + options.headers['content-type'] = + options.headers['content-type'] || 'application/json'; + } + options.headers['x-gerrit-auth'] = options.headers['x-gerrit-auth'] || + util.getCookie('XSRF_TOKEN'); + options.jsonPrefix = options.jsonPrefix || ')]}\''; + return this.$.xhr.send(options); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html similarity index 83% rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html index 6abf8c2..10c8a29 100644 --- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -14,8 +14,8 @@ limitations under the License. --> -<link rel="import" href="../../bower_components/polymer/polymer.html"> -<script src="../../bower_components/fetch/fetch.js"></script> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<script src="../../../bower_components/fetch/fetch.js"></script> <dom-module id="gr-rest-api-interface"> <template></template>
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js similarity index 100% rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface.js rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
diff --git a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html similarity index 88% rename from polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html index 5ea81b9..71d747e 100644 --- a/polygerrit-ui/app/elements/gr-rest-api-interface/gr-rest-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -18,10 +18,10 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-rest-api-interface</title> -<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> -<script src="../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> -<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-rest-api-interface.html"> <test-fixture id="basic">
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html new file mode 100644 index 0000000..900245b --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.html
@@ -0,0 +1,52 @@ +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../gr-button/gr-button.html"> + +<dom-module id="gr-search-bar"> + <template> + <style> + :host { + display: inline-block; + } + form { + display: flex; + } + input { + border: 1px solid #d1d2d3; + outline: none; + } + input { + flex: 1; + font: inherit; + border-radius: 2px 0 0 2px; + } + gr-button { + background-color: #f1f2f3; + border-radius: 0 2px 2px 0; + border-left-width: 0; + } + </style> + <form> + <input is="iron-input" id="searchInput" bind-value="{{_inputVal}}"> + <gr-button id="searchButton">Search</gr-button> + </form> + </template> + <script src="gr-search-bar.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js new file mode 100644 index 0000000..bef461d --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar.js
@@ -0,0 +1,74 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-search-bar', + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + listeners: { + 'searchInput.keydown': '_inputKeyDownHandler', + 'searchButton.tap': '_preventDefaultAndNavigateToInputVal', + }, + + properties: { + value: { + type: String, + value: '', + notify: true, + observer: '_valueChanged', + }, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, + + _inputVal: String, + }, + + _valueChanged: function(value) { + this._inputVal = value; + }, + + _inputKeyDownHandler: function(e) { + if (e.keyCode == 13) { // Enter key + this._preventDefaultAndNavigateToInputVal(e); + } + }, + + _preventDefaultAndNavigateToInputVal: function(e) { + e.preventDefault(); + Polymer.dom(e).rootTarget.blur(); + page.show('/q/' + this._inputVal); + }, + + _handleKey: function(e) { + if (this.shouldSupressKeyboardShortcut(e)) { return; } + switch (e.keyCode) { + case 191: // '/' or '?' with shift key. + // TODO(andybons): Localization using e.key/keypress event. + if (e.shiftKey) { break; } + e.preventDefault(); + var s = this.$.searchInput; + s.focus(); + s.setSelectionRange(0, s.value.length); + break; + } + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html new file mode 100644 index 0000000..84752e4 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-search-bar/gr-search-bar_test.html
@@ -0,0 +1,75 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-search-bar</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/page/page.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-search-bar.html"> +<script src="../../../scripts/util.js"></script> + +<test-fixture id="basic"> + <template> + <gr-search-bar></gr-search-bar> + </template> +</test-fixture> + +<script> + suite('gr-search-bar tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('value is propagated to _inputVal', function() { + element.value = 'foo'; + assert.equal(element._inputVal, 'foo'); + }); + + function getActiveElement() { + return document.activeElement.shadowRoot ? + document.activeElement.shadowRoot.activeElement : + document.activeElement; + } + + test('tap on search button triggers nav', function(done) { + sinon.stub(page, 'show', function() { + page.show.restore(); + assert.notEqual(getActiveElement(), element.$.searchInput); + assert.notEqual(getActiveElement(), element.$.searchButton); + done(); + }); + MockInteractions.tap(element.$.searchButton); + }); + + test('enter in search input triggers nav', function(done) { + sinon.stub(page, 'show', function() { + page.show.restore(); + assert.notEqual(getActiveElement(), element.$.searchInput); + assert.notEqual(getActiveElement(), element.$.searchButton); + done(); + }); + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput, 13); + }); + + }); +</script>