| /** |
| * @license |
| * 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. |
| */ |
| import '../../../styles/shared-styles'; |
| import '../../shared/gr-dropdown-list/gr-dropdown-list'; |
| import '../../shared/gr-select/gr-select'; |
| import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom'; |
| import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners'; |
| import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin'; |
| import {PolymerElement} from '@polymer/polymer/polymer-element'; |
| import {htmlTemplate} from './gr-patch-range-select_html'; |
| import {pluralize} from '../../../utils/string-util'; |
| import {appContext} from '../../../services/app-context'; |
| import { |
| computeLatestPatchNum, |
| findSortedIndex, |
| getParentIndex, |
| getRevisionByPatchNum, |
| isMergeParent, |
| sortRevisions, |
| PatchSet, |
| convertToPatchSetNum, |
| } from '../../../utils/patch-set-util'; |
| import {customElement, property, observe} from '@polymer/decorators'; |
| import {ReportingService} from '../../../services/gr-reporting/gr-reporting'; |
| import {hasOwnProperty} from '../../../utils/common-util'; |
| import { |
| ParentPatchSetNum, |
| PatchSetNum, |
| RevisionInfo, |
| Timestamp, |
| } from '../../../types/common'; |
| import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info'; |
| import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces'; |
| import {ChangeComments} from '../gr-comment-api/gr-comment-api'; |
| import { |
| DropdownItem, |
| DropDownValueChangeEvent, |
| GrDropdownList, |
| } from '../../shared/gr-dropdown-list/gr-dropdown-list'; |
| import {GeneratedWebLink} from '../../core/gr-navigation/gr-navigation'; |
| |
| // Maximum length for patch set descriptions. |
| const PATCH_DESC_MAX_LENGTH = 500; |
| |
| export interface PatchRangeChangeDetail { |
| patchNum?: PatchSetNum; |
| basePatchNum?: PatchSetNum; |
| } |
| |
| export type PatchRangeChangeEvent = CustomEvent<PatchRangeChangeDetail>; |
| |
| export interface FilesWebLinks { |
| meta_a: GeneratedWebLink[]; |
| meta_b: GeneratedWebLink[]; |
| } |
| |
| export interface GrPatchRangeSelect { |
| $: { |
| patchNumDropdown: GrDropdownList; |
| }; |
| } |
| |
| /** |
| * Fired when the patch range changes |
| * |
| * @event patch-range-change |
| * |
| * @property {string} patchNum |
| * @property {string} basePatchNum |
| * @extends PolymerElement |
| */ |
| @customElement('gr-patch-range-select') |
| export class GrPatchRangeSelect extends GestureEventListeners( |
| LegacyElementMixin(PolymerElement) |
| ) { |
| static get template() { |
| return htmlTemplate; |
| } |
| |
| @property({type: Array}) |
| availablePatches?: PatchSet[]; |
| |
| @property({ |
| type: Object, |
| computed: |
| '_computeBaseDropdownContent(availablePatches, patchNum,' + |
| '_sortedRevisions, changeComments, revisionInfo)', |
| }) |
| _baseDropdownContent?: DropdownItem[]; |
| |
| @property({ |
| type: Object, |
| computed: |
| '_computePatchDropdownContent(availablePatches,' + |
| 'basePatchNum, _sortedRevisions, changeComments)', |
| }) |
| _patchDropdownContent?: DropdownItem[]; |
| |
| @property({type: String}) |
| changeNum?: string; |
| |
| @property({type: Object}) |
| changeComments?: ChangeComments; |
| |
| @property({type: Object}) |
| filesWeblinks?: FilesWebLinks; |
| |
| @property({type: String}) |
| patchNum?: PatchSetNum; |
| |
| @property({type: String}) |
| basePatchNum?: PatchSetNum; |
| |
| @property({type: Object}) |
| revisions?: RevisionInfo[]; |
| |
| @property({type: Object}) |
| revisionInfo?: RevisionInfoClass; |
| |
| @property({type: Array}) |
| _sortedRevisions?: RevisionInfo[]; |
| |
| private readonly reporting: ReportingService = appContext.reportingService; |
| |
| constructor() { |
| super(); |
| this.reporting = appContext.reportingService; |
| } |
| |
| _getShaForPatch(patch: PatchSet) { |
| return patch.sha.substring(0, 10); |
| } |
| |
| _computeBaseDropdownContent( |
| availablePatches?: PatchSet[], |
| patchNum?: PatchSetNum, |
| _sortedRevisions?: RevisionInfo[], |
| changeComments?: ChangeComments, |
| revisionInfo?: RevisionInfoClass |
| ): DropdownItem[] | undefined { |
| // Polymer 2: check for undefined |
| if ( |
| availablePatches === undefined || |
| patchNum === undefined || |
| _sortedRevisions === undefined || |
| changeComments === undefined || |
| revisionInfo === undefined |
| ) { |
| return undefined; |
| } |
| |
| const parentCounts = revisionInfo.getParentCountMap(); |
| const currentParentCount = hasOwnProperty(parentCounts, patchNum) |
| ? parentCounts[patchNum as number] |
| : 1; |
| const maxParents = revisionInfo.getMaxParents(); |
| const isMerge = currentParentCount > 1; |
| |
| const dropdownContent: DropdownItem[] = []; |
| for (const basePatch of availablePatches) { |
| const basePatchNum = basePatch.num; |
| const entry: DropdownItem = this._createDropdownEntry( |
| basePatchNum, |
| 'Patchset ', |
| _sortedRevisions, |
| changeComments, |
| this._getShaForPatch(basePatch) |
| ); |
| dropdownContent.push({ |
| ...entry, |
| disabled: this._computeLeftDisabled( |
| basePatch.num, |
| patchNum, |
| _sortedRevisions |
| ), |
| }); |
| } |
| |
| dropdownContent.push({ |
| text: isMerge ? 'Auto Merge' : 'Base', |
| value: 'PARENT', |
| }); |
| |
| for (let idx = 0; isMerge && idx < maxParents; idx++) { |
| dropdownContent.push({ |
| disabled: idx >= currentParentCount, |
| triggerText: `Parent ${idx + 1}`, |
| text: `Parent ${idx + 1}`, |
| mobileText: `Parent ${idx + 1}`, |
| value: -(idx + 1), |
| }); |
| } |
| |
| return dropdownContent; |
| } |
| |
| _computeMobileText( |
| patchNum: PatchSetNum, |
| changeComments: ChangeComments, |
| revisions: RevisionInfo[] |
| ) { |
| return ( |
| `${patchNum}` + |
| `${this._computePatchSetCommentsString(changeComments, patchNum)}` + |
| `${this._computePatchSetDescription(revisions, patchNum, true)}` |
| ); |
| } |
| |
| _computePatchDropdownContent( |
| availablePatches?: PatchSet[], |
| basePatchNum?: PatchSetNum, |
| _sortedRevisions?: RevisionInfo[], |
| changeComments?: ChangeComments |
| ): DropdownItem[] | undefined { |
| // Polymer 2: check for undefined |
| if ( |
| availablePatches === undefined || |
| basePatchNum === undefined || |
| _sortedRevisions === undefined || |
| changeComments === undefined |
| ) { |
| return undefined; |
| } |
| |
| const dropdownContent: DropdownItem[] = []; |
| for (const patch of availablePatches) { |
| const patchNum = patch.num; |
| const entry = this._createDropdownEntry( |
| patchNum, |
| patchNum === 'edit' ? '' : 'Patchset ', |
| _sortedRevisions, |
| changeComments, |
| this._getShaForPatch(patch) |
| ); |
| dropdownContent.push({ |
| ...entry, |
| disabled: this._computeRightDisabled( |
| basePatchNum, |
| patchNum, |
| _sortedRevisions |
| ), |
| }); |
| } |
| return dropdownContent; |
| } |
| |
| _computeText( |
| patchNum: PatchSetNum, |
| prefix: string, |
| changeComments: ChangeComments, |
| sha: string |
| ) { |
| return ( |
| `${prefix}${patchNum}` + |
| `${this._computePatchSetCommentsString(changeComments, patchNum)}` + |
| ` | ${sha}` |
| ); |
| } |
| |
| _createDropdownEntry( |
| patchNum: PatchSetNum, |
| prefix: string, |
| sortedRevisions: RevisionInfo[], |
| changeComments: ChangeComments, |
| sha: string |
| ) { |
| const entry: DropdownItem = { |
| triggerText: `${prefix}${patchNum}`, |
| text: this._computeText(patchNum, prefix, changeComments, sha), |
| mobileText: this._computeMobileText( |
| patchNum, |
| changeComments, |
| sortedRevisions |
| ), |
| bottomText: `${this._computePatchSetDescription( |
| sortedRevisions, |
| patchNum |
| )}`, |
| value: patchNum, |
| }; |
| const date = this._computePatchSetDate(sortedRevisions, patchNum); |
| if (date) { |
| entry.date = date; |
| } |
| return entry; |
| } |
| |
| @observe('revisions.*') |
| _updateSortedRevisions( |
| revisionsRecord: PolymerDeepPropertyChange<RevisionInfo[], RevisionInfo[]> |
| ) { |
| const revisions = revisionsRecord.base; |
| if (!revisions) return; |
| this._sortedRevisions = sortRevisions(Object.values(revisions)); |
| } |
| |
| /** |
| * The basePatchNum should always be <= patchNum -- because sortedRevisions |
| * is sorted in reverse order (higher patchset nums first), invalid base |
| * patch nums have an index greater than the index of patchNum. |
| * |
| * @param basePatchNum The possible base patch num. |
| * @param patchNum The current selected patch num. |
| */ |
| _computeLeftDisabled( |
| basePatchNum: PatchSetNum, |
| patchNum: PatchSetNum, |
| sortedRevisions: RevisionInfo[] |
| ): boolean { |
| return ( |
| findSortedIndex(basePatchNum, sortedRevisions) <= |
| findSortedIndex(patchNum, sortedRevisions) |
| ); |
| } |
| |
| /** |
| * The basePatchNum should always be <= patchNum -- because sortedRevisions |
| * is sorted in reverse order (higher patchset nums first), invalid patch |
| * nums have an index greater than the index of basePatchNum. |
| * |
| * In addition, if the current basePatchNum is 'PARENT', all patchNums are |
| * valid. |
| * |
| * If the current basePatchNum is a parent index, then only patches that have |
| * at least that many parents are valid. |
| * |
| * @param basePatchNum The current selected base patch num. |
| * @param patchNum The possible patch num. |
| */ |
| _computeRightDisabled( |
| basePatchNum: PatchSetNum, |
| patchNum: PatchSetNum, |
| sortedRevisions: RevisionInfo[] |
| ): boolean { |
| if (basePatchNum === ParentPatchSetNum) { |
| return false; |
| } |
| |
| if (isMergeParent(basePatchNum)) { |
| if (!this.revisionInfo) { |
| return true; |
| } |
| // Note: parent indices use 1-offset. |
| return ( |
| this.revisionInfo.getParentCount(patchNum) < |
| getParentIndex(basePatchNum) |
| ); |
| } |
| |
| return ( |
| findSortedIndex(basePatchNum, sortedRevisions) <= |
| findSortedIndex(patchNum, sortedRevisions) |
| ); |
| } |
| |
| // TODO(dhruvsri): have ported comments contribute to this count |
| _computePatchSetCommentsString( |
| changeComments: ChangeComments, |
| patchNum: PatchSetNum |
| ) { |
| if (!changeComments) { |
| return; |
| } |
| |
| const commentThreadCount = changeComments.computeCommentThreadCount({ |
| patchNum, |
| }); |
| const commentThreadString = pluralize(commentThreadCount, 'comment'); |
| |
| const unresolvedCount = changeComments.computeUnresolvedNum({patchNum}); |
| const unresolvedString = |
| unresolvedCount === 0 ? '' : `${unresolvedCount} unresolved`; |
| |
| if (!commentThreadString.length && !unresolvedString.length) { |
| return ''; |
| } |
| |
| return ( |
| ` (${commentThreadString}` + |
| // Add a comma + space if both comment threads and unresolved |
| (commentThreadString && unresolvedString ? ', ' : '') + |
| `${unresolvedString})` |
| ); |
| } |
| |
| _computePatchSetDescription( |
| revisions: RevisionInfo[], |
| patchNum: PatchSetNum, |
| addFrontSpace?: boolean |
| ) { |
| const rev = getRevisionByPatchNum(revisions, patchNum); |
| return rev?.description |
| ? (addFrontSpace ? ' ' : '') + |
| rev.description.substring(0, PATCH_DESC_MAX_LENGTH) |
| : ''; |
| } |
| |
| _computePatchSetDate( |
| revisions: RevisionInfo[], |
| patchNum: PatchSetNum |
| ): Timestamp | undefined { |
| const rev = getRevisionByPatchNum(revisions, patchNum); |
| return rev ? rev.created : undefined; |
| } |
| |
| /** |
| * Catches value-change events from the patchset dropdowns and determines |
| * whether or not a patch change event should be fired. |
| */ |
| _handlePatchChange(e: DropDownValueChangeEvent) { |
| const detail: PatchRangeChangeDetail = { |
| patchNum: this.patchNum, |
| basePatchNum: this.basePatchNum, |
| }; |
| const target = (dom(e) as EventApi).localTarget; |
| const patchSetValue = convertToPatchSetNum(e.detail.value)!; |
| const latestPatchNum = computeLatestPatchNum(this.availablePatches); |
| if (target === this.$.patchNumDropdown) { |
| if (detail.patchNum === e.detail.value) return; |
| this.reporting.reportInteraction('right-patchset-changed', { |
| previous: detail.patchNum, |
| current: e.detail.value, |
| latest: latestPatchNum, |
| commentCount: this.changeComments?.computeCommentThreadCount({ |
| patchNum: e.detail.value as PatchSetNum, |
| }), |
| }); |
| detail.patchNum = patchSetValue; |
| } else { |
| if (detail.basePatchNum === patchSetValue) return; |
| this.reporting.reportInteraction('left-patchset-changed', { |
| previous: detail.basePatchNum, |
| current: e.detail.value, |
| commentCount: this.changeComments?.computeCommentThreadCount({ |
| patchNum: patchSetValue, |
| }), |
| }); |
| detail.basePatchNum = patchSetValue; |
| } |
| |
| this.dispatchEvent( |
| new CustomEvent('patch-range-change', {detail, bubbles: false}) |
| ); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-patch-range-select': GrPatchRangeSelect; |
| } |
| } |