diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index b62acfcd445904a05bc9a85d392a20e219c8a1c5..d65bbc0d808db1dcc6b7c48f25bcd1c4833fa8d5 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
 
   GitLabDropdown.prototype.shouldPropagate = function(e) {
     var $target;
-    if (this.options.multiSelect) {
+    if (this.options.multiSelect || this.options.shouldPropagate === false) {
       $target = $(e.target);
       if ($target && !$target.hasClass('dropdown-menu-close') &&
                      !$target.hasClass('dropdown-menu-close-icon') &&
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
   };
 
   GitLabDropdown.prototype.positionMenuAbove = function() {
-    var $button = $(this.el);
     var $menu = this.dropdown.find('.dropdown-menu');
 
-    $menu.css('top', ($button.height() + $menu.height()) * -1);
+    $menu.css('top', 'initial');
+    $menu.css('bottom', '100%');
   };
 
   GitLabDropdown.prototype.hidden = function(e) {
@@ -698,7 +698,7 @@ GitLabDropdown = (function() {
 
   GitLabDropdown.prototype.noResults = function() {
     var html;
-    return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
+    return html = '<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>';
   };
 
   GitLabDropdown.prototype.rowClicked = function(el) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 3f848e0859bb793de275204ae54f81adfd552f16..470c39c6f76378ac88b33b6c69c4210f48324428 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -10,8 +10,6 @@ import ZenMode from './zen_mode';
 
 (function() {
   this.IssuableForm = (function() {
-    IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
-
     IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
 
     function IssuableForm(form) {
@@ -26,7 +24,6 @@ import ZenMode from './zen_mode';
       new ZenMode();
       this.titleField = this.form.find("input[name*='[title]']");
       this.descriptionField = this.form.find("textarea[name*='[description]']");
-      this.issueMoveField = this.form.find("#move_to_project_id");
       if (!(this.titleField.length && this.descriptionField.length)) {
         return;
       }
@@ -34,7 +31,6 @@ import ZenMode from './zen_mode';
       this.form.on("submit", this.handleSubmit);
       this.form.on("click", ".btn-cancel", this.resetAutosave);
       this.initWip();
-      this.initMoveDropdown();
       $issuableDueDate = $('#issuable-due-date');
       if ($issuableDueDate.length) {
         calendar = new Pikaday({
@@ -56,12 +52,6 @@ import ZenMode from './zen_mode';
     };
 
     IssuableForm.prototype.handleSubmit = function() {
-      var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
-      if ((parseInt(fieldId, 10) || 0) > 0) {
-        if (!confirm(this.issueMoveConfirmMsg)) {
-          return false;
-        }
-      }
       return this.resetAutosave();
     };
 
@@ -113,48 +103,6 @@ import ZenMode from './zen_mode';
       return this.titleField.val("WIP: " + (this.titleField.val()));
     };
 
-    IssuableForm.prototype.initMoveDropdown = function() {
-      var $moveDropdown, pageSize;
-      $moveDropdown = $('.js-move-dropdown');
-      if ($moveDropdown.length) {
-        pageSize = $moveDropdown.data('page-size');
-        return $('.js-move-dropdown').select2({
-          ajax: {
-            url: $moveDropdown.data('projects-url'),
-            quietMillis: 125,
-            data: function(term, page, context) {
-              return {
-                search: term,
-                offset_id: context
-              };
-            },
-            results: function(data) {
-              var context,
-                more;
-
-              if (data.length >= pageSize)
-                more = true;
-
-              if (data[data.length - 1])
-                context = data[data.length - 1].id;
-
-              return {
-                results: data,
-                more: more,
-                context: context
-              };
-            }
-          },
-          formatResult: function(project) {
-            return project.name_with_namespace;
-          },
-          formatSelection: function(project) {
-            return project.name_with_namespace;
-          }
-        });
-      }
-    };
-
     return IssuableForm;
   })();
 }).call(window);
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index eaaafd4c149353f9b82b11fef98b085d6fed4118..e115ee402199349fe62c1ca4c489122a4269caa7 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -17,10 +17,6 @@ export default {
       required: true,
       type: String,
     },
-    canMove: {
-      required: true,
-      type: Boolean,
-    },
     canUpdate: {
       required: true,
       type: Boolean,
@@ -96,10 +92,6 @@ export default {
       type: String,
       required: true,
     },
-    projectsAutocompletePath: {
-      type: String,
-      required: true,
-    },
   },
   data() {
     const store = new Store({
@@ -142,7 +134,6 @@ export default {
           confidential: this.isConfidential,
           description: this.state.descriptionText,
           lockedWarningVisible: false,
-          move_to_project_id: 0,
           updateLoading: false,
         });
       }
@@ -151,16 +142,6 @@ export default {
       this.showForm = false;
     },
     updateIssuable() {
-      const canPostUpdate = this.store.formState.move_to_project_id !== 0 ?
-        confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert
-
-      if (!canPostUpdate) {
-        this.store.setFormState({
-          updateLoading: false,
-        });
-        return;
-      }
-
       this.service.updateIssuable(this.store.formState)
         .then(res => res.json())
         .then((data) => {
@@ -239,14 +220,12 @@ export default {
     <form-component
       v-if="canUpdate && showForm"
       :form-state="formState"
-      :can-move="canMove"
       :can-destroy="canDestroy"
       :issuable-templates="issuableTemplates"
       :markdown-docs-path="markdownDocsPath"
       :markdown-preview-path="markdownPreviewPath"
       :project-path="projectPath"
       :project-namespace="projectNamespace"
-      :projects-autocomplete-path="projectsAutocompletePath"
     />
     <div v-else>
       <title-component
diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue
deleted file mode 100644
index e514bebc5f60813ee7b40da0d0ce5d5a4d33812a..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issue_show/components/fields/project_move.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<script>
-  import tooltip from '../../../vue_shared/directives/tooltip';
-
-  export default {
-    directives: {
-      tooltip,
-    },
-    props: {
-      formState: {
-        type: Object,
-        required: true,
-      },
-      projectsAutocompletePath: {
-        type: String,
-        required: true,
-      },
-    },
-    mounted() {
-      const $moveDropdown = $(this.$refs['move-dropdown']);
-
-      $moveDropdown.select2({
-        ajax: {
-          url: this.projectsAutocompletePath,
-          quietMillis: 125,
-          data(term, page, context) {
-            return {
-              search: term,
-              offset_id: context,
-            };
-          },
-          results(data) {
-            const more = data.length >= 50;
-            const context = data[data.length - 1] ? data[data.length - 1].id : null;
-
-            return {
-              results: data,
-              more,
-              context,
-            };
-          },
-        },
-        formatResult(project) {
-          return project.name_with_namespace;
-        },
-        formatSelection(project) {
-          return project.name_with_namespace;
-        },
-      })
-      .on('change', (e) => {
-        this.formState.move_to_project_id = parseInt(e.target.value, 10);
-      });
-    },
-    beforeDestroy() {
-      $(this.$refs['move-dropdown']).select2('destroy');
-    },
-  };
-</script>
-
-<template>
-  <fieldset>
-    <label
-      for="issuable-move"
-      class="sr-only">
-      Move
-    </label>
-    <div class="issuable-form-select-holder append-right-5">
-      <input
-        ref="move-dropdown"
-        type="hidden"
-        id="issuable-move"
-        data-placeholder="Move to a different project" />
-    </div>
-    <span
-      v-tooltip
-      data-placement="auto top"
-      title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
-      <i
-        class="fa fa-question-circle"
-        aria-hidden="true">
-      </i>
-    </span>
-  </fieldset>
-</template>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index d9b53bc55cf015dd8fc297740c0918f7bbb83909..6a2dd502fe2a52901cf3d4b66e798ec405d1ad8c 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -4,15 +4,10 @@
   import descriptionField from './fields/description.vue';
   import editActions from './edit_actions.vue';
   import descriptionTemplate from './fields/description_template.vue';
-  import projectMove from './fields/project_move.vue';
   import confidentialCheckbox from './fields/confidential_checkbox.vue';
 
   export default {
     props: {
-      canMove: {
-        type: Boolean,
-        required: true,
-      },
       canDestroy: {
         type: Boolean,
         required: true,
@@ -42,10 +37,6 @@
         type: String,
         required: true,
       },
-      projectsAutocompletePath: {
-        type: String,
-        required: true,
-      },
     },
     components: {
       lockedWarning,
@@ -53,7 +44,6 @@
       descriptionField,
       descriptionTemplate,
       editActions,
-      projectMove,
       confidentialCheckbox,
     },
     computed: {
@@ -93,10 +83,6 @@
       :markdown-docs-path="markdownDocsPath" />
     <confidential-checkbox
       :form-state="formState" />
-    <project-move
-      v-if="canMove"
-      :form-state="formState"
-      :projects-autocomplete-path="projectsAutocompletePath" />
     <edit-actions
       :form-state="formState"
       :can-destroy="canDestroy" />
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 60b69b300fd6c990f689094cd640aa9b20c4e7ec..8053ef57e6c314927b579a162729aa5dee8abdff 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
         props: {
           canUpdate: this.canUpdate,
           canDestroy: this.canDestroy,
-          canMove: this.canMove,
           endpoint: this.endpoint,
           issuableRef: this.issuableRef,
           initialTitleHtml: this.initialTitleHtml,
@@ -41,7 +40,6 @@ document.addEventListener('DOMContentLoaded', () => {
           markdownDocsPath: this.markdownDocsPath,
           projectPath: this.projectPath,
           projectNamespace: this.projectNamespace,
-          projectsAutocompletePath: this.projectsAutocompletePath,
           updatedAt: this.updatedAt,
           updatedByName: this.updatedByName,
           updatedByPath: this.updatedByPath,
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 0c8bd6f1cc34604540354c223727c205000b2e9f..f4639e9ed2a4f2c50d7e96a64b7f69e738245a6f 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -6,7 +6,6 @@ export default class Store {
       confidential: false,
       description: '',
       lockedWarningVisible: false,
-      move_to_project_id: 0,
       updateLoading: false,
     };
   }
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index fa958d75fa4c9995e14211f42804f482e774ef44..4c87d46c96e24dc2171e651afa60e758189b1bb7 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -157,11 +157,16 @@ import SidebarHeightManager from './sidebar_height_manager';
     Sidebar.prototype.openDropdown = function(blockOrName) {
       var $block;
       $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
-      $block.find('.edit-link').trigger('click');
       if (!this.isOpen()) {
         this.setCollapseAfterUpdate($block);
-        return this.toggleSidebar('open');
+        this.toggleSidebar('open');
       }
+
+      // Wait for the sidebar to trigger('click') open
+      // so it doesn't cause our dropdown to close preemptively
+      setTimeout(() => {
+        $block.find('.js-sidebar-dropdown-toggle').trigger('click');
+      });
     };
 
     Sidebar.prototype.setCollapseAfterUpdate = function($block) {
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
index 5a6e47e566e00e70ac5b45ddddec1efc0ac6c2eb..77f070d48cc6925987edf9554428e9faf95a9411 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
@@ -36,7 +36,7 @@ export default {
       />
       <a
         v-if="editable"
-        class="edit-link pull-right"
+        class="js-sidebar-dropdown-toggle edit-link pull-right"
         href="#"
       >
         Edit
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
new file mode 100644
index 0000000000000000000000000000000000000000..1c15a1b877a4f64a96a0fc99437b28f9855c4428
--- /dev/null
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -0,0 +1,85 @@
+/* global Flash */
+
+function isValidProjectId(id) {
+  return id > 0;
+}
+
+class SidebarMoveIssue {
+  constructor(mediator, dropdownToggle, confirmButton) {
+    this.mediator = mediator;
+
+    this.$dropdownToggle = $(dropdownToggle);
+    this.$confirmButton = $(confirmButton);
+
+    this.onConfirmClickedWrapper = this.onConfirmClicked.bind(this);
+  }
+
+  init() {
+    this.initDropdown();
+    this.addEventListeners();
+  }
+
+  destroy() {
+    this.removeEventListeners();
+  }
+
+  initDropdown() {
+    this.$dropdownToggle.glDropdown({
+      search: {
+        fields: ['name_with_namespace'],
+      },
+      showMenuAbove: true,
+      selectable: true,
+      filterable: true,
+      filterRemote: true,
+      multiSelect: false,
+      // Keep the dropdown open after selecting an option
+      shouldPropagate: false,
+      data: (searchTerm, callback) => {
+        this.mediator.fetchAutocompleteProjects(searchTerm)
+          .then(callback)
+          .catch(() => new Flash('An error occured while fetching projects autocomplete.'));
+      },
+      renderRow: project => `
+        <li>
+          <a href="#" class="js-move-issue-dropdown-item">
+            ${project.name_with_namespace}
+          </a>
+        </li>
+      `,
+      clicked: (options) => {
+        const project = options.selectedObj;
+        const selectedProjectId = options.isMarking ? project.id : 0;
+        this.mediator.setMoveToProjectId(selectedProjectId);
+
+        this.$confirmButton.attr('disabled', !isValidProjectId(selectedProjectId));
+      },
+    });
+  }
+
+  addEventListeners() {
+    this.$confirmButton.on('click', this.onConfirmClickedWrapper);
+  }
+
+  removeEventListeners() {
+    this.$confirmButton.off('click', this.onConfirmClickedWrapper);
+  }
+
+  onConfirmClicked() {
+    if (isValidProjectId(this.mediator.store.moveToProjectId)) {
+      this.$confirmButton
+        .disable()
+        .addClass('is-loading');
+
+      this.mediator.moveIssue()
+        .catch(() => {
+          Flash('An error occured while moving the issue.');
+          this.$confirmButton
+            .enable()
+            .removeClass('is-loading');
+        });
+    }
+  }
+}
+
+export default SidebarMoveIssue;
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 5a82d01dc41e7ad72dee79f157aa453a3bc61718..604648407a4bcaefad3a1503f9af7bb2bfd34650 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
 Vue.use(VueResource);
 
 export default class SidebarService {
-  constructor(endpoint) {
+  constructor(endpointMap) {
     if (!SidebarService.singleton) {
-      this.endpoint = endpoint;
+      this.endpoint = endpointMap.endpoint;
+      this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
+      this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
 
       SidebarService.singleton = this;
     }
@@ -25,4 +27,18 @@ export default class SidebarService {
       emulateJSON: true,
     });
   }
+
+  getProjectsAutocomplete(searchTerm) {
+    return Vue.http.get(this.projectsAutocompleteEndpoint, {
+      params: {
+        search: searchTerm,
+      },
+    });
+  }
+
+  moveIssue(moveToProjectId) {
+    return Vue.http.post(this.moveIssueEndpoint, {
+      move_to_project_id: moveToProjectId,
+    });
+  }
 }
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 9edded3ead639957c6bbcb911a4b96878a224dec..3d8972050a9a9de1ccc27b47b828d3703dce4878 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
 import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
 import sidebarAssignees from './components/assignees/sidebar_assignees';
 import confidential from './components/confidential/confidential_issue_sidebar.vue';
+import SidebarMoveIssue from './lib/sidebar_move_issue';
 
 import Mediator from './sidebar_mediator';
 
@@ -31,6 +32,12 @@ function domContentLoaded() {
         service: mediator.service,
       },
     }).$mount(confidentialEl);
+
+    new SidebarMoveIssue(
+      mediator,
+      $('.js-move-issue'),
+      $('.js-move-issue-confirmation-button'),
+    ).init();
   }
 
   new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 721e92221cfb953c0217c29227b4f1cab0fda65b..e38a8db4cc59e04dd131bceee7d543d9b8c0b88c 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -7,7 +7,11 @@ export default class SidebarMediator {
   constructor(options) {
     if (!SidebarMediator.singleton) {
       this.store = new Store(options);
-      this.service = new Service(options.endpoint);
+      this.service = new Service({
+        endpoint: options.endpoint,
+        moveIssueEndpoint: options.moveIssueEndpoint,
+        projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
+      });
       SidebarMediator.singleton = this;
     }
 
@@ -26,6 +30,10 @@ export default class SidebarMediator {
     return this.service.update(field, selected.length === 0 ? [0] : selected);
   }
 
+  setMoveToProjectId(projectId) {
+    this.store.setMoveToProjectId(projectId);
+  }
+
   fetch() {
     this.service.get()
       .then(response => response.json())
@@ -35,4 +43,23 @@ export default class SidebarMediator {
       })
       .catch(() => new Flash('Error occured when fetching sidebar data'));
   }
+
+  fetchAutocompleteProjects(searchTerm) {
+    return this.service.getProjectsAutocomplete(searchTerm)
+      .then(response => response.json())
+      .then((data) => {
+        this.store.setAutocompleteProjects(data);
+        return this.store.autocompleteProjects;
+      });
+  }
+
+  moveIssue() {
+    return this.service.moveIssue(this.store.moveToProjectId)
+      .then(response => response.json())
+      .then((data) => {
+        if (location.pathname !== data.web_url) {
+          gl.utils.visitUrl(data.web_url);
+        }
+      });
+  }
 }
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 3356dd0191ffd02c1b6e2fcab346ab17548bde3a..cc04a2a3fcfffa703272229ca6aaa8db6b5e6e01 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -13,6 +13,8 @@ export default class SidebarStore {
       this.isFetching = {
         assignees: true,
       };
+      this.autocompleteProjects = [];
+      this.moveToProjectId = 0;
 
       SidebarStore.singleton = this;
     }
@@ -53,4 +55,12 @@ export default class SidebarStore {
   removeAllAssignees() {
     this.assignees = [];
   }
+
+  setAutocompleteProjects(projects) {
+    this.autocompleteProjects = projects;
+  }
+
+  setMoveToProjectId(moveToProjectId) {
+    this.moveToProjectId = moveToProjectId;
+  }
 }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 8872dec4c37fd2b3cf24f56f0646ae586f6acac7..9296128282a8dc2029c1670fed14581b4a60359f 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -193,7 +193,7 @@
   min-width: 240px;
   max-width: 500px;
   margin-top: 2px;
-  margin-bottom: 0;
+  margin-bottom: 2px;
   font-size: 14px;
   font-weight: $gl-font-weight-normal;
   padding: 8px 0;
@@ -622,6 +622,11 @@
   border-top: 1px solid $dropdown-divider-color;
 }
 
+.dropdown-footer-content {
+  padding-left: 10px;
+  padding-right: 10px;
+}
+
 .dropdown-due-date-footer {
   padding-top: 0;
   margin-left: 10px;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4b0b238a767b383701461cde561f92e3c7aa9e79..6523376ccc3e5106bea7158eb9fdca86abfc391a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -473,7 +473,7 @@
     padding-top: 6px;
   }
 
-  .open .dropdown-menu {
+  .dropdown-menu {
     width: 100%;
   }
 }
@@ -486,6 +486,24 @@
   }
 }
 
+.sidebar-move-issue-dropdown {
+  @include new-style-dropdown;
+}
+
+.sidebar-move-issue-confirmation-button {
+  width: 100%;
+
+  &.is-loading {
+    .sidebar-move-issue-confirmation-loading-icon {
+      display: inline-block;
+    }
+  }
+}
+
+.sidebar-move-issue-confirmation-loading-icon {
+  display: none;
+}
+
 .detail-page-description {
   padding: 16px 0;
 
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 59be955599db840d8cd274fe4668731b216a2a2a..dfc8bd0ba817f955ad45e47111d48bc74d214704 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -41,12 +41,6 @@ class AutocompleteController < ApplicationController
     project = Project.find_by_id(params[:project_id])
     projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
 
-    no_project = {
-      id: 0,
-      name_with_namespace: 'No project'
-    }
-    projects.unshift(no_project) unless params[:offset_id].present?
-
     render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
   end
 
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 349b19f72e20457e6109df38069f4ab342bd8e3a..0d4266f0899fc758c7c7b14bf35a42b86461a915 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController
   before_action :authorize_create_issue!, only: [:new, :create]
 
   # Allow modify issue
-  before_action :authorize_update_issue!, only: [:edit, :update]
+  before_action :authorize_update_issue!, only: [:edit, :update, :move]
 
   # Allow create a new branch and empty WIP merge request from current issue
   before_action :authorize_create_merge_request!, only: [:create_merge_request]
@@ -142,25 +142,33 @@ class Projects::IssuesController < Projects::ApplicationController
 
     @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
 
+    respond_to do |format|
+      format.html do
+        recaptcha_check_with_fallback { render :edit }
+      end
+
+      format.json do
+        render_issue_json
+      end
+    end
+
+  rescue ActiveRecord::StaleObjectError
+    render_conflict_response
+  end
+
+  def move
+    params.require(:move_to_project_id)
+
     if params[:move_to_project_id].to_i > 0
       new_project = Project.find(params[:move_to_project_id])
       return render_404 unless issue.can_move?(current_user, new_project)
 
-      move_service = Issues::MoveService.new(project, current_user)
-      @issue = move_service.execute(@issue, new_project)
+      @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
     end
 
     respond_to do |format|
-      format.html do
-        recaptcha_check_with_fallback { render :edit }
-      end
-
       format.json do
-        if @issue.valid?
-          render json: serializer.represent(@issue)
-        else
-          render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
-        end
+        render_issue_json
       end
     end
 
@@ -271,6 +279,14 @@ class Projects::IssuesController < Projects::ApplicationController
     return render_404 unless @project.feature_available?(:issues, current_user)
   end
 
+  def render_issue_json
+    if @issue.valid?
+      render json: serializer.represent(@issue)
+    else
+      render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
+    end
+  end
+
   def issue_params
     params.require(:issue).permit(*issue_params_attributes)
   end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index ff305fa39b4602a55e7910fb8ed3f526df7afbcd..5089da519dfd4edd6bcbc0c1ad57a7e720591189 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -97,9 +97,11 @@ module DropdownsHelper
     end
   end
 
-  def dropdown_footer(&block)
+  def dropdown_footer(add_content_class: false, &block)
     content_tag(:div, class: "dropdown-footer") do
-      if block
+      if add_content_class
+        content_tag(:div, capture(&block), class: "dropdown-footer-content")
+      else
         capture(&block)
       end
     end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 0fcd33470956f9ec9ea7187eaa33852023ba95fe..d81ba2c06ebbfd4899ad2bfeaaadaaa87b4a5fa7 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -207,12 +207,10 @@ module IssuablesHelper
       endpoint: project_issue_path(@project, issuable),
       canUpdate: can?(current_user, :update_issue, issuable),
       canDestroy: can?(current_user, :destroy_issue, issuable),
-      canMove: current_user ? issuable.can_move?(current_user) : false,
       issuableRef: issuable.to_reference,
       isConfidential: issuable.confidential,
       markdownPreviewPath: preview_markdown_path(@project),
       markdownDocsPath: help_page_path('user/markdown'),
-      projectsAutocompletePath: autocomplete_projects_path(project_id: @project.id),
       issuableTemplates: issuable_templates(issuable),
       projectPath: ref_project.path,
       projectNamespace: ref_project.namespace.full_path,
@@ -354,6 +352,8 @@ module IssuablesHelper
   def issuable_sidebar_options(issuable, can_edit_issuable)
     {
       endpoint: "#{issuable_json_path(issuable)}?basic=true",
+      moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
+      projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
       editable: can_edit_issuable,
       currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
       rootPath: root_path,
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
index f44a9d49a54ab67bdf5560aa4342da639fbd55fa..e8394eab21356370c72e218f43e765544ff42c3d 100644
--- a/app/views/projects/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -3,7 +3,7 @@
     Due date
     - if can?(current_user, :admin_issue, @project)
       = icon("spinner spin", class: "block-loading")
-      = link_to "Edit", "#", class: "edit-link pull-right"
+      = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
   .value
     .value-content
       %span.no-value{ "v-if" => "!issue.dueDate" }
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
index 7d0c35fe183b71a7631ff0ef683c34839671bb91..6b389736e8b0bd5ae6ed7b965ac22522e15bd7b1 100644
--- a/app/views/projects/boards/components/sidebar/_labels.html.haml
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -3,7 +3,7 @@
     Labels
     - if can?(current_user, :admin_issue, @project)
       = icon("spinner spin", class: "block-loading")
-      = link_to "Edit", "#", class: "edit-link pull-right"
+      = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
   .value.issuable-show-labels
     %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
       None
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 002e9994ee0edba65c74cc51d246d67c6b3bfa9b..a1ddb261ea3b1d7d30df2e22ad40ea02beec9bbc 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -3,7 +3,7 @@
     Milestone
     - if can?(current_user, :admin_issue, @project)
       = icon("spinner spin", class: "block-loading")
-      = link_to "Edit", "#", class: "edit-link pull-right"
+      = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
   .value
     %span.no-value{ "v-if" => "!issue.milestone" }
       None
diff --git a/app/views/shared/icons/_icon_arrow_right.svg.erb b/app/views/shared/icons/_icon_arrow_right.svg.erb
new file mode 100644
index 0000000000000000000000000000000000000000..24d64eb73bd22d210cb46437e788de25d27af179
--- /dev/null
+++ b/app/views/shared/icons/_icon_arrow_right.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></svg>
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c016aa2abcde3b935bef0de790339e9161d6197b..bb02dfa0d3af401c4daf09764ce4f9160c3108be 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -29,18 +29,6 @@
 
 = render 'shared/issuable/form/metadata', issuable: issuable, form: form
 
-- if issuable.can_move?(current_user)
-  %hr
-  .form-group
-    = label_tag :move_to_project_id, 'Move', class: 'control-label'
-    .col-sm-10
-      .issuable-form-select-holder
-        = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
-      &nbsp;
-      %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
-      title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
-        = icon('question-circle')
-
 = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
 
 = render 'shared/issuable/form/merge_params', issuable: issuable
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index c3f25c9d2554493ffc32047217d33181dfb4f10b..b07bc45512f4052c7c0083a64750e2d2c8d15df1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -34,7 +34,7 @@
           Milestone
           = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
           - if can_edit_issuable
-            = link_to 'Edit', '#', class: 'edit-link pull-right'
+            = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
         .value.hide-collapsed
           - if issuable.milestone
             = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
@@ -60,7 +60,7 @@
             Due date
             = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
             - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
-              = link_to 'Edit', '#', class: 'edit-link pull-right'
+              = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
           .value.hide-collapsed
             %span.value-content
               - if issuable.due_date
@@ -95,7 +95,7 @@
             Labels
             = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
             - if can_edit_issuable
-              = link_to 'Edit', '#', class: 'edit-link pull-right'
+              = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
           .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
             - if selected_labels.any?
               - selected_labels.each do |label|
@@ -141,5 +141,22 @@
             %cite{ title: project_ref }
               = project_ref
           = clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left")
+      - if current_user && issuable.can_move?(current_user)
+        .block.js-sidebar-move-issue-block
+          .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: 'Move issue' }
+            = custom_icon('icon_arrow_right')
+          .dropdown.sidebar-move-issue-dropdown.hide-collapsed
+            %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
+              data: { toggle: 'dropdown' } }
+              Move issue
+            .dropdown-menu.dropdown-menu-selectable
+              = dropdown_title('Move issue')
+              = dropdown_filter('Search project', search_id: 'sidebar-move-issue-dropdown-search')
+              = dropdown_content
+              = dropdown_loading
+              = dropdown_footer add_content_class: true do
+                %button.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true }
+                  Move
+                  = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
 
     %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 57392cd7fbb2122629bfe9b01e17291b5f19b4a5..58782fa5f5853dbc1179e310bee3c9c79a1d8b4e 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -13,7 +13,7 @@
     Assignee
     = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
     - if can_edit_issuable
-      = link_to 'Edit', '#', class: 'edit-link pull-right'
+      = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
     - if !signed_in
       %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
         = sidebar_gutter_toggle_icon
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
index 66091d95a9199a93a000fe4838605b89cd23cc08..9b2b6e572e71885090888e8e584b82c0005c1c1e 100644
--- a/app/views/shared/issuable/form/_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_issue_assignee.html.haml
@@ -11,7 +11,7 @@
     Assignee
     = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
     - if can_edit_issuable
-      = link_to 'Edit', '#', class: 'edit-link pull-right'
+      = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
   .value.hide-collapsed
     - if assignees.any?
       - assignees.each do |assignee|
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
index 18011d528a0bef796815e205cc57f6eae10450ff..bf8613b0f0d21f0ae343c88898b54729954dbd06 100644
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -9,7 +9,7 @@
     Assignee
     = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
     - if can_edit_issuable
-      = link_to 'Edit', '#', class: 'edit-link pull-right'
+      = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
   .value.hide-collapsed
     - if merge_request.assignee
       = link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 40379f48393e7186e4c9df4595ba75c59753fc97..2ae0145727cbcf59bb270a860f00bd088c0ae69b 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -21,7 +21,7 @@
       .title
         Start date
         - if @project && can?(current_user, :admin_milestone, @project)
-          = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right'
+          = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
       .value
         %span.value-content
           - if milestone.start_date
@@ -51,7 +51,7 @@
       .title.hide-collapsed
         Due date
         - if @project && can?(current_user, :admin_milestone, @project)
-          = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right'
+          = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
       .value.hide-collapsed
         %span.value-content
           - if milestone.due_date
diff --git a/changelogs/unreleased/34261-move-move-to-sidebar.yml b/changelogs/unreleased/34261-move-move-to-sidebar.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59fa1d4c2218551f1d978b8e2e8ab6c330cd6e50
--- /dev/null
+++ b/changelogs/unreleased/34261-move-move-to-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Move "Move issue" controls to right-sidebar
+merge_request:
+author:
+type: changed
diff --git a/config/routes/project.rb b/config/routes/project.rb
index c703a7294ed69de5544bdb8883987cb3d5f053b7..a15e7f8a344bb9c4f5720a8f399254f4990dfff6 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -303,6 +303,7 @@ constraints(ProjectUrlConstrainer.new) do
         member do
           post :toggle_subscription
           post :mark_as_spam
+          post :move
           get :referenced_merge_requests
           get :related_branches
           get :can_create_branch
diff --git a/doc/user/project/issues/img/sidebar_move_issue.png b/doc/user/project/issues/img/sidebar_move_issue.png
new file mode 100644
index 0000000000000000000000000000000000000000..111f786136417f6a4e4bec390092fbd11dbe8946
Binary files /dev/null and b/doc/user/project/issues/img/sidebar_move_issue.png differ
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 20901e01f6e8f962ac4ce57c727f9486cf832449..0f187946a4a2fc72713efd724f586c5ce7a4b0b7 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -86,6 +86,10 @@ Read through the [documentation on creating issues](create_new_issue.md).
 
 Learn distinct ways to [close issues](closing_issues.md) in GitLab.
 
+## Moving issues
+
+Read through the [documentation on moving issues](moving_issues.md).
+
 ## Create a merge request from an issue
 
 Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md#18-new-merge-request).
diff --git a/doc/user/project/issues/moving_issues.md b/doc/user/project/issues/moving_issues.md
new file mode 100644
index 0000000000000000000000000000000000000000..211a651b89e56ecb9b93d47f731e88f5d99b47dc
--- /dev/null
+++ b/doc/user/project/issues/moving_issues.md
@@ -0,0 +1,10 @@
+# Moving Issues
+
+Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
+
+Moving an issue will close it and duplicate it on the specified project.
+There will also be a system note added to both issues indicating where it came from or went to.
+
+You can move an issue with the "Move issue" button at the bottom of the right-sidebar when viewing the issue.
+
+![move issue - button](img/sidebar_move_issue.png)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 572b567cddffeb342f97a02c78bad92c80ca0e12..be27bbb42831f52b6bad701d38524cbe81a2cd94 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -241,13 +241,10 @@ describe AutocompleteController do
 
         it 'returns projects' do
           expect(json_response).to be_kind_of(Array)
-          expect(json_response.size).to eq(2)
-
-          expect(json_response.first['id']).to eq(0)
-          expect(json_response.first['name_with_namespace']).to eq 'No project'
+          expect(json_response.size).to eq(1)
 
-          expect(json_response.last['id']).to eq authorized_project.id
-          expect(json_response.last['name_with_namespace']).to eq authorized_project.name_with_namespace
+          expect(json_response.first['id']).to eq authorized_project.id
+          expect(json_response.first['name_with_namespace']).to eq authorized_project.name_with_namespace
         end
       end
     end
@@ -265,10 +262,10 @@ describe AutocompleteController do
 
         it 'returns projects' do
           expect(json_response).to be_kind_of(Array)
-          expect(json_response.size).to eq(2)
+          expect(json_response.size).to eq(1)
 
-          expect(json_response.last['id']).to eq authorized_search_project.id
-          expect(json_response.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace
+          expect(json_response.first['id']).to eq authorized_search_project.id
+          expect(json_response.first['name_with_namespace']).to eq authorized_search_project.name_with_namespace
         end
       end
     end
@@ -292,7 +289,7 @@ describe AutocompleteController do
 
         it 'returns projects' do
           expect(json_response).to be_kind_of(Array)
-          expect(json_response.size).to eq 3 # Of a total of 4
+          expect(json_response.size).to eq 2 # Of a total of 3
         end
       end
     end
@@ -312,9 +309,9 @@ describe AutocompleteController do
           get(:projects, project_id: project.id, offset_id: authorized_project.id)
         end
 
-        it 'returns "No project"' do
-          expect(json_response.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
-          expect(json_response.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
+        it 'returns projects' do
+          expect(json_response).to be_kind_of(Array)
+          expect(json_response.size).to eq 2 # Of a total of 3
         end
       end
     end
@@ -331,10 +328,9 @@ describe AutocompleteController do
           get(:projects, project_id: project.id)
         end
 
-        it 'returns a single "No project"' do
+        it 'returns no projects' do
           expect(json_response).to be_kind_of(Array)
-          expect(json_response.size).to eq(1) # 'No project'
-          expect(json_response.first['id']).to eq 0
+          expect(json_response.size).to eq(0)
         end
       end
     end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 65f4d09cfce8d10eb6528b4c1c87ab3ab971d3ac..5d9403c23ac0bbb2a3ba37ad0a84dc5dec2a26dc 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -233,144 +233,119 @@ describe Projects::IssuesController do
       end
     end
 
-    context 'when moving issue to another private project' do
-      let(:another_project) { create(:project, :private) }
-
-      context 'when user has access to move issue' do
-        before do
-          another_project.team << [user, :reporter]
-        end
-
-        it 'moves issue to another project' do
-          move_issue
+    context 'Akismet is enabled' do
+      let(:project) { create(:project_empty_repo, :public) }
 
-          expect(response).to have_http_status :found
-          expect(another_project.issues).not_to be_empty
-        end
+      before do
+        stub_application_setting(recaptcha_enabled: true)
+        allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
       end
 
-      context 'when user does not have access to move issue' do
-        it 'responds with 404' do
-          move_issue
+      context 'when an issue is not identified as spam' do
+        before do
+          allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+          allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
+        end
 
-          expect(response).to have_http_status :not_found
+        it 'normally updates the issue' do
+          expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo')
         end
       end
 
-      context 'Akismet is enabled' do
-        let(:project) { create(:project_empty_repo, :public) }
-
+      context 'when an issue is identified as spam' do
         before do
-          stub_application_setting(recaptcha_enabled: true)
-          allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+          allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
         end
 
-        context 'when an issue is not identified as spam' do
-          before do
-            allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
-            allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
+        context 'when captcha is not verified' do
+          def update_spam_issue
+            update_issue(title: 'Spam Title', description: 'Spam lives here')
           end
 
-          it 'normally updates the issue' do
-            expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo')
+          before do
+            allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
           end
-        end
 
-        context 'when an issue is identified as spam' do
-          before do
-            allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+          it 'rejects an issue recognized as a spam' do
+            expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true)
+            expect { update_spam_issue }.not_to change { issue.reload.title }
           end
 
-          context 'when captcha is not verified' do
-            def update_spam_issue
-              update_issue(title: 'Spam Title', description: 'Spam lives here')
-            end
+          it 'rejects an issue recognized as a spam when recaptcha disabled' do
+            stub_application_setting(recaptcha_enabled: false)
 
-            before do
-              allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
-            end
+            expect { update_spam_issue }.not_to change { issue.reload.title }
+          end
 
-            it 'rejects an issue recognized as a spam' do
-              expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true)
-              expect { update_spam_issue }.not_to change { issue.reload.title }
-            end
+          it 'creates a spam log' do
+            update_spam_issue
 
-            it 'rejects an issue recognized as a spam when recaptcha disabled' do
-              stub_application_setting(recaptcha_enabled: false)
+            spam_logs = SpamLog.all
 
-              expect { update_spam_issue }.not_to change { issue.reload.title }
-            end
+            expect(spam_logs.count).to eq(1)
+            expect(spam_logs.first.title).to eq('Spam Title')
+            expect(spam_logs.first.recaptcha_verified).to be_falsey
+          end
 
-            it 'creates a spam log' do
+          context 'as HTML' do
+            it 'renders verify template' do
               update_spam_issue
 
-              spam_logs = SpamLog.all
-
-              expect(spam_logs.count).to eq(1)
-              expect(spam_logs.first.title).to eq('Spam Title')
-              expect(spam_logs.first.recaptcha_verified).to be_falsey
+              expect(response).to render_template(:verify)
             end
+          end
 
-            context 'as HTML' do
-              it 'renders verify template' do
-                update_spam_issue
-
-                expect(response).to render_template(:verify)
-              end
+          context 'as JSON' do
+            before do
+              update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json)
             end
 
-            context 'as JSON' do
-              before do
-                update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json)
-              end
-
-              it 'renders json errors' do
-                expect(json_response)
-                  .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
-              end
+            it 'renders json errors' do
+              expect(json_response)
+                .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
+            end
 
-              it 'returns 422 status' do
-                expect(response).to have_http_status(422)
-              end
+            it 'returns 422 status' do
+              expect(response).to have_http_status(422)
             end
           end
+        end
 
-          context 'when captcha is verified' do
-            let(:spammy_title) { 'Whatever' }
-            let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }
+        context 'when captcha is verified' do
+          let(:spammy_title) { 'Whatever' }
+          let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }
 
-            def update_verified_issue
-              update_issue({ title: spammy_title },
-                           { spam_log_id: spam_logs.last.id,
-                             recaptcha_verification: true })
-            end
+          def update_verified_issue
+            update_issue({ title: spammy_title },
+                         { spam_log_id: spam_logs.last.id,
+                           recaptcha_verification: true })
+          end
 
-            before do
-              allow_any_instance_of(described_class).to receive(:verify_recaptcha)
-                .and_return(true)
-            end
+          before do
+            allow_any_instance_of(described_class).to receive(:verify_recaptcha)
+              .and_return(true)
+          end
 
-            it 'redirect to issue page' do
-              update_verified_issue
+          it 'redirect to issue page' do
+            update_verified_issue
 
-              expect(response)
-                .to redirect_to(project_issue_path(project, issue))
-            end
+            expect(response)
+              .to redirect_to(project_issue_path(project, issue))
+          end
 
-            it 'accepts an issue after recaptcha is verified' do
-              expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title)
-            end
+          it 'accepts an issue after recaptcha is verified' do
+            expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title)
+          end
 
-            it 'marks spam log as recaptcha_verified' do
-              expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
-            end
+          it 'marks spam log as recaptcha_verified' do
+            expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
+          end
 
-            it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
-              spam_log = create(:spam_log)
+          it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
+            spam_log = create(:spam_log)
 
-              expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }
-                .not_to change { SpamLog.last.recaptcha_verified }
-            end
+            expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }
+              .not_to change { SpamLog.last.recaptcha_verified }
           end
         end
       end
@@ -385,13 +360,45 @@ describe Projects::IssuesController do
 
         put :update, params
       end
+    end
+  end
+
+  describe 'POST #move' do
+    before do
+      sign_in(user)
+      project.add_developer(user)
+    end
+
+    context 'when moving issue to another private project' do
+      let(:another_project) { create(:project, :private) }
+
+      context 'when user has access to move issue' do
+        before do
+          another_project.add_reporter(user)
+        end
+
+        it 'moves issue to another project' do
+          move_issue
+
+          expect(response).to have_http_status :ok
+          expect(another_project.issues).not_to be_empty
+        end
+      end
+
+      context 'when user does not have access to move issue' do
+        it 'responds with 404' do
+          move_issue
+
+          expect(response).to have_http_status :not_found
+        end
+      end
 
       def move_issue
-        put :update,
+        post :move,
+          format: :json,
           namespace_id: project.namespace.to_param,
           project_id: project,
           id: issue.iid,
-          issue: { title: 'New title' },
           move_to_project_id: another_project.id
       end
     end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 494c309c9ea1f0fdea2e69af1f5c2e8e0eadf874..b2724945da4afecad1e11dd8ae30eea32d25884f 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -15,11 +15,11 @@ feature 'issue move to another project' do
     background do
       old_project.team << [user, :guest]
 
-      edit_issue(issue)
+      visit issue_path(issue)
     end
 
     scenario 'moving issue to another project not allowed' do
-      expect(page).to have_no_selector('#move_to_project_id')
+      expect(page).to have_no_selector('.js-sidebar-move-issue-block')
     end
   end
 
@@ -34,12 +34,14 @@ feature 'issue move to another project' do
       old_project.team << [user, :reporter]
       new_project.team << [user, :reporter]
 
-      edit_issue(issue)
+      visit issue_path(issue)
     end
 
     scenario 'moving issue to another project', js: true do
-      find('#issuable-move', visible: false).set(new_project.id)
-      click_button('Save changes')
+      find('.js-move-issue').trigger('click')
+      wait_for_requests
+      all('.js-move-issue-dropdown-item')[0].click
+      find('.js-move-issue-confirmation-button').click
 
       expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
       expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}")
@@ -50,13 +52,12 @@ feature 'issue move to another project' do
     scenario 'searching project dropdown', js: true do
       new_project_search.team << [user, :reporter]
 
-      page.within '.detail-page-description' do
-        first('.select2-choice').click
-      end
+      find('.js-move-issue').trigger('click')
+      wait_for_requests
 
-      fill_in('s2id_autogen1_search', with: new_project_search.name)
+      page.within '.js-sidebar-move-issue-block' do
+        fill_in('sidebar-move-issue-dropdown-search', with: new_project_search.name)
 
-      page.within '.select2-drop' do
         expect(page).to have_content(new_project_search.name)
         expect(page).not_to have_content(new_project.name)
       end
@@ -68,10 +69,10 @@ feature 'issue move to another project' do
       background { another_project.team << [user, :guest] }
 
       scenario 'browsing projects in projects select' do
-        click_link 'Move to a different project'
+        find('.js-move-issue').trigger('click')
+        wait_for_requests
 
-        page.within '.select2-results' do
-          expect(page).to have_content 'No project'
+        page.within '.js-sidebar-move-issue-block' do
           expect(page).to have_content new_project.name_with_namespace
         end
       end
@@ -89,11 +90,6 @@ feature 'issue move to another project' do
     end
   end
 
-  def edit_issue(issue)
-    visit issue_path(issue)
-    page.within('.issuable-actions') { first(:link, 'Edit').click }
-  end
-
   def issue_path(issue)
     project_issue_path(issue.project, issue)
   end
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 3af26e2f28f13c741b61125057490022d522b2fc..39065814bc29b0c40f1d57b348d899fcf9d7bc90 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -34,7 +34,6 @@ describe('Issuable output', () => {
       propsData: {
         canUpdate: true,
         canDestroy: true,
-        canMove: true,
         endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
         issuableRef: '#1',
         initialTitleHtml: '',
@@ -43,7 +42,6 @@ describe('Issuable output', () => {
         initialDescriptionText: '',
         markdownPreviewPath: '/',
         markdownDocsPath: '/',
-        projectsAutocompletePath: '/',
         isConfidential: false,
         projectNamespace: '/',
         projectPath: '/',
@@ -226,7 +224,7 @@ describe('Issuable output', () => {
       });
     });
 
-    it('redirects if issue is moved', (done) => {
+    it('redirects if returned web_url has changed', (done) => {
       spyOn(gl.utils, 'visitUrl');
       spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
         resolve({
@@ -250,23 +248,6 @@ describe('Issuable output', () => {
       });
     });
 
-    it('does not update issuable if project move confirm is false', (done) => {
-      spyOn(window, 'confirm').and.returnValue(false);
-      spyOn(vm.service, 'updateIssuable');
-
-      vm.store.formState.move_to_project_id = 1;
-
-      vm.updateIssuable();
-
-      setTimeout(() => {
-        expect(
-          vm.service.updateIssuable,
-        ).not.toHaveBeenCalled();
-
-        done();
-      });
-    });
-
     it('closes form on error', (done) => {
       spyOn(window, 'Flash').and.callThrough();
       spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js
deleted file mode 100644
index 8b6ed6a03a9992dda0350583a54a8731b3cde6bf..0000000000000000000000000000000000000000
--- a/spec/javascripts/issue_show/components/fields/project_move_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import projectMove from '~/issue_show/components/fields/project_move.vue';
-
-describe('Project move field component', () => {
-  let vm;
-  let formState;
-
-  beforeEach((done) => {
-    const Component = Vue.extend(projectMove);
-
-    formState = {
-      move_to_project_id: 0,
-    };
-
-    vm = new Component({
-      propsData: {
-        formState,
-        projectsAutocompletePath: '/autocomplete',
-      },
-    }).$mount();
-
-    Vue.nextTick(done);
-  });
-
-  it('mounts select2 element', () => {
-    expect(
-      vm.$el.querySelector('.select2-container'),
-    ).not.toBeNull();
-  });
-
-  it('updates formState on change', () => {
-    $(vm.$refs['move-dropdown']).val(2).trigger('change');
-
-    expect(
-      formState.move_to_project_id,
-    ).toBe(2);
-  });
-});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
index d8af52874311cd6af7e94e1b5c3d2dfadfde8fc1..6e89528a3ea02d4f170a99c4b983ea33741e2b98 100644
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -12,7 +12,6 @@ describe('Inline edit form component', () => {
     vm = new Component({
       propsData: {
         canDestroy: true,
-        canMove: true,
         formState: {
           title: 'b',
           description: 'a',
@@ -20,7 +19,6 @@ describe('Inline edit form component', () => {
         },
         markdownPreviewPath: '/',
         markdownDocsPath: '/',
-        projectsAutocompletePath: '/',
         projectPath: '/',
         projectNamespace: '/',
       },
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 9fc8667ecc9a7230b3c6ffdfd707a76a6515fab6..e2b6bcabc984e915fb948ad1fa9949233d9fb70d 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -66,17 +66,57 @@ const sidebarMockData = {
       },
       labels: [],
     },
+    '/autocomplete/projects?project_id=15': [
+      {
+        'id': 0,
+        'name_with_namespace': 'No project',
+      }, {
+        'id': 20,
+        'name_with_namespace': 'foo / bar',
+      },
+    ],
   },
   'PUT': {
     '/gitlab-org/gitlab-shell/issues/5.json': {
       data: {},
     },
   },
+  'POST': {
+    '/gitlab-org/gitlab-shell/issues/5/move': {
+      id: 123,
+      iid: 5,
+      author_id: 1,
+      description: 'some description',
+      lock_version: 5,
+      milestone_id: null,
+      state: 'opened',
+      title: 'some title',
+      updated_by_id: 1,
+      created_at: '2017-06-27T19:54:42.437Z',
+      updated_at: '2017-08-18T03:39:49.222Z',
+      deleted_at: null,
+      time_estimate: 0,
+      total_time_spent: 0,
+      human_time_estimate: null,
+      human_total_time_spent: null,
+      branch_name: null,
+      confidential: false,
+      assignees: [],
+      due_date: null,
+      moved_to_id: null,
+      project_id: 7,
+      milestone: null,
+      labels: [],
+      web_url: '/root/some-project/issues/5',
+    },
+  },
 };
 
 export default {
   mediator: {
     endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+    moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
+    projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
     editable: true,
     currentUser: {
       id: 1,
@@ -85,6 +125,7 @@ export default {
       avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
     },
     rootPath: '/',
+    fullPath: '/gitlab-org/gitlab-shell',
   },
   time: {
     time_estimate: 3600,
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index e246f41ee823ab0fb50ab3729eb746ca591421f9..3aa8ca5db0d7c8699dafb225f73eb17cb4e9a4a1 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -30,7 +30,7 @@ describe('Sidebar mediator', () => {
         expect(resp.status).toEqual(200);
         done();
       })
-      .catch(() => {});
+      .catch(done.fail);
   });
 
   it('fetches the data', () => {
@@ -38,4 +38,42 @@ describe('Sidebar mediator', () => {
     this.mediator.fetch();
     expect(this.mediator.service.get).toHaveBeenCalled();
   });
+
+  it('sets moveToProjectId', () => {
+    const projectId = 7;
+    spyOn(this.mediator.store, 'setMoveToProjectId').and.callThrough();
+
+    this.mediator.setMoveToProjectId(projectId);
+
+    expect(this.mediator.store.setMoveToProjectId).toHaveBeenCalledWith(projectId);
+  });
+
+  it('fetches autocomplete projects', (done) => {
+    const searchTerm = 'foo';
+    spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough();
+    spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough();
+
+    this.mediator.fetchAutocompleteProjects(searchTerm)
+      .then(() => {
+        expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
+        expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
+        done();
+      })
+      .catch(done.fail);
+  });
+
+  it('moves issue', (done) => {
+    const moveToProjectId = 7;
+    this.mediator.store.setMoveToProjectId(moveToProjectId);
+    spyOn(this.mediator.service, 'moveIssue').and.callThrough();
+    spyOn(gl.utils, 'visitUrl');
+
+    this.mediator.moveIssue()
+      .then(() => {
+        expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
+        expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+        done();
+      })
+      .catch(done.fail);
+  });
 });
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8b0d51bbcc819c8853afb3ca6ce38a229505a1d0
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -0,0 +1,142 @@
+import Vue from 'vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
+import Mock from './mock_data';
+
+describe('SidebarMoveIssue', () => {
+  beforeEach(() => {
+    Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
+    this.mediator = new SidebarMediator(Mock.mediator);
+    this.$content = $(`
+      <div class="dropdown">
+        <div class="js-toggle"></div>
+        <div class="dropdown-content"></div>
+        <div class="js-confirm-button"></div>
+      </div>
+    `);
+    this.$toggleButton = this.$content.find('.js-toggle');
+    this.$confirmButton = this.$content.find('.js-confirm-button');
+
+    this.sidebarMoveIssue = new SidebarMoveIssue(
+      this.mediator,
+      this.$toggleButton,
+      this.$confirmButton,
+    );
+    this.sidebarMoveIssue.init();
+  });
+
+  afterEach(() => {
+    SidebarService.singleton = null;
+    SidebarStore.singleton = null;
+    SidebarMediator.singleton = null;
+
+    this.sidebarMoveIssue.destroy();
+
+    Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
+  });
+
+  describe('init', () => {
+    it('should initialize the dropdown and listeners', () => {
+      spyOn(this.sidebarMoveIssue, 'initDropdown');
+      spyOn(this.sidebarMoveIssue, 'addEventListeners');
+
+      this.sidebarMoveIssue.init();
+
+      expect(this.sidebarMoveIssue.initDropdown).toHaveBeenCalled();
+      expect(this.sidebarMoveIssue.addEventListeners).toHaveBeenCalled();
+    });
+  });
+
+  describe('destroy', () => {
+    it('should remove the listeners', () => {
+      spyOn(this.sidebarMoveIssue, 'removeEventListeners');
+
+      this.sidebarMoveIssue.destroy();
+
+      expect(this.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled();
+    });
+  });
+
+  describe('initDropdown', () => {
+    it('should initialize the gl_dropdown', () => {
+      spyOn($.fn, 'glDropdown');
+
+      this.sidebarMoveIssue.initDropdown();
+
+      expect($.fn.glDropdown).toHaveBeenCalled();
+    });
+  });
+
+  describe('onConfirmClicked', () => {
+    it('should move the issue with valid project ID', () => {
+      spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.resolve());
+      this.mediator.setMoveToProjectId(7);
+
+      this.sidebarMoveIssue.onConfirmClicked();
+
+      expect(this.mediator.moveIssue).toHaveBeenCalled();
+      expect(this.$confirmButton.attr('disabled')).toBe('disabled');
+      expect(this.$confirmButton.hasClass('is-loading')).toBe(true);
+    });
+
+    it('should remove loading state from confirm button on failure', (done) => {
+      spyOn(window, 'Flash');
+      spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.reject());
+      this.mediator.setMoveToProjectId(7);
+
+      this.sidebarMoveIssue.onConfirmClicked();
+
+      expect(this.mediator.moveIssue).toHaveBeenCalled();
+      // Wait for the move issue request to fail
+      setTimeout(() => {
+        expect(window.Flash).toHaveBeenCalled();
+        expect(this.$confirmButton.attr('disabled')).toBe(undefined);
+        expect(this.$confirmButton.hasClass('is-loading')).toBe(false);
+        done();
+      });
+    });
+
+    it('should not move the issue with id=0', () => {
+      spyOn(this.mediator, 'moveIssue');
+      this.mediator.setMoveToProjectId(0);
+
+      this.sidebarMoveIssue.onConfirmClicked();
+
+      expect(this.mediator.moveIssue).not.toHaveBeenCalled();
+    });
+  });
+
+  it('should set moveToProjectId on dropdown item "No project" click', (done) => {
+    spyOn(this.mediator, 'setMoveToProjectId');
+
+    // Open the dropdown
+    this.$toggleButton.dropdown('toggle');
+
+    // Wait for the autocomplete request to finish
+    setTimeout(() => {
+      this.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
+
+      expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
+      expect(this.$confirmButton.attr('disabled')).toBe('disabled');
+      done();
+    }, 0);
+  });
+
+  it('should set moveToProjectId on dropdown item click', (done) => {
+    spyOn(this.mediator, 'setMoveToProjectId');
+
+    // Open the dropdown
+    this.$toggleButton.dropdown('toggle');
+
+    // Wait for the autocomplete request to finish
+    setTimeout(() => {
+      this.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
+
+      expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
+      expect(this.$confirmButton.attr('disabled')).toBe(undefined);
+      done();
+    }, 0);
+  });
+});
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
index 91a4dd669a7529c60d1a990bb032d9c4e089722d..a4bd8ba8d88a8ffb9c977db51065404b4d69f549 100644
--- a/spec/javascripts/sidebar/sidebar_service_spec.js
+++ b/spec/javascripts/sidebar/sidebar_service_spec.js
@@ -5,7 +5,11 @@ import Mock from './mock_data';
 describe('Sidebar service', () => {
   beforeEach(() => {
     Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
-    this.service = new SidebarService('/gitlab-org/gitlab-shell/issues/5.json');
+    this.service = new SidebarService({
+      endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+      moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
+      projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
+    });
   });
 
   afterEach(() => {
@@ -19,7 +23,7 @@ describe('Sidebar service', () => {
         expect(resp).toBeDefined();
         done();
       })
-      .catch(() => {});
+      .catch(done.fail);
   });
 
   it('updates the data', (done) => {
@@ -28,6 +32,24 @@ describe('Sidebar service', () => {
         expect(resp).toBeDefined();
         done();
       })
-      .catch(() => {});
+      .catch(done.fail);
+  });
+
+  it('gets projects for autocomplete', (done) => {
+    this.service.getProjectsAutocomplete()
+      .then((resp) => {
+        expect(resp).toBeDefined();
+        done();
+      })
+      .catch(done.fail);
+  });
+
+  it('moves the issue to another project', (done) => {
+    this.service.moveIssue(123)
+      .then((resp) => {
+        expect(resp).toBeDefined();
+        done();
+      })
+      .catch(done.fail);
   });
 });
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index b3fa156eb647608bccacf0f7838a5132363cd710..69eb3839d675e1cd0c5536b9413da22c0e737de4 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -82,4 +82,18 @@ describe('Sidebar store', () => {
     expect(this.store.humanTimeEstimate).toEqual(Mock.time.human_time_estimate);
     expect(this.store.humanTotalTimeSpent).toEqual(Mock.time.human_total_time_spent);
   });
+
+  it('set autocomplete projects', () => {
+    const projects = [{ id: 0 }];
+    this.store.setAutocompleteProjects(projects);
+
+    expect(this.store.autocompleteProjects).toEqual(projects);
+  });
+
+  it('set move to project ID', () => {
+    const projectId = 7;
+    this.store.setMoveToProjectId(projectId);
+
+    expect(this.store.moveToProjectId).toEqual(projectId);
+  });
 });