Merge "Consolidate button styles and update disabled"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 79f06fe..aa40a6e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -104,7 +104,7 @@
     },
 
     _computeProjectURL(project) {
-      return Gerrit.Nav.getUrlForProject(project, true);
+      return Gerrit.Nav.getUrlForProjectChanges(project, true);
     },
 
     _computeProjectBranchURL(change) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index e95c494..32fa7a6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -314,7 +314,7 @@
     },
 
     _computeProjectURL(project) {
-      return Gerrit.Nav.getUrlForProject(project);
+      return Gerrit.Nav.getUrlForProjectChanges(project);
     },
 
     _computeBranchURL(project, branch) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 21cd100..02a0514 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -39,6 +39,8 @@
 
   const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500;
 
+  const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
+
   Polymer({
     is: 'gr-change-view',
 
@@ -298,7 +300,8 @@
     },
 
     _handleCommitMessageSave(e) {
-      const message = e.detail.content;
+      // Trim trailing whitespace from each line.
+      const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
 
       this.$.jsAPI.handleCommitMessage(this._change, message);
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 6c95046..d0f3d90 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -638,6 +638,22 @@
           _change));
     });
 
+    test('_handleCommitMessageSave trims trailing whitespace', () => {
+      const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
+          .returns(Promise.resolve({}));
+
+      const mockEvent = content => { return {detail: {content}}; };
+
+      element._handleCommitMessageSave(mockEvent('test \n  test '));
+      assert.equal(putStub.lastCall.args[1], 'test\n  test');
+
+      element._handleCommitMessageSave(mockEvent('  test\ntest'));
+      assert.equal(putStub.lastCall.args[1], '  test\ntest');
+
+      element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
+      assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
+    });
+
     test('_computeChangeIdCommitMessageError', () => {
       let commitMessage =
         'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
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 054f0f1..ac15882 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
@@ -430,6 +430,10 @@
     },
 
     _getFiles() {
+      if (this.editLoaded) {
+        return this.$.restAPI.getChangeEditFilesAsSpeciallySortedArray(
+            this.changeNum, this.patchRange);
+      }
       return this.$.restAPI.getChangeFilesAsSpeciallySortedArray(
           this.changeNum, this.patchRange);
     },
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 be03e38..7e9f843 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
@@ -116,6 +116,48 @@
       });
     });
 
