Add repository/branch picker element

Add a new element that simplifies user selection of a repository and
branch pair named gr-repo-branch-picker. Also introduces a wrapper
around gr-autocomplete that provides additional visual styling named
gr-labeled-autocomplete.

Change-Id: I434690b249fd4632989bbdd73cad6f5229399ced
This commit is contained in:
Wyatt Allen
2018-08-27 10:03:48 -07:00
parent 0949afbda4
commit f2cb22dcf7
7 changed files with 514 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<!--
@license
Copyright (C) 2018 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="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-labeled-autocomplete">
<template>
<style include="shared-styles">
:host {
display: block;
width: 12em;
}
#container {
background: var(--chip-background-color);
border-radius: 1em;
padding: .5em;
}
#header {
color: var(--deemphasized-text-color);
font-family: var(--font-family-bold);
font-size: var(--font-size-small);
}
#body {
display: flex;
}
gr-autocomplete {
height: 1.5em;
--gr-autocomplete: {
border: none;
}
}
#trigger {
border-left: 1px solid var(--deemphasized-text-color);
color: var(--deemphasized-text-color);
cursor: pointer;
padding-left: .4em;
}
#trigger:hover {
color: var(--primary-text-color);
}
</style>
<div id="container">
<div id="header">[[label]]</div>
<div id="body">
<gr-autocomplete
id="autocomplete"
threshold="[[_autocompleteThreshold]]"
query="[[query]]"
disabled="[[disabled]]"
placeholder="[[placeholder]]"
borderless></gr-autocomplete>
<div id="trigger" on-tap="_handleTriggerTap"></div>
</div>
</div>
</template>
<script src="gr-labeled-autocomplete.js"></script>
</dom-module>

View File

@@ -0,0 +1,73 @@
/**
* @license
* Copyright (C) 2018 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-labeled-autocomplete',
/**
* Fired when a value is chosen.
*
* @event commit
*/
properties: {
/**
* Used just like the query property of gr-autocomplete.
*
* @type {function(string): Promise<?>}
*/
query: {
type: Function,
value() {
return function() {
return Promise.resolve([]);
};
},
},
text: {
type: String,
value: '',
notify: true,
},
label: String,
placeholder: String,
disabled: Boolean,
_autocompleteThreshold: {
type: Number,
value: 0,
readOnly: true,
},
},
_handleTriggerTap() {
this.$.autocomplete.focus();
},
setText(text) {
this.$.autocomplete.setText(text);
},
clear() {
this.setText('');
},
});
})();

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2018 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-labeled-autocomplete</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-labeled-autocomplete.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-labeled-autocomplete></gr-labeled-autocomplete>
</template>
</test-fixture>
<script>
suite('gr-labeled-autocomplete tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => { sandbox.restore(); });
test('tapping trigger focuses autocomplete', () => {
sandbox.stub(element.$.autocomplete, 'focus');
element._handleTriggerTap();
assert.isTrue(element.$.autocomplete.focus.calledOnce);
});
test('setText', () => {
sandbox.stub(element.$.autocomplete, 'setText');
element.setText('foo-bar');
assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar'));
});
});
</script>

View File

@@ -0,0 +1,60 @@
<!--
@license
Copyright (C) 2018 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="../../../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="../../shared/gr-icons/gr-icons.html">
<link rel="import" href="../../shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-repo-branch-picker">
<template>
<style include="shared-styles">
:host {
display: block;
}
gr-labeled-autocomplete,
iron-icon {
display: inline-block;
}
iron-icon {
margin-bottom: 1.2em;
}
</style>
<div>
<gr-labeled-autocomplete
id="repoInput"
label="Repository"
placeholder="Select repo"
on-commit="_repoCommitted"
query="[[_repoQuery]]">
</gr-labeled-autocomplete>
<iron-icon icon="gr-icons:chevron-right"></iron-icon>
<gr-labeled-autocomplete
id="branchInput"
label="Branch"
placeholder="Select branch"
disabled="[[_branchDisabled]]"
on-commit="_branchCommitted"
query="[[_query]]">
</gr-labeled-autocomplete>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo-branch-picker.js"></script>
</dom-module>

View File

