Allow users to select which changes to cherry pick

When cherry pickinging an entire topic to another branch, there
are abandoned/unwanted changes with the same topic name.
Allow users to select which changes to cherry pick by showing a
checkbox next to each change.
Also show the status of each change(Abandoned, Merged, New) to help
users select which change to select/remove.

Change-Id: I453bf77aa9a6c4ff7c0568c70dad1ee7ba78bc3b
This commit is contained in:
Dhruv Srivastava
2021-01-05 17:08:10 +01:00
parent 284d289a1a
commit fc96e3e4da
4 changed files with 80 additions and 8 deletions

View File

@@ -742,11 +742,11 @@ suite('gr-change-actions tests', () => {
const changes = [
{
change_id: '12345678901234', topic: 'T', subject: 'random',
project: 'A',
project: 'A', status: 'MERGED',
},
{
change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
project: 'B',
project: 'B', status: 'NEW',
},
];
setup(done => {
@@ -769,8 +769,8 @@ suite('gr-change-actions tests', () => {
flush(() => {
const changesTable = dialog.shadowRoot.querySelector('table');
const headers = Array.from(changesTable.querySelectorAll('th'));
const expectedHeadings = ['Change', 'Subject', 'Project',
'Status', ''];
const expectedHeadings = ['', 'Change', 'Status', 'Subject',
'Project', 'Progress', ''];
const headings = headers.map(header => header.innerText);
assert.equal(headings.length, expectedHeadings.length);
for (let i = 0; i < headings.length; i++) {
@@ -779,7 +779,7 @@ suite('gr-change-actions tests', () => {
const changeRows = changesTable.querySelectorAll('tbody > tr');
const change = Array.from(changeRows[0].querySelectorAll('td'))
.map(e => e.innerText);
const expectedChange = ['1234567890', 'random', 'A',
const expectedChange = ['', '1234567890', 'MERGED', 'random', 'A',
'NOT STARTED', ''];
for (let i = 0; i < change.length; i++) {
assert.equal(change[i].trim(), expectedChange[i]);

View File

@@ -31,6 +31,7 @@ import {
RepoName,
BranchName,
CommitId,
ChangeInfoId,
} from '../../../types/common';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {customElement, property, observe} from '@polymer/decorators';
@@ -40,6 +41,7 @@ import {
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {HttpMethod, ChangeStatus} from '../../../constants/constants';
import {hasOwnProperty} from '../../../utils/common-util';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
const SUGGESTIONS_LIMIT = 15;
const CHANGE_SUBJECT_LIMIT = 50;
@@ -146,6 +148,8 @@ export class GrConfirmCherrypickDialog extends GestureEventListeners(
@property({type: Object})
reporting: ReportingService;
private selectedChangeIds = new Set<ChangeInfoId>();
private restApiService = appContext.restApiService;
constructor() {
@@ -161,6 +165,7 @@ export class GrConfirmCherrypickDialog extends GestureEventListeners(
const projects: {[projectName: string]: boolean} = {};
this._duplicateProjectChanges = false;
changes.forEach(change => {
this.selectedChangeIds.add(change.id);
if (projects[change.project]) {
this._duplicateProjectChanges = true;
}
@@ -178,6 +183,19 @@ export class GrConfirmCherrypickDialog extends GestureEventListeners(
);
}
_isChangeSelected(changeId: ChangeInfoId) {
return this.selectedChangeIds.has(changeId);
}
_toggleChangeSelected(e: Event) {
const changeId = ((dom(e) as EventApi).localTarget as HTMLElement).dataset[
'item'
]! as ChangeInfoId;
if (this.selectedChangeIds.has(changeId))
this.selectedChangeIds.delete(changeId);
else this.selectedChangeIds.add(changeId);
}
_computeTopicErrorMessage(duplicateProjectChanges: boolean) {
if (duplicateProjectChanges) {
return 'Two changes cannot be of the same project';
@@ -293,8 +311,16 @@ export class GrConfirmCherrypickDialog extends GestureEventListeners(
}
_handleCherryPickTopic() {
const topic = this._generateRandomCherryPickTopic(this.changes[0]);
this.changes.forEach(change => {
const changes = this.changes.filter(change =>
this.selectedChangeIds.has(change.id)
);
if (!changes.length) {
const errorSpan = this.shadowRoot?.querySelector('.error-message');
errorSpan!.innerHTML = 'No change selected';
return;
}
const topic = this._generateRandomCherryPickTopic(changes[0]);
changes.forEach(change => {
this.updateStatus(change, {status: ProgressStatus.RUNNING});
const payload = {
destination: this.branch,

View File

@@ -179,10 +179,12 @@ export const htmlTemplate = html`
<table>
<thead>
<tr>
<th></th>
<th>Change</th>
<th>Status</th>
<th>Subject</th>
<th>Project</th>
<th>Status</th>
<th>Progress</th>
<!-- Error Message -->
<th></th>
</tr>
@@ -190,7 +192,16 @@ export const htmlTemplate = html`
<tbody>
<template is="dom-repeat" items="[[changes]]">
<tr>
<td>
<input
type="checkbox"
data-item$="[[item.id]]"
on-change="_toggleChangeSelected"
checked="[[_isChangeSelected(item.id)]]"
/>
</td>
<td><span> [[_getChangeId(item)]] </span></td>
<td><span> [[item.status]] </span></td>
<td>
<span> [[_getTrimmedChangeSubject(item.subject)]] </span>
</td>

View File

@@ -88,6 +88,7 @@ suite('gr-confirm-cherrypick-dialog tests', () => {
suite('cherry pick topic', () => {
const changes = [
{
id: '1234',
change_id: '12345678901234', topic: 'T', subject: 'random',
project: 'A',
_number: 1,
@@ -97,6 +98,7 @@ suite('gr-confirm-cherrypick-dialog tests', () => {
current_revision: 'a',
},
{
id: '5678',
change_id: '23456', topic: 'T', subject: 'a'.repeat(100),
project: 'B',
_number: 2,
@@ -109,6 +111,7 @@ suite('gr-confirm-cherrypick-dialog tests', () => {
setup(() => {
element.updateChanges(changes);
element._cherryPickType = CHERRY_PICK_TYPES.TOPIC;
flush();
});
test('cherry pick topic submit', done => {
@@ -129,6 +132,38 @@ suite('gr-confirm-cherrypick-dialog tests', () => {
});
});
test('deselecting a change removes it from being cherry picked', () => {
element.branch = 'master';
const executeChangeActionStub = sinon.stub(element.restApiService,
'executeChangeAction').returns(Promise.resolve([]));
const checkboxes = element.shadowRoot.querySelectorAll(
'input[type="checkbox"]');
assert.equal(checkboxes.length, 2);
assert.isTrue(checkboxes[0].checked);
MockInteractions.tap(checkboxes[0]);
MockInteractions.tap(element.shadowRoot.
querySelector('gr-dialog').$.confirm);
flush();
assert.equal(executeChangeActionStub.callCount, 1);
});
test('deselecting all change shows error message', () => {
element.branch = 'master';
const executeChangeActionStub = sinon.stub(element.restApiService,
'executeChangeAction').returns(Promise.resolve([]));
const checkboxes = element.shadowRoot.querySelectorAll(
'input[type="checkbox"]');
assert.equal(checkboxes.length, 2);
MockInteractions.tap(checkboxes[0]);
MockInteractions.tap(checkboxes[1]);
MockInteractions.tap(element.shadowRoot.
querySelector('gr-dialog').$.confirm);
flush();
assert.equal(executeChangeActionStub.callCount, 0);
assert.equal(element.shadowRoot.querySelector('.error-message').innerText
, 'No change selected');
});
test('_computeStatusClass', () => {
assert.equal(element._computeStatusClass({id: 1}, {1: {status: 'RUNNING'},
}), '');