| /** | 
 |  * @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 '../../../styles/shared-styles.js'; | 
 | import '../../../styles/gr-change-metadata-shared-styles.js'; | 
 | import '../../../styles/gr-change-view-integration-shared-styles.js'; | 
 | import '../../../styles/gr-voting-styles.js'; | 
 | import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; | 
 | import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; | 
 | import '../../plugins/gr-external-style/gr-external-style.js'; | 
 | import '../../shared/gr-account-chip/gr-account-chip.js'; | 
 | import '../../shared/gr-account-link/gr-account-link.js'; | 
 | import '../../shared/gr-date-formatter/gr-date-formatter.js'; | 
 | import '../../shared/gr-editable-label/gr-editable-label.js'; | 
 | import '../../shared/gr-icons/gr-icons.js'; | 
 | import '../../shared/gr-limited-text/gr-limited-text.js'; | 
 | import '../../shared/gr-linked-chip/gr-linked-chip.js'; | 
 | import '../../shared/gr-tooltip-content/gr-tooltip-content.js'; | 
 | import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; | 
 | import '../gr-change-requirements/gr-change-requirements.js'; | 
 | import '../gr-commit-info/gr-commit-info.js'; | 
 | import '../gr-reviewer-list/gr-reviewer-list.js'; | 
 | import '../../shared/gr-account-list/gr-account-list.js'; | 
 | import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; | 
 | import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.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-change-metadata_html.js'; | 
 | import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; | 
 | import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js'; | 
 | import {GerritNav} from '../../core/gr-navigation/gr-navigation.js'; | 
 | import {ChangeStatus} from '../../../constants/constants.js'; | 
 |  | 
 | const HASHTAG_ADD_MESSAGE = 'Add Hashtag'; | 
 |  | 
 | const SubmitTypeLabel = { | 
 |   FAST_FORWARD_ONLY: 'Fast Forward Only', | 
 |   MERGE_IF_NECESSARY: 'Merge if Necessary', | 
 |   REBASE_IF_NECESSARY: 'Rebase if Necessary', | 
 |   MERGE_ALWAYS: 'Always Merge', | 
 |   REBASE_ALWAYS: 'Rebase Always', | 
 |   CHERRY_PICK: 'Cherry Pick', | 
 | }; | 
 |  | 
 | const NOT_CURRENT_MESSAGE = 'Not current - rebase possible'; | 
 |  | 
 | /** | 
 |  * @enum {string} | 
 |  */ | 
 | const CertificateStatus = { | 
 |   /** | 
 |    * This certificate status is bad. | 
 |    */ | 
 |   BAD: 'BAD', | 
 |   /** | 
 |    * This certificate status is OK. | 
 |    */ | 
 |   OK: 'OK', | 
 |   /** | 
 |    * This certificate status is TRUSTED. | 
 |    */ | 
 |   TRUSTED: 'TRUSTED', | 
 | }; | 
 |  | 
 | /** | 
 |  * @extends PolymerElement | 
 |  */ | 
 | class GrChangeMetadata extends mixinBehaviors( [ | 
 |   RESTClientBehavior, | 
 | ], GestureEventListeners( | 
 |     LegacyElementMixin( | 
 |         PolymerElement))) { | 
 |   static get template() { return htmlTemplate; } | 
 |  | 
 |   static get is() { return 'gr-change-metadata'; } | 
 |   /** | 
 |    * Fired when the change topic is changed. | 
 |    * | 
 |    * @event topic-changed | 
 |    */ | 
 |  | 
 |   static get properties() { | 
 |     return { | 
 |     /** @type {?} */ | 
 |       change: Object, | 
 |       labels: { | 
 |         type: Object, | 
 |         notify: true, | 
 |       }, | 
 |       account: Object, | 
 |       /** @type {?} */ | 
 |       revision: Object, | 
 |       commitInfo: Object, | 
 |       _mutable: { | 
 |         type: Boolean, | 
 |         computed: '_computeIsMutable(account)', | 
 |       }, | 
 |       /** @type {?} */ | 
 |       serverConfig: Object, | 
 |       parentIsCurrent: Boolean, | 
 |       _notCurrentMessage: { | 
 |         type: String, | 
 |         value: NOT_CURRENT_MESSAGE, | 
 |         readOnly: true, | 
 |       }, | 
 |       _topicReadOnly: { | 
 |         type: Boolean, | 
 |         computed: '_computeTopicReadOnly(_mutable, change)', | 
 |       }, | 
 |       _hashtagReadOnly: { | 
 |         type: Boolean, | 
 |         computed: '_computeHashtagReadOnly(_mutable, change)', | 
 |       }, | 
 |       /** | 
 |        * @type {Gerrit.PushCertificateValidation} | 
 |        */ | 
 |       _pushCertificateValidation: { | 
 |         type: Object, | 
 |         computed: '_computePushCertificateValidation(serverConfig, change)', | 
 |       }, | 
 |       _showRequirements: { | 
 |         type: Boolean, | 
 |         computed: '_computeShowRequirements(change)', | 
 |       }, | 
 |  | 
 |       _assignee: Array, | 
 |       _isWip: { | 
 |         type: Boolean, | 
 |         computed: '_computeIsWip(change)', | 
 |       }, | 
 |       _newHashtag: String, | 
 |  | 
 |       _settingTopic: { | 
 |         type: Boolean, | 
 |         value: false, | 
 |       }, | 
 |  | 
 |       _currentParents: { | 
 |         type: Array, | 
 |         computed: '_computeParents(change, revision)', | 
 |       }, | 
 |  | 
 |       /** @type {?} */ | 
 |       _CHANGE_ROLE: { | 
 |         type: Object, | 
 |         readOnly: true, | 
 |         value: { | 
 |           OWNER: 'owner', | 
 |           UPLOADER: 'uploader', | 
 |           AUTHOR: 'author', | 
 |           COMMITTER: 'committer', | 
 |         }, | 
 |       }, | 
 |     }; | 
 |   } | 
 |  | 
 |   static get observers() { | 
 |     return [ | 
 |       '_changeChanged(change)', | 
 |       '_labelsChanged(change.labels)', | 
 |       '_assigneeChanged(_assignee.*)', | 
 |     ]; | 
 |   } | 
 |  | 
 |   _labelsChanged(labels) { | 
 |     this.labels = Object.assign({}, labels) || null; | 
 |   } | 
 |  | 
 |   _changeChanged(change) { | 
 |     this._assignee = change.assignee ? [change.assignee] : []; | 
 |   } | 
 |  | 
 |   _assigneeChanged(assigneeRecord) { | 
 |     if (!this.change || !this._isAssigneeEnabled(this.serverConfig)) { | 
 |       return; | 
 |     } | 
 |     const assignee = assigneeRecord.base; | 
 |     if (assignee.length) { | 
 |       const acct = assignee[0]; | 
 |       if (this.change.assignee && | 
 |           acct._account_id === this.change.assignee._account_id) { return; } | 
 |       this.set(['change', 'assignee'], acct); | 
 |       this.$.restAPI.setAssignee(this.change._number, acct._account_id); | 
 |     } else { | 
 |       if (!this.change.assignee) { return; } | 
 |       this.set(['change', 'assignee'], undefined); | 
 |       this.$.restAPI.deleteAssignee(this.change._number); | 
 |     } | 
 |   } | 
 |  | 
 |   _computeHideStrategy(change) { | 
 |     return !this.changeIsOpen(change); | 
 |   } | 
 |  | 
 |   /** | 
 |    * @param {Object} commitInfo | 
 |    * @return {?Array} If array is empty, returns null instead so | 
 |    * an existential check can be used to hide or show the webLinks | 
 |    * section. | 
 |    */ | 
 |   _computeWebLinks(commitInfo, serverConfig) { | 
 |     if (!commitInfo) { return null; } | 
 |     const weblinks = GerritNav.getChangeWeblinks( | 
 |         this.change ? this.change.repo : '', | 
 |         commitInfo.commit, | 
 |         { | 
 |           weblinks: commitInfo.web_links, | 
 |           config: serverConfig, | 
 |         }); | 
 |     return weblinks.length ? weblinks : null; | 
 |   } | 
 |  | 
 |   _isAssigneeEnabled(serverConfig) { | 
 |     return serverConfig && serverConfig.change | 
 |         && !!serverConfig.change.enable_assignee; | 
 |   } | 
 |  | 
 |   _computeStrategy(change) { | 
 |     return SubmitTypeLabel[change.submit_type]; | 
 |   } | 
 |  | 
 |   _computeLabelNames(labels) { | 
 |     return Object.keys(labels).sort(); | 
 |   } | 
 |  | 
 |   _handleTopicChanged(e, topic) { | 
 |     const lastTopic = this.change.topic; | 
 |     if (!topic.length) { topic = null; } | 
 |     this._settingTopic = true; | 
 |     this.$.restAPI.setChangeTopic(this.change._number, topic) | 
 |         .then(newTopic => { | 
 |           this._settingTopic = false; | 
 |           this.set(['change', 'topic'], newTopic); | 
 |           if (newTopic !== lastTopic) { | 
 |             this.dispatchEvent(new CustomEvent( | 
 |                 'topic-changed', {bubbles: true, composed: true})); | 
 |           } | 
 |         }); | 
 |   } | 
 |  | 
 |   _showAddTopic(changeRecord, settingTopic) { | 
 |     const hasTopic = !!changeRecord && | 
 |         !!changeRecord.base && !!changeRecord.base.topic; | 
 |     return !hasTopic && !settingTopic; | 
 |   } | 
 |  | 
 |   _showTopicChip(changeRecord, settingTopic) { | 
 |     const hasTopic = !!changeRecord && | 
 |         !!changeRecord.base && !!changeRecord.base.topic; | 
 |     return hasTopic && !settingTopic; | 
 |   } | 
 |  | 
 |   _showCherryPickOf(changeRecord) { | 
 |     const hasCherryPickOf = !!changeRecord && | 
 |         !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change && | 
 |         !!changeRecord.base.cherry_pick_of_patch_set; | 
 |     return hasCherryPickOf; | 
 |   } | 
 |  | 
 |   _handleHashtagChanged(e) { | 
 |     const lastHashtag = this.change.hashtag; | 
 |     if (!this._newHashtag.length) { return; } | 
 |     const newHashtag = this._newHashtag; | 
 |     this._newHashtag = ''; | 
 |     this.$.restAPI.setChangeHashtag( | 
 |         this.change._number, {add: [newHashtag]}).then(newHashtag => { | 
 |       this.set(['change', 'hashtags'], newHashtag); | 
 |       if (newHashtag !== lastHashtag) { | 
 |         this.dispatchEvent( | 
 |             new CustomEvent('hashtag-changed', { | 
 |               bubbles: true, composed: true})); | 
 |       } | 
 |     }); | 
 |   } | 
 |  | 
 |   _computeTopicReadOnly(mutable, change) { | 
 |     return !mutable || | 
 |         !change || | 
 |         !change.actions || | 
 |         !change.actions.topic || | 
 |         !change.actions.topic.enabled; | 
 |   } | 
 |  | 
 |   _computeHashtagReadOnly(mutable, change) { | 
 |     return !mutable || | 
 |         !change || | 
 |         !change.actions || | 
 |         !change.actions.hashtags || | 
 |         !change.actions.hashtags.enabled; | 
 |   } | 
 |  | 
 |   _computeAssigneeReadOnly(mutable, change) { | 
 |     return !mutable || | 
 |         !change || | 
 |         !change.actions || | 
 |         !change.actions.assignee || | 
 |         !change.actions.assignee.enabled; | 
 |   } | 
 |  | 
 |   _computeTopicPlaceholder(_topicReadOnly) { | 
 |     // Action items in Material Design are uppercase -- placeholder label text | 
 |     // is sentence case. | 
 |     return _topicReadOnly ? 'No topic' : 'ADD TOPIC'; | 
 |   } | 
 |  | 
 |   _computeHashtagPlaceholder(_hashtagReadOnly) { | 
 |     return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE; | 
 |   } | 
 |  | 
 |   _computeShowRequirements(change) { | 
 |     if (change.status !== ChangeStatus.NEW) { | 
 |       // TODO(maximeg) change this to display the stored | 
 |       // requirements, once it is implemented server-side. | 
 |       return false; | 
 |     } | 
 |     const hasRequirements = !!change.requirements && | 
 |         Object.keys(change.requirements).length > 0; | 
 |     const hasLabels = !!change.labels && | 
 |         Object.keys(change.labels).length > 0; | 
 |     return hasRequirements || hasLabels || !!change.work_in_progress; | 
 |   } | 
 |  | 
 |   /** | 
 |    * @return {?Gerrit.PushCertificateValidation} object representing data for | 
 |    *     the push validation. | 
 |    */ | 
 |   _computePushCertificateValidation(serverConfig, change) { | 
 |     if (!change || !serverConfig || !serverConfig.receive || | 
 |         !serverConfig.receive.enable_signed_push) { | 
 |       return null; | 
 |     } | 
 |     const rev = change.revisions[change.current_revision]; | 
 |     if (!rev.push_certificate || !rev.push_certificate.key) { | 
 |       return { | 
 |         class: 'help', | 
 |         icon: 'gr-icons:help', | 
 |         message: 'This patch set was created without a push certificate', | 
 |       }; | 
 |     } | 
 |  | 
 |     const key = rev.push_certificate.key; | 
 |     switch (key.status) { | 
 |       case CertificateStatus.BAD: | 
 |         return { | 
 |           class: 'invalid', | 
 |           icon: 'gr-icons:close', | 
 |           message: this._problems('Push certificate is invalid', key), | 
 |         }; | 
 |       case CertificateStatus.OK: | 
 |         return { | 
 |           class: 'notTrusted', | 
 |           icon: 'gr-icons:info', | 
 |           message: this._problems( | 
 |               'Push certificate is valid, but key is not trusted', key), | 
 |         }; | 
 |       case CertificateStatus.TRUSTED: | 
 |         return { | 
 |           class: 'trusted', | 
 |           icon: 'gr-icons:check', | 
 |           message: this._problems( | 
 |               'Push certificate is valid and key is trusted', key), | 
 |         }; | 
 |       default: | 
 |         throw new Error(`unknown certificate status: ${key.status}`); | 
 |     } | 
 |   } | 
 |  | 
 |   _problems(msg, key) { | 
 |     if (!key || !key.problems || key.problems.length === 0) { | 
 |       return msg; | 
 |     } | 
 |  | 
 |     return [msg + ':'].concat(key.problems).join('\n'); | 
 |   } | 
 |  | 
 |   _computeShowRepoBranchTogether(repo, branch) { | 
 |     return !!repo && !!branch && repo.length + branch.length < 40; | 
 |   } | 
 |  | 
 |   _computeProjectUrl(project) { | 
 |     return GerritNav.getUrlForProjectChanges(project); | 
 |   } | 
 |  | 
 |   _computeBranchUrl(project, branch) { | 
 |     if (!this.change || !this.change.status) return ''; | 
 |     return GerritNav.getUrlForBranch(branch, project, | 
 |         this.change.status == ChangeStatus.NEW ? 'open' : | 
 |           this.change.status.toLowerCase()); | 
 |   } | 
 |  | 
 |   _computeCherryPickOfUrl(change, patchset, project) { | 
 |     return GerritNav.getUrlForChangeById(change, project, patchset); | 
 |   } | 
 |  | 
 |   _computeTopicUrl(topic) { | 
 |     return GerritNav.getUrlForTopic(topic); | 
 |   } | 
 |  | 
 |   _computeHashtagUrl(hashtag) { | 
 |     return GerritNav.getUrlForHashtag(hashtag); | 
 |   } | 
 |  | 
 |   _handleTopicRemoved(e) { | 
 |     const target = dom(e).rootTarget; | 
 |     target.disabled = true; | 
 |     this.$.restAPI.setChangeTopic(this.change._number, null) | 
 |         .then(() => { | 
 |           target.disabled = false; | 
 |           this.set(['change', 'topic'], ''); | 
 |           this.dispatchEvent( | 
 |               new CustomEvent('topic-changed', | 
 |                   {bubbles: true, composed: true})); | 
 |         }) | 
 |         .catch(err => { | 
 |           target.disabled = false; | 
 |           return; | 
 |         }); | 
 |   } | 
 |  | 
 |   _handleHashtagRemoved(e) { | 
 |     e.preventDefault(); | 
 |     const target = dom(e).rootTarget; | 
 |     target.disabled = true; | 
 |     this.$.restAPI.setChangeHashtag(this.change._number, | 
 |         {remove: [target.text]}) | 
 |         .then(newHashtag => { | 
 |           target.disabled = false; | 
 |           this.set(['change', 'hashtags'], newHashtag); | 
 |         }) | 
 |         .catch(err => { | 
 |           target.disabled = false; | 
 |           return; | 
 |         }); | 
 |   } | 
 |  | 
 |   _computeIsWip(change) { | 
 |     return !!change.work_in_progress; | 
 |   } | 
 |  | 
 |   _computeShowRoleClass(change, role) { | 
 |     return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay'; | 
 |   } | 
 |  | 
 |   /** | 
 |    * Get the user with the specified role on the change. Returns null if the | 
 |    * user with that role is the same as the owner. | 
 |    * | 
 |    * @param {!Object} change | 
 |    * @param {string} role One of the values from _CHANGE_ROLE | 
 |    * @return {Object|null} either an account or null. | 
 |    */ | 
 |   _getNonOwnerRole(change, role) { | 
 |     if (!change || !change.current_revision || | 
 |         !change.revisions[change.current_revision]) { | 
 |       return null; | 
 |     } | 
 |  | 
 |     const rev = change.revisions[change.current_revision]; | 
 |     if (!rev) { return null; } | 
 |  | 
 |     if (role === this._CHANGE_ROLE.UPLOADER && | 
 |         rev.uploader && | 
 |         change.owner._account_id !== rev.uploader._account_id) { | 
 |       return rev.uploader; | 
 |     } | 
 |  | 
 |     if (role === this._CHANGE_ROLE.AUTHOR && | 
 |         rev.commit && rev.commit.author && | 
 |         change.owner.email !== rev.commit.author.email) { | 
 |       return rev.commit.author; | 
 |     } | 
 |  | 
 |     if (role === this._CHANGE_ROLE.COMMITTER && | 
 |         rev.commit && rev.commit.committer && | 
 |         change.owner.email !== rev.commit.committer.email) { | 
 |       return rev.commit.committer; | 
 |     } | 
 |  | 
 |     return null; | 
 |   } | 
 |  | 
 |   _computeParents(change, revision) { | 
 |     if (!revision || !revision.commit) { | 
 |       if (!change || !change.current_revision) { return []; } | 
 |       revision = change.revisions[change.current_revision]; | 
 |       if (!revision || !revision.commit) { return []; } | 
 |     } | 
 |     return revision.commit.parents; | 
 |   } | 
 |  | 
 |   _computeParentsLabel(parents) { | 
 |     return parents && parents.length > 1 ? 'Parents' : 'Parent'; | 
 |   } | 
 |  | 
 |   _computeParentListClass(parents, parentIsCurrent) { | 
 |     // Undefined check for polymer 2 | 
 |     if (parents === undefined || parentIsCurrent === undefined) { | 
 |       return ''; | 
 |     } | 
 |  | 
 |     return [ | 
 |       'parentList', | 
 |       parents && parents.length > 1 ? 'merge' : 'nonMerge', | 
 |       parentIsCurrent ? 'current' : 'notCurrent', | 
 |     ].join(' '); | 
 |   } | 
 |  | 
 |   _computeIsMutable(account) { | 
 |     return !!Object.keys(account).length; | 
 |   } | 
 |  | 
 |   editTopic() { | 
 |     if (this._topicReadOnly || this.change.topic) { return; } | 
 |     // Cannot use `this.$.ID` syntax because the element exists inside of a | 
 |     // dom-if. | 
 |     this.shadowRoot.querySelector('.topicEditableLabel').open(); | 
 |   } | 
 |  | 
 |   _getReviewerSuggestionsProvider(change) { | 
 |     const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, | 
 |         change._number, SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY); | 
 |     provider.init(); | 
 |     return provider; | 
 |   } | 
 | } | 
 |  | 
 | customElements.define(GrChangeMetadata.is, GrChangeMetadata); |