Merge "Introduce gr-editor-view"
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
<!--
|
||||
Copyright (C) 2017 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">
|
||||
|
||||
<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="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
|
||||
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
|
||||
<link rel="import" href="../../shared/gr-button/gr-button.html">
|
||||
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
|
||||
<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
|
||||
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
|
||||
<link rel="import" href="../../../styles/shared-styles.html">
|
||||
|
||||
|
||||
<dom-module id="gr-editor-view">
|
||||
<template>
|
||||
<style include="shared-styles">
|
||||
:host {
|
||||
background-color: var(--view-background-color);
|
||||
}
|
||||
gr-fixed-panel {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px #eee solid;
|
||||
z-index: 1;
|
||||
}
|
||||
header,
|
||||
.subHeader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .75em var(--default-horizontal-margin);
|
||||
}
|
||||
header gr-editable-label {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.textareaWrapper {
|
||||
margin: var(--default-horizontal-margin);
|
||||
}
|
||||
.textareaWrapper textarea {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--monospace-font-family);
|
||||
min-height: 60vh;
|
||||
resize: none;
|
||||
white-space: pre;
|
||||
width: 100%;
|
||||
}
|
||||
.textareaWrapper textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
.textareaWrapper .editButtons {
|
||||
display: none;
|
||||
}
|
||||
.rightControls {
|
||||
justify-content: flex-end
|
||||
}
|
||||
</style>
|
||||
<gr-fixed-panel
|
||||
class$="[[_computeContainerClass(_editLoaded)]]"
|
||||
floating-disabled="[[_panelFloatingDisabled]]"
|
||||
keep-on-scroll
|
||||
ready-for-measure="[[!_loading]]">
|
||||
<header>
|
||||
<gr-editable-label
|
||||
label-text="File path"
|
||||
value="[[_path]]"
|
||||
placeholder="File path..."
|
||||
on-changed="_handlePathChanged"></gr-editable-label>
|
||||
<span class="rightControls">
|
||||
<gr-button
|
||||
id="save"
|
||||
disabled$="[[_saveDisabled]]"
|
||||
primary
|
||||
on-tap="_saveEdit">Save</gr-button>
|
||||
<gr-button id="cancel" on-tap="_handleCancelTap">Cancel</gr-button>
|
||||
</span>
|
||||
</header>
|
||||
</gr-fixed-panel>
|
||||
<div class="textareaWrapper">
|
||||
<textarea id="file">{{_newContent}}</textarea>
|
||||
</div>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
</template>
|
||||
<script src="gr-editor-view.js"></script>
|
||||
</dom-module>
|
139
polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
Normal file
139
polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright (C) 2017 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() {
|
||||
'use strict';
|
||||
|
||||
Polymer({
|
||||
is: 'gr-editor-view',
|
||||
|
||||
/**
|
||||
* Fired when the title of the page should change.
|
||||
*
|
||||
* @event title-change
|
||||
*/
|
||||
|
||||
properties: {
|
||||
/**
|
||||
* URL params passed from the router.
|
||||
*/
|
||||
params: {
|
||||
type: Object,
|
||||
observer: '_paramsChanged',
|
||||
},
|
||||
|
||||
_change: Object,
|
||||
_changeEditDetail: Object,
|
||||
_changeNum: String,
|
||||
_loggedIn: Boolean,
|
||||
_path: String,
|
||||
_content: String,
|
||||
_newContent: String,
|
||||
_saveDisabled: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
computed: '_computeSaveDisabled(_content, _newContent)',
|
||||
},
|
||||
},
|
||||
|
||||
behaviors: [
|
||||
Gerrit.KeyboardShortcutBehavior,
|
||||
Gerrit.PatchSetBehavior,
|
||||
Gerrit.PathListBehavior,
|
||||
],
|
||||
|
||||
attached() {
|
||||
this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; });
|
||||
},
|
||||
|
||||
_getLoggedIn() {
|
||||
return this.$.restAPI.getLoggedIn();
|
||||
},
|
||||
|
||||
_paramsChanged(value) {
|
||||
if (value.view !== Gerrit.Nav.View.EDIT) { return; }
|
||||
|
||||
this._changeNum = value.changeNum;
|
||||
this._path = value.path;
|
||||
|
||||
// NOTE: This may be called before attachment (e.g. while parentElement is
|
||||
// null). Fire title-change in an async so that, if attachment to the DOM
|
||||
// has been queued, the event can bubble up to the handler in gr-app.
|
||||
this.async(() => {
|
||||
const title = `Editing ${this.computeTruncatedPath(this._path)}`;
|
||||
this.fire('title-change', {title});
|
||||
});
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this._getChangeDetail(this._changeNum));
|
||||
promises.push(this._getFileContent(this._changeNum, this._path)
|
||||
.then(fileContent => {
|
||||
this._content = fileContent;
|
||||
this._newContent = fileContent;
|
||||
}));
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
_getChangeDetail(changeNum) {
|
||||
return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => {
|
||||
this._change = change;
|
||||
});
|
||||
},
|
||||
|
||||
_handlePathChanged(e) {
|
||||
const path = e.detail;
|
||||
if (path === this._path) { return Promise.resolve(); }
|
||||
return this.$.restAPI.renameFileInChangeEdit(this._changeNum,
|
||||
this._path, path).then(res => {
|
||||
if (!res.ok) { return; }
|
||||
this._viewEditInChangeView();
|
||||
});
|
||||
},
|
||||
|
||||
_viewEditInChangeView() {
|
||||
Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME);
|
||||
},
|
||||
|
||||
_getFileContent(changeNum, path) {
|
||||
return this.$.restAPI.getFileInChangeEdit(changeNum, path).then(res => {
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) {
|
||||
// No edits have been made yet.
|
||||
return this.$.restAPI.getFileInChangeEdit(changeNum, path, true)
|
||||
.then(res => res.text().then(text => atob(text)));
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return res.text().then(text => atob(text));
|
||||
});
|
||||
},
|
||||
|
||||
_saveEdit() {
|
||||
return this.$.restAPI.saveChangeEdit(this._changeNum, this._path,
|
||||
this._newContent).then(res => {
|
||||
if (!res.ok) { return; }
|
||||
this._viewEditInChangeView();
|
||||
});
|
||||
},
|
||||
|
||||
_computeSaveDisabled(content, newContent) {
|
||||
return content === newContent;
|
||||
},
|
||||
|
||||
_handleCancelTap() {
|
||||
// TODO(kaspern): Add a confirm dialog if there are unsaved changes.
|
||||
this._viewEditInChangeView();
|
||||
},
|
||||
});
|
||||
})();
|
@@ -0,0 +1,183 @@
|
||||
<!--
|
||||
Copyright (C) 2017 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-editor-view</title>
|
||||
|
||||
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
|
||||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||
<link rel="import" href="../../../test/common-test-setup.html"/>
|
||||
|
||||
<link rel="import" href="gr-editor-view.html">
|
||||
|
||||
<script>void(0);</script>
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<gr-editor-view></gr-editor-view>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-editor-view tests', () => {
|
||||
let element;
|
||||
let sandbox;
|
||||
let savePathStub;
|
||||
let saveFileStub;
|
||||
let changeDetailStub;
|
||||
let navigateStub;
|
||||
const mockParams = {
|
||||
changeNum: '42',
|
||||
path: 'foo/bar.baz',
|
||||
};
|
||||
|
||||
setup(() => {
|
||||
stub('gr-rest-api-interface', {
|
||||
getLoggedIn() { return Promise.resolve(true); },
|
||||
});
|
||||
sandbox = sinon.sandbox.create();
|
||||
element = fixture('basic');
|
||||
savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit');
|
||||
saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit');
|
||||
changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail');
|
||||
navigateStub = sandbox.stub(element, '_viewEditInChangeView');
|
||||
});
|
||||
|
||||
teardown(() => { sandbox.restore(); });
|
||||
|
||||
suite('_paramsChanged', () => {
|
||||
test('incorrect view returns immediately', () => {
|
||||
element._paramsChanged(
|
||||
Object.assign({}, mockParams, {view: Gerrit.Nav.View.DIFF}));
|
||||
assert.notOk(element._changeNum);
|
||||
});
|
||||
|
||||
test('good params proceed', () => {
|
||||
changeDetailStub.returns(Promise.resolve({}));
|
||||
const fileStub = sandbox.stub(element, '_getFileContent')
|
||||
.returns(Promise.resolve('text'));
|
||||
|
||||
const promises = element._paramsChanged(
|
||||
Object.assign({}, mockParams, {view: Gerrit.Nav.View.EDIT}));
|
||||
|
||||
flushAsynchronousOperations();
|
||||
assert.equal(element._changeNum, mockParams.changeNum);
|
||||
assert.equal(element._path, mockParams.path);
|
||||
assert.deepEqual(changeDetailStub.lastCall.args[0],
|
||||
mockParams.changeNum);
|
||||
assert.deepEqual(fileStub.lastCall.args,
|
||||
[mockParams.changeNum, mockParams.path]);
|
||||
|
||||
return promises.then(() => {
|
||||
assert.equal(element._content, 'text');
|
||||
assert.equal(element._newContent, 'text');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('edit file path', done => {
|
||||
element._changeNum = mockParams.changeNum;
|
||||
element._path = mockParams.path;
|
||||
savePathStub.onFirstCall().returns(Promise.resolve({}));
|
||||
savePathStub.onSecondCall().returns(Promise.resolve({ok: true}));
|
||||
|
||||
// Calling with the same path should not navigate.
|
||||
element._handlePathChanged({detail: mockParams.path}).then(() => {
|
||||
assert.isFalse(savePathStub.called);
|
||||
// !ok response
|
||||
element._handlePathChanged({detail: 'newPath'}).then(() => {
|
||||
assert.isTrue(savePathStub.called);
|
||||
assert.isFalse(navigateStub.called);
|
||||
// ok response
|
||||
element._handlePathChanged({detail: 'newPath'}).then(() => {
|
||||
assert.isTrue(navigateStub.called);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('edit file content', () => {
|
||||
const originalText = 'file text';
|
||||
const newText = 'file text changed';
|
||||
|
||||
setup(() => {
|
||||
element._changeNum = mockParams.changeNum;
|
||||
element._path = mockParams.path;
|
||||
element._content = originalText;
|
||||
element._newContent = originalText;
|
||||
flushAsynchronousOperations();
|
||||
});
|
||||
|
||||
test('initial load', () => {
|
||||
assert.equal(element.$.file.value, originalText);
|
||||
assert.isTrue(element.$.save.hasAttribute('disabled'));
|
||||
});
|
||||
|
||||
test('file modification and save, !ok response', done => {
|
||||
const saveSpy = sandbox.spy(element, '_saveEdit');
|
||||
saveFileStub.returns(Promise.resolve({ok: false}));
|
||||
element._newContent = newText;
|
||||
flushAsynchronousOperations();
|
||||
|
||||
assert.equal(element.$.file.value, newText);
|
||||
assert.isFalse(element.$.save.hasAttribute('disabled'));
|
||||
|
||||
MockInteractions.tap(element.$.save);
|
||||
assert(saveSpy.called);
|
||||
saveSpy.lastCall.returnValue.then(() => {
|
||||
assert.isTrue(saveFileStub.called);
|
||||
assert.deepEqual(saveFileStub.lastCall.args,
|
||||
[mockParams.changeNum, mockParams.path, newText]);
|
||||
assert.isFalse(navigateStub.called);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('file modification and save', done => {
|
||||
const saveSpy = sandbox.spy(element, '_saveEdit');
|
||||
saveFileStub.returns(Promise.resolve({ok: true}));
|
||||
element._newContent = newText;
|
||||
flushAsynchronousOperations();
|
||||
|
||||
assert.equal(element.$.file.value, newText);
|
||||
assert.isFalse(element.$.save.hasAttribute('disabled'));
|
||||
|
||||
MockInteractions.tap(element.$.save);
|
||||
assert.isTrue(saveSpy.called);
|
||||
saveSpy.lastCall.returnValue.then(() => {
|
||||
assert.isTrue(saveFileStub.called);
|
||||
assert.isTrue(navigateStub.called);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('file modification and cancel', () => {
|
||||
const cancelSpy = sandbox.spy(element, '_handleCancelTap');
|
||||
element._newContent = newText;
|
||||
flushAsynchronousOperations();
|
||||
|
||||
assert.equal(element.$.file.value, newText);
|
||||
assert.isFalse(element.$.save.hasAttribute('disabled'));
|
||||
|
||||
MockInteractions.tap(element.$.cancel);
|
||||
assert.isTrue(cancelSpy.called);
|
||||
assert.isFalse(saveFileStub.called);
|
||||
assert.isTrue(navigateStub.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
@@ -46,6 +46,7 @@ limitations under the License.
|
||||
<link rel="import" href="./core/gr-reporting/gr-reporting.html">
|
||||
<link rel="import" href="./core/gr-router/gr-router.html">
|
||||
<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
|
||||
<link rel="import" href="./edit/gr-editor-view/gr-editor-view.html">
|
||||
<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
|
||||
<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
|
||||
<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
|
||||
@@ -151,11 +152,15 @@ limitations under the License.
|
||||
view-state="{{_viewState.changeView}}"
|
||||
back-page="[[_lastSearchPage]]"></gr-change-view>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showDiffView]]" restamp="true">
|
||||
<gr-diff-view
|
||||
params="[[params]]"
|
||||
change-view-state="{{_viewState.changeView}}"></gr-diff-view>
|
||||
<template is="dom-if" if="[[_showEditorView]]" restamp="true">
|
||||
<gr-editor-view
|
||||
params="[[params]]"></gr-editor-view>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showDiffView]]" restamp="true">
|
||||
<gr-diff-view
|
||||
params="[[params]]"
|
||||
change-view-state="{{_viewState.changeView}}"></gr-diff-view>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showSettingsView]]" restamp="true">
|
||||
<gr-settings-view
|
||||
params="[[params]]"
|
||||
|
@@ -56,6 +56,7 @@
|
||||
_showSettingsView: Boolean,
|
||||
_showAdminView: Boolean,
|
||||
_showCLAView: Boolean,
|
||||
_showEditorView: Boolean,
|
||||
/** @type {?} */
|
||||
_viewState: Object,
|
||||
/** @type {?} */
|
||||
@@ -139,6 +140,7 @@
|
||||
this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS);
|
||||
this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN);
|
||||
this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS);
|
||||
this.set('_showEditorView', view === Gerrit.Nav.View.EDIT);
|
||||
if (this.params.justRegistered) {
|
||||
this.$.registration.open();
|
||||
}
|
||||
|
@@ -1249,9 +1249,18 @@
|
||||
.then(response => this.getResponseObject(response));
|
||||
},
|
||||
|
||||
getFileInChangeEdit(changeNum, path) {
|
||||
/**
|
||||
* Gets a file in a change edit.
|
||||
* @param {number|string} changeNum
|
||||
* @param {string} path
|
||||
* @param {boolean=} opt_base If specified, file contents come from change
|
||||
* edit's base patchset.
|
||||
*/
|
||||
getFileInChangeEdit(changeNum, path, opt_base) {
|
||||
const e = '/edit/' + encodeURIComponent(path);
|
||||
return this.getChangeURLAndSend(changeNum, 'GET', null, e);
|
||||
let payload = null;
|
||||
if (opt_base) { payload = {base: true}; }
|
||||
return this.getChangeURLAndSend(changeNum, 'GET', null, e, payload);
|
||||
},
|
||||
|
||||
rebaseChangeEdit(changeNum) {
|
||||
@@ -1279,7 +1288,8 @@
|
||||
|
||||
saveChangeEdit(changeNum, path, contents) {
|
||||
const e = '/edit/' + encodeURIComponent(path);
|
||||
return this.getChangeURLAndSend(changeNum, 'PUT', null, e, contents);
|
||||
return this.getChangeURLAndSend(changeNum, 'PUT', null, e, contents, null,
|
||||
null, 'text/plain');
|
||||
},
|
||||
|
||||
// Deprecated, prefer to use putChangeCommitMessage instead.
|
||||
@@ -1845,7 +1855,7 @@
|
||||
* @param {?string} endpoint gets passed as null.
|
||||
* @param {?Object|number|string=} opt_payload gets passed as null, string,
|
||||
* Object, or number.
|
||||
* @param {function(?Response, string=)=} opt_errFn
|
||||
* @param {?function(?Response, string=)=} opt_errFn
|
||||
* @param {?=} opt_ctx
|
||||
* @param {?=} opt_contentType
|
||||
* @return {!Promise<!Object>}
|
||||
|
@@ -102,6 +102,7 @@ limitations under the License.
|
||||
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
|
||||
'diff/gr-syntax-layer/gr-syntax-layer_test.html',
|
||||
'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
|
||||
'edit/gr-editor-view/gr-editor-view_test.html',
|
||||
'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
|
||||
'plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html',
|
||||
'plugins/gr-event-helper/gr-event-helper_test.html',
|
||||
|
Reference in New Issue
Block a user