Introduce gr-edit-file-controls

Component is a simple `edit` button and a `more` menu for taking
per-file edit related actions, like reverting, renaming, and deleting
files.

Clicking any button will fire an event that will be caught and handled
by a higher-level component that has yet to be implemented.

Bug: Issue 4437
Change-Id: I1ded6d261562039b094ac2bafa60d7aa9603444b
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index f31841f..f80ff0a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -23,6 +23,7 @@
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
 <link rel="import" href="../../diff/gr-diff/gr-diff.html">
 <link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
+<link rel="import" href="../../edit/gr-edit-file-controls/gr-edit-file-controls.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
@@ -50,6 +51,12 @@
       :host(.editLoaded) .hideOnEdit {
         display: none;
       }
+      .showOnEdit {
+        display: none;
+      }
+      :host(.editLoaded) .showOnEdit {
+        display: initial;
+      }
       .reviewed,
       .status {
         align-items: center;
@@ -184,6 +191,10 @@
         display: initial;
         opacity: 100;
       }
+      .editFileControls {
+        margin-left: 1em;
+        width: 4em;
+      }
       @media screen and (max-width: 50em) {
         .desktop {
           display: none;
@@ -299,6 +310,12 @@
               <span class="markReviewed" title="Mark as reviewed (shortcut: r)">[[_computeReviewedText(file.isReviewed)]]</span>
             </label>
           </div>
+          <div class="editFileControls showOnEdit">
+            <gr-edit-file-controls
+                class$="[[_computeClass('', file.__path)]]"
+                file-path="[[file.__path]]"
+                on-edit-tap="_handleEditTap"></gr-edit-file-controls>
+          </div>
         </div>
         <template is="dom-if"
             if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
@@ -338,6 +355,7 @@
       </div>
       <!-- Empty div here exists to keep spacing in sync with file rows. -->
       <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden></div>
+      <div class="editFileControls showOnEdit"></div>
     </div>
     <div
         class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 11a4a29..70f1d4d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -926,5 +926,10 @@
     _computeReviewedText(isReviewed) {
       return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
     },
+
+    _handleEditTap(e) {
+      const url = Gerrit.Nav.getEditUrlForDiff(this.change, e.detail.path);
+      Gerrit.Nav.navigateToRelativeUrl(url);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index b80a20f..b01c105 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -1238,6 +1238,20 @@
         });
       });
     });
+
+    test('editing actions', () => {
+      element.editLoaded = true;
+      element.change = {_number: '42', project: 'test'};
+      const navStub = sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+      const editControls =
+          Polymer.dom(element.root).querySelectorAll('.row:not(.header)')
+            .map(row => row.querySelector('gr-edit-file-controls'));
+
+      // Commit message should not have edit controls.
+      assert.isTrue(editControls[0].classList.contains('invisible'));
+      MockInteractions.tap(editControls[1].$.edit);
+      assert.isTrue(navStub.called);
+    });
   });
   a11ySuite('basic');
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index b03f2e5..8453da9 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -57,6 +57,7 @@
       console.warn('Use of uninitialized routing');
     };
 
+    const EDIT_PATCHNUM = 'edit';
     const PARENT_PATCHNUM = 'PARENT';
 
     window.Gerrit.Nav = {
@@ -277,6 +278,7 @@
           changeNum,
           project,
           path,
+          patchNum: EDIT_PATCHNUM,
         });
       },
 
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 98a0ae5..708cb17 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -191,8 +191,9 @@
     _generateUrl(params) {
       const base = this.getBaseUrl();
       let url = '';
+      const Views = Gerrit.Nav.View;
 
-      if (params.view === Gerrit.Nav.View.SEARCH) {
+      if (params.view === Views.SEARCH) {
         const operators = [];
         if (params.owner) {
           operators.push('owner:' + this.encodeURL(params.owner, false));
@@ -223,7 +224,7 @@
           }
         }
         url = '/q/' + operators.join('+');
-      } else if (params.view === Gerrit.Nav.View.CHANGE) {
+      } else if (params.view === Views.CHANGE) {
         let range = this._getPatchRangeExpression(params);
         if (range.length) { range = '/' + range; }
         if (params.project) {
@@ -231,7 +232,7 @@
         } else {
           url = `/c/${params.changeNum}${range}`;
         }
-      } else if (params.view === Gerrit.Nav.View.DASHBOARD) {
+      } else if (params.view === Views.DASHBOARD) {
         if (params.sections) {
           // Custom dashboard.
           const queryParams = params.sections.map(section => {
@@ -247,11 +248,14 @@
           // User dashboard.
           url = `/dashboard/${params.user || 'self'}`;
         }
-      } else if (params.view === Gerrit.Nav.View.DIFF) {
+      } else if (params.view === Views.DIFF || params.view === Views.EDIT) {
         let range = this._getPatchRangeExpression(params);
         if (range.length) { range = '/' + range; }
 
         let suffix = `${range}/${this.encodeURL(params.path, true)}`;
+
+        if (params.view === Views.EDIT) { suffix += ',edit'; }
+
         if (params.lineNum) {
           suffix += '#';
           if (params.leftSide) { suffix += 'b'; }
@@ -263,9 +267,6 @@
         } else {
           url = `/c/${params.changeNum}${suffix}`;
         }
-        if (params.edit) {
-          url += ',edit';
-        }
       } else {
         throw new Error('Can\'t generate');
       }
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 823c701..4aae65f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -280,6 +280,17 @@
             '/c/test/+/42/2/file.cpp#b123');
       });
 
