| /** |
| * @license |
| * Copyright 2016 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../styles/shared-styles'; |
| import '../../../styles/gr-font-styles'; |
| import '../../../styles/gr-change-metadata-shared-styles'; |
| import '../../../styles/gr-change-view-integration-shared-styles'; |
| import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator'; |
| import '../../plugins/gr-endpoint-param/gr-endpoint-param'; |
| import '../../plugins/gr-external-style/gr-external-style'; |
| import '../../shared/gr-account-chip/gr-account-chip'; |
| import '../../shared/gr-date-formatter/gr-date-formatter'; |
| import '../../shared/gr-editable-label/gr-editable-label'; |
| import '../../shared/gr-icon/gr-icon'; |
| import '../../shared/gr-limited-text/gr-limited-text'; |
| import '../../shared/gr-linked-chip/gr-linked-chip'; |
| import '../../shared/gr-tooltip-content/gr-tooltip-content'; |
| import '../gr-submit-requirements/gr-submit-requirements'; |
| import '../gr-commit-info/gr-commit-info'; |
| import '../gr-reviewer-list/gr-reviewer-list'; |
| import { |
| ChangeStatus, |
| GpgKeyInfoStatus, |
| InheritedBooleanInfoConfiguredValue, |
| SubmitType, |
| } from '../../../constants/constants'; |
| import {changeIsOpen, isOwner} from '../../../utils/change-util'; |
| import { |
| AccountDetailInfo, |
| AccountInfo, |
| ApprovalInfo, |
| BranchName, |
| ChangeInfo, |
| CommitId, |
| CommitInfo, |
| ConfigInfo, |
| GpgKeyInfo, |
| Hashtag, |
| isAccount, |
| isDetailedLabelInfo, |
| LabelInfo, |
| LabelNameToInfoMap, |
| NumericChangeId, |
| ParentCommitInfo, |
| RevisionPatchSetNum, |
| RepoName, |
| RevisionInfo, |
| ServerInfo, |
| } from '../../../types/common'; |
| import {assertIsDefined, assertNever, unique} from '../../../utils/common-util'; |
| import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label'; |
| import {GrLinkedChip} from '../../shared/gr-linked-chip/gr-linked-chip'; |
| import {getAppContext} from '../../../services/app-context'; |
| import { |
| Metadata, |
| isSectionSet, |
| DisplayRules, |
| } from '../../../utils/change-metadata-util'; |
| import {fireAlert, fireEvent, fireReload} from '../../../utils/event-util'; |
| import { |
| EditRevisionInfo, |
| isDefined, |
| ParsedChangeInfo, |
| } from '../../../types/types'; |
| import { |
| AutocompleteQuery, |
| AutocompleteSuggestion, |
| } from '../../shared/gr-autocomplete/gr-autocomplete'; |
| import {getRevertCreatedChangeIds} from '../../../utils/message-util'; |
| import {Interaction} from '../../../constants/reporting'; |
| import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util'; |
| import {LitElement, css, html, nothing, PropertyValues} from 'lit'; |
| import {customElement, property, query, state} from 'lit/decorators.js'; |
| import {sharedStyles} from '../../../styles/shared-styles'; |
| import {fontStyles} from '../../../styles/gr-font-styles'; |
| import {changeMetadataStyles} from '../../../styles/gr-change-metadata-shared-styles'; |
| import {when} from 'lit/directives/when.js'; |
| import {ifDefined} from 'lit/directives/if-defined.js'; |
| import {createSearchUrl} from '../../../models/views/search'; |
| import {createChangeUrl} from '../../../models/views/change'; |
| import {GeneratedWebLink, getChangeWeblinks} from '../../../utils/weblink-util'; |
| import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper'; |
| |
| const HASHTAG_ADD_MESSAGE = 'Add Hashtag'; |
| |
| export enum ChangeRole { |
| OWNER = 'owner', |
| UPLOADER = 'uploader', |
| AUTHOR = 'author', |
| COMMITTER = 'committer', |
| } |
| |
| export interface CommitInfoWithRequiredCommit extends CommitInfo { |
| // gr-change-view always assigns commit to CommitInfo |
| commit: CommitId; |
| } |
| |
| const SubmitTypeLabel = new Map<SubmitType, string>([ |
| [SubmitType.FAST_FORWARD_ONLY, 'Fast Forward Only'], |
| [SubmitType.MERGE_IF_NECESSARY, 'Merge if Necessary'], |
| [SubmitType.REBASE_IF_NECESSARY, 'Rebase if Necessary'], |
| [SubmitType.MERGE_ALWAYS, 'Always Merge'], |
| [SubmitType.REBASE_ALWAYS, 'Rebase Always'], |
| [SubmitType.CHERRY_PICK, 'Cherry Pick'], |
| ]); |
| |
| const NOT_CURRENT_MESSAGE = 'Not current - rebase possible'; |
| |
| interface PushCertificateValidationInfo { |
| class: string; |
| icon: string; |
| message: string; |
| } |
| |
| @customElement('gr-change-metadata') |
| export class GrChangeMetadata extends LitElement { |
| @query('#webLinks') webLinks?: HTMLElement; |
| |
| @property({type: Object}) change?: ParsedChangeInfo; |
| |
| @property({type: Object}) revertedChange?: ChangeInfo; |
| |
| @property({type: Object}) account?: AccountDetailInfo; |
| |
| @property({type: Object}) revision?: RevisionInfo | EditRevisionInfo; |
| |
| @property({type: Object}) commitInfo?: CommitInfoWithRequiredCommit; |
| |
| @property({type: Object}) serverConfig?: ServerInfo; |
| |
| @property({type: Boolean}) parentIsCurrent?: boolean; |
| |
| @property({type: Object}) repoConfig?: ConfigInfo; |
| |
| // private but used in test |
| @state() mutable = false; |
| |
| @state() private readonly notCurrentMessage = NOT_CURRENT_MESSAGE; |
| |
| // private but used in test |
| @state() topicReadOnly = true; |
| |
| // private but used in test |
| @state() hashtagReadOnly = true; |
| |
| @state() private pushCertificateValidation?: PushCertificateValidationInfo; |
| |
| // private but used in test |
| @state() settingTopic = false; |
| |
| // private but used in test |
| @state() currentParents: ParentCommitInfo[] = []; |
| |
| @state() private showAllSections = false; |
| |
| @state() private queryTopic?: AutocompleteQuery; |
| |
| @state() private queryHashtag?: AutocompleteQuery; |
| |
| private restApiService = getAppContext().restApiService; |
| |
| private readonly reporting = getAppContext().reportingService; |
| |
| constructor() { |
| super(); |
| this.queryTopic = (input: string) => this.getTopicSuggestions(input); |
| this.queryHashtag = (input: string) => this.getHashtagSuggestions(input); |
| } |
| |
| static override styles = [ |
| sharedStyles, |
| fontStyles, |
| changeMetadataStyles, |
| css` |
| :host { |
| display: table; |
| } |
| gr-submit-requirements { |
| --requirements-horizontal-padding: var(--metadata-horizontal-padding); |
| } |
| gr-editable-label { |
| max-width: 9em; |
| } |
| .webLink { |
| display: block; |
| } |
| gr-account-chip[disabled], |
| gr-linked-chip[disabled] { |
| opacity: 0; |
| pointer-events: none; |
| } |
| .hashtagChip { |
| padding-bottom: var(--spacing-s); |
| } |
| /* consistent with section .title, .value */ |
| .hashtagChip:not(last-of-type) { |
| padding-bottom: var(--spacing-s); |
| } |
| .hashtagChip:last-of-type { |
| display: inline; |
| vertical-align: top; |
| } |
| .parentList.merge { |
| list-style-type: decimal; |
| padding-left: var(--spacing-l); |
| } |
| .parentList gr-commit-info { |
| display: inline-block; |
| } |
| .hideDisplay, |
| #parentNotCurrentMessage { |
| display: none; |
| } |
| .icon { |
| margin: -3px 0; |
| } |
| .icon.help, |
| .icon.notTrusted { |
| color: var(--warning-foreground); |
| } |
| .icon.invalid { |
| color: var(--negative-red-text-color); |
| } |
| .icon.trusted { |
| color: var(--positive-green-text-color); |
| } |
| .parentList.notCurrent.nonMerge #parentNotCurrentMessage { |
| --arrow-color: var(--warning-foreground); |
| display: inline-block; |
| } |
| .oldSeparatedSection { |
| margin-top: var(--spacing-l); |
| padding: var(--spacing-m) 0; |
| } |
| .separatedSection { |
| padding: var(--spacing-m) 0; |
| } |
| .hashtag gr-linked-chip, |
| .topic gr-linked-chip { |
| --linked-chip-text-color: var(--link-color); |
| } |
| gr-reviewer-list { |
| --account-max-length: 100px; |
| max-width: 285px; |
| } |
| .metadata-title { |
| color: var(--deemphasized-text-color); |
| padding-left: var(--metadata-horizontal-padding); |
| } |
| .metadata-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: flex-end; |
| /* The goal is to achieve alignment of the owner account chip and the |
| commit message box. Their top border should be on the same line. */ |
| margin-bottom: var(--spacing-s); |
| } |
| .show-all-button gr-icon { |
| color: inherit; |
| font-size: 18px; |
| } |
| gr-vote-chip { |
| --gr-vote-chip-width: 14px; |
| --gr-vote-chip-height: 14px; |
| } |
| `, |
| ]; |
| |
| override render() { |
| if (!this.change) return nothing; |
| return html`<div> |
| <div class="metadata-header"> |
| <h3 class="metadata-title heading-3">Change Info</h3> |
| ${this.renderShowAllButton()} |
| </div> |
| ${this.renderSubmitted()} ${this.renderUpdated()} ${this.renderOwner()} |
| ${this.renderNonOwner(ChangeRole.UPLOADER)} |
| ${this.renderNonOwner(ChangeRole.AUTHOR)} |
| ${this.renderNonOwner(ChangeRole.COMMITTER)} ${this.renderReviewers()} |
| ${this.renderCCs()} ${this.renderProjectBranch()} ${this.renderParent()} |
| ${this.renderMergedAs()} ${this.renderShowRevertCreatedAs()} |
| ${this.renderTopic()} ${this.renderCherryPickOf()} |
| ${this.renderStrategy()} ${this.renderHashTags()} |
| ${this.renderSubmitRequirements()} ${this.renderWeblinks()} |
| <gr-endpoint-decorator name="change-metadata-item"> |
| <gr-endpoint-param |
| name="labels" |
| .value=${{...this.change?.labels}} |
| ></gr-endpoint-param> |
| <gr-endpoint-param |
| name="change" |
| .value=${this.change} |
| ></gr-endpoint-param> |
| <gr-endpoint-param |
| name="revision" |
| .value=${this.revision} |
| ></gr-endpoint-param> |
| </gr-endpoint-decorator> |
| </div>`; |
| } |
| |
| private renderShowAllButton() { |
| return html`<gr-button |
| link |
| class="show-all-button" |
| @click=${this.onShowAllClick} |
| >${this.showAllSections ? 'Show less' : 'Show all'} |
| <gr-icon icon="expand_more" ?hidden=${this.showAllSections}></gr-icon> |
| <gr-icon icon="expand_less" ?hidden=${!this.showAllSections}></gr-icon> |
| </gr-button>`; |
| } |
| |
| private renderSubmitted() { |
| if (!this.change!.submitted) return nothing; |
| return html`<section class=${this.computeDisplayState(Metadata.SUBMITTED)}> |
| <span class="title">Submitted</span> |
| <span class="value"> |
| <gr-date-formatter |
| withTooltip |
| .dateStr=${this.change!.submitted} |
| showYesterday |
| ></gr-date-formatter> |
| </span> |
| </section> `; |
| } |
| |
| private renderUpdated() { |
| return html`<section class=${this.computeDisplayState(Metadata.UPDATED)}> |
| <span class="title"> |
| <gr-tooltip-content |
| has-tooltip |
| title="Last update of (meta)data for this change." |
| > |
| Updated |
| </gr-tooltip-content> |
| </span> |
| <span class="value"> |
| <gr-date-formatter |
| withTooltip |
| .dateStr=${this.change!.updated} |
| showYesterday |
| ></gr-date-formatter> |
| </span> |
| </section>`; |
| } |
| |
| private renderOwner() { |
| const change = this.change!; |
| return html`<section class=${this.computeDisplayState(Metadata.OWNER)}> |
| <span class="title"> |
| <gr-tooltip-content |
| has-tooltip |
| title="This user created or uploaded the first patchset of this change." |
| > |
| Owner |
| </gr-tooltip-content> |
| </span> |
| <span class="value"> |
| <gr-account-chip |
| .account=${change.owner} |
| .change=${change} |
| highlightAttention |
| .vote=${this.computeVote(change.owner)} |
| .label=${this.computeCodeReviewLabel()} |
| > |
| <gr-vote-chip |
| slot="vote-chip" |
| .vote=${this.computeVote(change.owner)} |
| .label=${this.computeCodeReviewLabel()} |
| circle-shape |
| ></gr-vote-chip> |
| </gr-account-chip> |
| ${when( |
| this.pushCertificateValidation, |
| () => html`<gr-tooltip-content |
| has-tooltip |
| title=${this.pushCertificateValidation!.message} |
| > |
| <gr-icon |
| icon=${this.pushCertificateValidation!.icon} |
| class="icon ${this.pushCertificateValidation!.class}" |
| ></gr-icon> |
| </gr-tooltip-content>` |
| )} |
| </span> |
| </section>`; |
| } |
| |
| renderNonOwner(role: ChangeRole) { |
| if (!this.getNonOwnerRole(role)) return nothing; |
| let title = ''; |
| let name = ''; |
| if (role === ChangeRole.UPLOADER) { |
| title = |
| "This user uploaded the patchset to Gerrit (typically by running the 'git push' command)."; |
| name = 'Uploader'; |
| } else if (role === ChangeRole.AUTHOR) { |
| title = 'This user wrote the code change.'; |
| name = 'Author'; |
| } else if (role === ChangeRole.COMMITTER) { |
| title = |
| 'This user committed the code change to the Git repository (typically to the local Git repo before uploading).'; |
| name = 'Committer'; |
| } |
| return html`<section> |
| <span class="title"> |
| <gr-tooltip-content has-tooltip .title=${title}> |
| ${name} |
| </gr-tooltip-content> |
| </span> |
| <span class="value"> |
| <gr-account-chip |
| .account=${this.getNonOwnerRole(role)} |
| .change=${this.change} |
| ?highlightAttention=${role === ChangeRole.UPLOADER} |
| .vote=${this.computeVoteForRole(role)} |
| .label=${this.computeCodeReviewLabel()} |
| > |
| <gr-vote-chip |
| slot="vote-chip" |
| .vote=${this.computeVoteForRole(role)} |
| .label=${this.computeCodeReviewLabel()} |
| circle-shape |
| ></gr-vote-chip> |
| </gr-account-chip> |
| </span> |
| </section>`; |
| } |
| |
| private renderReviewers() { |
| return html`<section class=${this.computeDisplayState(Metadata.REVIEWERS)}> |
| <span class="title">Reviewers</span> |
| <span class="value"> |
| <gr-reviewer-list |
| .change=${this.change} |
| ?mutable=${this.mutable} |
| reviewers-only |
| .account=${this.account} |
| ></gr-reviewer-list> |
| </span> |
| </section>`; |
| } |
| |
| private renderCCs() { |
| return html`<section class=${this.computeDisplayState(Metadata.CC)}> |
| <span class="title">CC</span> |
| <span class="value"> |
| <gr-reviewer-list |
| .change=${this.change} |
| ?mutable=${this.mutable} |
| ccs-only |
| .account=${this.account} |
| ></gr-reviewer-list> |
| </span> |
| </section>`; |
| } |
| |
| private renderProjectBranch() { |
| const change = this.change!; |
| return when( |
| this.computeShowRepoBranchTogether(), |
| () => |
| html`<section class=${this.computeDisplayState(Metadata.REPO_BRANCH)}> |
| <span class="title"> |
| <gr-tooltip-content |
| has-tooltip |
| title="Repository and branch that the change will be merged into if submitted." |
| > |
| Repo | Branch |
| </gr-tooltip-content> |
| </span> |
| <span class="value"> |
| <a href=${this.computeProjectUrl(change.project)} |
| >${change.project}</a |
| > |
| | |
| <a href=${this.computeBranchUrl(change.project, change.branch)} |
| >${change.branch}</a |
| > |
| </span> |
| </section>`, |
| |
| () => html`<section |
| class=${this.computeDisplayState(Metadata.REPO_BRANCH)} |
| > |
| <span class="title"> |
| <gr-tooltip-content |
| has-tooltip |
| title="Repository that the change will be merged into if submitted." |
| > |
| Repo |
| </gr-tooltip-content> |
| </span> |
| <span class="value"> |
| <a href=${this.computeProjectUrl(change.project)}> |
| <gr-limited-text |
| limit="40" |
| .text=${change.project} |
| ></gr-limited-text> |
| </a> |
| </span> |
| </section> |
| <section class=${this.computeDisplayState(Metadata.REPO_BRANCH)}> |
| <span class="title"> |
| <gr-tooltip-content |
| has-tooltip |
| title="Branch that the change will be merged into if submitted." |
| > |
| Branch |
| </gr-tooltip-content> |
| </span> |
| <span class="value"> |
| <a href=${this.computeBranchUrl(change.project, change.branch)}> |
| <gr-limited-text |
| limit="40" |
| .text=${change.branch} |
| ></gr-limited-text> |
| </a> |
| </span> |
| </section>` |
| ); |
| } |
| |
| private renderParent() { |
| return html`<section class=${this.computeDisplayState(Metadata.PARENT)}> |
| <span class="title" |
| >${this.currentParents.length > 1 ? 'Parents' : 'Parent'}</span |
| > |
| <span class="value"> |
| <ol class=${this.computeParentListClass()}> |
| ${this.currentParents.map( |
| parent => html` <li> |
| <gr-commit-info .commitInfo=${parent}></gr-commit-info> |
| <gr-tooltip-content |
| id="parentNotCurrentMessage" |
| has-tooltip |
| show-icon |
| .title=${this.notCurrentMessage} |
| ></gr-tooltip-content> |
| </li>` |
| )} |
| </ol> |
| </span> |
| </section>`; |
| } |
| |
| private renderMergedAs() { |
| const changeMerged = this.change?.status === ChangeStatus.MERGED; |
| if (!changeMerged) return nothing; |
| return html`<section class=${this.computeDisplayState(Metadata.MERGED_AS)}> |
| <span class="title">Merged As</span> |
| <span class="value"> |
| <gr-commit-info |
| .commitInfo=${this.computeMergedCommitInfo( |
| this.change?.current_revision, |
| this.change?.revisions |
| )} |
| ></gr-commit-info> |
| </span> |
| </section>`; |
| } |
| |
| private renderShowRevertCreatedAs() { |
| if (!this.showRevertCreatedAs()) return nothing; |
| |
| return html`<section |
| class=${this.computeDisplayState(Metadata.REVERT_CREATED_AS)} |
| > |
| <span class="title">${this.getRevertSectionTitle()}</span> |
| <span class="value"> |
| <gr-commit-info |
| .commitInfo=${this.computeRevertCommit()} |
| ></gr-commit-info> |
| </span> |
| </section>`; |
| } |
| |
| private renderTopic() { |
| const showTopic = this.change?.topic || !this.topicReadOnly; |
| if (!showTopic) return nothing; |
| |
| return html`<section |
| class="topic ${this.computeDisplayState(Metadata.TOPIC, this.account)}" |
| > |
| <span class="title">Topic</span> |
| <span class="value"> |
| ${when( |
| this.showTopicChip(), |
| () => html` <gr-linked-chip |
| .text=${this.change?.topic} |
| limit="40" |
| href=${createSearchUrl({topic: this.change!.topic!})} |
| ?removable=${!this.topicReadOnly} |
| @remove=${this.handleTopicRemoved} |
| ></gr-linked-chip>` |
| )} |
| ${when( |
| this.showAddTopic(), |
| () => |
| html` <gr-editable-label |
| class="topicEditableLabel" |
| labelText="Set topic" |
| .confirmLabel=${'Set Topic'} |
| .value=${this.change?.topic} |
| maxLength="1024" |
| .placeholder=${this.computeTopicPlaceholder()} |
| ?readOnly=${this.topicReadOnly} |
| @changed=${this.handleTopicChanged} |
| showAsEditPencil |
| autocomplete |
| .query=${this.queryTopic} |
| ></gr-editable-label>` |
| )} |
| </span> |
| </section>`; |
| } |
| |
| private renderCherryPickOf() { |
| if (!this.showCherryPickOf()) return nothing; |
| return html` <section |
| class=${this.computeDisplayState(Metadata.CHERRY_PICK_OF)} |
| > |
| <span class="title">Cherry pick of</span> |
| <span class="value"> |
| <a |
| href=${this.computeCherryPickOfUrl( |
| this.change?.cherry_pick_of_change, |
| this.change?.cherry_pick_of_patch_set, |
| this.change?.project |
| )} |
| > |
| <gr-limited-text |
| text="${this.change?.cherry_pick_of_change},${this.change |
| ?.cherry_pick_of_patch_set}" |
| limit="40" |
| > |
| </gr-limited-text> |
| </a> |
| </span> |
| </section>`; |
| } |
| |
| private renderStrategy() { |
| if (!changeIsOpen(this.change)) return nothing; |
| return html`<section |
| class="strategy ${this.computeDisplayState(Metadata.STRATEGY)}" |
| > |
| <span class="title">Strategy</span> |
| <span class="value">${this.computeStrategy()}</span> |
| </section>`; |
| } |
| |
| private renderHashTags() { |
| return html`<section |
| class="hashtag ${this.computeDisplayState(Metadata.HASHTAGS)}" |
| > |
| <span class="title">Hashtags</span> |
| <span class="value"> |
| ${(this.change?.hashtags ?? []).map( |
| hashtag => html`<gr-linked-chip |
| class="hashtagChip" |
| .text=${hashtag} |
| href=${this.computeHashtagUrl(hashtag)} |
| ?removable=${!this.hashtagReadOnly} |
| @remove=${this.handleHashtagRemoved} |
| limit="40" |
| > |
| </gr-linked-chip>` |
| )} |
| ${when( |
| !this.hashtagReadOnly, |
| () => html` |
| <gr-editable-label |
| uppercase |
| labelText="Add a hashtag" |
| .placeholder=${this.computeHashtagPlaceholder()} |
| .readOnly=${this.hashtagReadOnly} |
| @changed=${this.handleHashtagChanged} |
| showAsEditPencil |
| autocomplete |
| .query=${this.queryHashtag} |
| ></gr-editable-label> |
| ` |
| )} |
| </span> |
| </section>`; |
| } |
| |
| private renderSubmitRequirements() { |
| return html`<div class="separatedSection"> |
| <gr-submit-requirements |
| .change=${this.change} |
| .account=${this.account} |
| .mutable=${this.mutable} |
| ></gr-submit-requirements> |
| </div>`; |
| } |
| |
| private renderWeblinks() { |
| const webLinks = this.computeWebLinks(); |
| if (!webLinks.length) return nothing; |
| return html`<section id="webLinks"> |
| <span class="title">Links</span> |
| <span class="value"> |
| ${webLinks.map( |
| link => html`<a |
| href=${ifDefined(link.url)} |
| class="webLink" |
| rel="noopener" |
| target="_blank" |
| > |
| ${link.name} |
| </a>` |
| )} |
| </span> |
| </section>`; |
| } |
| |
| override willUpdate(changedProperties: PropertyValues) { |
| if (changedProperties.has('account')) { |
| this.mutable = this.computeIsMutable(); |
| } |
| if (changedProperties.has('mutable') || changedProperties.has('change')) { |
| this.topicReadOnly = this.computeTopicReadOnly(); |
| this.hashtagReadOnly = this.computeHashtagReadOnly(); |
| } |
| if (changedProperties.has('change')) { |
| this.settingTopic = false; |
| } |
| if ( |
| changedProperties.has('serverConfig') || |
| changedProperties.has('change') || |
| changedProperties.has('repoConfig') |
| ) { |
| this.pushCertificateValidation = this.computePushCertificateValidation(); |
| } |
| if (changedProperties.has('revision') || changedProperties.has('change')) { |
| this.currentParents = this.computeParents(); |
| } |
| } |
| |
| // private but used in test |
| computeWebLinks(): GeneratedWebLink[] { |
| return getChangeWeblinks(this.commitInfo?.web_links, this.serverConfig); |
| } |
| |
| private computeStrategy() { |
| if (!this.change?.submit_type) { |
| return ''; |
| } |
| |
| return SubmitTypeLabel.get(this.change.submit_type); |
| } |
| |
| // private but used in test |
| computeLabelNames(labels?: LabelNameToInfoMap) { |
| return labels ? Object.keys(labels).sort() : []; |
| } |
| |
| // private but used in test |
| async handleTopicChanged(e: CustomEvent<string>) { |
| assertIsDefined(this.change, 'change'); |
| const topic = e.detail.length ? e.detail : undefined; |
| this.settingTopic = true; |
| try { |
| fireAlert(this, 'Saving topic and reloading ...'); |
| await this.restApiService.setChangeTopic(this.change._number, topic); |
| } finally { |
| this.settingTopic = false; |
| } |
| fireEvent(this, 'hide-alert'); |
| fireReload(this); |
| } |
| |
| // private but used in test |
| showAddTopic() { |
| const hasTopic = !!this.change?.topic; |
| return !hasTopic && !this.settingTopic && this.topicReadOnly === false; |
| } |
| |
| // private but used in test |
| showTopicChip() { |
| const hasTopic = !!this.change?.topic; |
| return hasTopic && !this.settingTopic; |
| } |
| |
| // private but used in test |
| showCherryPickOf() { |
| const hasCherryPickOf = |
| !!this.change?.cherry_pick_of_change && |
| !!this.change?.cherry_pick_of_patch_set; |
| return hasCherryPickOf; |
| } |
| |
| // private but used in test |
| async handleHashtagChanged(e: CustomEvent<string>) { |
| assertIsDefined(this.change, 'change'); |
| const newHashtag = e.detail.length ? e.detail : undefined; |
| if (!newHashtag?.length) return; |
| fireAlert(this, 'Saving hashtag and reloading ...'); |
| await this.restApiService.setChangeHashtag(this.change._number, { |
| add: [newHashtag as Hashtag], |
| }); |
| fireEvent(this, 'hide-alert'); |
| fireReload(this); |
| } |
| |
| // private but used in test |
| computeTopicReadOnly() { |
| return !this.mutable || !this.change?.actions?.topic?.enabled; |
| } |
| |
| // private but used in test |
| computeHashtagReadOnly() { |
| return !this.mutable || !this.change?.actions?.hashtags?.enabled; |
| } |
| |
| private computeTopicPlaceholder() { |
| // Action items in Material Design are uppercase -- placeholder label text |
| // is sentence case. |
| return this.topicReadOnly ? 'No topic' : 'Set Topic'; |
| } |
| |
| private computeHashtagPlaceholder() { |
| return this.hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE; |
| } |
| |
| /** |
| * private but used in test |
| * |
| * @return object representing data for the push validation. |
| */ |
| computePushCertificateValidation(): |
| | PushCertificateValidationInfo |
| | undefined { |
| if (!this.change || !this.serverConfig?.receive?.enable_signed_push) |
| return undefined; |
| |
| if (!this.isEnabledSignedPushOnRepo()) { |
| return undefined; |
| } |
| const rev = this.change.revisions[this.change.current_revision]; |
| if (!rev.push_certificate?.key) { |
| return { |
| class: 'help filled', |
| icon: 'help', |
| message: 'This patch set was created without a push certificate', |
| }; |
| } |
| |
| const key = rev.push_certificate.key; |
| switch (key.status) { |
| case GpgKeyInfoStatus.BAD: |
| return { |
| class: 'invalid', |
| icon: 'close', |
| message: this.problems('Push certificate is invalid', key), |
| }; |
| case GpgKeyInfoStatus.OK: |
| return { |
| class: 'notTrusted filled', |
| icon: 'info', |
| message: this.problems( |
| 'Push certificate is valid, but key is not trusted', |
| key |
| ), |
| }; |
| case GpgKeyInfoStatus.TRUSTED: |
| return { |
| class: 'trusted', |
| icon: 'check', |
| message: this.problems( |
| 'Push certificate is valid and key is trusted', |
| key |
| ), |
| }; |
| case undefined: |
| // TODO(TS): Process it correctly |
| throw new Error('deleted certificate'); |
| default: |
| assertNever(key.status, `unknown certificate status: ${key.status}`); |
| } |
| } |
| |
| // private but used in test |
| isEnabledSignedPushOnRepo() { |
| if (!this.repoConfig?.enable_signed_push) return false; |
| |
| const enableSignedPush = this.repoConfig.enable_signed_push; |
| return ( |
| (enableSignedPush.configured_value === |
| InheritedBooleanInfoConfiguredValue.INHERIT && |
| enableSignedPush.inherited_value) || |
| enableSignedPush.configured_value === |
| InheritedBooleanInfoConfiguredValue.TRUE |
| ); |
| } |
| |
| private problems(msg: string, key: GpgKeyInfo) { |
| if (!key?.problems || key.problems.length === 0) { |
| return msg; |
| } |
| |
| return [msg + ':'].concat(key.problems).join('\n'); |
| } |
| |
| private computeShowRepoBranchTogether() { |
| const {project, branch} = this.change!; |
| return !!project && !!branch && project.length + branch.length < 40; |
| } |
| |
| private computeProjectUrl(project?: RepoName) { |
| if (!project) return ''; |
| return createSearchUrl({repo: project}); |
| } |
| |
| private computeBranchUrl(project?: RepoName, branch?: BranchName) { |
| if (!project || !branch || !this.change || !this.change.status) return ''; |
| return createSearchUrl({ |
| branch, |
| repo: project, |
| statuses: |
| this.change.status === ChangeStatus.NEW |
| ? ['open'] |
| : [this.change.status.toLowerCase()], |
| }); |
| } |
| |
| private computeCherryPickOfUrl( |
| change?: NumericChangeId, |
| patchset?: RevisionPatchSetNum, |
| project?: RepoName |
| ) { |
| if (!change || !project) { |
| return ''; |
| } |
| return createChangeUrl({ |
| changeNum: change, |
| repo: project, |
| usp: 'metadata', |
| patchNum: patchset, |
| }); |
| } |
| |
| private computeHashtagUrl(hashtag: Hashtag) { |
| return createSearchUrl({hashtag, statuses: ['open', 'merged']}); |
| } |
| |
| private async handleTopicRemoved(e: CustomEvent) { |
| assertIsDefined(this.change, 'change'); |
| const target = e.composedPath()[0] as GrLinkedChip; |
| target.disabled = true; |
| try { |
| fireAlert(this, 'Removing topic and reloading ...'); |
| await this.restApiService.setChangeTopic(this.change._number); |
| } finally { |
| target.disabled = false; |
| } |
| fireEvent(this, 'hide-alert'); |
| fireReload(this); |
| } |
| |
| // private but used in test |
| async handleHashtagRemoved(e: CustomEvent) { |
| e.preventDefault(); |
| assertIsDefined(this.change, 'change'); |
| const target = e.target as GrLinkedChip; |
| target.disabled = true; |
| try { |
| fireAlert(this, 'Removing hashtag and reloading ...'); |
| await this.restApiService.setChangeHashtag(this.change._number, { |
| remove: [target.text as Hashtag], |
| }); |
| } finally { |
| target.disabled = false; |
| } |
| fireEvent(this, 'hide-alert'); |
| fireReload(this); |
| } |
| |
| private computeDisplayState(section: Metadata, account?: AccountDetailInfo) { |
| // special case for Topic - show always for owners, others when set |
| if (section === Metadata.TOPIC) { |
| if ( |
| this.showAllSections || |
| isOwner(this.change, account) || |
| isSectionSet(section, this.change) |
| ) { |
| return ''; |
| } else { |
| return 'hideDisplay'; |
| } |
| } |
| if ( |
| this.showAllSections || |
| DisplayRules.ALWAYS_SHOW.includes(section) || |
| (DisplayRules.SHOW_IF_SET.includes(section) && |
| isSectionSet(section, this.change)) |
| ) { |
| return ''; |
| } |
| return 'hideDisplay'; |
| } |
| |
| // private but used in test |
| computeMergedCommitInfo( |
| currentrevision?: CommitId, |
| revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo} |
| ): CommitInfo | undefined { |
| if (!currentrevision || !revisions) return; |
| const rev = revisions[currentrevision]; |
| if (!rev || !rev.commit) return; |
| // CommitInfo.commit is optional. Set commit in all cases to avoid error |
| // in <gr-commit-info>. @see Issue 5337 |
| if (!rev.commit.commit) { |
| rev.commit.commit = currentrevision; |
| } |
| return rev.commit; |
| } |
| |
| private getRevertSectionTitle() { |
| return this.revertedChange?.status === ChangeStatus.MERGED |
| ? 'Revert Submitted As' |
| : 'Revert Created As'; |
| } |
| |
| // private but used in test |
| showRevertCreatedAs() { |
| if (!this.change?.messages) return false; |
| return getRevertCreatedChangeIds(this.change.messages).length > 0; |
| } |
| |
| // private but used in test |
| computeRevertCommit(): CommitInfo | undefined { |
| const {revertedChange, change} = this; |
| if (revertedChange?.current_revision && revertedChange?.revisions) { |
| // TODO(TS): Fix typing |
| return { |
| commit: this.computeMergedCommitInfo( |
| revertedChange.current_revision, |
| revertedChange.revisions |
| ), |
| } as CommitInfo; |
| } |
| if (!change?.messages) return undefined; |
| // TODO(TS): Fix typing |
| return { |
| commit: getRevertCreatedChangeIds(change.messages)?.[0], |
| } as unknown as CommitInfo; |
| } |
| |
| // private but used in test |
| onShowAllClick() { |
| this.showAllSections = !this.showAllSections; |
| this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, { |
| sectionName: 'metadata', |
| toState: this.showAllSections ? 'Show all' : 'Show less', |
| }); |
| } |
| |
| /** |
| * Get the user with the specified role on the change. Returns undefined if the |
| * user with that role is the same as the owner. |
| * private but used in test |
| */ |
| getNonOwnerRole(role: ChangeRole) { |
| if (!this.change?.revisions?.[this.change.current_revision]) |
| return undefined; |
| |
| const rev = this.change.revisions[this.change.current_revision]; |
| if (!rev) return undefined; |
| |
| if ( |
| role === ChangeRole.UPLOADER && |
| rev.uploader && |
| this.change.owner._account_id !== rev.uploader._account_id |
| ) { |
| return rev.uploader; |
| } |
| |
| if ( |
| role === ChangeRole.AUTHOR && |
| rev.commit?.author && |
| this.change.owner.email !== rev.commit.author.email |
| ) { |
| return rev.commit.author; |
| } |
| |
| if ( |
| role === ChangeRole.COMMITTER && |
| rev.commit?.committer && |
| this.change.owner.email !== rev.commit.committer.email && |
| !( |
| rev.uploader?.email && rev.uploader.email === rev.commit.committer.email |
| ) |
| ) { |
| return rev.commit.committer; |
| } |
| |
| return undefined; |
| } |
| |
| // private but used in test |
| computeParents(): ParentCommitInfo[] { |
| const {change, revision} = this; |
| if (!revision?.commit) { |
| if (!change?.current_revision) return []; |
| const newRevision = change.revisions[change.current_revision]; |
| return newRevision?.commit?.parents ?? []; |
| } |
| return revision?.commit?.parents ?? []; |
| } |
| |
| // private but used in test |
| computeParentListClass() { |
| return [ |
| 'parentList', |
| this.currentParents.length > 1 ? 'merge' : 'nonMerge', |
| this.parentIsCurrent ? 'current' : 'notCurrent', |
| ].join(' '); |
| } |
| |
| private computeIsMutable() { |
| return !!this.account && !!Object.keys(this.account).length; |
| } |
| |
| editTopic() { |
| if (this.topicReadOnly || !this.change || this.change.topic) { |
| return; |
| } |
| const topicEditableLabel = this.shadowRoot!.querySelector<GrEditableLabel>( |
| '.topicEditableLabel' |
| ); |
| if (topicEditableLabel) { |
| topicEditableLabel.open(); |
| } |
| } |
| |
| private getTopicSuggestions( |
| input: string |
| ): Promise<AutocompleteSuggestion[]> { |
| return this.restApiService |
| .getChangesWithSimilarTopic(input, throwingErrorCallback) |
| .then(response => |
| (response ?? []) |
| .map(change => change.topic) |
| .filter(isDefined) |
| .filter(unique) |
| .map(topic => { |
| return {name: topic, value: topic}; |
| }) |
| ); |
| } |
| |
| private getHashtagSuggestions( |
| input: string |
| ): Promise<AutocompleteSuggestion[]> { |
| return this.restApiService |
| .getChangesWithSimilarHashtag(input, throwingErrorCallback) |
| .then(response => |
| (response ?? []) |
| .flatMap(change => change.hashtags ?? []) |
| .filter(isDefined) |
| .filter(unique) |
| .map(hashtag => { |
| return {name: hashtag, value: hashtag}; |
| }) |
| ); |
| } |
| |
| private computeVoteForRole(role: ChangeRole) { |
| const reviewer = this.getNonOwnerRole(role); |
| if (reviewer && isAccount(reviewer)) { |
| return this.computeVote(reviewer); |
| } else { |
| return; |
| } |
| } |
| |
| private computeVote(reviewer: AccountInfo): ApprovalInfo | undefined { |
| const codeReviewLabel = this.computeCodeReviewLabel(); |
| if (!codeReviewLabel || !isDetailedLabelInfo(codeReviewLabel)) return; |
| return getApprovalInfo(codeReviewLabel, reviewer); |
| } |
| |
| private computeCodeReviewLabel(): LabelInfo | undefined { |
| if (!this.change?.labels) return; |
| return getCodeReviewLabel(this.change.labels); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-change-metadata': GrChangeMetadata; |
| } |
| } |