Merge "Switch to using fetch for gr-change-actions"

This commit is contained in:
Andrew Bonventre
2016-04-05 14:08:58 +00:00
committed by Gerrit Code Review
5 changed files with 263 additions and 190 deletions

View File

@@ -18,11 +18,10 @@ limitations under the License.
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-ajax/gr-ajax.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-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-request/gr-request.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
<link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
@@ -33,6 +32,14 @@ limitations under the License.
:host {
display: block;
}
section {
margin-top: 1em;
}
.groupLabel {
color: #666;
margin-bottom: .15em;
text-align: center;
}
gr-button {
display: block;
margin-bottom: .5em;
@@ -49,30 +56,33 @@ limitations under the License.
}
}
</style>
<gr-ajax id="actionsXHR"
url="[[_computeRevisionActionsPath(changeNum, patchNum)]]"
last-response="{{_revisionActions}}"
loading="{{_loading}}"></gr-ajax>
<div>
<template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
hidden$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
on-tap="_handleActionTap"></gr-button>
</template>
<template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
disabled$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
on-tap="_handleActionTap"></gr-button>
</template>
<section hidden$="[[!_keyCount(actions)]]" hidden>
<div class="groupLabel">Change</div>
<template is="dom-repeat" items="[[_computeActionValues(actions, 'change')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
hidden$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
on-tap="_handleActionTap"></gr-button>
</template>
</section>
<section hidden$="[[!_keyCount(_revisionActions)]]" hidden>
<div class="groupLabel">Revision</div>
<template is="dom-repeat" items="[[_computeActionValues(_revisionActions, 'revision')]]" as="action">
<gr-button title$="[[action.title]]"
primary$="[[_computePrimary(action.__key)]]"
disabled$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
on-tap="_handleActionTap"></gr-button>
</template>
</section>
</div>
<gr-overlay id="overlay" with-backdrop>
<gr-confirm-rebase-dialog id="confirmRebase"
@@ -88,6 +98,7 @@ limitations under the License.
hidden></gr-confirm-cherrypick-dialog>
</gr-overlay>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-change-actions.js"></script>
</dom-module>

View File

