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:
parent
e137acc455
commit
b6db19e072
|
@ -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.
|
|
@ -341,6 +341,15 @@ screen.
|
||||||
|
|
||||||
Deprecated. Use link:#plugin-settings[`plugin.settings()`] instead.
|
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
|
=== theme
|
||||||
`plugin.theme()`
|
`plugin.theme()`
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,11 @@ link:rest-api-changes.html#change-info[ChangeInfo]
|
||||||
current revision displayed, an instance of
|
current revision displayed, an instance of
|
||||||
link:rest-api-changes.html#revision-info[RevisionInfo]
|
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
|
=== robot-comment-controls
|
||||||
The `robot-comment-controls` extension point is located inside each comment
|
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
|
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.
|
This endpoint decorator wraps the voting buttons in the reply dialog.
|
||||||
|
|
||||||
=== header-title
|
=== header-title
|
||||||
This endpoint wraps the title-text in the application header.
|
This endpoint wraps the title-text in the application header.
|
||||||
|
|
|
@ -29,7 +29,7 @@ limitations under the License.
|
||||||
|
|
||||||
<test-fixture id="element">
|
<test-fixture id="element">
|
||||||
<template>
|
<template>
|
||||||
<gr-change-metadata></gr-change-metadata>
|
<gr-change-metadata mutable="true"></gr-change-metadata>
|
||||||
</template>
|
</template>
|
||||||
</test-fixture>
|
</test-fixture>
|
||||||
|
|
||||||
|
@ -51,20 +51,42 @@ limitations under the License.
|
||||||
'section.topic',
|
'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) {
|
const getStyle = function(selector, name) {
|
||||||
return window.getComputedStyle(
|
return window.getComputedStyle(
|
||||||
Polymer.dom(element.root).querySelector(selector))[name];
|
Polymer.dom(element.root).querySelector(selector))[name];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function createElement() {
|
||||||
|
const element = fixture('element');
|
||||||
|
element.change = {labels, status: 'NEW'};
|
||||||
|
element.revision = {};
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
setup(() => {
|
setup(() => {
|
||||||
sandbox = sinon.sandbox.create();
|
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', {
|
stub('gr-change-metadata', {
|
||||||
_computeShowLabelStatus() { return true; },
|
_computeShowLabelStatus() { return true; },
|
||||||
_computeShowReviewersByState() { return true; },
|
_computeShowReviewersByState() { return true; },
|
||||||
ready() {
|
|
||||||
this.change = {labels: [], status: 'NEW'};
|
|
||||||
this.serverConfig = {};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -76,7 +98,7 @@ limitations under the License.
|
||||||
|
|
||||||
suite('by default', () => {
|
suite('by default', () => {
|
||||||
setup(done => {
|
setup(done => {
|
||||||
element = fixture('element');
|
element = createElement();
|
||||||
flush(done);
|
flush(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,7 +121,7 @@ limitations under the License.
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
element = fixture('element');
|
element = createElement();
|
||||||
const importSpy = sandbox.spy(element.$.externalStyle, '_import');
|
const importSpy = sandbox.spy(element.$.externalStyle, '_import');
|
||||||
Gerrit.awaitPluginsLoaded().then(() => {
|
Gerrit.awaitPluginsLoaded().then(() => {
|
||||||
Promise.all(importSpy.returnValues).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>
|
</script>
|
||||||
|
|
|
@ -316,12 +316,12 @@ limitations under the License.
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<template is="dom-repeat"
|
<template is="dom-repeat"
|
||||||
items="[[_computeLabelNames(change.labels)]]" as="labelName">
|
items="[[_computeLabelNames(labels)]]" as="labelName">
|
||||||
<section>
|
<section>
|
||||||
<span class="title">[[labelName]]</span>
|
<span class="title">[[labelName]]</span>
|
||||||
<span class="value">
|
<span class="value">
|
||||||
<template is="dom-repeat"
|
<template is="dom-repeat"
|
||||||
items="[[_computeLabelValues(labelName, change.labels.*)]]"
|
items="[[_computeLabelValues(labelName, labels.*)]]"
|
||||||
as="label">
|
as="label">
|
||||||
<div class="labelValueContainer">
|
<div class="labelValueContainer">
|
||||||
<span>
|
<span>
|
||||||
|
@ -379,6 +379,7 @@ limitations under the License.
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
<gr-endpoint-decorator name="change-metadata-item">
|
<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="change" value="[[change]]"></gr-endpoint-param>
|
||||||
<gr-endpoint-param name="revision" value="[[revision]]"></gr-endpoint-param>
|
<gr-endpoint-param name="revision" value="[[revision]]"></gr-endpoint-param>
|
||||||
</gr-endpoint-decorator>
|
</gr-endpoint-decorator>
|
||||||
|
|
|
@ -42,6 +42,10 @@
|
||||||
properties: {
|
properties: {
|
||||||
/** @type {?} */
|
/** @type {?} */
|
||||||
change: Object,
|
change: Object,
|
||||||
|
labels: {
|
||||||
|
type: Object,
|
||||||
|
notify: true,
|
||||||
|
},
|
||||||
/** @type {?} */
|
/** @type {?} */
|
||||||
revision: Object,
|
revision: Object,
|
||||||
commitInfo: Object,
|
commitInfo: Object,
|
||||||
|
@ -98,9 +102,14 @@
|
||||||
|
|
||||||
observers: [
|
observers: [
|
||||||
'_changeChanged(change)',
|
'_changeChanged(change)',
|
||||||
|
'_labelsChanged(change.labels)',
|
||||||
'_assigneeChanged(_assignee.*)',
|
'_assigneeChanged(_assignee.*)',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
_labelsChanged(labels) {
|
||||||
|
this.labels = Object.assign({}, labels) || null;
|
||||||
|
},
|
||||||
|
|
||||||
_changeChanged(change) {
|
_changeChanged(change) {
|
||||||
this._assignee = change.assignee ? [change.assignee] : [];
|
this._assignee = change.assignee ? [change.assignee] : [];
|
||||||
},
|
},
|
||||||
|
@ -243,17 +252,22 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
_computeTopicReadOnly(mutable, change) {
|
_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) {
|
_computeHashtagReadOnly(mutable, change) {
|
||||||
return !mutable ||
|
return !mutable ||
|
||||||
|
!change.actions ||
|
||||||
!change.actions.hashtags ||
|
!change.actions.hashtags ||
|
||||||
!change.actions.hashtags.enabled;
|
!change.actions.hashtags.enabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
_computeAssigneeReadOnly(mutable, change) {
|
_computeAssigneeReadOnly(mutable, change) {
|
||||||
return !mutable ||
|
return !mutable ||
|
||||||
|
!change.actions ||
|
||||||
!change.actions.assignee ||
|
!change.actions.assignee ||
|
||||||
!change.actions.assignee.enabled;
|
!change.actions.assignee.enabled;
|
||||||
},
|
},
|
||||||
|
@ -283,7 +297,9 @@
|
||||||
* change-metadata section is modifiable by the current user.
|
* change-metadata section is modifiable by the current user.
|
||||||
*/
|
*/
|
||||||
_computeCanDeleteVote(reviewer, mutable) {
|
_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++) {
|
for (let i = 0; i < this.change.removable_reviewers.length; i++) {
|
||||||
if (this.change.removable_reviewers[i]._account_id ===
|
if (this.change.removable_reviewers[i]._account_id ===
|
||||||
reviewer._account_id) {
|
reviewer._account_id) {
|
||||||
|
@ -313,6 +329,7 @@
|
||||||
if (!response.ok) { return response; }
|
if (!response.ok) { return response; }
|
||||||
const label = this.change.labels[labelName];
|
const label = this.change.labels[labelName];
|
||||||
const labels = label.all || [];
|
const labels = label.all || [];
|
||||||
|
let wasChanged = false;
|
||||||
for (let i = 0; i < labels.length; i++) {
|
for (let i = 0; i < labels.length; i++) {
|
||||||
if (labels[i]._account_id === accountID) {
|
if (labels[i]._account_id === accountID) {
|
||||||
for (const key in label) {
|
for (const key in label) {
|
||||||
|
@ -320,13 +337,18 @@
|
||||||
label[key]._account_id === accountID) {
|
label[key]._account_id === accountID) {
|
||||||
// Remove special label field, keeping change label values
|
// Remove special label field, keeping change label values
|
||||||
// in sync with the backend.
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (wasChanged) {
|
||||||
|
this.notifyPath('change.labels');
|
||||||
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
target.disabled = false;
|
target.disabled = false;
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -523,36 +523,26 @@ limitations under the License.
|
||||||
assert.isFalse(button.hasAttribute('hidden'));
|
assert.isFalse(button.hasAttribute('hidden'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('deletes votes', done => {
|
test('deletes votes', () => {
|
||||||
const deleteStub = sandbox.stub(element.$.restAPI, 'deleteVote')
|
const deleteResponse = Promise.resolve({ok: true});
|
||||||
.returns(Promise.resolve({ok: true}));
|
const deleteStub = sandbox.stub(
|
||||||
|
element.$.restAPI, 'deleteVote').returns(deleteResponse);
|
||||||
|
|
||||||
element.change.removable_reviewers = [
|
element.change.removable_reviewers = [{
|
||||||
{
|
_account_id: 1,
|
||||||
_account_id: 1,
|
name: 'bojack',
|
||||||
name: 'bojack',
|
}];
|
||||||
},
|
|
||||||
];
|
|
||||||
element.change.labels.test.recommended = {_account_id: 1};
|
element.change.labels.test.recommended = {_account_id: 1};
|
||||||
element.mutable = true;
|
element.mutable = true;
|
||||||
flushAsynchronousOperations();
|
|
||||||
const chip = element.$$('gr-account-chip');
|
const chip = element.$$('gr-account-chip');
|
||||||
const button = chip.$$('gr-button');
|
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);
|
MockInteractions.tap(button);
|
||||||
assert.isTrue(chip.disabled);
|
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', () => {
|
test('changing topic', () => {
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||||
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
|
<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-admin-api/gr-admin-api.html">
|
||||||
<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.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-dom-hooks/gr-dom-hooks.html">
|
||||||
<link rel="import" href="../../plugins/gr-event-helper/gr-event-helper.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">
|
<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
|
||||||
|
|
|
@ -223,6 +223,10 @@
|
||||||
return new GrRepoApi(this);
|
return new GrRepoApi(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Plugin.prototype.changeMetadata = function() {
|
||||||
|
return new GrChangeMetadataApi(this);
|
||||||
|
};
|
||||||
|
|
||||||
Plugin.prototype.admin = function() {
|
Plugin.prototype.admin = function() {
|
||||||
return new GrAdminApi(this);
|
return new GrAdminApi(this);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue