Add restore action to edit controls
This change adds the restore action, dialog, and button to gr-edit-controls. By default, the button itself is hidden -- it is intended to only be functional with a path. As opposed to the dialog taking a file path as user input, it functions as a confirm dialog for the provided file. An <input> element is used in the restore dialog exclusively for visual alignment with the rest of the dialogs. TODO: - Implement per-file actions for use of the restore dialog, where the public `openRestoreDialog` will be called with a file path. - Make the _path property of gr-edit-controls public to allow a default value. - Add gr-edit-controls to the diff-view and editor-view, which both need the restore button shown. Bug: Issue 4437 Change-Id: Ideb2f9df30a3a0393c031ba9ad01264c930ac467
This commit is contained in:
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
|
||||
<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
|
||||
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
|
||||
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
|
||||
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
|
||||
@@ -71,6 +72,7 @@ limitations under the License.
|
||||
<template is="dom-repeat" items="[[_actions]]" as="action">
|
||||
<gr-button
|
||||
id$="[[action.key]]"
|
||||
class$="[[_computeIsInvisible(action.key, hiddenActions)]]"
|
||||
link
|
||||
on-tap="_handleTap">[[action.label]]</gr-button>
|
||||
</template>
|
||||
@@ -125,6 +127,20 @@ limitations under the License.
|
||||
placeholder="Enter the new path."/>
|
||||
</div>
|
||||
</gr-confirm-dialog>
|
||||
<gr-confirm-dialog
|
||||
id="restoreDialog"
|
||||
class="invisible dialog"
|
||||
confirm-label="Restore"
|
||||
on-confirm="_handleRestoreConfirm"
|
||||
on-cancel="_handleDialogCancel">
|
||||
<div class="header">Restore this file?</div>
|
||||
<div class="main">
|
||||
<input
|
||||
is="iron-input"
|
||||
disabled
|
||||
bind-value="{{_path}}"/>
|
||||
</div>
|
||||
</gr-confirm-dialog>
|
||||
</gr-overlay>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
</template>
|
||||
|
||||
@@ -14,20 +14,30 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* TODO(kaspern): move this dictionary to a shareable constants file.
|
||||
*/
|
||||
const Actions = {
|
||||
EDIT: {label: 'Edit', key: 'edit'},
|
||||
DELETE: {label: 'Delete', key: 'delete'},
|
||||
RENAME: {label: 'Rename', key: 'rename'},
|
||||
/* TODO(kaspern): Implement these actions.
|
||||
REVERT: {label: 'Revert', key: 'revert'},
|
||||
CHECKOUT: {label: 'Check out', key: 'checkout'},
|
||||
*/
|
||||
RESTORE: {label: 'Restore', key: 'restore'},
|
||||
};
|
||||
|
||||
Polymer({
|
||||
is: 'gr-edit-controls',
|
||||
properties: {
|
||||
change: Object,
|
||||
/**
|
||||
* TODO(kaspern): by default, the RESTORE action should be hidden in the
|
||||
* file-list as it is a per-file action only. Remove this default value
|
||||
* when the Actions dictionary is moved to a shared constants file and
|
||||
* use the hiddenActions property in the parent component.
|
||||
*/
|
||||
hiddenActions: {
|
||||
type: Array,
|
||||
value() { return [Actions.RESTORE.key]; },
|
||||
},
|
||||
|
||||
_actions: {
|
||||
type: Array,
|
||||
@@ -67,6 +77,9 @@
|
||||
case Actions.RENAME.key:
|
||||
this.openRenameDialog();
|
||||
return;
|
||||
case Actions.RESTORE.key:
|
||||
this.openRestoreDialog();
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -85,6 +98,11 @@
|
||||
return this._showDialog(this.$.renameDialog);
|
||||
},
|
||||
|
||||
openRestoreDialog(opt_path) {
|
||||
if (opt_path) { this._path = opt_path; }
|
||||
return this._showDialog(this.$.restoreDialog);
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a path string, checks that it is a valid file path.
|
||||
* @param {string} path
|
||||
@@ -114,19 +132,22 @@
|
||||
_showDialog(dialog) {
|
||||
return this.$.overlay.open().then(() => {
|
||||
dialog.classList.toggle('invisible', false);
|
||||
dialog.querySelector('gr-autocomplete').focus();
|
||||
const autocomplete = dialog.querySelector('gr-autocomplete');
|
||||
if (autocomplete) { autocomplete.focus(); }
|
||||
this.async(() => { this.$.overlay.center(); }, 1);
|
||||
});
|
||||
},
|
||||
|
||||
_closeDialog(dialog) {
|
||||
// Dialog may have autocompletes and plain inputs -- as these have
|
||||
// different properties representing their bound text, it is easier to
|
||||
// just make two separate queries.
|
||||
dialog.querySelectorAll('gr-autocomplete')
|
||||
.forEach(input => { input.text = ''; });
|
||||
dialog.querySelectorAll('input')
|
||||
.forEach(input => { input.bindValue = ''; });
|
||||
_closeDialog(dialog, clearInputs) {
|
||||
if (clearInputs) {
|
||||
// Dialog may have autocompletes and plain inputs -- as these have
|
||||
// different properties representing their bound text, it is easier to
|
||||
// just make two separate queries.
|
||||
dialog.querySelectorAll('gr-autocomplete')
|
||||
.forEach(input => { input.text = ''; });
|
||||
dialog.querySelectorAll('input')
|
||||
.forEach(input => { input.bindValue = ''; });
|
||||
}
|
||||
|
||||
dialog.classList.toggle('invisible', true);
|
||||
return this.$.overlay.close();
|
||||
@@ -139,14 +160,23 @@
|
||||
_handleEditConfirm(e) {
|
||||
const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path);
|
||||
Gerrit.Nav.navigateToRelativeUrl(url);
|
||||
this._closeDialog(this._getDialogFromEvent(e));
|
||||
this._closeDialog(this._getDialogFromEvent(e), true);
|
||||
},
|
||||
|
||||
_handleDeleteConfirm(e) {
|
||||
this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
|
||||
.then(res => {
|
||||
if (!res.ok) { return; }
|
||||
this._closeDialog(this._getDialogFromEvent(e));
|
||||
this._closeDialog(this._getDialogFromEvent(e), true);
|
||||
Gerrit.Nav.navigateToChange(this.change);
|
||||
});
|
||||
},
|
||||
|
||||
_handleRestoreConfirm(e) {
|
||||
this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path)
|
||||
.then(res => {
|
||||
if (!res.ok) { return; }
|
||||
this._closeDialog(this._getDialogFromEvent(e), true);
|
||||
Gerrit.Nav.navigateToChange(this.change);
|
||||
});
|
||||
},
|
||||
@@ -155,7 +185,7 @@
|
||||
return this.$.restAPI.renameFileInChangeEdit(this.change._number,
|
||||
this._path, this._newPath).then(res => {
|
||||
if (!res.ok) { return; }
|
||||
this._closeDialog(this._getDialogFromEvent(e));
|
||||
this._closeDialog(this._getDialogFromEvent(e), true);
|
||||
Gerrit.Nav.navigateToChange(this.change);
|
||||
});
|
||||
},
|
||||
@@ -166,5 +196,9 @@
|
||||
return {name: file};
|
||||
}));
|
||||
},
|
||||
|
||||
_computeIsInvisible(key, hiddenActions) {
|
||||
return hiddenActions.includes(key) ? 'invisible' : '';
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -100,7 +100,7 @@ suite('gr-edit-controls tests', () => {
|
||||
MockInteractions.tap(element.$.editDialog.$$('gr-button'));
|
||||
for (const stub of navStubs) { assert.isFalse(stub.called); }
|
||||
assert.isTrue(closeDialogSpy.called);
|
||||
assert.equal(element._path, '');
|
||||
assert.equal(element._path, 'src/test.cpp');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -130,6 +130,7 @@ suite('gr-edit-controls tests', () => {
|
||||
assert.isTrue(deleteStub.called);
|
||||
|
||||
return deleteStub.lastCall.returnValue.then(() => {
|
||||
assert.equal(element._path, '');
|
||||
assert.isTrue(navStub.called);
|
||||
assert.isTrue(closeDialogSpy.called);
|
||||
});
|
||||
@@ -168,7 +169,7 @@ suite('gr-edit-controls tests', () => {
|
||||
MockInteractions.tap(element.$.deleteDialog.$$('gr-button'));
|
||||
assert.isFalse(navStub.called);
|
||||
assert.isTrue(closeDialogSpy.called);
|
||||
assert.equal(element._path, '');
|
||||
assert.equal(element._path, 'src/test.cpp');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -203,6 +204,7 @@ suite('gr-edit-controls tests', () => {
|
||||
assert.isTrue(renameStub.called);
|
||||
|
||||
return renameStub.lastCall.returnValue.then(() => {
|
||||
assert.equal(element._path, '');
|
||||
assert.isTrue(navStub.called);
|
||||
assert.isTrue(closeDialogSpy.called);
|
||||
});
|
||||
@@ -248,8 +250,68 @@ suite('gr-edit-controls tests', () => {
|
||||
MockInteractions.tap(element.$.renameDialog.$$('gr-button'));
|
||||
assert.isFalse(navStub.called);
|
||||
assert.isTrue(closeDialogSpy.called);
|
||||
assert.equal(element._path, '');
|
||||
assert.equal(element._newPath, '');
|
||||
assert.equal(element._path, 'src/test.cpp');
|
||||
assert.equal(element._newPath, 'src/test.newPath');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('restore button CUJ', () => {
|
||||
let navStub;
|
||||
let restoreStub;
|
||||
|
||||
setup(() => {
|
||||
navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
|
||||
restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit');
|
||||
});
|
||||
|
||||
test('restore hidden by default', () => {
|
||||
assert.isTrue(element.$$('#restore').classList.contains('invisible'));
|
||||
});
|
||||
|
||||
test('restore', () => {
|
||||
restoreStub.returns(Promise.resolve({ok: true}));
|
||||
element._path = 'src/test.cpp';
|
||||
MockInteractions.tap(element.$$('#restore'));
|
||||
return showDialogSpy.lastCall.returnValue.then(() => {
|
||||
MockInteractions.tap(element.$.restoreDialog.$$('gr-button[primary]'));
|
||||
flushAsynchronousOperations();
|
||||
|
||||
assert.isTrue(restoreStub.called);
|
||||
assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
|
||||
return restoreStub.lastCall.returnValue.then(() => {
|
||||
assert.equal(element._path, '');
|
||||
assert.isTrue(navStub.called);
|
||||
assert.isTrue(closeDialogSpy.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('restore fails', () => {
|
||||
restoreStub.returns(Promise.resolve({ok: false}));
|
||||
element._path = 'src/test.cpp';
|
||||
MockInteractions.tap(element.$$('#restore'));
|
||||
return showDialogSpy.lastCall.returnValue.then(() => {
|
||||
MockInteractions.tap(element.$.restoreDialog.$$('gr-button[primary]'));
|
||||
flushAsynchronousOperations();
|
||||
|
||||
assert.isTrue(restoreStub.called);
|
||||
assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp');
|
||||
return restoreStub.lastCall.returnValue.then(() => {
|
||||
assert.isFalse(navStub.called);
|
||||
assert.isFalse(closeDialogSpy.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('cancel', () => {
|
||||
element._path = 'src/test.cpp';
|
||||
MockInteractions.tap(element.$$('#restore'));
|
||||
return showDialogSpy.lastCall.returnValue.then(() => {
|
||||
MockInteractions.tap(element.$.restoreDialog.$$('gr-button'));
|
||||
assert.isFalse(navStub.called);
|
||||
assert.isTrue(closeDialogSpy.called);
|
||||
assert.equal(element._path, 'src/test.cpp');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -284,4 +346,4 @@ suite('gr-edit-controls tests', () => {
|
||||
assert.notOk(spy.lastCall.returnValue);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user