+    test('get file list with change edit', done => {
+      element.editLoaded = true;
+
+      sandbox.stub(element.$.restAPI,
+          'getChangeEditFiles', () => {
+            return Promise.resolve({
+              commit: {},
+              files: {
+                '/COMMIT_MSG': {
+                  lines_inserted: 9,
+                },
+                'tags.html': {
+                  lines_deleted: 123,
+                },
+                'about.txt': {},
+              },
+            });
+          });
+
+      element._getFiles().then(files => {
+        const filenames = files.map(f => { return f.__path; });
+        assert.deepEqual(filenames, ['/COMMIT_MSG', 'about.txt', 'tags.html']);
+        assert.deepEqual(files[0], {
+          lines_inserted: 9,
+          lines_deleted: 0,
+          __path: '/COMMIT_MSG',
+        });
+        assert.deepEqual(files[1], {
+          lines_inserted: 0,
+          lines_deleted: 0,
+          __path: 'about.txt',
+        });
+        assert.deepEqual(files[2], {
+          lines_inserted: 0,
+          lines_deleted: 123,
+          __path: 'tags.html',
+        });
+
+        done();
+      });
+    });
+
     test('calculate totals for patch number', () => {
       element._files = [
         {__path: '/COMMIT_MSG', lines_inserted: 9},
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 6ff78d8..afb3585 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -130,7 +130,7 @@
        *     the project.
        * @return {string}
        */
-      getUrlForProject(project, opt_openOnly) {
+      getUrlForProjectChanges(project, opt_openOnly) {
         return this._getUrlFor({
           view: Gerrit.Nav.View.SEARCH,
           project,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 335ed36..dc75073 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -46,7 +46,8 @@
       :host([disabled]) {
         pointer-events: none;
       }
-      :host([disabled]) .container {
+      :host([disabled]) .body,
+      :host([disabled]) .date {
         opacity: .5;
       }
       :host([is-robot-comment]) {
@@ -210,6 +211,12 @@
       #deleteBtn.showDeleteButtons {
         display: block;
       }
+      #savingMessage {
+        display: none;
+      }
+      :host([disabled]) #savingMessage {
+        display: inline;
+      }
     </style>
     <div id="container"
         class="container"
@@ -224,6 +231,7 @@
               title="This draft is only visible to you. To publish drafts, click the red 'Reply' button at the top of the change or press the 'A' key."
               max-width="20em"
               show-icon></gr-tooltip-content>
+          <span id="savingMessage">[[_savingMessage]]</span>
         </div>
         <div class="headerMiddle">
           <span class="collapsedContent">[[comment.message]]</span>
@@ -249,64 +257,66 @@
           </label>
         </div>
       </div>
-      <template is="dom-if" if="[[comment.robot_id]]">
-        <div class="robotId" hidden$="[[collapsed]]">
-          [[comment.robot_id]]
+      <div class="body">
+        <template is="dom-if" if="[[comment.robot_id]]">
+          <div class="robotId" hidden$="[[collapsed]]">
+            [[comment.robot_id]]
+          </div>
+        </template>
+        <gr-textarea
+            id="editTextarea"
+            class="editMessage"
+            autocomplete="on"
+            monospace
+            disabled="{{disabled}}"
+            rows="4"
+            text="{{_messageText}}"></gr-textarea>
+        <gr-formatted-text class="message"
+            content="[[comment.message]]"
+            no-trailing-margin="[[!comment.__draft]]"
+            collapsed="[[collapsed]]"
+            config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+        <div hidden$="[[!comment.robot_run_id]]">
+          <div class="runIdInformation" hidden$="[[collapsed]]">
+            Run ID:
+            <a class="robotRunLink" href$="[[comment.url]]">
+              <span class="robotRun">[[comment.robot_run_id]]</span>
+            </a>
+          </div>
         </div>
-      </template>
-      <gr-textarea
-          id="editTextarea"
-          class="editMessage"
-          autocomplete="on"
-          monospace
-          disabled="{{disabled}}"
-          rows="4"
-          text="{{_messageText}}"></gr-textarea>
-      <gr-formatted-text class="message"
-          content="[[comment.message]]"
-          no-trailing-margin="[[!comment.__draft]]"
-          collapsed="[[collapsed]]"
-          config="[[projectConfig.commentlinks]]"></gr-formatted-text>
-      <div hidden$="[[!comment.robot_run_id]]">
-        <div class="runIdInformation" hidden$="[[collapsed]]">
-          Run ID:
-          <a class="robotRunLink" href$="[[comment.url]]">
-            <span class="robotRun">[[comment.robot_run_id]]</span>
-          </a>
+        <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
+          <div class="action resolve hideOnPublished">
+            <label>
+              <input type="checkbox"
+                  checked$="[[resolved]]"
+                  on-change="_handleToggleResolved">
+              Resolved
+            </label>
+          </div>
+          <div class="action unresolved hideOnPublished" hidden$="[[resolved]]">
+            Unresolved
+          </div>
+          <div class="rightActions">
+            <gr-button link class="action cancel hideOnPublished"
+                on-tap="_handleCancel" hidden>Cancel</gr-button>
+            <gr-button link class="action discard hideOnPublished"
+                on-tap="_handleDiscard">Discard</gr-button>
+            <gr-button link class="action edit hideOnPublished"
+                on-tap="_handleEdit">Edit</gr-button>
+            <gr-button link class="action save hideOnPublished"
+                on-tap="_handleSave"
+                disabled$="[[_computeSaveDisabled(_messageText)]]">Save
+            </gr-button>
+          </div>
         </div>
-      </div>
-      <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
-        <div class="action resolve hideOnPublished">
-          <label>
-            <input type="checkbox"
-                checked$="[[resolved]]"
-                on-change="_handleToggleResolved">
-            Resolved
-          </label>
-        </div>
-        <div class="action unresolved hideOnPublished" hidden$="[[resolved]]">
-          Unresolved
-        </div>
-        <div class="rightActions">
-          <gr-button link class="action cancel hideOnPublished"
-              on-tap="_handleCancel" hidden>Cancel</gr-button>
-          <gr-button link class="action discard hideOnPublished"
-              on-tap="_handleDiscard">Discard</gr-button>
-          <gr-button link class="action edit hideOnPublished"
-              on-tap="_handleEdit">Edit</gr-button>
-          <gr-button link class="action save hideOnPublished"
-              on-tap="_handleSave"
-              disabled$="[[_computeSaveDisabled(_messageText)]]">Save
+        <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
+          <gr-button link class="action fix"
+              on-tap="_handleFix"
+              disabled="[[robotButtonDisabled]]">
+            Please Fix
           </gr-button>
         </div>
       </div>
-      <div class="actions robotActions" hidden$="[[!_showRobotActions]]">
-        <gr-button link class="action fix"
-            on-tap="_handleFix"
-            disabled="[[robotButtonDisabled]]">
-          Please Fix
-        </gr-button>
-      </div>
     </div>
     <gr-overlay id="confirmDeleteOverlay" with-backdrop>
       <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index e8717b5..c5dd524 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -21,6 +21,8 @@
   const DRAFT_SINGULAR = 'draft...';
   const DRAFT_PLURAL = 'drafts...';
   const SAVED_MESSAGE = 'All changes saved';
+  const SAVING_PROGRESS_MESSAGE = 'Saving draft...';
+  const DiSCARDING_PROGRESS_MESSAGE = 'Discarding draft...';
 
   Polymer({
     is: 'gr-diff-comment',
@@ -120,6 +122,8 @@
         type: Object,
         value: {number: 0}, // Intentional to share the object across instances.
       },
+
+      _savingMessage: String,
     },
 
     observers: [
@@ -433,6 +437,7 @@
       if (!this.comment.__draft) {
         throw Error('Cannot discard a non-draft comment.');
       }
+      this._savingMessage = DiSCARDING_PROGRESS_MESSAGE;
       this.editing = false;
       this.disabled = true;
       this._eraseDraftComment();
@@ -497,6 +502,7 @@
     },
 
     _saveDraft(draft) {
+      this._savingMessage = SAVING_PROGRESS_MESSAGE;
       this._showStartRequest();
       return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
           .then(result => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 1dc6c45..0d0b90e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -698,5 +698,22 @@
         assert.equal(element._numPendingDraftRequests.number, 0);
       });
     });
+
+    suite('saving progress indicators', () => {
+      setup(() => {
+        sandbox.stub(element, '_deleteDraft').returns(Promise.resolve());
+        element._savingMessage = '';
+      });
+
+      test('saving', () => {
+        element._saveDraft();
+        assert.equal(element._savingMessage, 'Saving draft...');
+      });
+
+      test('discarding', () => {
+        element._discardDraft();
+        assert.equal(element._savingMessage, 'Discarding draft...');
+      });
+    });
   });
 </script>
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 b6a2ad2..c6cf7ce 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
@@ -99,6 +99,8 @@
       <gr-endpoint-decorator name="editor">
         <gr-endpoint-param name="fileContent" value="[[_newContent]]">
         </gr-endpoint-param>
+        <gr-endpoint-param name="prefs" value="[[_prefs]]">
+        </gr-endpoint-param>
         <textarea value="{{_newContent::input}}" id="file"></textarea>
       </gr-endpoint-decorator>
     </div>
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 5652793..52ce943 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
@@ -35,7 +35,6 @@
       _change: Object,
       _changeEditDetail: Object,
       _changeNum: String,
-      _loggedIn: Boolean,
       _path: String,
       _content: String,
       _newContent: String,
@@ -44,6 +43,7 @@
         value: true,
         computed: '_computeSaveDisabled(_content, _newContent)',
       },
+      _prefs: Object,
     },
 
     behaviors: [
@@ -53,13 +53,17 @@
     ],
 
     attached() {
-      this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; });
+      this._getEditPrefs().then(prefs => { this._prefs = prefs; });
     },
 
     _getLoggedIn() {
       return this.$.restAPI.getLoggedIn();
     },
 
