Implement star action

Star action is available on change-list and change-view.

Change-Id: I5c146349515277a02b3e73d1bfe597b43a379c61
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 585141a..e53b1a5 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -105,7 +105,8 @@
     </header>
     <main>
       <template is="dom-if" if="{{_showChangeListView}}" restamp="true">
-        <gr-change-list-view params="[[params]]"></gr-change-list-view>
+        <gr-change-list-view params="[[params]]"
+            logged-in="[[_computeLoggedIn(account)]]"></gr-change-list-view>
       </template>
       <template is="dom-if" if="{{_showDashboardView}}" restamp="true">
         <gr-dashboard-view params="[[params]]"></gr-dashboard-view>
@@ -224,6 +225,10 @@
             window.location.pathname + window.location.hash));
       },
 
+      _computeLoggedIn: function(account) { // argument used for binding update only
+        return this.loggedIn;
+      },
+
     });
   })();
   </script>
diff --git a/polygerrit-ui/app/elements/gr-change-list-item.html b/polygerrit-ui/app/elements/gr-change-list-item.html
index 46c0513..9c77a06 100644
--- a/polygerrit-ui/app/elements/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/gr-change-list-item.html
@@ -17,6 +17,7 @@
 <link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../styles/gr-change-list-styles.html">
 <link rel="import" href="gr-account-link.html">
+<link rel="import" href="gr-change-star.html">
 <link rel="import" href="gr-date-formatter.html">
 
 <dom-module id="gr-change-list-item">
@@ -60,6 +61,9 @@
     <span class="cell keyboard">
       <span class="positionIndicator">&#x25b6;</span>
     </span>
+    <span class="cell star" hidden$="[[!showStar]]">
+      <gr-change-star change="{{change}}"></gr-change-star>
+    </span>
     <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
     <span class="cell status">[[_computeChangeStatusString(change)]]</span>
     <span class="cell owner">
@@ -94,6 +98,10 @@
           type: String,
           computed: '_computeChangeURL(change._number)',
         },
+        showStar: {
+          type: Boolean,
+          value: false,
+        },
       },
 
       _computeChangeURL: function(changeNum) {
diff --git a/polygerrit-ui/app/elements/gr-change-list-view.html b/polygerrit-ui/app/elements/gr-change-list-view.html
index 403d87c..aef69c5 100644
--- a/polygerrit-ui/app/elements/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/gr-change-list-view.html
@@ -57,8 +57,9 @@
         last-response="{{_changes}}"
         loading="{{_loading}}"></gr-ajax>
     <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-    <div hidden$="[[_loading]]" hidden>
-      <gr-change-list changes="{{_changes}}"></gr-change-list>
+    <div hidden$="[[_loading]]">
+      <gr-change-list changes="{{_changes}}"
+          show-star="[[loggedIn]]"></gr-change-list>
       <nav>
         <a href$="[[_computeNavLink(query, offset, -1)]]"
            hidden$="[[_hidePrevArrow(offset)]]">&larr; Prev</a>
@@ -86,6 +87,14 @@
         },
 
         /**
+         * True when user is logged in.
+         */
+        loggedIn: {
+          type: Boolean,
+          value: false,
+        },
+
+        /**
          * Change objects loaded from the server.
          */
         _changes: Array,
diff --git a/polygerrit-ui/app/elements/gr-change-list.html b/polygerrit-ui/app/elements/gr-change-list.html
index 1c25d91..fd2b520 100644
--- a/polygerrit-ui/app/elements/gr-change-list.html
+++ b/polygerrit-ui/app/elements/gr-change-list.html
@@ -42,6 +42,7 @@
     <style include="gr-change-list-styles"></style>
     <div class="headerRow">
       <span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
+      <span class="topHeader star" hidden$="[[!showStar]]"></span>
       <span class="topHeader subject">Subject</span>
       <span class="topHeader status">Status</span>
       <span class="topHeader owner">Owner</span>
@@ -57,7 +58,7 @@
         <div class="groupHeader">[[_groupTitle(groupIndex)]]</div>
       </template>
       <template is="dom-repeat" items="[[changeGroup]]" as="change">
-        <gr-change-list-item change="[[change]]"></gr-change-list-item>
+        <gr-change-list-item change="[[change]]" show-star="[[showStar]]"></gr-change-list-item>
       </template>
     </template>
   </template>
@@ -100,6 +101,10 @@
           value: 0,
           observer: '_selectedIndexChanged',
         },
