Merge changes Ie54178b7,I6fda4cce,I2f82060e,I9f95d6fc,I1bbdda5a

* changes:
  Add Gerrit.getLoggedIn to JS API
  Add change view reply dialog JS interface
  Add change view actions JS interface
  Add experimental labelchange event in JS API
  Add optional version parameter to JS API
This commit is contained in:
Andrew Bonventre
2016-06-29 21:05:13 +00:00
committed by Gerrit Code Review
15 changed files with 470 additions and 56 deletions

View File

@@ -59,7 +59,7 @@ limitations under the License.
}
</style>
<div>
<section hidden$="[[!_keyCount(actions)]]" hidden>
<section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]">
<div class="groupLabel">Change</div>
<template is="dom-repeat" items="[[_changeActionValues]]" as="action">
<gr-button title$="[[action.title]]"
@@ -72,7 +72,7 @@ limitations under the License.
on-tap="_handleActionTap"></gr-button>
</template>
</section>
<section hidden$="[[!_keyCount(_revisionActions)]]" hidden>
<section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]">
<div class="groupLabel">Revision</div>
<template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
<gr-button title$="[[action.title]]"

View File

@@ -59,7 +59,10 @@
*/
properties: {
actions: Object,
actions: {
type: Object,
value: function() { return {}; },
},
primaryActionKeys: {
type: Array,
value: function() {
@@ -77,7 +80,10 @@
type: Boolean,
value: true,
},
_revisionActions: Object,
_revisionActions: {
type: Object,
value: function() { return {}; },
},
_revisionActionValues: {
type: Array,
computed: '_computeRevisionActionValues(_revisionActions.*, ' +
@@ -103,7 +109,7 @@
],
observers: [
'_actionsChanged(actions, _revisionActions, _additionalActions)',
'_actionsChanged(actions.*, _revisionActions.*, _additionalActions.*)',
],
ready: function() {
@@ -129,7 +135,7 @@
}.bind(this));
},
addActionButton: function(key, type, label) {
addActionButton: function(type, label) {
if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
throw Error('Invalid action type: ' + type);
}
@@ -137,39 +143,59 @@
enabled: true,
label: label,
__type: type,
__key: ADDITIONAL_ACTION_KEY_PREFIX + key + Math.random().toString(36),
__key: ADDITIONAL_ACTION_KEY_PREFIX + Math.random().toString(36),
};
this.push('_additionalActions', action);
return action.__key;
},
removeActionButton: function(key) {
var idx = -1;
for (var i = 0; i < this._additionalActions.length; i++) {
if (this._additionalActions[i].__key === key) {
idx = i;
break;
}
}
var idx = this._indexOfActionButtonWithKey(key);
if (idx === -1) {
console.error('Could not find action button with key:', key);
return;
}
this.splice('_additionalActions', idx, 1);
},
setActionButtonProp: function(key, prop, value) {
this.set([
'_additionalActions',
this._indexOfActionButtonWithKey(key),
prop,
], value);
},
_indexOfActionButtonWithKey: function(key) {
for (var i = 0; i < this._additionalActions.length; i++) {
if (this._additionalActions[i].__key === key) {
return i;
}
}
return -1;
},
_getRevisionActions: function() {
return this.$.restAPI.getChangeRevisionActions(this.changeNum,
this.patchNum);
},
_keyCount: function(obj) {
return Object.keys(obj).length;
_actionCount: function(actionsChangeRecord, additionalActionsChangeRecord) {
var additionalActions = (additionalActionsChangeRecord &&
additionalActionsChangeRecord.base) || [];
return this._keyCount(actionsChangeRecord) + additionalActions.length;
},
_actionsChanged: function(actions, revisionActions, additionalActions) {
this.hidden = this._keyCount(actions) === 0 &&
this._keyCount(revisionActions) === 0 &&
this._keyCount(additionalActions) === 0;
_keyCount: function(changeRecord) {
return Object.keys((changeRecord && changeRecord.base) || {}).length;
},
_actionsChanged: function(actionsChangeRecord, revisionActionsChangeRecord,
additionalActionsChangeRecord) {
var additionalActions = (additionalActionsChangeRecord &&
additionalActionsChangeRecord.base) || [];
this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
this._keyCount(revisionActionsChangeRecord) === 0 &&
additionalActions.length === 0;
},
_getValuesFor: function(obj) {

View File

@@ -203,8 +203,7 @@ limitations under the License.
test('custom actions', function(done) {
// Add a button with the same key as a server-based one to ensure
// collisions are taken care of.
var key = element.addActionButton('submit', element.ActionType.REVISION,
'Bork!');
var key = element.addActionButton(element.ActionType.REVISION, 'Bork!');
element.addEventListener(key + '-tap', function(e) {
assert.equal(e.detail.node.getAttribute('data-action-key'), key);
element.removeActionButton(key);

View File

@@ -93,6 +93,10 @@
Gerrit.RESTClientBehavior,
],
observers: [
'_labelsChanged(_change.labels.*)',
],
ready: function() {
this._headerEl = this.$$('.header');
},
@@ -485,6 +489,13 @@
}
},
_labelsChanged: function(changeRecord) {
if (!changeRecord) { return; }
this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
change: this._change,
});
},
_openReplyDialog: function() {
this.$.replyOverlay.open().then(function() {
this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());

View File

@@ -19,6 +19,7 @@ limitations under the License.
<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-reply-dialog">
@@ -154,6 +155,7 @@ limitations under the License.
on-tap="_cancelTapHandler">Cancel</gr-button>
</section>
</div>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-reply-dialog.js"></script>

View File

@@ -59,6 +59,10 @@
}.bind(this));
},
ready: function() {
this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
},
focus: function() {
this.async(function() {
this.$.textarea.textarea.focus();
@@ -72,6 +76,48 @@
};
},
setLabelValue: function(label, value) {
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
// The selector may not be present if its not at the latest patch set.
if (!selectorEl) { return; }
var item = selectorEl.$$('gr-button[data-value="' + value + '"]');
if (!item) { return; }
selectorEl.selectIndex(selectorEl.indexOf(item));
},
send: function() {
var obj = {
drafts: 'PUBLISH_ALL_REVISIONS',
labels: {},
};
for (var label in this.permittedLabels) {
if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
// The selector may not be present if its not at the latest patch set.
if (!selectorEl) { continue; }
var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
selectedVal = parseInt(selectedVal, 10);
obj.labels[label] = selectedVal;
}
if (this.draft != null) {
obj.message = this.draft;
}
this.disabled = true;
return this._saveReview(obj).then(function(response) {
this.disabled = false;
if (!response.ok) { return response; }
this.draft = '';
this.fire('send', null, {bubbles: false});
}.bind(this)).catch(function(err) {
this.disabled = false;
throw err;
}.bind(this));
},
_computeShowLabels: function(patchNum, revisions) {
var num = parseInt(patchNum, 10);
for (var rev in revisions) {
@@ -147,34 +193,7 @@
_sendTapHandler: function(e) {
e.preventDefault();
var obj = {
drafts: 'PUBLISH_ALL_REVISIONS',
labels: {},
};
for (var label in this.permittedLabels) {
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
// The selector may not be present if its not at the latest patch set.
if (!selectorEl) { continue; }
var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
selectedVal = parseInt(selectedVal, 10);
obj.labels[label] = selectedVal;
}
if (this.draft != null) {
obj.message = this.draft;
}
this.disabled = true;
this._saveReview(obj).then(function(response) {
this.disabled = false;
if (!response.ok) { return response; }
this.draft = '';
this.fire('send', null, {bubbles: false});
}.bind(this)).catch(function(err) {
this.disabled = false;
throw err;
}.bind(this));
this.send();
},
_saveReview: function(review) {

View File

@@ -0,0 +1,62 @@
// Copyright (C) 2016 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 GrChangeActionsInterface(el) {
this._el = el;
this.RevisionActions = el.RevisionActions;
this.ChangeActions = el.ChangeActions;
this.ActionType = el.ActionType;
}
GrChangeActionsInterface.prototype.addPrimaryActionKey = function(key) {
if (this._el.primaryActionKeys.indexOf(key) !== -1) { return; }
this._el.push('primaryActionKeys', key);
};
GrChangeActionsInterface.prototype.removePrimaryActionKey = function(key) {
this._el.primaryActionKeys = this._el.primaryActionKeys.filter(function(k) {
return k !== key;
});
};
GrChangeActionsInterface.prototype.add = function(type, label) {
return this._el.addActionButton(type, label);
};
GrChangeActionsInterface.prototype.remove = function(key) {
return this._el.removeActionButton(key);
};
GrChangeActionsInterface.prototype.addTapListener = function(key, handler) {
this._el.addEventListener(key + '-tap', handler);
};
GrChangeActionsInterface.prototype.removeTapListener = function(key,
handler) {
this._el.removeEventListener(key + '-tap', handler);
};
GrChangeActionsInterface.prototype.setLabel = function(key, text) {
this._el.setActionButtonProp(key, 'label', text);
};
GrChangeActionsInterface.prototype.setEnabled = function(key, enabled) {
this._el.setActionButtonProp(key, 'enabled', enabled);
};
window.GrChangeActionsInterface = GrChangeActionsInterface;
})(window);

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<!--
Copyright (C) 2016 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.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions-js-api</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<!--
This must refer to the element this interface is wrapping around. Otherwise
breaking changes to gr-change-actions wont be noticed.
-->
<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
<test-fixture id="basic">
<template>
<gr-change-actions></gr-change-actions>
</template>
</test-fixture>
<script>
suite('gr-js-api-interface tests', function() {
var element;
var changeActions;
setup(function() {
element = fixture('basic');
var plugin;
Gerrit.install(function(p) { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
changeActions = plugin.changeActions();
});
teardown(function() {
changeActions = null;
});
test('property existence', function() {
[
'ActionType',
'ChangeActions',
'RevisionActions',
].forEach(function(p) {
assertArraysEqual(changeActions[p], element[p]);
});
});
// Because deepEqual doesnt behave in Safari.
function assertArraysEqual(actual, expected) {
assert.equal(actual.length, expected.length);
for (var i = 0; i < actual.length; i++) {
assert.equal(actual[i], expected[i]);
}
}
test('add/remove primary action keys', function() {
element.primaryActionKeys = [];
changeActions.addPrimaryActionKey('foo');
assertArraysEqual(element.primaryActionKeys, ['foo']);
changeActions.addPrimaryActionKey('foo');
assertArraysEqual(element.primaryActionKeys, ['foo']);
changeActions.addPrimaryActionKey('bar');
assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']);
changeActions.removePrimaryActionKey('foo');
assertArraysEqual(element.primaryActionKeys, ['bar']);
changeActions.removePrimaryActionKey('baz');
assertArraysEqual(element.primaryActionKeys, ['bar']);
changeActions.removePrimaryActionKey('bar');
assertArraysEqual(element.primaryActionKeys, []);
});
test('action buttons', function(done) {
var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
var handler = sinon.spy();
changeActions.addTapListener(key, handler);
flush(function() {
MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
assert(handler.calledOnce);
changeActions.removeTapListener(key, handler);
MockInteractions.tap(element.$$('[data-action-key="' + key + '"]'));
assert(handler.calledOnce);
changeActions.remove(key);
flush(function() {
assert.isNull(element.$$('[data-action-key="' + key + '"]'));
done();
});
});
});
test('action button properties', function(done) {
var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
flush(function() {
var button = element.$$('[data-action-key="' + key + '"]');
assert.isOk(button);
assert.equal(button.getAttribute('data-label'), 'Bork!');
assert.isFalse(button.disabled);
changeActions.setLabel(key, 'Yo');
changeActions.setEnabled(key, false);
flush(function() {
assert.equal(button.getAttribute('data-label'), 'Yo');
assert.isTrue(button.disabled);
done();
});
});
});
});
</script>

View File

@@ -0,0 +1,30 @@
// Copyright (C) 2016 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 GrChangeReplyInterface(el) {
this._el = el;
}
GrChangeReplyInterface.prototype.setLabelValue = function(label, value) {
this._el.setLabelValue(label, value);
};
GrChangeReplyInterface.prototype.send = function() {
return this._el.send();
};
window.GrChangeReplyInterface = GrChangeReplyInterface;
})(window);

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<!--
Copyright (C) 2016 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.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-reply-js-api</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<!--
This must refer to the element this interface is wrapping around. Otherwise
breaking changes to gr-reply-dialog wont be noticed.
-->
<link rel="import" href="../../change/gr-reply-dialog/gr-reply-dialog.html">
<test-fixture id="basic">
<template>
<gr-reply-dialog></gr-reply-dialog>
</template>
</test-fixture>
<script>
suite('gr-change-reply-js-api tests', function() {
var element;
var sandbox;
var changeReply;
setup(function() {
stub('gr-rest-api-interface', {
getAccount: function() { return Promise.resolve(null); },
});
element = fixture('basic');
sandbox = sinon.sandbox.create();
var plugin;
Gerrit.install(function(p) { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
changeReply = plugin.changeReply();
});
teardown(function() {
changeReply = null;
sandbox.restore();
});
test('calls', function() {
var setLabelValueStub = sinon.stub(element, 'setLabelValue');
changeReply.setLabelValue('My-Label', '+1337');
assert(setLabelValueStub.calledWithExactly('My-Label', '+1337'));
var sendStub = sinon.stub(element, 'send');
changeReply.send();
assert(sendStub.calledWithExactly());
});
});
</script>

View File

@@ -14,9 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface">
<template></template>
<script src="gr-change-actions-js-api.js"></script>
<script src="gr-change-reply-js-api.js"></script>
<script src="gr-js-api-interface.js"></script>
<script src="gr-public-js-api.js"></script>
</dom-module>

View File

@@ -16,6 +16,7 @@
var EventType = {
HISTORY: 'history',
LABEL_CHANGE: 'labelchange',
SHOW_CHANGE: 'showchange',
SUBMIT_CHANGE: 'submitchange',
COMMENT: 'comment',
@@ -23,6 +24,7 @@
var Element = {
CHANGE_ACTIONS: 'changeactions',
REPLY_DIALOG: 'replydialog',
};
Polymer({
@@ -53,6 +55,9 @@
case EventType.COMMENT:
this._handleComment(detail);
break;
case EventType.LABEL_CHANGE:
this._handleLabelChange(detail);
break;
default:
console.warn('handleEvent called with unsupported event type:', type);
break;
@@ -133,6 +138,16 @@
});
},
_handleLabelChange: function(detail) {
this._getEventCallbacks(EventType.LABEL_CHANGE).forEach(function(cb) {
try {
cb(detail.change);
} catch (err) {
console.error(err);
}
});
},
_getEventCallbacks: function(type) {
return this._eventCallbacks[type] || [];
},

View File

@@ -38,9 +38,14 @@ limitations under the License.
};
setup(function() {
stub('gr-rest-api-interface', {
getAccount: function() {
return Promise.resolve({name: 'Judy Hopps'});
},
})
element = fixture('basic');
errorStub = sinon.stub(console, 'error');
Gerrit.install(function(p) { plugin = p; },
Gerrit.install(function(p) { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
});
@@ -97,6 +102,17 @@ limitations under the License.
element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
});
test('labelchange event', function(done) {
var testChange = {_number: 42};
plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
plugin.on(element.EventType.LABEL_CHANGE, function(change) {
assert.deepEqual(change, testChange);
assert.isTrue(errorStub.calledOnce);
done();
});
element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange});
});
test('submitchange', function() {
plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn);
plugin.on(element.EventType.SUBMIT_CHANGE, function() { return true; });
@@ -108,5 +124,18 @@ limitations under the License.
assert.isTrue(errorStub.calledTwice);
});
test('versioning', function() {
var callback = sinon.spy();
Gerrit.install(callback, '0.0pre-alpha');
assert(callback.notCalled);
});
test('getAccount', function(done) {
Gerrit.getLoggedIn().then(function(loggedIn) {
assert.isTrue(loggedIn);
done();
});
});
});
</script>