+    _getEditPrefs() {
+      return this.$.restAPI.getEditPrefs();
+    },
+
     _paramsChanged(value) {
       if (value.view !== Gerrit.Nav.View.EDIT) { return; }
 
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index e3e6474..b3bcb22 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -46,7 +46,7 @@
 
   setup(() => {
     stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
+      getEditPrefs() { return Promise.resolve({}); },
     });
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
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 9a7a59e..3bb0178 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
@@ -915,6 +915,18 @@
 
     /**
      * @param {number|string} changeNum
+     * @param {!Promise<?Object>} patchRange
+     */
+    getChangeEditFiles(changeNum, patchRange) {
+      let endpoint = '/edit?list';
+      if (patchRange.basePatchNum !== 'PARENT') {
+        endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum);
+      }
+      return this._getChangeURLAndFetch(changeNum, endpoint);
+    },
+
+    /**
+     * @param {number|string} changeNum
      * @param {number|string} patchNum
      * @param {string} query
      * @return {!Promise<!Object>}
@@ -929,6 +941,11 @@
           this._normalizeChangeFilesResponse.bind(this));
     },
 
+    getChangeEditFilesAsSpeciallySortedArray(changeNum, patchRange) {
+      return this.getChangeEditFiles(changeNum, patchRange).then(files =>
+            this._normalizeChangeFilesResponse(files.files));
+    },
+
     /**
      * The closure compiler doesn't realize this.specialFilePathCompare is
      * valid.
@@ -1330,6 +1347,10 @@
           '/edit:publish');
     },
 
+    getEditPrefs() {
+      return this._fetchSharedCacheURL('/accounts/self/preferences.edit');
+    },
+
     putChangeCommitMessage(changeNum, message) {
       const p = {message};
       return this.getChangeURLAndSend(changeNum, 'PUT', null, '/message', p);