Plugin API for label score updates

Plugin may use following API to receive saved label updates:

``` js
plugin.changeMetadata().onLabelsChanged(
    labels => console.log(labels));
```

Feature: Issue 820117
Change-Id: Id428606db25086cfdf5a5b420e34777cc8b27303
This commit is contained in:
Viktar Donich 2018-04-03 15:32:54 -07:00
parent e137acc455
commit b6db19e072
11 changed files with 201 additions and 37 deletions

View File

@ -0,0 +1,16 @@
= Gerrit Code Review - Change metadata plugin API
This API is provided by
link:pg-plugin-dev.html#change-metadata[plugin.changeMetadata()] and provides
interface for customization and data updates of change metadata.
== onLabelsChanged
`changeMetadataApi.onLabelsChanged(callback)`
.Params
- *callback* function that's executed when labels changed on the server.
Callback receives labels with scores applied to the change, map of the label
names to link:rest-api-changes.html#label-info[LabelInfo] entries
.Returns
- `GrChangeMetadataApi` for chaining.

View File

@ -341,6 +341,15 @@ screen.
Deprecated. Use link:#plugin-settings[`plugin.settings()`] instead.
=== changeMetadata
`plugin.changeMetadata()`
.Params:
- none
.Returns:
- Instance of link:pg-plugin-change-metadata-api.html[GrChangeMetadataApi].
=== theme
`plugin.theme()`

View File

@ -69,6 +69,11 @@ link:rest-api-changes.html#change-info[ChangeInfo]
current revision displayed, an instance of
link:rest-api-changes.html#revision-info[RevisionInfo]
* `labels`
+
labels with scores applied to the change, map of the label names to
link:rest-api-changes.html#label-info[LabelInfo] entries
=== robot-comment-controls
The `robot-comment-controls` extension point is located inside each comment
rendered on the diff page, and is only visible when the comment is a robot
@ -118,4 +123,4 @@ This endpoint wraps the textarea in the reply dialog.
This endpoint decorator wraps the voting buttons in the reply dialog.
=== header-title
This endpoint wraps the title-text in the application header.
This endpoint wraps the title-text in the application header.

View File

@ -29,7 +29,7 @@ limitations under the License.
<test-fixture id="element">
<template>
<gr-change-metadata></gr-change-metadata>
<gr-change-metadata mutable="true"></gr-change-metadata>
</template>
</test-fixture>
@ -51,20 +51,42 @@ limitations under the License.
'section.topic',
];
const labels = {
CI: {
all: [
{value: 1, name: 'user 2', _account_id: 1},
{value: 2, name: 'user '},
],
values: {
' 0': 'Don\'t submit as-is',
'+1': 'No score',
'+2': 'Looks good to me',
},
},
};
const getStyle = function(selector, name) {
return window.getComputedStyle(
Polymer.dom(element.root).querySelector(selector))[name];
};
function createElement() {
const element = fixture('element');
element.change = {labels, status: 'NEW'};
element.revision = {};
return element;
}
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); },
getLoggedIn() { return Promise.resolve(false); },
deleteVote() { return Promise.resolve({ok: true}); },
});
stub('gr-change-metadata', {
_computeShowLabelStatus() { return true; },
_computeShowReviewersByState() { return true; },
ready() {
this.change = {labels: [], status: 'NEW'};
this.serverConfig = {};
},
});
});
@ -76,7 +98,7 @@ limitations under the License.
suite('by default', () => {
setup(done => {
element = fixture('element');
element = createElement();
flush(done);
});
@ -99,7 +121,7 @@ limitations under the License.
],
},
};
element = fixture('element');
element = createElement();
const importSpy = sandbox.spy(element.$.externalStyle, '_import');
Gerrit.awaitPluginsLoaded().then(() => {
Promise.all(importSpy.returnValues).then(() => {
@ -114,5 +136,38 @@ limitations under the License.
});
}
});
suite('label updates', () => {
let plugin;
let labelChangeStub;
setup(done => {
Gerrit.install(p => plugin = p, '0.1',
new URL('test/plugin.html?' + Math.random(),
window.location.href).toString());
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
Gerrit._resolveAllPluginsLoaded();
element = createElement();
sandbox.stub(element, '_computeCanDeleteVote').returns(true);
labelChangeStub = sandbox.stub();
plugin.changeMetadata().onLabelsChanged(labelChangeStub);
flush(done);
});
test('labels changed callback', done => {
assert.equal(labelChangeStub.callCount, 1);
assert.isTrue(labelChangeStub.calledWithExactly(labels));
assert.equal(labelChangeStub.args[0][0]['CI'].all.length, 2);
MockInteractions.tap(Polymer.dom(element.root).querySelector(
'gr-account-chip').$.remove);
// Wait for fake rest API response.
flush(() => {
assert.equal(labelChangeStub.callCount, 2);
assert.equal(labelChangeStub.args[1][0]['CI'].all.length, 1);
done();
});
});
});
});
</script>

View File

@ -316,12 +316,12 @@ limitations under the License.
</section>
</template>
<template is="dom-repeat"
items="[[_computeLabelNames(change.labels)]]" as="labelName">
items="[[_computeLabelNames(labels)]]" as="labelName">
<section>
<span class="title">[[labelName]]</span>
<span class="value">
<template is="dom-repeat"
items="[[_computeLabelValues(labelName, change.labels.*)]]"
items="[[_computeLabelValues(labelName, labels.*)]]"
as="label">
<div class="labelValueContainer">
<span>
@ -379,6 +379,7 @@ limitations under the License.
</span>
</section>
<gr-endpoint-decorator name="change-metadata-item">
<gr-endpoint-param name="labels" value="[[labels]]"></gr-endpoint-param>
<gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
<gr-endpoint-param name="revision" value="[[revision]]"></gr-endpoint-param>
</gr-endpoint-decorator>

View File

@ -42,6 +42,10 @@
properties: {
/** @type {?} */
change: Object,
labels: {
type: Object,
notify: true,
},
/** @type {?} */
revision: Object,
commitInfo: Object,
@ -98,9 +102,14 @@
observers: [
'_changeChanged(change)',
'_labelsChanged(change.labels)',
'_assigneeChanged(_assignee.*)',
],
_labelsChanged(labels) {
this.labels = Object.assign({}, labels) || null;
},
_changeChanged(change) {
this._assignee = change.assignee ? [change.assignee] : [];
},
@ -243,17 +252,22 @@
},
_computeTopicReadOnly(mutable, change) {
return !mutable || !change.actions.topic || !change.actions.topic.enabled;
return !mutable ||
!change.actions ||
!change.actions.topic ||
!change.actions.topic.enabled;
},
_computeHashtagReadOnly(mutable, change) {
return !mutable ||
!change.actions ||
!change.actions.hashtags ||
!change.actions.hashtags.enabled;
},
_computeAssigneeReadOnly(mutable, change) {
return !mutable ||
!change.actions ||
!change.actions.assignee ||
!change.actions.assignee.enabled;
},
@ -283,7 +297,9 @@
* change-metadata section is modifiable by the current user.
*/
_computeCanDeleteVote(reviewer, mutable) {
if (!mutable) { return false; }
if (!mutable || !this.change || !this.change.removable_reviewers) {
return false;
}
for (let i = 0; i < this.change.removable_reviewers.length; i++) {
if (this.change.removable_reviewers[i]._account_id ===
reviewer._account_id) {
@ -313,6 +329,7 @@
if (!response.ok) { return response; }
const label = this.change.labels[labelName];
const labels = label.all || [];
let wasChanged = false;
for (let i = 0; i < labels.length; i++) {
if (labels[i]._account_id === accountID) {
for (const key in label) {
@ -320,13 +337,18 @@
label[key]._account_id === accountID) {
// Remove special label field, keeping change label values
// in sync with the backend.
this.set(['change.labels', labelName, key], null);
this.change.labels[labelName][key] = null;
wasChanged = true;
}
}
this.splice(['change.labels', labelName, 'all'], i, 1);
this.change.labels[labelName].all.splice(i, 1);
wasChanged = true;
break;
}
}
if (wasChanged) {
this.notifyPath('change.labels');
}
}).catch(err => {
target.disabled = false;
return;

View File

@ -523,36 +523,26 @@ limitations under the License.
assert.isFalse(button.hasAttribute('hidden'));
});
test('deletes votes', done => {
const deleteStub = sandbox.stub(element.$.restAPI, 'deleteVote')
.returns(Promise.resolve({ok: true}));
test('deletes votes', () => {
const deleteResponse = Promise.resolve({ok: true});
const deleteStub = sandbox.stub(
element.$.restAPI, 'deleteVote').returns(deleteResponse);
element.change.removable_reviewers = [
{
_account_id: 1,
name: 'bojack',
},
];
element.change.removable_reviewers = [{
_account_id: 1,
name: 'bojack',
}];
element.change.labels.test.recommended = {_account_id: 1};
element.mutable = true;
flushAsynchronousOperations();
const chip = element.$$('gr-account-chip');
const button = chip.$$('gr-button');
const spliceStub = sandbox.stub(element, 'splice', (path, index,
length) => {
assert.isFalse(chip.disabled);
assert.deepEqual(path, ['change.labels', 'test', 'all']);
assert.equal(index, 0);
assert.equal(length, 1);
assert.notOk(element.change.labels.test.recommended);
assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
spliceStub.restore();
done();
});
MockInteractions.tap(button);
assert.isTrue(chip.disabled);
return deleteResponse.then(() => {
assert.isFalse(chip.disabled);
assert.notOk(element.change.labels.test.recommended);
assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
});
});
test('changing topic', () => {

View File

@ -0,0 +1,22 @@
<!--
@license
Copyright (C) 2018 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.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<dom-module id="gr-change-metadata-api">
<script src="gr-change-metadata-api.js"></script>
</dom-module>

View File

@ -0,0 +1,39 @@
/**
* @license
* Copyright (C) 2018 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.
*/
(function(window) {
'use strict';
function GrChangeMetadataApi(plugin) {
this._hook = null;
this.plugin = plugin;
}
GrChangeMetadataApi.prototype._createHook = function() {
this._hook = this.plugin.hook('change-metadata-item');
};
GrChangeMetadataApi.prototype.onLabelsChanged = function(callback) {
if (!this._hook) {
this._createHook();
}
this._hook.onAttached(element =>
this.plugin.attributeHelper(element).bind('labels', callback));
return this;
};
window.GrChangeMetadataApi = GrChangeMetadataApi;
})(window);

View File

@ -20,6 +20,7 @@ limitations under the License.
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../plugins/gr-admin-api/gr-admin-api.html">
<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
<link rel="import" href="../../plugins/gr-change-metadata-api/gr-change-metadata-api.html">
<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
<link rel="import" href="../../plugins/gr-event-helper/gr-event-helper.html">
<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">

View File

@ -223,6 +223,10 @@
return new GrRepoApi(this);
};
Plugin.prototype.changeMetadata = function() {
return new GrChangeMetadataApi(this);
};
Plugin.prototype.admin = function() {
return new GrAdminApi(this);
};