@@ -30,6 +30,21 @@
SUBMIT: 'submit',
};
var ActionLoadingLabels = {
'abandon': 'Abandoning...',
'cherrypick': 'Cherry-Picking...',
'delete': 'Deleting...',
'publish': 'Publishing...',
'rebase': 'Rebasing...',
'restore': 'Restoring...',
'submit': 'Submitting...',
};
var ActionType = {
CHANGE: 'change',
REVISION: 'revision',
};
Polymer({
is: 'gr-change-actions',
@@ -65,15 +80,31 @@
if (!this.changeNum || !this.patchNum) {
return Promise.resolve();
}
return this.$.actionsXHR.generateRequest().completes;
this._loading = true;
return this._getRevisionActions().then(function(revisionActions) {
this._revisionActions = revisionActions;
this._loading = false;
}.bind(this)).catch(function(err) {
alert('Couldnt load revision actions. Check the console ' +
'and contact the PolyGerrit team for assistance.');
this._loading = false;
throw err;
});
},
_getRevisionActions: function() {
return this.$.restAPI.getChangeRevisionActions(this.changeNum,
this.patchNum);
},
_keyCount: function(obj) {
return Object.keys(obj).length;
},
_actionsChanged: function(actions, revisionActions) {
this.hidden = actions.length == 0 && revisionActions.length == 0;
},
_computeRevisionActionsPath: function(changeNum, patchNum) {
return this.changeBaseURL(changeNum, patchNum) + '/actions';
this.hidden = this._keyCount(actions) === 0 &&
this._keyCount(revisionActions) === 0;
},
_getValuesFor: function(obj) {
@@ -85,9 +116,9 @@
_computeActionValues: function(actions, type) {
var result = [];
var values = this._getValuesFor(
type == 'change' ? ChangeActions : RevisionActions);
type === ActionType.CHANGE ? ChangeActions : RevisionActions);
for (var a in actions) {
if (values.indexOf(a) == -1) { continue; }
if (values.indexOf(a) === -1) { continue; }
actions[a].__key = a;
actions[a].__type = type;
result.push(actions[a]);
@@ -96,23 +127,12 @@
},
_computeLoadingLabel: function(action) {
return {
'cherrypick': 'Cherry-Picking...',
'rebase': 'Rebasing...',
'submit': 'Submitting...',
}[action];
return ActionLoadingLabels[action] || 'Working...';
},
_computePrimary: function(actionKey) {
return actionKey == 'submit';
},
_computeButtonClass: function(action) {
if ([RevisionActions.SUBMIT,
RevisionActions.PUBLISH].indexOf(action) != -1) {
return 'primary';
}
return '';
return actionKey === RevisionActions.SUBMIT ||
actionKey === RevisionActions.PUBLISH;
},
_canSubmitChange: function() {
@@ -123,28 +143,28 @@
e.preventDefault();
var el = Polymer.dom(e).rootTarget;
var key = el.getAttribute('data-action-key');
if (key == RevisionActions.SUBMIT &&
if (key === RevisionActions.SUBMIT &&
this._canSubmitChange() === false) {
return;
}
var type = el.getAttribute('data-action-type');
if (type == 'revision') {
if (key == RevisionActions.REBASE) {
if (type === ActionType.REVISION) {
if (key === RevisionActions.REBASE) {
this._showActionDialog(this.$.confirmRebase);
return;
} else if (key == RevisionActions.CHERRYPICK) {
} else if (key === RevisionActions.CHERRYPICK) {
this._showActionDialog(this.$.confirmCherrypick);
return;
}
this._fireRevisionAction(this._prependSlash(key),
this._revisionActions[key]);
this._fireAction(this._prependSlash(key),
this._revisionActions[key], true);
} else {
this._fireChangeAction(this._prependSlash(key), this.actions[key]);
this._fireAction(this._prependSlash(key), this.actions[key], false);
}
},
_prependSlash: function(key) {
return key == '/' ? key : '/' + key;
return key === '/' ? key : '/' + key;
},
_handleConfirmDialogCancel: function() {
@@ -172,8 +192,7 @@
}
this.$.overlay.close();
el.hidden = false;
this._fireRevisionAction('/rebase', this._revisionActions.rebase,
payload);
this._fireAction('/rebase', this._revisionActions.rebase, true, payload);
},
_handleCherrypickConfirm: function() {
@@ -189,8 +208,10 @@
}
this.$.overlay.close();
el.hidden = false;
this._fireRevisionAction('/cherrypick',
this._fireAction(
'/cherrypick',
this._revisionActions.cherrypick,
true,
{
destination: el.branch,
message: el.message,
@@ -198,46 +219,21 @@
);
},
_fireChangeAction: function(endpoint, action) {
this._send(action.method, {}, endpoint).then(
function() {
// We cant reload a change that was deleted.
if (endpoint == ChangeActions.DELETE) {
page.show('/');
} else {
this.fire('reload-change', null, {bubbles: false});
}
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
},
_fireRevisionAction: function(endpoint, action, opt_payload) {
var buttonEl = this.$$('[data-action-key="' + action.__key + '"]');
_setLoadingOnButtonWithKey: function(key) {
var buttonEl = this.$$('[data-action-key="' + key + '"]');
buttonEl.setAttribute('loading', true);
buttonEl.disabled = true;
function enableButton() {
return function() {
buttonEl.removeAttribute('loading');
buttonEl.disabled = false;
}
},
this._send(action.method, opt_payload, endpoint, true).then(
function(req) {
if (action.__key == RevisionActions.CHERRYPICK) {
page.show(this.changePath(req.response._number));
} else {
this.fire('reload-change', null, {bubbles: false});
}
enableButton();
}.bind(this)).catch(function(err) {
// TODO(andybons): Handle merge conflict (409 status);
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
enableButton();
throw err;
});
_fireAction: function(endpoint, action, revAction, opt_payload) {
var cleanupFn = this._setLoadingOnButtonWithKey(action.__key);
this._send(action.method, opt_payload, endpoint, revAction, cleanupFn)
.then(this._handleResponse.bind(this, action));
},
_showActionDialog: function(dialog) {
@@ -245,16 +241,42 @@
this.$.overlay.open();
},
_send: function(method, payload, actionEndpoint, revisionAction) {
var xhr = document.createElement('gr-request');
this._xhrPromise = xhr.send({
method: method,
url: this.changeBaseURL(this.changeNum,
revisionAction ? this.patchNum : null) + actionEndpoint,
body: payload,
});
_handleResponse: function(action, response) {
return this.$.restAPI.getResponseObject(response).then(function(obj) {
switch (action.__key) {
case RevisionActions.CHERRYPICK:
page.show(this.changePath(obj._number));
break;
case RevisionActions.DELETE:
page.show(this.changePath(this.changeNum));
break;
case ChangeActions.DELETE:
page.show('/');
break;
default:
this.fire('reload-change', null, {bubbles: false});
break;
}
}.bind(this));
},
return this._xhrPromise;
_handleResponseError: function(response) {
if (response.ok) { return response; }
return response.text().then(function(errText) {
alert('Could not perform action: ' + errText);
throw Error(errText);
});
},
_send: function(method, payload, actionEndpoint, revisionAction,
cleanupFn) {
var url = this.$.restAPI.getChangeActionURL(this.changeNum,
revisionAction ? this.patchNum : null, actionEndpoint);
return this.$.restAPI.send(method, url, payload).then(function(response) {
cleanupFn.call(this);
return response;
}.bind(this)).then(this._handleResponseError.bind(this));
},
});
})();

View File

@@ -34,20 +34,13 @@ limitations under the License.
<script>
suite('gr-change-actions tests', function() {
var element;
var server;
var response = {
ok: true,
};
setup(function(done) {
element = fixture('basic');
server = sinon.fakeServer.create();
server.respondWith(
'GET',
'/changes/42/revisions/2/actions',
[
200,
{'Content-Type': 'application/json'},
')]}\'\n' +
JSON.stringify({
stub('gr-rest-api-interface', {
getChangeRevisionActions: function() {
return Promise.resolve({
cherrypick: {
method: 'POST',
label: 'Cherry Pick',
@@ -65,41 +58,31 @@ limitations under the License.
title: 'Submit patch set 1 into master',
enabled: true
}
}),
]
);
});
},
send: function(method, url, payload) {
if (method !== 'POST') { return Promise.reject('bad method'); }
server.respondWith(
'POST',
'/changes/42/revisions/2/submit',
[
200,
{'Content-Type': 'application/json'},
')]}\'\n{}', // The response is not used by the element.
]
);
if (url === '/changes/42/revisions/2/submit') {
return Promise.resolve({
ok: true,
text: function() { return Promise.resolve(')]}\'\n{}'); },
});
} else if (url === '/changes/42/revisions/2/rebase') {
return Promise.resolve({
ok: true,
text: function() { return Promise.resolve(')]}\'\n{}'); },
});
}
server.respondWith(
'POST',
'/changes/42/revisions/2/rebase',
[
200,
{'Content-Type': 'application/json'},
')]}\'\n{}', // The response is not used by the element.
]
);
element.changeNum = '42';
element.patchNum = '2';
element.reload().then(function() {
done();
return Promise.reject('bad url');
},
});
server.respond();
});
teardown(function() {
server.restore();
element = fixture('basic');
element.changeNum = '42';
element.patchNum = '2';
element.reload().then(function() { done(); });
});
test('submit, rebase, and cherry-pick buttons show', function(done) {
@@ -116,7 +99,6 @@ limitations under the License.
var submitButton = element.$$('gr-button[data-action-key="submit"]');
assert.ok(submitButton);
MockInteractions.tap(submitButton);
server.respond();
// Upon success it should fire the reload-change event.
element.addEventListener('reload-change', function(e) {
@@ -128,50 +110,91 @@ limitations under the License.
test('submit change with plugin hook', function(done) {
var canSubmitStub = sinon.stub(element, '_canSubmitChange',
function() { return false; });
var fireRevisionActionStub = sinon.stub(element, '_fireRevisionAction');
var fireActionStub = sinon.stub(element, '_fireAction');
flush(function() {
var submitButton = element.$$('gr-button[data-action-key="submit"]');
assert.ok(submitButton);
MockInteractions.tap(submitButton);
assert.equal(fireRevisionActionStub.callCount, 0);
assert.equal(fireActionStub.callCount, 0);
canSubmitStub.restore();
fireRevisionActionStub.restore();
fireActionStub.restore();
done();
});
});
test('rebase change', function(done) {
var fireActionStub = sinon.stub(element, '_fireAction');
flush(function() {
var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
MockInteractions.tap(rebaseButton);
var rebaseAction = {
__key: 'rebase',
__type: 'revision',
label: 'Rebase',
method: 'POST',
title: 'Rebase onto tip of branch or parent change',
};
element.$.confirmRebase.base = '1234';
element._handleRebaseConfirm();
server.respond();
var lastRequest = server.requests[server.requests.length - 1];
assert.equal(lastRequest.requestBody, '{"base":"1234"}');
assert.deepEqual(fireActionStub.lastCall.args,
['/rebase', rebaseAction, true, {base: '1234'}]);
element.$.confirmRebase.base = '';
element._handleRebaseConfirm();
server.respond();
lastRequest = server.requests[server.requests.length - 1];
assert.equal(lastRequest.requestBody, '{}');
assert.deepEqual(fireActionStub.lastCall.args,
['/rebase', rebaseAction, true, {}]);
element.$.confirmRebase.base = 'does not matter';
element.$.confirmRebase.clearParent = true;
element._handleRebaseConfirm();
server.respond();
lastRequest = server.requests[server.requests.length - 1];
assert.equal(lastRequest.requestBody, '{"base":""}');
assert.deepEqual(fireActionStub.lastCall.args,
['/rebase', rebaseAction, true, {base: ''}]);
// Upon each request success it should fire the reload-change event.
var numEvents = 0;
element.addEventListener('reload-change', function(e) {
if (++numEvents == 3) { done(); }
});
fireActionStub.restore();
done();
});
});
test('cherry-pick change', function(done) {
var fireActionStub = sinon.stub(element, '_fireAction');
var alertStub = sinon.stub(window, 'alert');
flush(function() {
var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
MockInteractions.tap(rebaseButton);
var action = {
__key: 'cherrypick',
__type: 'revision',
enabled: true,
label: 'Cherry Pick',
method: 'POST',
title: 'Cherry pick change to a different branch',
};
element._handleCherrypickConfirm();
assert.equal(fireActionStub.callCount, 0);
element.$.confirmCherrypick.branch = 'master';
element._handleCherrypickConfirm();
assert.equal(fireActionStub.callCount, 0); // Still needs a message.
element.$.confirmCherrypick.message = 'foo message';
element._handleCherrypickConfirm();
assert.deepEqual(fireActionStub.lastCall.args, [
'/cherrypick', action, true, {
destination: 'master',
message: 'foo message',
}
]);
fireActionStub.restore();
alertStub.restore();
done();
});
});
});
</script>

View File

@@ -215,15 +215,17 @@
e.stopPropagation();
var el = Polymer.dom(e).rootTarget;
el.disabled = true;
this._saveDiffPreferences().then(function() {
this.$.prefsOverlay.close();
this._saveDiffPreferences().then(function(response) {
el.disabled = false;
}.bind(this)).catch(function(err) {
el.disabled = false;
alert('Oops. Something went wrong. Check the console and bug the ' +
if (!response.ok) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
return response.text().then(function(text) {
console.error(text);
});
}
this.$.prefsOverlay.close();
}.bind(this));
},
_saveDiffPreferences: function() {

View File

@@ -110,16 +110,8 @@
return;
}
return response.text().then(function(text) {
var result;
try {
result = JSON.parse(text.substring(JSON_PREFIX.length));
} catch (_) {
result = null;
}
return result;
});
}).catch(function(err) {
return this.getResponseObject(response);
}.bind(this)).catch(function(err) {
if (opt_opts.noCredentials) {
throw err;
} else {
@@ -130,6 +122,18 @@
}.bind(this));
},
getResponseObject: function(response) {
return response.text().then(function(text) {
var result;
try {
result = JSON.parse(text.substring(JSON_PREFIX.length));
} catch (_) {
result = null;
}
return result;
});
},
getConfig: function() {
return this._fetchSharedCacheURL('/config/server/info');
},
@@ -148,7 +152,7 @@
},
saveDiffPreferences: function(prefs, opt_errFn, opt_ctx) {
return this._save('PUT', '/accounts/self/preferences.diff', prefs,
return this.send('PUT', '/accounts/self/preferences.diff', prefs,
opt_errFn, opt_ctx);
},
@@ -188,6 +192,10 @@
return this._sharedFetchPromises[url];
},
getChangeActionURL: function(changeNum, opt_patchNum, endpoint) {
return this._changeBaseURL(changeNum, opt_patchNum) + endpoint;
},
getChangeDetail: function(changeNum, opt_cancelCondition) {
var options = this._listChangesOptionsToHex(
ListChangesOption.ALL_REVISIONS,
@@ -195,36 +203,41 @@
ListChangesOption.DOWNLOAD_COMMANDS
);
return this.fetchJSON(
this._changeBaseURL(changeNum) + '/detail',
this.getChangeActionURL(changeNum, null, '/detail'),
opt_cancelCondition,
{O: options});
},
getChangeCommitInfo: function(changeNum, patchNum) {
return this.fetchJSON(
this._changeBaseURL(changeNum, patchNum) + '/commit?links');
this.getChangeActionURL(changeNum, patchNum, '/commit?links'));
},
getChangeFiles: function(changeNum, patchNum) {
return this.fetchJSON(
this._changeBaseURL(changeNum, patchNum) + '/files');
this.getChangeActionURL(changeNum, patchNum, '/files'));
},
getChangeRevisionActions: function(changeNum, patchNum) {
return this.fetchJSON(
this.getChangeActionURL(changeNum, patchNum, '/actions'));
},
getReviewedFiles: function(changeNum, patchNum) {
return this.fetchJSON(
this._changeBaseURL(changeNum, patchNum) + '/files?reviewed');
this.getChangeActionURL(changeNum, patchNum, '/files?reviewed'));
},
saveFileReviewed: function(changeNum, patchNum, path, reviewed, opt_errFn,
opt_ctx) {
var method = reviewed ? 'PUT' : 'DELETE';
var url = this._changeBaseURL(changeNum, patchNum) + '/files/' +
encodeURIComponent(path) + '/reviewed';
var url = this.getChangeActionURL(changeNum, patchNum,
'/files/' + encodeURIComponent(path) + '/reviewed');
return this._save(method, url, null, opt_errFn, opt_ctx);
return this.send(method, url, null, opt_errFn, opt_ctx);
},
_save: function(method, url, opt_body, opt_errFn, opt_ctx) {
send: function(method, url, opt_body, opt_errFn, opt_ctx) {
var headers = new Headers({
'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'),
});
@@ -332,6 +345,8 @@
return v;
},
// Derived from
// gerrit-extension-api/src/main/j/c/g/gerrit/extensions/client/ListChangesOption.java
_listChangesOptionsToHex: function() {
var v = 0;
for (var i = 0; i < arguments.length; i++) {