@@ -0,0 +1,109 @@
/**
* @license
* Copyright (C) 2018 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';
const SUGGESTIONS_LIMIT = 15;
const REF_PREFIX = 'refs/heads/';
Polymer({
is: 'gr-repo-branch-picker',
properties: {
repo: {
type: String,
notify: true,
observer: '_repoChanged',
},
branch: {
type: String,
notify: true,
},
_branchDisabled: Boolean,
_query: {
type: Function,
value() {
return this._getRepoBranchesSuggestions.bind(this);
},
},
_repoQuery: {
type: Function,
value() {
return this._getRepoSuggestions.bind(this);
},
},
},
behaviors: [
Gerrit.URLEncodingBehavior,
],
attached() {
if (this.repo) {
this.$.repoInput.setText(this.repo);
}
},
ready() {
this._branchDisabled = !this.repo;
},
_getRepoBranchesSuggestions(input) {
if (!this.repo) { return Promise.resolve([]); }
if (input.startsWith(REF_PREFIX)) {
input = input.substring(REF_PREFIX.length);
}
return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT)
.then(this._branchResponseToSuggestions.bind(this));
},
_getRepoSuggestions(input) {
return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT)
.then(this._repoResponseToSuggestions.bind(this));
},
_repoResponseToSuggestions(res) {
return res.map(repo => ({
name: repo.name,
value: this.singleDecodeURL(repo.id),
}));
},
_branchResponseToSuggestions(res) {
return Object.keys(res).map(key => {
let branch = res[key].ref;
if (branch.startsWith(REF_PREFIX)) {
branch = branch.substring(REF_PREFIX.length);
}
return {name: branch, value: branch};
});
},
_repoCommitted(e) {
this.repo = e.detail.value;
},
_branchCommitted(e) {
this.branch = e.detail.value;
},
_repoChanged() {
this.$.branchInput.clear();
this._branchDisabled = !this.repo;
},
});
})();

View File

@@ -0,0 +1,140 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2018 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-repo-branch-picker</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-repo-branch-picker.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-repo-branch-picker></gr-repo-branch-picker>
</template>
</test-fixture>
<script>
suite('gr-repo-branch-picker tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => { sandbox.restore(); });
suite('_getRepoSuggestions', () => {
setup(() => {
sandbox.stub(element.$.restAPI, 'getRepos')
.returns(Promise.resolve([
{
id: 'plugins%2Favatars-external',
name: 'plugins/avatars-external',
}, {
id: 'plugins%2Favatars-gravatar',
name: 'plugins/avatars-gravatar',
}, {
id: 'plugins%2Favatars%2Fexternal',
name: 'plugins/avatars/external',
}, {
id: 'plugins%2Favatars%2Fgravatar',
name: 'plugins/avatars/gravatar',
},
]));
});
test('converts to suggestion objects', () => {
const input = 'plugins/avatars';
return element._getRepoSuggestions(input).then(suggestions => {
assert.isTrue(element.$.restAPI.getRepos.calledWith(input));
const unencodedNames = [
'plugins/avatars-external',
'plugins/avatars-gravatar',
'plugins/avatars/external',
'plugins/avatars/gravatar',
];
assert.deepEqual(suggestions.map(s => s.name), unencodedNames);
assert.deepEqual(suggestions.map(s => s.value), unencodedNames);
});
});
});
suite('_getRepoBranchesSuggestions', () => {
setup(() => {
sandbox.stub(element.$.restAPI, 'getRepoBranches')
.returns(Promise.resolve([
{ref: 'refs/heads/stable-2.10'},
{ref: 'refs/heads/stable-2.11'},
{ref: 'refs/heads/stable-2.12'},
{ref: 'refs/heads/stable-2.13'},
{ref: 'refs/heads/stable-2.14'},
{ref: 'refs/heads/stable-2.15'},
]));
});
test('converts to suggestion objects', () => {
const repo = 'gerrit';
const branchInput = 'stable-2.1';
element.repo = repo;
return element._getRepoBranchesSuggestions(branchInput)
.then(suggestions => {
assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
branchInput, repo, 15));
const refNames = [
'stable-2.10',
'stable-2.11',
'stable-2.12',
'stable-2.13',
'stable-2.14',
'stable-2.15',
];
assert.deepEqual(suggestions.map(s => s.name), refNames);
assert.deepEqual(suggestions.map(s => s.value), refNames);
});
});
test('filters out ref prefix', () => {
const repo = 'gerrit';
const branchInput = 'refs/heads/stable-2.1';
element.repo = repo;
return element._getRepoBranchesSuggestions(branchInput)
.then(suggestions => {
assert.isTrue(element.$.restAPI.getRepoBranches.calledWith(
'stable-2.1', repo, 15));
});
});
test('does not query when repo is unset', () => {
return element._getRepoBranchesSuggestions('')
.then(() => {
assert.isFalse(element.$.restAPI.getRepoBranches.called);
element.repo = 'gerrit';
return element._getRepoBranchesSuggestions('');
})
.then(() => {
assert.isTrue(element.$.restAPI.getRepoBranches.called);
});
});
});
});
</script>

View File

@@ -167,12 +167,14 @@ limitations under the License.
'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
'shared/gr-fixed-panel/gr-fixed-panel_test.html',
'shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html',
'shared/gr-lib-loader/gr-lib-loader_test.html',
'shared/gr-limited-text/gr-limited-text_test.html',
'shared/gr-linked-chip/gr-linked-chip_test.html',
'shared/gr-linked-text/gr-linked-text_test.html',
'shared/gr-list-view/gr-list-view_test.html',
'shared/gr-page-nav/gr-page-nav_test.html',
'shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html',
'shared/gr-rest-api-interface/gr-auth_test.html',
'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',