+      test('edit', () => {
+        const params = {
+          view: Gerrit.Nav.View.EDIT,
+          changeNum: '42',
+          project: 'test',
+          path: 'x+y/path.cpp',
+        };
+        assert.equal(element._generateUrl(params),
+            '/c/test/+/42/x%252By/path.cpp,edit');
+      });
+
       test('_getPatchRangeExpression', () => {
         const params = {};
         let actual = element._getPatchRangeExpression(params);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
new file mode 100644
index 0000000..bfbe11a
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
@@ -0,0 +1,48 @@
+<!--
+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="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
+
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-edit-file-controls">
+  <template>
+    <style include="shared-styles">
+      :host {
+        align-items: center;
+        display: flex;
+        justify-content: flex-end;
+      }
+      #edit {
+        margin-right: .5em;
+        text-decoration: none;
+      }
+    </style>
+    <gr-button
+        id="edit"
+        link
+        on-tap="_handleEditTap">Edit</gr-button>
+    <!-- TODO(kaspern): implement more menu. -->
+    <gr-dropdown
+        id="more"
+        hidden
+        link>More</gr-dropdown>
+  </template>
+  <script src="gr-edit-file-controls.js"></script>
+</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
new file mode 100644
index 0000000..1c87621
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -0,0 +1,34 @@
+// 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-edit-file-controls',
+
+    /**
+     * Fired when the edit button is pressed.
+     *
+     * @event edit-tap
+     */
+
+    properties: {
+      filePath: String,
+    },
+
+    _handleEditTap() {
+      this.fire('edit-tap', {path: this.filePath});
+    },
+  });
+})();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
new file mode 100644
index 0000000..250e208
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -0,0 +1,56 @@
+<!--
+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-edit-file-controls</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-edit-file-controls.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-edit-file-controls></gr-edit-file-controls>
+  </template>
+</test-fixture>
+
+<script>
+suite('gr-edit-file-controls tests', () => {
+  let element;
+  let sandbox;
+
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+    element = fixture('basic');
+  });
+
+  teardown(() => { sandbox.restore(); });
+
+  test('edit tap emits event', () => {
+    const handler = sandbox.stub();
+    element.addEventListener('edit-tap', handler);
+    element.filePath = 'foo';
+
+    MockInteractions.tap(element.$.edit);
+    assert.isTrue(handler.called);
+    assert.equal(handler.lastCall.args[0].detail.path, 'foo');
+  });
+});
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index ff144c3..df2ac93 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -36,7 +36,7 @@
       gr-fixed-panel {
         background-color: #fff;
         border-bottom: 1px #eee solid;
-        z-index: 1;
+        z-index: 10;
       }
       header,
       .subHeader {
@@ -94,7 +94,7 @@
       </header>
     </gr-fixed-panel>
     <div class="textareaWrapper">
-        <textarea id="file">{{_newContent}}</textarea>
+      <textarea value="{{_newContent::input}}" id="file"></textarea>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index b8a3f9d..86594d3 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -111,11 +111,11 @@
           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)));
+                .then(res => res.text);
           }
           return '';
         }
-        return res.text().then(text => atob(text));
+        return res.text;
       });
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index c124e54..c206b20 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -1265,7 +1265,14 @@
       const e = '/edit/' + encodeURIComponent(path);
       let payload = null;
       if (opt_base) { payload = {base: true}; }
-      return this.getChangeURLAndSend(changeNum, 'GET', null, e, payload);
+      return this.getChangeURLAndSend(changeNum, 'GET', null, e, payload)
+          .then(res => {
+            if (!res.ok) { return res; }
+            return res.text().then(text => {
+              res.text = atob(text);
+              return res;
+            });
+          });
     },
 
     rebaseChangeEdit(changeNum) {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index ace6a2d..ee3111e 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -102,6 +102,7 @@
     '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-edit-file-controls/gr-edit-file-controls_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',