| /** |
| * @license |
| * 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. |
| */ |
| import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; |
| import '../../../styles/shared-styles.js'; |
| import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; |
| import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; |
| import '../../plugins/gr-endpoint-slot/gr-endpoint-slot.js'; |
| import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; |
| import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; |
| import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; |
| import {PolymerElement} from '@polymer/polymer/polymer-element.js'; |
| import {htmlTemplate} from './gr-related-changes-list_html.js'; |
| import {GerritNav} from '../../core/gr-navigation/gr-navigation.js'; |
| import {ChangeStatus} from '../../../constants/constants.js'; |
| import {patchNumEquals} from '../../../utils/patch-set-util.js'; |
| import {changeIsOpen} from '../../../utils/change-util.js'; |
| import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js'; |
| |
| /** |
| * @extends PolymerElement |
| */ |
| class GrRelatedChangesList extends GestureEventListeners( |
| LegacyElementMixin(PolymerElement)) { |
| static get template() { return htmlTemplate; } |
| |
| static get is() { return 'gr-related-changes-list'; } |
| /** |
| * Fired when a new section is loaded so that the change view can determine |
| * a show more button is needed, sometimes before all the sections finish |
| * loading. |
| * |
| * @event new-section-loaded |
| */ |
| |
| static get properties() { |
| return { |
| change: Object, |
| hasParent: { |
| type: Boolean, |
| notify: true, |
| value: false, |
| }, |
| patchNum: String, |
| parentChange: Object, |
| hidden: { |
| type: Boolean, |
| value: false, |
| reflectToAttribute: true, |
| }, |
| loading: { |
| type: Boolean, |
| notify: true, |
| }, |
| mergeable: Boolean, |
| _connectedRevisions: { |
| type: Array, |
| computed: '_computeConnectedRevisions(change, patchNum, ' + |
| '_relatedResponse.changes)', |
| }, |
| /** @type {?} */ |
| _relatedResponse: { |
| type: Object, |
| value() { return {changes: []}; }, |
| }, |
| /** @type {?} */ |
| _submittedTogether: { |
| type: Object, |
| value() { return {changes: []}; }, |
| }, |
| _conflicts: { |
| type: Array, |
| value() { return []; }, |
| }, |
| _cherryPicks: { |
| type: Array, |
| value() { return []; }, |
| }, |
| _sameTopic: { |
| type: Array, |
| value() { return []; }, |
| }, |
| }; |
| } |
| |
| static get observers() { |
| return [ |
| '_resultsChanged(_relatedResponse, _submittedTogether, ' + |
| '_conflicts, _cherryPicks, _sameTopic)', |
| ]; |
| } |
| |
| clear() { |
| this.loading = true; |
| this.hidden = true; |
| |
| this._relatedResponse = {changes: []}; |
| this._submittedTogether = {changes: []}; |
| this._conflicts = []; |
| this._cherryPicks = []; |
| this._sameTopic = []; |
| } |
| |
| reload() { |
| if (!this.change || !this.patchNum) { |
| return Promise.resolve(); |
| } |
| this.loading = true; |
| const promises = [ |
| this._getRelatedChanges().then(response => { |
| this._relatedResponse = response; |
| this._fireReloadEvent(); |
| this.hasParent = this._calculateHasParent(this.change.change_id, |
| response.changes); |
| }), |
| this._getSubmittedTogether().then(response => { |
| this._submittedTogether = response; |
| this._fireReloadEvent(); |
| }), |
| this._getCherryPicks().then(response => { |
| this._cherryPicks = response; |
| this._fireReloadEvent(); |
| }), |
| ]; |
| |
| // Get conflicts if change is open and is mergeable. |
| if (changeIsOpen(this.change) && this.mergeable) { |
| promises.push(this._getConflicts().then(response => { |
| // Because the server doesn't always return a response and the |
| // template expects an array, always return an array. |
| this._conflicts = response ? response : []; |
| this._fireReloadEvent(); |
| })); |
| } |
| |
| promises.push(this._getServerConfig().then(config => { |
| if (this.change.topic && !config.change.submit_whole_topic) { |
| return this._getChangesWithSameTopic().then(response => { |
| this._sameTopic = response; |
| }); |
| } else { |
| this._sameTopic = []; |
| } |
| return this._sameTopic; |
| })); |
| |
| return Promise.all(promises).then(() => { |
| this.loading = false; |
| }); |
| } |
| |
| _fireReloadEvent() { |
| // The listener on the change computes height of the related changes |
| // section, so they have to be rendered first, and inside a dom-repeat, |
| // that requires a flush. |
| flush(); |
| this.dispatchEvent(new CustomEvent('new-section-loaded')); |
| } |
| |
| /** |
| * Determines whether or not the given change has a parent change. If there |
| * is a relation chain, and the change id is not the last item of the |
| * relation chain, there is a parent. |
| * |
| * @param {number} currentChangeId |
| * @param {!Array} relatedChanges |
| * @return {boolean} |
| */ |
| _calculateHasParent(currentChangeId, relatedChanges) { |
| return relatedChanges.length > 0 && |
| relatedChanges[relatedChanges.length - 1].change_id !== |
| currentChangeId; |
| } |
| |
| _getRelatedChanges() { |
| return this.$.restAPI.getRelatedChanges(this.change._number, |
| this.patchNum); |
| } |
| |
| _getSubmittedTogether() { |
| return this.$.restAPI.getChangesSubmittedTogether(this.change._number); |
| } |
| |
| _getServerConfig() { |
| return this.$.restAPI.getConfig(); |
| } |
| |
| _getConflicts() { |
| return this.$.restAPI.getChangeConflicts(this.change._number); |
| } |
| |
| _getCherryPicks() { |
| return this.$.restAPI.getChangeCherryPicks(this.change.project, |
| this.change.change_id, this.change._number); |
| } |
| |
| _getChangesWithSameTopic() { |
| return this.$.restAPI.getChangesWithSameTopic(this.change.topic, |
| this.change._number); |
| } |
| |
| /** |
| * @param {number} changeNum |
| * @param {string} project |
| * @param {number=} opt_patchNum |
| * @return {string} |
| */ |
| _computeChangeURL(changeNum, project, opt_patchNum) { |
| return GerritNav.getUrlForChangeById(changeNum, project, opt_patchNum); |
| } |
| |
| _computeChangeContainerClass(currentChange, relatedChange) { |
| const classes = ['changeContainer']; |
| if ([relatedChange, currentChange].includes(undefined)) { |
| return classes; |
| } |
| if (this._changesEqual(relatedChange, currentChange)) { |
| classes.push('thisChange'); |
| } |
| return classes.join(' '); |
| } |
| |
| /** |
| * Do the given objects describe the same change? Compares the changes by |
| * their numbers. |
| * |
| * @see /Documentation/rest-api-changes.html#change-info |
| * @see /Documentation/rest-api-changes.html#related-change-and-commit-info |
| * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo |
| * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo |
| * @return {boolean} |
| */ |
| _changesEqual(a, b) { |
| const aNum = this._getChangeNumber(a); |
| const bNum = this._getChangeNumber(b); |
| return aNum === bNum; |
| } |
| |
| /** |
| * Get the change number from either a ChangeInfo (such as those included in |
| * SubmittedTogetherInfo responses) or get the change number from a |
| * RelatedChangeAndCommitInfo (such as those included in a |
| * RelatedChangesInfo response). |
| * |
| * @see /Documentation/rest-api-changes.html#change-info |
| * @see /Documentation/rest-api-changes.html#related-change-and-commit-info |
| * |
| * @param {!Object} change Either a ChangeInfo or a |
| * RelatedChangeAndCommitInfo object. |
| * @return {number} |
| */ |
| _getChangeNumber(change) { |
| // Default to 0 if change property is not defined. |
| if (!change) return 0; |
| |
| if (change.hasOwnProperty('_change_number')) { |
| return change._change_number; |
| } |
| return change._number; |
| } |
| |
| _computeLinkClass(change) { |
| const statuses = []; |
| if (change.status == ChangeStatus.ABANDONED) { |
| statuses.push('strikethrough'); |
| } |
| if (change.submittable) { |
| statuses.push('submittable'); |
| } |
| return statuses.join(' '); |
| } |
| |
| _computeChangeStatusClass(change) { |
| const 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 == ChangeStatus.NEW) { |
| classes.push('hidden'); |
| } |
| return classes.join(' '); |
| } |
| |
| _computeChangeStatus(change) { |
| switch (change.status) { |
| case ChangeStatus.MERGED: |
| return 'Merged'; |
| case ChangeStatus.ABANDONED: |
| return 'Abandoned'; |
| } |
| 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 ''; |
| } |
| |
| /** @override */ |
| attached() { |
| super.attached(); |
| // We listen to `new-section-loaded` events to allow plugins to trigger |
| // visibility computations, if their content or visibility changed. |
| this.addEventListener('new-section-loaded', |
| () => this._handleNewSectionLoaded()); |
| } |
| |
| _handleNewSectionLoaded() { |
| // A plugin sent a `new-section-loaded` event, so its visibility likely |
| // changed. Hence, we update our visibility if needed. |
| this._resultsChanged(this._relatedResponse, this._submittedTogether, |
| this._conflicts, this._cherryPicks, this._sameTopic); |
| } |
| |
| _resultsChanged(related, submittedTogether, conflicts, |
| cherryPicks, sameTopic) { |
| // Polymer 2: check for undefined |
| if ([ |
| related, |
| submittedTogether, |
| conflicts, |
| cherryPicks, |
| sameTopic, |
| ].includes(undefined)) { |
| return; |
| } |
| |
| const results = [ |
| related && related.changes, |
| // If there are either visible or non-visible changes, we need a |
| // non-empty list to fire the event and set visibility. |
| submittedTogether && ((submittedTogether.changes || []) |
| + (submittedTogether.non_visible_changes ? [{}] : [])), |
| conflicts, |
| cherryPicks, |
| sameTopic, |
| ]; |
| for (let i = 0; i < results.length; i++) { |
| if (results[i] && results[i].length > 0) { |
| this.hidden = false; |
| this.dispatchEvent(new CustomEvent('update', { |
| composed: true, bubbles: false, |
| })); |
| return; |
| } |
| } |
| |
| this._computeHidden(); |
| } |
| |
| _computeHidden() { |
| // None of the built-in change lists had elements. So all of them are |
| // hidden. But since plugins might have injected visible content, we need |
| // to check for that and stay visible if we find any such visible content. |
| // (We consider plugins visible except if it's main element has the hidden |
| // attribute set to true.) |
| const plugins = getPluginEndpoints().getDetails('related-changes-section'); |
| this.hidden = !(plugins.some(plugin => ( |
| (!plugin.domHook) |
| || plugin.domHook.getAllAttached().some( |
| instance => !instance.hidden)))); |
| } |
| |
| _isIndirectAncestor(change) { |
| return !this._connectedRevisions.includes(change.commit.commit); |
| } |
| |
| _computeConnectedRevisions(change, patchNum, relatedChanges) { |
| // Polymer 2: check for undefined |
| if ([change, patchNum, relatedChanges].includes(undefined)) { |
| return undefined; |
| } |
| |
| const connected = []; |
| let changeRevision; |
| if (!change) { return []; } |
| for (const rev in change.revisions) { |
| if (patchNumEquals(change.revisions[rev]._number, patchNum)) { |
| changeRevision = rev; |
| } |
| } |
| const commits = relatedChanges.map(c => c.commit); |
| let pos = commits.length - 1; |
| |
| while (pos >= 0) { |
| const commit = commits[pos].commit; |
| connected.push(commit); |
| if (commit == changeRevision) { |
| break; |
| } |
| pos--; |
| } |
| while (pos >= 0) { |
| for (let i = 0; i < commits[pos].parents.length; i++) { |
| if (connected.includes(commits[pos].parents[i].commit)) { |
| connected.push(commits[pos].commit); |
| break; |
| } |
| } |
| --pos; |
| } |
| return connected; |
| } |
| |
| _computeSubmittedTogetherClass(submittedTogether) { |
| if (!submittedTogether || ( |
| submittedTogether.changes.length === 0 && |
| !submittedTogether.non_visible_changes)) { |
| return 'hidden'; |
| } |
| return ''; |
| } |
| |
| _computeNonVisibleChangesNote(n) { |
| const noun = n === 1 ? 'change' : 'changes'; |
| return `(+ ${n} non-visible ${noun})`; |
| } |
| } |
| |
| customElements.define(GrRelatedChangesList.is, GrRelatedChangesList); |