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
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
new file mode 100644
index 0000000..5825301
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
new file mode 100644
index 0000000..cb3d546
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -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('');
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
new file mode 100644
index 0000000..f7632d3
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
new file mode 100644
index 0000000..d794dd6
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
new file mode 100644
index 0000000..e2298c3
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -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;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
new file mode 100644
index 0000000..989e838
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
@@ -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>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 3ec55dd..a489627 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -167,12 +167,14 @@
     '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',