Add Publish button to editor

Add a Publish button to editor view that saves the edit, publishes
the edit and then navigates user to the change page.

Change-Id: I927f0f9db3f72ed2b416903b52fe9bb86fc08f60
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index a9024ef..814ae8d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1833,8 +1833,6 @@
   }
 
   _handlePublishEditTap() {
-    // Type of payload is PublishChangeEditInput.
-    const payload = {notify: NotifyType.NONE};
     if (!this.actions.publishEdit) {
       return;
     }
@@ -1842,7 +1840,7 @@
       '/edit:publish',
       assertUIActionInfo(this.actions.publishEdit),
       false,
-      payload
+      {notify: NotifyType.NONE}
     );
   }
 
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 874614b..a0562de 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -34,7 +34,10 @@
 import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util';
 import {computeTruncatedPath} from '../../../utils/path-list-util';
 import {customElement, property} from '@polymer/decorators';
-import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {
+  RestApiService,
+  ErrorCallback,
+} from '../../../services/services/gr-rest-api/gr-rest-api';
 import {
   ChangeInfo,
   PatchSetNum,
@@ -43,11 +46,14 @@
   NumericChangeId,
 } from '../../../types/common';
 import {GrStorage} from '../../shared/gr-storage/gr-storage';
+import {HttpMethod, NotifyType} from '../../../constants/constants';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const SAVING_MESSAGE = 'Saving changes...';
 const SAVED_MESSAGE = 'All changes saved';
 const SAVE_FAILED_MSG = 'Failed to save changes';
+const PUBLISHING_EDIT_MSG = 'Publishing edit...';
+const PUBLISH_FAILED_MSG = 'Failed to publish edit';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
 
@@ -335,6 +341,34 @@
     });
   }
 
+  _handlePublishTap() {
+    if (!this._changeNum) throw new Error('missing changeNum');
+
+    const changeNum = this._changeNum;
+    this._saveEdit().then(() => {
+      const handleError: ErrorCallback = response => {
+        this._showAlert(PUBLISH_FAILED_MSG);
+        console.error(response);
+      };
+
+      this._showAlert(PUBLISHING_EDIT_MSG);
+
+      this.$.restAPI
+        .executeChangeAction(
+          changeNum,
+          HttpMethod.POST,
+          '/edit:publish',
+          undefined,
+          {notify: NotifyType.NONE},
+          handleError
+        )
+        .then(() => {
+          if (!this._change) throw new Error('missing change');
+          GerritNav.navigateToChange(this._change);
+        });
+    });
+  }
+
   _handleContentChange(e: CustomEvent<{value: string}>) {
     this.debounce(
       'store',
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
index 8eaee7d..1e05232 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
@@ -103,6 +103,15 @@
           on-click="_handleSaveTap"
           >Save</gr-button
         >
+        <gr-button
+          id="publish"
+          link=""
+          primary=""
+          title="Publish your edit. A new patchset will be created."
+          on-click="_handlePublishTap"
+          disabled$="[[_saveDisabled]]"
+          >Save & Publish</gr-button
+        >
       </span>
     </header>
   </div>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
index 03f3987..b04277e 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
@@ -19,6 +19,7 @@
 import './gr-editor-view.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
+import {HttpMethod} from '../../../constants/constants.js';
 
 const basicFixture = fixtureFromElement('gr-editor-view');
 
@@ -194,6 +195,43 @@
       });
     });
 
+    test('file modification and publish', () => {
+      const saveSpy = sinon.spy(element, '_saveEdit');
+      const alertStub = sinon.stub(element, '_showAlert');
+      const changeActionsStub =
+        sinon.stub(element.$.restAPI, 'executeChangeAction');
+      saveFileStub.returns(Promise.resolve({ok: true}));
+      element._newContent = newText;
+      flush();
+
+      assert.isFalse(element._saving);
+      assert.isFalse(element.$.save.hasAttribute('disabled'));
+
+      MockInteractions.tap(element.$.publish);
+      assert.isTrue(saveSpy.called);
+      assert.equal(alertStub.getCall(0).args[0], 'Saving changes...');
+      assert.isTrue(element._saving);
+      assert.isTrue(element.$.save.hasAttribute('disabled'));
+
+      return saveSpy.lastCall.returnValue.then(() => {
+        assert.isTrue(saveFileStub.called);
+        assert.isFalse(element._saving);
+
+        assert.equal(alertStub.getCall(1).args[0], 'All changes saved');
+        assert.equal(alertStub.getCall(2).args[0], 'Publishing edit...');
+
+        assert.isTrue(element.$.save.hasAttribute('disabled'));
+        assert.equal(element._content, element._newContent);
+        assert.isTrue(element._successfulSave);
+        assert.isFalse(navigateStub.called);
+
+        const args = changeActionsStub.lastCall.args;
+        assert.equal(args[0], '42');
+        assert.equal(args[1], HttpMethod.POST);
+        assert.equal(args[2], '/edit:publish');
+      });
+    });
+
     test('file modification and close', () => {
       const closeSpy = sinon.spy(element, '_handleCloseTap');
       element._newContent = newText;