View File

@@ -14,11 +14,19 @@
(function(window) {
'use strict';
var API_VERSION = '0.1';
// GWT JSNI uses $wnd to refer to window.
// http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
window.$wnd = window;
function Plugin(opt_url) {
if (!opt_url) {
console.warn('Plugin not being loaded from /plugins base path.',
'Unable to determine name.');
return;
}
this._url = new URL(opt_url);
if (this._url.pathname.indexOf('/plugins') !== 0) {
console.warn('Plugin not being loaded from /plugins base path:',
@@ -44,9 +52,14 @@
return this._url.origin + '/plugins/' + this._name + (opt_path || '/');
};
Plugin.prototype.getChangeActionsElement = function() {
return Plugin._sharedAPIElement.getElement(
Plugin._sharedAPIElement.Element.CHANGE_ACTIONS);
Plugin.prototype.changeActions = function() {
return new GrChangeActionsInterface(Plugin._sharedAPIElement.getElement(
Plugin._sharedAPIElement.Element.CHANGE_ACTIONS));
};
Plugin.prototype.changeReply = function() {
return new GrChangeReplyInterface(Plugin._sharedAPIElement.getElement(
Plugin._sharedAPIElement.Element.REPLY_DIALOG));
};
var Gerrit = window.Gerrit || {};
@@ -68,12 +81,22 @@
return name;
};
Gerrit.install = function(callback, opt_src) {
Gerrit.install = function(callback, opt_version, opt_src) {
if (opt_version && opt_version !== API_VERSION) {
console.warn('Only version ' + API_VERSION +
' is supported in PolyGerrit. ' + opt_version + ' was given.');
return;
}
// TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
var src = opt_src || (document.currentScript && document.currentScript.src);
callback(new Plugin(src));
};
Gerrit.getLoggedIn = function() {
return document.createElement('gr-rest-api-interface').getLoggedIn();
};
Gerrit.installGwt = function() {
// NOOP since PolyGerrit doesnt support GWT plugins.
};

View File

@@ -75,6 +75,8 @@ limitations under the License.
'shared/gr-date-formatter/gr-date-formatter_test.html',
'shared/gr-editable-content/gr-editable-content_test.html',
'shared/gr-editable-label/gr-editable-label_test.html',
'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
'shared/gr-js-api-interface/gr-js-api-interface_test.html',
'shared/gr-linked-text/gr-linked-text_test.html',
'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',