Make edit file input an autocomplete

Uses the /files endpoint to query possible files.

Bug: Issue 4437
Change-Id: I439100b5f85de05cba8988daa3fd71502b6af07f
This commit is contained in:
Kasper Nilsson
2017-10-05 15:21:50 -07:00
parent c98af21c4b
commit c4e64fc8f9
7 changed files with 94 additions and 44 deletions

View File

@@ -16,13 +16,14 @@ limitations under the License.
<link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../bower_components/paper-input/paper-input.html"> <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html"> <link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../../styles/shared-styles.html"> <link rel="import" href="../../../styles/shared-styles.html">
@@ -41,21 +42,21 @@ limitations under the License.
margin-left: 1em; margin-left: 1em;
text-decoration: none; text-decoration: none;
} }
paper-input {
--paper-input-container: {
padding: 0;
min-width: 15em;
}
--paper-input-container-input: {
font-size: 1em;
}
}
gr-confirm-dialog { gr-confirm-dialog {
width: 50em; width: 50em;
} }
gr-confirm-dialog .main { gr-confirm-dialog .main {
width: 100%; width: 100%;
} }
gr-autocomplete {
--gr-autocomplete: {
border: 1px solid #d1d2d3;
border-radius: 2px;
font-size: 1em;
height: 2em;
padding: 0 .15em;
}
}
</style> </style>
<template is="dom-repeat" items="[[_actions]]" as="action"> <template is="dom-repeat" items="[[_actions]]" as="action">
<gr-button <gr-button
@@ -74,13 +75,15 @@ limitations under the License.
<div class="header">Edit a file</div> <div class="header">Edit a file</div>
<div class="main"> <div class="main">
<!-- TODO(kaspern): Make this an autocomplete. --> <!-- TODO(kaspern): Make this an autocomplete. -->
<paper-input <gr-autocomplete
class="input" class="input"
label="Enter an existing or new full file path." placeholder="Enter an existing or new full file path."
value="{{_path}}"></paper-input> query="[[_query]]"
text="{{_path}}"></gr-autocomplete>
</div> </div>
</gr-confirm-dialog> </gr-confirm-dialog>
</gr-overlay> </gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template> </template>
<script src="gr-edit-controls.js"></script> <script src="gr-edit-controls.js"></script>
</dom-module> </dom-module>

View File

@@ -37,8 +37,18 @@
type: String, type: String,
value: '', value: '',
}, },
_query: {
type: Function,
value() {
return this._queryFiles.bind(this);
},
},
}, },
behaviors: [
Gerrit.PatchSetBehavior,
],
_handleTap(e) { _handleTap(e) {
e.preventDefault(); e.preventDefault();
const action = Polymer.dom(e).localTarget.id; const action = Polymer.dom(e).localTarget.id;
@@ -73,7 +83,8 @@
}, },
_closeDialog(dialog) { _closeDialog(dialog) {
dialog.querySelectorAll('.input').forEach(input => { input.value = ''; }); dialog.querySelectorAll('gr-autocomplete')
.forEach(input => { input.text = ''; });
dialog.classList.toggle('invisible', true); dialog.classList.toggle('invisible', true);
return this.$.overlay.close(); return this.$.overlay.close();
}, },
@@ -87,5 +98,12 @@
Gerrit.Nav.navigateToRelativeUrl(url); Gerrit.Nav.navigateToRelativeUrl(url);
this._closeDialog(Polymer.dom(e).localTarget); this._closeDialog(Polymer.dom(e).localTarget);
}, },
_queryFiles(input) {
return this.$.restAPI.queryChangeFiles(this.change._number,
this.EDIT_NAME, input).then(res => res.map(file => {
return {name: file};
}));
},
}); });
})(); })();

View File

