Allow delete change message from the UI

Bug: Issue 10444
Change-Id: I89c60cfbe7df2de95b16c1b29d7261087dc39521
This commit is contained in:
Tao Zhou
2020-02-10 15:57:44 +01:00
parent fb8801bef5
commit 9aafabab82
6 changed files with 411 additions and 220 deletions

View File

@@ -413,6 +413,8 @@
this.addEventListener('comment-refresh', this._reloadDrafts.bind(this));
this.addEventListener('comment-discard',
this._handleCommentDiscard.bind(this));
this.addEventListener('change-message-deleted',
() => this._reload());
this.addEventListener('editable-content-save',
this._handleCommitMessageSave.bind(this));
this.addEventListener('editable-content-cancel',

View File

@@ -81,11 +81,15 @@ limitations under the License.
margin: 0 -4px;
}
.collapsed gr-comment-list,
.collapsed .replyContainer,
.collapsed .replyBtn,
.collapsed .deleteBtn,
.collapsed .hideOnCollapsed,
.hideOnOpen {
display: none;
}
.replyBtn {
margin-right: var(--spacing-m);
}
.collapsed .hideOnOpen {
display: block;
}
@@ -219,8 +223,18 @@ limitations under the License.
content="[[_messageContentExpanded]]"
config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
<template is="dom-if" if="[[!_isMessageContentEmpty()]]">
<div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
<gr-button link small on-click="_handleReplyTap">Reply</gr-button>
<div class="replyActionContainer" hidden$="[[!showReplyButton]]" hidden>
<gr-button
class="replyBtn"
link small on-click="_handleReplyTap">
Reply
</gr-button>
<gr-button
disabled$=[[_isDeletingChangeMsg]]
class="deleteBtn" hidden$="[[!_isAdmin]]" hidden
link small on-click="_handleDeleteMessage">
Delete
</gr-button>
</div>
</template>
<gr-comment-list

View File

