| /** |
| * @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); |