Files
gerrit/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
Dhruv Srivastava 430c1dd3d6 Allow porting comments to contribute to comment count string
Ported comments now contribute to the thread, unresolved and draft count
shown on the file list and the diff view dropdown.
For the file list count and the file dropdown in diff view, we have a
path and patchset range already defined.
We then figure out if the ported thread will be rendered or not and add
it to the count accordingly.

Also refactor to
* Use getThreadsBySideForFile to calculate thread count which
automatically includes ported comments since we are now calculating
these numbers based on threads and not comments.

* Merge comment string computed by gr-file-list and gr-diff-view and
move it to gr-comment-api.

Change-Id: Ibf6959abf5a0cf437a4964ef32e35f6304d0de86
2020-12-15 21:38:25 +01:00

466 lines
13 KiB
TypeScript

/**
* @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,
patchNumEquals,
sortRevisions,
PatchSet,
} 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 (patchNumEquals(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 = e.detail.value as PatchSetNum;
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 (patchNumEquals(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;
}
}