+        showStar: {
+          type: Boolean,
+          value: false,
+        },
         _boundKeyHandler: {
           type: Function,
           value: function() { return this._handleKey.bind(this); },
diff --git a/polygerrit-ui/app/elements/gr-change-star.html b/polygerrit-ui/app/elements/gr-change-star.html
new file mode 100644
index 0000000..9311aff
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-star.html
@@ -0,0 +1,88 @@
+<!--
+Copyright (C) 2015 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="gr-request.html">
+
+<dom-module id="gr-change-star">
+  <template>
+    <style>
+      :host {
+        display: inline;
+      }
+      .starButton {
+        font-size: .95em;
+        margin-right: .25em;
+        cursor: pointer;
+        background-color: transparent;
+        border-color: transparent;
+      }
+      .star {
+        color: #fbc02d;
+      }
+      .unstar {
+        color: #666;
+      }
+    </style>
+    <button class="starButton" on-tap="_handleStarTap">
+      <span class="star" hidden$="[[!change.starred]]">&#9733;</span>
+      <span class="unstar" hidden$="[[change.starred]]">&#9734;</span>
+    </button>
+  </template>
+  <script>
+  (function() {
+    'use strict';
+
+    Polymer({
+      is: 'gr-change-star',
+
+      properties: {
+        change: {
+          type: Object,
+          notify: true,
+        },
+
+        _xhrPromise: Object,  // Used for testing.
+      },
+
+      _handleStarTap: function() {
+        var method = this.change.starred ? 'DELETE' : 'PUT';
+        this.set('change.starred', !this.change.starred);
+        this._send(method, this._restEndpoint()).catch(function(err) {
+          this.set('change.starred', !this.change.starred);
+          alert('Change couldn’t be starred. Check the console and contact ' +
+              'the PolyGerrit team for assistance.');
+          throw err;
+        }.bind(this));
+      },
+
+      _send: function(method, url) {
+        var xhr = document.createElement('gr-request');
+        this._xhrPromise = xhr.send({
+          method: method,
+          url: url,
+        });
+        return this._xhrPromise;
+      },
+
+      _restEndpoint: function() {
+        return '/accounts/self/starred.changes/' + this.change._number;
+      },
+
+    });
+  })();
+  </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
index be63e59..1388d8a 100644
--- a/polygerrit-ui/app/elements/gr-change-view.html
+++ b/polygerrit-ui/app/elements/gr-change-view.html
@@ -18,6 +18,7 @@
 <link rel="import" href="gr-account-link.html">
 <link rel="import" href="gr-ajax.html">
 <link rel="import" href="gr-change-actions.html">
+<link rel="import" href="gr-change-star.html">
 <link rel="import" href="gr-date-formatter.html">
 <link rel="import" href="gr-file-list.html">
 <link rel="import" href="gr-linked-text.html">
@@ -149,6 +150,7 @@
       <div class="headerContainer">
         <div class="header">
           <span class="header-title">
+            <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
             <a href$="[[_computeChangePath(_change._number)]]">[[_change._number]]</a><span>:</span>
             <span>[[_change.subject]]</span>
             <span class="changeStatus">[[_computeChangeStatus(_change.status)]]</span>
diff --git a/polygerrit-ui/app/elements/gr-dashboard-view.html b/polygerrit-ui/app/elements/gr-dashboard-view.html
index 88997f6..d04b9f6 100644
--- a/polygerrit-ui/app/elements/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/gr-dashboard-view.html
@@ -34,7 +34,7 @@
         url="/changes/"
         params="[[_computeQueryParams()]]"
         last-response="{{_results}}"></gr-ajax>
-    <gr-change-list groups="{{_results}}"
+    <gr-change-list groups="{{_results}}" show-star
         group-titles="[[_groupTitles]]"></gr-change-list>
   </template>
   <script>
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 0ead9ac..5a96b6c 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -16,7 +16,8 @@
 <dom-module id="gr-change-list-styles">
   <template>
     <style>
-      .keyboard {
+      .keyboard,
+      .star {
         width: 2em;
       }
       .subject {
diff --git a/polygerrit-ui/app/test/gr-change-star-test.html b/polygerrit-ui/app/test/gr-change-star-test.html
new file mode 100644
index 0000000..25eacf4
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-star-test.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 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-change-star</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/page/page.js"></script>
+<script src="../scripts/fake-app.js"></script>
+<script src="../scripts/util.js"></script>
+
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../elements/gr-change-star.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-change-star></gr-change-star>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-change-star tests', function() {
+    var element;
+    var server;
+
+    setup(function() {
+      element = fixture('basic');
+      element.change = {
+        _number: 2,
+        starred: true,
+      };
+
+      server = sinon.fakeServer.create();
+      server.respondWith(
+        'PUT',
+        '/accounts/self/starred.changes/2',
+        [
+          204,
+          { 'Content-Type': 'application/json' },
+          ''
+        ]
+      );
+
+      server.respondWith(
+        'DELETE',
+        '/accounts/self/starred.changes/2',
+        [
+          204,
+          { 'Content-Type': 'application/json' },
+          ''
+        ]
+      );
+    });
+
+    teardown(function() {
+      server.restore();
+    });
+
+    function isVisible(el) {
+      assert.ok(el);
+      return getComputedStyle(el).getPropertyValue('display') != 'none';
+    }
+
+    test('star visibility states', function() {
+      element.set('change.starred', true);
+      assert.isTrue(isVisible(element.$$('.star')), 'star is visible');
+      assert.isFalse(isVisible(element.$$('.unstar')), 'unstar is not visible');
+
+      element.set('change.starred', false);
+      assert.isTrue(isVisible(element.$$('.unstar')), 'unstar is visible');
+      assert.isFalse(isVisible(element.$$('.star')), 'star is not visible');
+    });
+
+    test('starring', function(done) {
+      element.set('change.starred', false);
+      MockInteractions.tap(element.$$('button'));
+
+      server.respond();
+
+      element._xhrPromise.then(function(req) {
+        assert.equal(req.status, 204);
+        assert.equal(req.url, '/accounts/self/starred.changes/2');
+        assert.equal(element.change.starred, true);
+        done();
+      });
+    });
+
+    test('unstarring', function(done) {
+      element.set('change.starred', true);
+      MockInteractions.tap(element.$$('button'));
+
+      server.respond();
+
+      element._xhrPromise.then(function(req) {
+        assert.equal(req.status, 204);
+        assert.equal(req.url, '/accounts/self/starred.changes/2');
+        assert.equal(element.change.starred, false);
+        done();
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index e415b2e..eff75db 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -29,6 +29,7 @@
     'gr-change-actions-test.html',
     'gr-change-list-item-test.html',
     'gr-change-list-test.html',
+    'gr-change-star-test.html',
     'gr-change-view-test.html',
     'gr-date-formatter-test.html',
     'gr-diff-comment-test.html',