@@ -37,12 +37,16 @@ suite('gr-edit-controls tests', () => {
let sandbox; let sandbox;
let showDialogSpy; let showDialogSpy;
let closeDialogSpy; let closeDialogSpy;
let queryStub;
setup(() => { setup(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
element = fixture('basic'); element = fixture('basic');
element.change = {_number: '42'};
showDialogSpy = sandbox.spy(element, '_showDialog'); showDialogSpy = sandbox.spy(element, '_showDialog');
closeDialogSpy = sandbox.spy(element, '_closeDialog'); closeDialogSpy = sandbox.spy(element, '_closeDialog');
queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
.returns(Promise.resolve([]));
flushAsynchronousOperations(); flushAsynchronousOperations();
}); });
@@ -67,7 +71,9 @@ suite('gr-edit-controls tests', () => {
MockInteractions.tap(element.$$('#edit')); MockInteractions.tap(element.$$('#edit'));
return showDialogSpy.lastCall.returnValue.then(() => { return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.editDialog.disabled); assert.isTrue(element.$.editDialog.disabled);
element._path = 'src/test.cpp'; assert.isFalse(queryStub.called);
element.$.editDialog.querySelector('.input').text = 'src/test.cpp';
assert.isTrue(queryStub.called);
assert.isFalse(element.$.editDialog.disabled); assert.isFalse(element.$.editDialog.disabled);
MockInteractions.tap(element.$.editDialog.$$('gr-button[primary]')); MockInteractions.tap(element.$.editDialog.$$('gr-button[primary]'));
for (const stub of navStubs) { assert.isTrue(stub.called); } for (const stub of navStubs) { assert.isTrue(stub.called); }
@@ -79,7 +85,7 @@ suite('gr-edit-controls tests', () => {
MockInteractions.tap(element.$$('#edit')); MockInteractions.tap(element.$$('#edit'));
return showDialogSpy.lastCall.returnValue.then(() => { return showDialogSpy.lastCall.returnValue.then(() => {
assert.isTrue(element.$.editDialog.disabled); assert.isTrue(element.$.editDialog.disabled);
element._path = 'src/test.cpp'; element.$.editDialog.querySelector('.input').text = 'src/test.cpp';
assert.isFalse(element.$.editDialog.disabled); assert.isFalse(element.$.editDialog.disabled);
MockInteractions.tap(element.$.editDialog.$$('gr-button')); MockInteractions.tap(element.$.editDialog.$$('gr-button'));
for (const stub of navStubs) { assert.isFalse(stub.called); } for (const stub of navStubs) { assert.isFalse(stub.called); }
@@ -92,7 +98,7 @@ suite('gr-edit-controls tests', () => {
test('openEditDialog', () => { test('openEditDialog', () => {
return element.openEditDialog('test/path.cpp').then(() => { return element.openEditDialog('test/path.cpp').then(() => {
assert.isFalse(element.$.editDialog.hasAttribute('hidden')); assert.isFalse(element.$.editDialog.hasAttribute('hidden'));
assert.equal(element.$.editDialog.querySelector('.input').value, assert.equal(element.$.editDialog.querySelector('.input').text,
'test/path.cpp'); 'test/path.cpp');
}); });
}); });

View File

@@ -46,6 +46,7 @@
}, },
suggestions: { suggestions: {
type: Array, type: Array,
value: () => [],
observer: '_resetCursorStops', observer: '_resetCursorStops',
}, },
_suggestionEls: { _suggestionEls: {
@@ -151,8 +152,12 @@
}, },
_resetCursorStops() { _resetCursorStops() {
Polymer.dom.flush(); if (this.suggestions.length > 0) {
this._suggestionEls = this.$.suggestions.querySelectorAll('li'); Polymer.dom.flush();
this._suggestionEls = this.$.suggestions.querySelectorAll('li');
} else {
this._suggestionEls = [];
}
}, },
_resetCursorIndex() { _resetCursorIndex() {

View File

@@ -38,31 +38,29 @@ limitations under the License.
color: red; color: red;
} }
</style> </style>
<div> <input
<input id="input"
id="input" class$="[[_computeClass(borderless)]]"
class$="[[_computeClass(borderless)]]" is="iron-input"
is="iron-input" disabled$="[[disabled]]"
disabled$="[[disabled]]" bind-value="{{text}}"
bind-value="{{text}}" placeholder="[[placeholder]]"
placeholder="[[placeholder]]" on-keydown="_handleKeydown"
on-keydown="_handleKeydown" on-focus="_onInputFocus"
on-focus="_onInputFocus" on-blur="_onInputBlur"
on-blur="_onInputBlur" autocomplete="off"/>
autocomplete="off"/> <gr-autocomplete-dropdown
<gr-autocomplete-dropdown vertical-align="top"
vertical-align="top" vertical-offset="20"
vertical-offset="20" horizontal-align="auto"
horizontal-align="auto" id="suggestions"
id="suggestions" on-item-selected="_handleItemSelect"
on-item-selected="_handleItemSelect" on-keydown="_handleKeydown"
on-keydown="_handleKeydown" suggestions="[[_suggestions]]"
suggestions="[[_suggestions]]" role="listbox"
role="listbox" index="[[_index]]"
index="[[_index]]" position-target="[[_inputElement]]">
position-target="[[_inputElement]]"> </gr-autocomplete-dropdown>
</gr-autocomplete-dropdown>
</div>
</template> </template>
<script src="gr-autocomplete.js"></script> <script src="gr-autocomplete.js"></script>
</dom-module> </dom-module>

View File

@@ -904,6 +904,17 @@
patchRange.patchNum); patchRange.patchNum);
}, },
/**
* @param {number|string} changeNum
* @param {number|string} patchNum
* @param {string} query
* @return {!Promise<!Object>}
*/
queryChangeFiles(changeNum, patchNum, query) {
return this._getChangeURLAndFetch(changeNum,
`/files?q=${encodeURIComponent(query)}`, patchNum);
},
getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) { getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) {
return this.getChangeFiles(changeNum, patchRange).then( return this.getChangeFiles(changeNum, patchRange).then(
this._normalizeChangeFilesResponse.bind(this)); this._normalizeChangeFilesResponse.bind(this));

View File

@@ -701,6 +701,15 @@ limitations under the License.
assert.equal(sendStub.lastCall.args[1], '/projects/x%2Fy'); assert.equal(sendStub.lastCall.args[1], '/projects/x%2Fy');
}); });
test('queryChangeFiles', () => {
const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch')
.returns(Promise.resolve());
return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => {
assert.deepEqual(fetchStub.lastCall.args,
['42', '/files?q=test%2Fpath.js', 'edit']);
});
});
test('getProjects', () => { test('getProjects', () => {
sandbox.stub(element, '_fetchSharedCacheURL'); sandbox.stub(element, '_fetchSharedCacheURL');
element.getProjects('test', 25); element.getProjects('test', 25);