@@ -42,6 +42,12 @@
* @event message-anchor-tap
*/
/**
* Fired when a change message is deleted.
*
* @event change-message-deleted
*/
static get properties() {
return {
changeNum: Number,
@@ -115,6 +121,14 @@
type: Boolean,
value: false,
},
_isAdmin: {
type: Boolean,
value: false,
},
_isDeletingChangeMsg: {
type: Boolean,
value: false,
},
};
}
@@ -140,6 +154,9 @@
this.$.restAPI.getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
this.$.restAPI.getIsAdmin().then(isAdmin => {
this._isAdmin = isAdmin;
});
}
_updateExpandedClass(expanded) {
@@ -344,6 +361,17 @@
this.fire('reply', {message: this.message});
}
_handleDeleteMessage(e) {
e.preventDefault();
if (!this.message || !this.message.id) return;
this._isDeletingChangeMsg = true;
this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id)
.then(() => {
this._isDeletingChangeMsg = false;
this.fire('change-message-deleted', {message: this.message});
});
}
_projectNameChanged(name) {
this.$.restAPI.getProjectConfig(name).then(config => {
this._projectConfig = config;

View File

@@ -40,261 +40,380 @@ limitations under the License.
await readyToTest();
let element;
setup(done => {
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(false); },
getConfig() { return Promise.resolve({}); },
suite('when admin and logged in', () => {
setup(done => {
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
getPreferences() { return Promise.resolve({}); },
getConfig() { return Promise.resolve({}); },
getIsAdmin() { return Promise.resolve(true); },
deleteChangeCommitMessage() { return Promise.resolve({}); },
});
element = fixture('basic');
flush(done);
});
element = fixture('basic');
flush(done);
});
test('reply event', done => {
element.message = {
id: '47c43261_55aa2c41',
author: {
_account_id: 1115495,
name: 'Andrew Bonventre',
email: 'andybons@chromium.org',
},
date: '2016-01-12 20:24:49.448000000',
message: 'Uploaded patch set 1.',
_revision_number: 1,
};
test('reply event', done => {
element.message = {
id: '47c43261_55aa2c41',
author: {
_account_id: 1115495,
name: 'Andrew Bonventre',
email: 'andybons@chromium.org',
},
date: '2016-01-12 20:24:49.448000000',
message: 'Uploaded patch set 1.',
_revision_number: 1,
};
element.addEventListener('reply', e => {
assert.deepEqual(e.detail.message, element.message);
done();
element.addEventListener('reply', e => {
assert.deepEqual(e.detail.message, element.message);
done();
});
flushAsynchronousOperations();
assert.isFalse(
element.shadowRoot.querySelector('.replyActionContainer').hidden
);
MockInteractions.tap(element.shadowRoot.querySelector('.replyBtn'));
});
flushAsynchronousOperations();
MockInteractions.tap(element.$$('.replyContainer gr-button'));
});
test('autogenerated prefix hiding', () => {
element.message = {
tag: 'autogenerated:gerrit:test',
updated: '2016-01-12 20:24:49.448000000',
};
test('can see delete button', () => {
element.message = {
id: '47c43261_55aa2c41',
author: {
_account_id: 1115495,
name: 'Andrew Bonventre',
email: 'andybons@chromium.org',
},
date: '2016-01-12 20:24:49.448000000',
message: 'Uploaded patch set 1.',
_revision_number: 1,
};
assert.isTrue(element.isAutomated);
assert.isFalse(element.hidden);
flushAsynchronousOperations();
assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').hidden);
});
element.hideAutomated = true;
test('delete change message', done => {
element.message = {
id: '47c43261_55aa2c41',
author: {
_account_id: 1115495,
name: 'Andrew Bonventre',
email: 'andybons@chromium.org',
},
date: '2016-01-12 20:24:49.448000000',
message: 'Uploaded patch set 1.',
_revision_number: 1,
};
assert.isTrue(element.hidden);
});
element.addEventListener('change-message-deleted', e => {
assert.deepEqual(e.detail.message, element.message);
assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').disabled);
done();
});
flushAsynchronousOperations();
MockInteractions.tap(element.shadowRoot.querySelector('.deleteBtn'));
assert.isTrue(element.shadowRoot.querySelector('.deleteBtn').disabled);
});
test('reviewer message treated as autogenerated', () => {
element.message = {
tag: 'autogenerated:gerrit:test',
updated: '2016-01-12 20:24:49.448000000',
reviewer: {},
};
test('autogenerated prefix hiding', () => {
element.message = {
tag: 'autogenerated:gerrit:test',
updated: '2016-01-12 20:24:49.448000000',
};
assert.isTrue(element.isAutomated);
assert.isFalse(element.hidden);
assert.isTrue(element.isAutomated);
assert.isFalse(element.hidden);
element.hideAutomated = true;
element.hideAutomated = true;
assert.isTrue(element.hidden);
});
assert.isTrue(element.hidden);
});
test('batch reviewer message treated as autogenerated', () => {
element.message = {
type: 'REVIEWER_UPDATE',
updated: '2016-01-12 20:24:49.448000000',
reviewer: {},
};
test('reviewer message treated as autogenerated', () => {
element.message = {
tag: 'autogenerated:gerrit:test',
updated: '2016-01-12 20:24:49.448000000',
reviewer: {},
};
assert.isTrue(element.isAutomated);
assert.isFalse(element.hidden);
assert.isTrue(element.isAutomated);
assert.isFalse(element.hidden);
element.hideAutomated = true;
element.hideAutomated = true;
assert.isTrue(element.hidden);
});
assert.isTrue(element.hidden);
});
test('tag that is not autogenerated prefix does not hide', () => {
element.message = {
tag: 'something',
updated: '2016-01-12 20:24:49.448000000',
};
test('batch reviewer message treated as autogenerated', () => {
element.message = {
type: 'REVIEWER_UPDATE',
updated: '2016-01-12 20:24:49.448000000',
reviewer: {},
};
assert.isFalse(element.isAutomated);
assert.isFalse(element.hidden);
assert.isTrue(element.isAutomated);
assert.isFalse(element.hidden);
element.hideAutomated = true;
element.hideAutomated = true;
assert.isFalse(element.hidden);
});
assert.isTrue(element.hidden);
});
test('reply button hidden unless logged in', () => {
const message = {
message: 'Uploaded patch set 1.',
};
assert.isFalse(element._computeShowReplyButton(message, false));
assert.isTrue(element._computeShowReplyButton(message, true));
});
test('tag that is not autogenerated prefix does not hide', () => {
element.message = {
tag: 'something',
updated: '2016-01-12 20:24:49.448000000',
};
test('_computeShowOnBehalfOf', () => {
const message = {
message: '...',
};
assert.isNotOk(element._computeShowOnBehalfOf(message));
message.author = {_account_id: 1115495};
assert.isNotOk(element._computeShowOnBehalfOf(message));
message.real_author = {_account_id: 1115495};
assert.isNotOk(element._computeShowOnBehalfOf(message));
message.real_author._account_id = 123456;
assert.isOk(element._computeShowOnBehalfOf(message));
message.updated_by = message.author;
delete message.author;
assert.isOk(element._computeShowOnBehalfOf(message));
delete message.updated_by;
assert.isNotOk(element._computeShowOnBehalfOf(message));
});
assert.isFalse(element.isAutomated);
assert.isFalse(element.hidden);
['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
test(`${label} ignored for color voting`, () => {
element.hideAutomated = true;
assert.isFalse(element.hidden);
});
test('reply button hidden unless logged in', () => {
const message = {
message: 'Uploaded patch set 1.',
};
assert.isFalse(element._computeShowReplyButton(message, false));
assert.isTrue(element._computeShowReplyButton(message, true));
});
test('_computeShowOnBehalfOf', () => {
const message = {
message: '...',
};
assert.isNotOk(element._computeShowOnBehalfOf(message));
message.author = {_account_id: 1115495};
assert.isNotOk(element._computeShowOnBehalfOf(message));
message.real_author = {_account_id: 1115495};
assert.isNotOk(element._computeShowOnBehalfOf(message));
message.real_author._account_id = 123456;
assert.isOk(element._computeShowOnBehalfOf(message));
message.updated_by = message.author;
delete message.author;
assert.isOk(element._computeShowOnBehalfOf(message));
delete message.updated_by;
assert.isNotOk(element._computeShowOnBehalfOf(message));
});
['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
test(`${label} ignored for color voting`, () => {
element.message = {
author: {},
expanded: false,
message: `Patch Set 1: ${label}+1`,
};
assert.isNotOk(
Polymer.dom(element.root).querySelector('.negativeVote'));
assert.isNotOk(
Polymer.dom(element.root).querySelector('.positiveVote'));
});
});
test('clicking on date link fires event', () => {
element.message = {
type: 'REVIEWER_UPDATE',
updated: '2016-01-12 20:24:49.448000000',
reviewer: {},
id: '47c43261_55aa2c41',
};
flushAsynchronousOperations();
const stub = sinon.stub();
element.addEventListener('message-anchor-tap', stub);
const dateEl = element.$$('.date');
assert.ok(dateEl);
MockInteractions.tap(dateEl);
assert.isTrue(stub.called);
assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
});
suite('compute messages', () => {
test('empty', () => {
assert.equal(element._computeMessageContent('', '', true), '');
assert.equal(element._computeMessageContent('', '', false), '');
});
test('new patchset', () => {
const original = 'Uploaded patch set 1.';
const tag = 'autogenerated:gerrit:newPatchSet';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, original);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, original);
});
test('new patchset rebased', () => {
const original = 'Patch Set 27: Patch Set 26 was rebased';
const tag = 'autogenerated:gerrit:newPatchSet';
const expected = 'Patch Set 26 was rebased';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
});
test('ready for review', () => {
const original = 'Patch Set 1:\n\nThis change is ready for review.';
const tag = undefined;
const expected = 'This change is ready for review.';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
});
test('vote', () => {
const original = 'Patch Set 1: Code-Style+1';
const tag = undefined;
const expected = '';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
});
test('comments', () => {
const original = 'Patch Set 1:\n\n(3 comments)';
const tag = undefined;
const expected = '';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
});
});
test('votes', () => {
element.message = {
author: {},
expanded: false,
message: `Patch Set 1: ${label}+1`,
message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
};
assert.isNotOk(
Polymer.dom(element.root).querySelector('.negativeVote'));
assert.isNotOk(
Polymer.dom(element.root).querySelector('.positiveVote'));
element.labelExtremes = {
'Verified': {max: 1, min: -1},
'Code-Review': {max: 2, min: -2},
'Trybot-Label3': {max: 3, min: 0},
};
flushAsynchronousOperations();
const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
assert.equal(scoreChips.length, 3);
assert.isTrue(scoreChips[0].classList.contains('positive'));
assert.isTrue(scoreChips[0].classList.contains('max'));
assert.isTrue(scoreChips[1].classList.contains('negative'));
assert.isTrue(scoreChips[1].classList.contains('min'));
assert.isTrue(scoreChips[2].classList.contains('positive'));
assert.isFalse(scoreChips[2].classList.contains('min'));
});
test('removed votes', () => {
element.message = {
author: {},
expanded: false,
message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
};
element.labelExtremes = {
'Verified': {max: 1, min: -1},
'Code-Review': {max: 2, min: -2},
'Commit-Queue': {max: 3, min: 0},
};
flushAsynchronousOperations();
const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
assert.equal(scoreChips.length, 3);
assert.isTrue(scoreChips[1].classList.contains('removed'));
assert.isTrue(scoreChips[2].classList.contains('removed'));
});
test('false negative vote', () => {
element.message = {
author: {},
expanded: false,
message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
};
element.labelExtremes = {};
const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
assert.equal(scoreChips.length, 0);
});
});
test('clicking on date link fires event', () => {
element.message = {
type: 'REVIEWER_UPDATE',
updated: '2016-01-12 20:24:49.448000000',
reviewer: {},
id: '47c43261_55aa2c41',
};
flushAsynchronousOperations();
const stub = sinon.stub();
element.addEventListener('message-anchor-tap', stub);
const dateEl = element.$$('.date');
assert.ok(dateEl);
MockInteractions.tap(dateEl);
assert.isTrue(stub.called);
assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
});
suite('compute messages', () => {
test('empty', () => {
assert.equal(element._computeMessageContent('', '', true), '');
assert.equal(element._computeMessageContent('', '', false), '');
suite('when not logged in', () => {
setup(done => {
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(false); },
getPreferences() { return Promise.resolve({}); },
getConfig() { return Promise.resolve({}); },
getIsAdmin() { return Promise.resolve(false); },
deleteChangeCommitMessage() { return Promise.resolve({}); },
});
element = fixture('basic');
flush(done);
});
test('new patchset', () => {
const original = 'Uploaded patch set 1.';
const tag = 'autogenerated:gerrit:newPatchSet';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, original);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, original);
});
test('reply and delete button should be hidden', () => {
element.message = {
id: '47c43261_55aa2c41',
author: {
_account_id: 1115495,
name: 'Andrew Bonventre',
email: 'andybons@chromium.org',
},
date: '2016-01-12 20:24:49.448000000',
message: 'Uploaded patch set 1.',
_revision_number: 1,
};
test('new patchset rebased', () => {
const original = 'Patch Set 27: Patch Set 26 was rebased';
const tag = 'autogenerated:gerrit:newPatchSet';
const expected = 'Patch Set 26 was rebased';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
});
test('ready for review', () => {
const original = 'Patch Set 1:\n\nThis change is ready for review.';
const tag = undefined;
const expected = 'This change is ready for review.';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
});
test('vote', () => {
const original = 'Patch Set 1: Code-Style+1';
const tag = undefined;
const expected = '';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
});
test('comments', () => {
const original = 'Patch Set 1:\n\n(3 comments)';
const tag = undefined;
const expected = '';
let actual = element._computeMessageContent(original, tag, true);
assert.equal(actual, expected);
actual = element._computeMessageContent(original, tag, false);
assert.equal(actual, expected);
flushAsynchronousOperations();
assert.isTrue(
element.shadowRoot.querySelector('.replyActionContainer').hidden
);
assert.isTrue(
element.shadowRoot.querySelector('.deleteBtn').hidden
);
});
});
test('votes', () => {
element.message = {
author: {},
expanded: false,
message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1',
};
element.labelExtremes = {
'Verified': {max: 1, min: -1},
'Code-Review': {max: 2, min: -2},
'Trybot-Label3': {max: 3, min: 0},
};
flushAsynchronousOperations();
const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
assert.equal(scoreChips.length, 3);
suite('when logged in but not admin', () => {
setup(done => {
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
getConfig() { return Promise.resolve({}); },
getIsAdmin() { return Promise.resolve(false); },
deleteChangeCommitMessage() { return Promise.resolve({}); },
});
element = fixture('basic');
flush(done);
});
assert.isTrue(scoreChips[0].classList.contains('positive'));
assert.isTrue(scoreChips[0].classList.contains('max'));
test('can see reply but not delete button', () => {
element.message = {
id: '47c43261_55aa2c41',
author: {
_account_id: 1115495,
name: 'Andrew Bonventre',
email: 'andybons@chromium.org',
},
date: '2016-01-12 20:24:49.448000000',
message: 'Uploaded patch set 1.',
_revision_number: 1,
};
assert.isTrue(scoreChips[1].classList.contains('negative'));
assert.isTrue(scoreChips[1].classList.contains('min'));
assert.isTrue(scoreChips[2].classList.contains('positive'));
assert.isFalse(scoreChips[2].classList.contains('min'));
});
test('removed votes', () => {
element.message = {
author: {},
expanded: false,
message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue',
};
element.labelExtremes = {
'Verified': {max: 1, min: -1},
'Code-Review': {max: 2, min: -2},
'Commit-Queue': {max: 3, min: 0},
};
flushAsynchronousOperations();
const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
assert.equal(scoreChips.length, 3);
assert.isTrue(scoreChips[1].classList.contains('removed'));
assert.isTrue(scoreChips[2].classList.contains('removed'));
});
test('false negative vote', () => {
element.message = {
author: {},
expanded: false,
message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
};
element.labelExtremes = {};
const scoreChips = Polymer.dom(element.root).querySelectorAll('.score');
assert.equal(scoreChips.length, 0);
flushAsynchronousOperations();
assert.isFalse(
element.shadowRoot.querySelector('.replyActionContainer').hidden
);
assert.isTrue(
element.shadowRoot.querySelector('.deleteBtn').hidden
);
});
});
});
</script>

View File

@@ -1866,6 +1866,15 @@
});
}
deleteChangeCommitMessage(changeNum, messageId) {
return this._getChangeURLAndSend({
changeNum,
method: 'DELETE',
endpoint: '/messages/' + messageId,
reportEndpointAsIs: true,
});
}
saveChangeStarred(changeNum, starred) {
// Some servers may require the project name to be provided
// alongside the change number, so resolve the project name

View File

@@ -716,6 +716,25 @@ limitations under the License.
});
});
test('deleteChangeCommitMessage', () => {
element._projectLookup = {1: 'test'};
const change_num = '1';
const messageId = 'abc';
sandbox.stub(element._restApiHelper, 'send').returns(
Promise.resolve([change_num, messageId]));
sandbox.stub(element, 'getResponseObject')
.returns(Promise.resolve([change_num, messageId]));
return element.deleteChangeCommitMessage(change_num, messageId).then(() => {
assert.isTrue(element._restApiHelper.send.calledOnce);
assert.equal(
element._restApiHelper.send.lastCall.args[0].method,
'DELETE'
);
assert.equal(element._restApiHelper.send.lastCall.args[0].url,
'/changes/test~1/messages/abc');
});
});
test('startWorkInProgress', () => {
const sendStub = sandbox.stub(element, '_getChangeURLAndSend')
.returns(Promise.resolve('ok'));