diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 3062cd51ee3707bc3b3c4fab41bb8fea060bee38..a20c6ca7a21ab12dc83589e8485e80c1108ff784 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -99,7 +99,7 @@ export default class FileTemplateMediator {
     });
   }
 
-  selectTemplateType(item, el, e) {
+  selectTemplateType(item, e) {
     if (e) {
       e.preventDefault();
     }
@@ -117,6 +117,10 @@ export default class FileTemplateMediator {
     this.cacheToggleText();
   }
 
+  selectTemplateTypeOptions(options) {
+    this.selectTemplateType(options.selectedObj, options.e);
+  }
+
   selectTemplateFile(selector, query, data) {
     selector.renderLoading();
     // in case undo menu is already already there
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index 31dd45fac89ad7d840b31f12b148a3099140c545..ab5b3751c4e2fd3ca9f984d0ec2725e1a6deca26 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -52,9 +52,17 @@ export default class FileTemplateSelector {
       .removeClass('fa-spinner fa-spin');
   }
 
-  reportSelection(query, el, e, data) {
+  reportSelection(options) {
+    const { query, e, data } = options;
     e.preventDefault();
     return this.mediator.selectTemplateFile(this, query, data);
   }
+
+  reportSelectionName(options) {
+    const opts = options;
+    opts.query = options.selectedObj.name;
+
+    this.reportSelection(opts);
+  }
 }
 
diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js
index 216f069ef71cd24e3d3230a20f17c38319db3287..d52d69b1274888586b6c89791aabe150d1d06366 100644
--- a/app/assets/javascripts/blob/target_branch_dropdown.js
+++ b/app/assets/javascripts/blob/target_branch_dropdown.js
@@ -37,8 +37,8 @@ class TargetBranchDropDown {
         }
         return SELECT_ITEM_MSG;
       },
-      clicked(item, el, e) {
-        e.preventDefault();
+      clicked(options) {
+        options.e.preventDefault();
         self.onClick.call(self);
       },
       fieldName: self.fieldName,
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index d7c1c32efbd40336ea881746c25c9d694cdd48a5..888883163c5b27a924c888bd81ad70d9902c6768 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -24,7 +24,7 @@ export default class TemplateSelector {
       search: {
         fields: ['name'],
       },
-      clicked: (item, el, e) => this.fetchFileTemplate(item, el, e),
+      clicked: options => this.fetchFileTemplate(options),
       text: item => item.name,
     });
   }
@@ -51,7 +51,10 @@ export default class TemplateSelector {
     return this.$dropdownContainer.removeClass('hidden');
   }
 
-  fetchFileTemplate(item, el, e) {
+  fetchFileTemplate(options) {
+    const { e } = options;
+    const item = options.selectedObj;
+
     e.preventDefault();
     return this.requestFile(item);
   }
diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
index 935df07677cb2928d3a36a5e80fb0dbf8afbd17d..f2f81af137b4b215b1b6ad90a21f18f590622171 100644
--- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
@@ -25,7 +25,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector {
       search: {
         fields: ['name'],
       },
-      clicked: (query, el, e) => this.reportSelection(query.name, el, e),
+      clicked: options => this.reportSelectionName(options),
       text: item => item.name,
     });
   }
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index b4b4d09c315b336d0640874b26a6af29a02295f5..3cb7b960aaa7c09d563298938a3e909b0962c813 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -25,7 +25,7 @@ export default class DockerfileSelector extends FileTemplateSelector {
       search: {
         fields: ['name'],
       },
-      clicked: (query, el, e) => this.reportSelection(query.name, el, e),
+      clicked: options => this.reportSelectionName(options),
       text: item => item.name,
     });
   }
diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
index aefae54ae71a4647903dbfb4bfb34ff681fa670a..7efda8e7f50d8b7c2cb32daa45499d098fb3f0ff 100644
--- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
@@ -24,7 +24,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector {
       search: {
         fields: ['name'],
       },
-      clicked: (query, el, e) => this.reportSelection(query.name, el, e),
+      clicked: options => this.reportSelectionName(options),
       text: item => item.name,
     });
   }
diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js
index c8abd689ab4efcb65e55900923116596726ae745..1d757332f6c9ffae4fafa59beb447261dd5e24ed 100644
--- a/app/assets/javascripts/blob/template_selectors/license_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/license_selector.js
@@ -24,13 +24,22 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
       search: {
         fields: ['name'],
       },
-      clicked: (query, el, e) => {
+      clicked: (options) => {
+        const { e } = options;
+        const el = options.$el;
+        const query = options.selectedObj;
+
         const data = {
           project: this.$dropdown.data('project'),
           fullname: this.$dropdown.data('fullname'),
         };
 
-        this.reportSelection(query.id, el, e, data);
+        this.reportSelection({
+          query: query.id,
+          el,
+          e,
+          data,
+        });
       },
       text: item => item.name,
     });
diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js
index 56f23ef05687a0bd4a07c0f3d82ae4365525159a..a09381014a754364dc80e23ee913a66ee660f0f4 100644
--- a/app/assets/javascripts/blob/template_selectors/type_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/type_selector.js
@@ -17,7 +17,7 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector {
       filterable: false,
       selectable: true,
       toggleLabel: item => item.name,
-      clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e),
+      clicked: options => this.mediator.selectTemplateTypeOptions(options),
       text: item => item.name,
     });
   }
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 8c08b2d4db38f6476007a5546e100ddbc91bcc38..88eb425133964d09b1a6f34260df44dc094a3924 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -11,7 +11,7 @@ require('./models/issue');
 require('./models/label');
 require('./models/list');
 require('./models/milestone');
-require('./models/user');
+require('./models/assignee');
 require('./stores/boards_store');
 require('./stores/modal_store');
 require('./services/board_service');
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js
index 0fa85b6fe14060f2469d2c5ec242369474a34560..1ce95b62138d9e39e020f7c64c5bade252462798 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js
+++ b/app/assets/javascripts/boards/components/board_new_issue.js
@@ -26,6 +26,7 @@ export default {
         title: this.title,
         labels,
         subscribed: true,
+        assignees: [],
       });
 
       this.list.newIssue(issue)
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index f0066d4ec5d722b65b60f32ef5e8e7a067921e67..317cef9f2275028f5c29df75ad0f6d8a9645d07e 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -3,8 +3,13 @@
 /* global MilestoneSelect */
 /* global LabelsSelect */
 /* global Sidebar */
+/* global Flash */
 
 import Vue from 'vue';
+import eventHub from '../../sidebar/event_hub';
+
+import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
+import Assignees from '../../sidebar/components/assignees/assignees';
 
 require('./sidebar/remove_issue');
 
@@ -22,6 +27,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
       detail: Store.detail,
       issue: {},
       list: {},
+      loadingAssignees: false,
     };
   },
   computed: {
@@ -43,6 +49,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
 
         this.issue = this.detail.issue;
         this.list = this.detail.list;
+
+        this.$nextTick(() => {
+          this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
+        });
       },
       deep: true
     },
@@ -53,12 +63,57 @@ gl.issueBoards.BoardSidebar = Vue.extend({
           $('.right-sidebar').getNiceScroll().resize();
         });
       }
-    }
+
+      this.issue = this.detail.issue;
+      this.list = this.detail.list;
+    },
+    deep: true
   },
   methods: {
     closeSidebar () {
       this.detail.issue = {};
-    }
+    },
+    assignSelf () {
+      // Notify gl dropdown that we are now assigning to current user
+      this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
+
+      this.addAssignee(this.currentUser);
+      this.saveAssignees();
+    },
+    removeAssignee (a) {
+      gl.issueBoards.BoardsStore.detail.issue.removeAssignee(a);
+    },
+    addAssignee (a) {
+      gl.issueBoards.BoardsStore.detail.issue.addAssignee(a);
+    },
+    removeAllAssignees () {
+      gl.issueBoards.BoardsStore.detail.issue.removeAllAssignees();
+    },
+    saveAssignees () {
+      this.loadingAssignees = true;
+
+      gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint)
+        .then(() => {
+          this.loadingAssignees = false;
+        })
+        .catch(() => {
+          this.loadingAssignees = false;
+          return new Flash('An error occurred while saving assignees');
+        });
+    },
+  },
+  created () {
+    // Get events from glDropdown
+    eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
+    eventHub.$on('sidebar.addAssignee', this.addAssignee);
+    eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
+    eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
+  },
+  beforeDestroy() {
+    eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
+    eventHub.$off('sidebar.addAssignee', this.addAssignee);
+    eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
+    eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
   },
   mounted () {
     new IssuableContext(this.currentUser);
@@ -70,5 +125,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
   },
   components: {
     removeBtn: gl.issueBoards.RemoveIssueBtn,
+    'assignee-title': AssigneeTitle,
+    assignees: Assignees,
   },
 });
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index fc154ee7b8b031f6d79d1454c0c0ad133d8b9001..710207db0c7416934af32fef28402e551d9d6bf8 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -31,18 +31,36 @@ gl.issueBoards.IssueCardInner = Vue.extend({
       default: false,
     },
   },
+  data() {
+    return {
+      limitBeforeCounter: 3,
+      maxRender: 4,
+      maxCounter: 99,
+    };
+  },
   computed: {
-    cardUrl() {
-      return `${this.issueLinkBase}/${this.issue.id}`;
+    numberOverLimit() {
+      return this.issue.assignees.length - this.limitBeforeCounter;
     },
-    assigneeUrl() {
-      return `${this.rootPath}${this.issue.assignee.username}`;
+    assigneeCounterTooltip() {
+      return `${this.assigneeCounterLabel} more`;
+    },
+    assigneeCounterLabel() {
+      if (this.numberOverLimit > this.maxCounter) {
+        return `${this.maxCounter}+`;
+      }
+
+      return `+${this.numberOverLimit}`;
     },
-    assigneeUrlTitle() {
-      return `Assigned to ${this.issue.assignee.name}`;
+    shouldRenderCounter() {
+      if (this.issue.assignees.length <= this.maxRender) {
+        return false;
+      }
+
+      return this.issue.assignees.length > this.numberOverLimit;
     },
-    avatarUrlTitle() {
-      return `Avatar for ${this.issue.assignee.name}`;
+    cardUrl() {
+      return `${this.issueLinkBase}/${this.issue.id}`;
     },
     issueId() {
       return `#${this.issue.id}`;
@@ -52,6 +70,28 @@ gl.issueBoards.IssueCardInner = Vue.extend({
     },
   },
   methods: {
+    isIndexLessThanlimit(index) {
+      return index < this.limitBeforeCounter;
+    },
+    shouldRenderAssignee(index) {
+      // Eg. maxRender is 4,
+      // Render up to all 4 assignees if there are only 4 assigness
+      // Otherwise render up to the limitBeforeCounter
+      if (this.issue.assignees.length <= this.maxRender) {
+        return index < this.maxRender;
+      }
+
+      return index < this.limitBeforeCounter;
+    },
+    assigneeUrl(assignee) {
+      return `${this.rootPath}${assignee.username}`;
+    },
+    assigneeUrlTitle(assignee) {
+      return `Assigned to ${assignee.name}`;
+    },
+    avatarUrlTitle(assignee) {
+      return `Avatar for ${assignee.name}`;
+    },
     showLabel(label) {
       if (!this.list) return true;
 
@@ -105,25 +145,39 @@ gl.issueBoards.IssueCardInner = Vue.extend({
             {{ issueId }}
           </span>
         </h4>
-        <a
-          class="card-assignee has-tooltip js-no-trigger"
-          :href="assigneeUrl"
-          :title="assigneeUrlTitle"
-          v-if="issue.assignee"
-          data-container="body"
-        >
-          <img
-            class="avatar avatar-inline s20 js-no-trigger"
-            :src="issue.assignee.avatar"
-            width="20"
-            height="20"
-            :alt="avatarUrlTitle"
-          />
-        </a>
+        <div class="card-assignee">
+          <a
+            class="has-tooltip js-no-trigger"
+            :href="assigneeUrl(assignee)"
+            :title="assigneeUrlTitle(assignee)"
+            v-for="(assignee, index) in issue.assignees"
+            v-if="shouldRenderAssignee(index)"
+            data-container="body"
+            data-placement="bottom"
+          >
+            <img
+              class="avatar avatar-inline s20"
+              :src="assignee.avatar"
+              width="20"
+              height="20"
+              :alt="avatarUrlTitle(assignee)"
+            />
+          </a>
+          <span
+            class="avatar-counter has-tooltip"
+            :title="assigneeCounterTooltip"
+            v-if="shouldRenderCounter"
+          >
+           {{ assigneeCounterLabel }}
+          </span>
+        </div>
       </div>
-      <div class="card-footer" v-if="showLabelFooter">
+      <div
+        class="card-footer"
+        v-if="showLabelFooter"
+      >
         <button
-          class="label color-label has-tooltip js-no-trigger"
+          class="label color-label has-tooltip"
           v-for="label in issue.labels"
           type="button"
           v-if="showLabel(label)"
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 7e3bb79af1d4aaf35514c97a4f74d3d4322dcc82..f29b6caa1acec7e0f3cfe9df5fa630f7fbcece2e 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -52,7 +52,9 @@ gl.issueBoards.newListDropdownInit = () => {
       filterable: true,
       selectable: true,
       multiSelect: true,
-      clicked (label, $el, e) {
+      clicked (options) {
+        const { e } = options;
+        const label = options.selectedObj;
         e.preventDefault();
 
         if (!Store.findList('title', label.title)) {
diff --git a/app/assets/javascripts/boards/models/user.js b/app/assets/javascripts/boards/models/assignee.js
similarity index 65%
rename from app/assets/javascripts/boards/models/user.js
rename to app/assets/javascripts/boards/models/assignee.js
index 2af583c3279f240465e0eccff46e687a7261b93e..05dd449e4fd8d7dc4cce388962f048dfba6c64a1 100644
--- a/app/assets/javascripts/boards/models/user.js
+++ b/app/assets/javascripts/boards/models/assignee.js
@@ -1,4 +1,6 @@
-class ListUser {
+/* eslint-disable no-unused-vars */
+
+class ListAssignee {
   constructor(user, defaultAvatar) {
     this.id = user.id;
     this.name = user.name;
@@ -7,4 +9,4 @@ class ListUser {
   }
 }
 
-window.ListUser = ListUser;
+window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index db783467f87723eda45d5d2b6a19a0f66377ceab..6c2d8a3781b38ab057444ae92d5091fc29e1fde9 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -1,7 +1,7 @@
 /* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
 /* global ListLabel */
 /* global ListMilestone */
-/* global ListUser */
+/* global ListAssignee */
 
 import Vue from 'vue';
 
@@ -14,14 +14,10 @@ class ListIssue {
     this.dueDate = obj.due_date;
     this.subscribed = obj.subscribed;
     this.labels = [];
+    this.assignees = [];
     this.selected = false;
-    this.assignee = false;
     this.position = obj.relative_position || Infinity;
 
-    if (obj.assignee) {
-      this.assignee = new ListUser(obj.assignee, defaultAvatar);
-    }
-
     if (obj.milestone) {
       this.milestone = new ListMilestone(obj.milestone);
     }
@@ -29,6 +25,8 @@ class ListIssue {
     obj.labels.forEach((label) => {
       this.labels.push(new ListLabel(label));
     });
+
+    this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
   }
 
   addLabel (label) {
@@ -51,6 +49,26 @@ class ListIssue {
     labels.forEach(this.removeLabel.bind(this));
   }
 
+  addAssignee (assignee) {
+    if (!this.findAssignee(assignee)) {
+      this.assignees.push(new ListAssignee(assignee));
+    }
+  }
+
+  findAssignee (findAssignee) {
+    return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
+  }
+
+  removeAssignee (removeAssignee) {
+    if (removeAssignee) {
+      this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
+    }
+  }
+
+  removeAllAssignees () {
+    this.assignees = [];
+  }
+
   getLists () {
     return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
   }
@@ -60,7 +78,7 @@ class ListIssue {
       issue: {
         milestone_id: this.milestone ? this.milestone.id : null,
         due_date: this.dueDate,
-        assignee_id: this.assignee ? this.assignee.id : null,
+        assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0],
         label_ids: this.labels.map((label) => label.id)
       }
     };
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a03f1202a6d4c80ad26263129a4f3bfe7d7d7971..0c9eb84f0ebe682a447de3d994975104f2b0f5a7 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -255,7 +255,8 @@ GitLabDropdown = (function() {
               }
             };
           // Remote data
-          })(this)
+          })(this),
+          instance: this,
         });
       }
     }
@@ -269,6 +270,7 @@ GitLabDropdown = (function() {
         remote: this.options.filterRemote,
         query: this.options.data,
         keys: searchFields,
+        instance: this,
         elements: (function(_this) {
           return function() {
             selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
@@ -343,21 +345,26 @@ GitLabDropdown = (function() {
       }
       this.dropdown.on("click", selector, function(e) {
         var $el, selected, selectedObj, isMarking;
-        $el = $(this);
+        $el = $(e.currentTarget);
         selected = self.rowClicked($el);
         selectedObj = selected ? selected[0] : null;
         isMarking = selected ? selected[1] : null;
-        if (self.options.clicked) {
-          self.options.clicked(selectedObj, $el, e, isMarking);
+        if (this.options.clicked) {
+          this.options.clicked.call(this, {
+            selectedObj,
+            $el,
+            e,
+            isMarking,
+          });
         }
 
         // Update label right after all modifications in dropdown has been done
-        if (self.options.toggleLabel) {
-          self.updateLabel(selectedObj, $el, self);
+        if (this.options.toggleLabel) {
+          this.updateLabel(selectedObj, $el, this);
         }
 
         $el.trigger('blur');
-      });
+      }.bind(this));
     }
   }
 
@@ -439,15 +446,34 @@ GitLabDropdown = (function() {
     }
   };
 
+  GitLabDropdown.prototype.filteredFullData = function() {
+    return this.fullData.filter(r => typeof r === 'object'
+      && !Object.prototype.hasOwnProperty.call(r, 'beforeDivider')
+      && !Object.prototype.hasOwnProperty.call(r, 'header')
+    );
+  };
+
   GitLabDropdown.prototype.opened = function(e) {
     var contentHtml;
     this.resetRows();
     this.addArrowKeyEvent();
 
+    const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
+    const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
+    const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
+
     // Makes indeterminate items effective
-    if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
+    if (this.fullData && hasFilterBulkUpdate) {
       this.parseData(this.fullData);
     }
+
+    // Process the data to make sure rendered data
+    // matches the correct layout
+    if (this.fullData && hasMultiSelect && this.options.processData) {
+      const inputValue = this.filterInput.val();
+      this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
+    }
+
     contentHtml = $('.dropdown-content', this.dropdown).html();
     if (this.remote && contentHtml === "") {
       this.remote.execute();
@@ -709,6 +735,11 @@ GitLabDropdown = (function() {
     if (this.options.inputId != null) {
       $input.attr('id', this.options.inputId);
     }
+
+    if (this.options.inputMeta) {
+      $input.attr('data-meta', selectedObject[this.options.inputMeta]);
+    }
+
     return this.dropdown.before($input);
   };
 
@@ -829,7 +860,14 @@ GitLabDropdown = (function() {
     if (instance == null) {
       instance = null;
     }
-    return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
+
+    let toggleText = this.options.toggleLabel(selected, el, instance);
+    if (this.options.updateLabel) {
+      // Option to override the dropdown label text
+      toggleText = this.options.updateLabel;
+    }
+
+    return $(this.el).find(".dropdown-toggle-text").text(toggleText);
   };
 
   GitLabDropdown.prototype.clearField = function(field, isInput) {
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js b/app/assets/javascripts/issuable/issuable_bundle.js
deleted file mode 100644
index e927cc0077c5b5e3e5353adf9d11f0284e6a1b42..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/issuable_bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-require('./time_tracking/time_tracking_bundle');
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
deleted file mode 100644
index aec13e78f42acaf70bd124bb690aec5e471310b8..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Vue from 'vue';
-import stopwatchSvg from 'icons/_icon_stopwatch.svg';
-
-require('../../../lib/utils/pretty_time');
-
-(() => {
-  Vue.component('time-tracking-collapsed-state', {
-    name: 'time-tracking-collapsed-state',
-    props: [
-      'showComparisonState',
-      'showSpentOnlyState',
-      'showEstimateOnlyState',
-      'showNoTimeTrackingState',
-      'timeSpentHumanReadable',
-      'timeEstimateHumanReadable',
-    ],
-    methods: {
-      abbreviateTime(timeStr) {
-        return gl.utils.prettyTime.abbreviateTime(timeStr);
-      },
-    },
-    template: `
-      <div class='sidebar-collapsed-icon'>
-        ${stopwatchSvg}
-        <div class='time-tracking-collapsed-summary'>
-          <div class='compare' v-if='showComparisonState'>
-            <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
-          </div>
-          <div class='estimate-only' v-if='showEstimateOnlyState'>
-            <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
-          </div>
-          <div class='spend-only' v-if='showSpentOnlyState'>
-            <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
-          </div>
-          <div class='no-tracking' v-if='showNoTimeTrackingState'>
-            <span class='no-value'>None</span>
-          </div>
-        </div>
-      </div>
-      `,
-  });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
deleted file mode 100644
index c55e263f6f475c468458af0c1f6574661b14b87b..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import Vue from 'vue';
-
-require('../../../lib/utils/pretty_time');
-
-(() => {
-  const prettyTime = gl.utils.prettyTime;
-
-  Vue.component('time-tracking-comparison-pane', {
-    name: 'time-tracking-comparison-pane',
-    props: [
-      'timeSpent',
-      'timeEstimate',
-      'timeSpentHumanReadable',
-      'timeEstimateHumanReadable',
-    ],
-    computed: {
-      parsedRemaining() {
-        const diffSeconds = this.timeEstimate - this.timeSpent;
-        return prettyTime.parseSeconds(diffSeconds);
-      },
-      timeRemainingHumanReadable() {
-        return prettyTime.stringifyTime(this.parsedRemaining);
-      },
-      timeRemainingTooltip() {
-        const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
-        return `${prefix} ${this.timeRemainingHumanReadable}`;
-      },
-      /* Diff values for comparison meter */
-      timeRemainingMinutes() {
-        return this.timeEstimate - this.timeSpent;
-      },
-      timeRemainingPercent() {
-        return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
-      },
-      timeRemainingStatusClass() {
-        return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
-      },
-      /* Parsed time values */
-      parsedEstimate() {
-        return prettyTime.parseSeconds(this.timeEstimate);
-      },
-      parsedSpent() {
-        return prettyTime.parseSeconds(this.timeSpent);
-      },
-    },
-    template: `
-      <div class='time-tracking-comparison-pane'>
-        <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
-          :aria-valuenow='timeRemainingTooltip'
-          :title='timeRemainingTooltip'
-          :data-original-title='timeRemainingTooltip'
-          :class='timeRemainingStatusClass'>
-          <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
-            <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
-          </div>
-          <div class='compare-display-container'>
-            <div class='compare-display pull-left'>
-              <span class='compare-label'>Spent</span>
-              <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
-            </div>
-            <div class='compare-display estimated pull-right'>
-              <span class='compare-label'>Est</span>
-              <span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
deleted file mode 100644
index a7fbd704c40d386b6cf1c88536cdc7bfdf8c32c9..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import Vue from 'vue';
-
-(() => {
-  Vue.component('time-tracking-estimate-only-pane', {
-    name: 'time-tracking-estimate-only-pane',
-    props: ['timeEstimateHumanReadable'],
-    template: `
-      <div class='time-tracking-estimate-only-pane'>
-        <span class='bold'>Estimated:</span>
-        {{ timeEstimateHumanReadable }}
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js b/app/assets/javascripts/issuable/time_tracking/components/help_state.js
deleted file mode 100644
index 344b29ebea4d4dde9503fda5770e071f82f7cf2d..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Vue from 'vue';
-
-(() => {
-  Vue.component('time-tracking-help-state', {
-    name: 'time-tracking-help-state',
-    props: ['docsUrl'],
-    template: `
-      <div class='time-tracking-help-state'>
-        <div class='time-tracking-info'>
-          <h4>Track time with slash commands</h4>
-          <p>Slash commands can be used in the issues description and comment boxes.</p>
-          <p>
-            <code>/estimate</code>
-            will update the estimated time with the latest command.
-          </p>
-          <p>
-            <code>/spend</code>
-            will update the sum of the time spent.
-          </p>
-          <a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a>
-        </div>
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
deleted file mode 100644
index b081adf5e643b210b4c1603995d78d2ce55ff05c..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import Vue from 'vue';
-
-(() => {
-  Vue.component('time-tracking-no-tracking-pane', {
-    name: 'time-tracking-no-tracking-pane',
-    template: `
-      <div class='time-tracking-no-tracking-pane'>
-        <span class='no-value'>No estimate or time spent</span>
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
deleted file mode 100644
index edb9169112ff65bea6920329a314948abbce13d5..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import Vue from 'vue';
-
-(() => {
-  Vue.component('time-tracking-spent-only-pane', {
-    name: 'time-tracking-spent-only-pane',
-    props: ['timeSpentHumanReadable'],
-    template: `
-      <div class='time-tracking-spend-only-pane'>
-        <span class='bold'>Spent:</span>
-        {{ timeSpentHumanReadable }}
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
deleted file mode 100644
index 0213522f5519be553c1d02e65c52242ae6f33795..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import Vue from 'vue';
-
-require('./help_state');
-require('./collapsed_state');
-require('./spent_only_pane');
-require('./no_tracking_pane');
-require('./estimate_only_pane');
-require('./comparison_pane');
-
-(() => {
-  Vue.component('issuable-time-tracker', {
-    name: 'issuable-time-tracker',
-    props: [
-      'time_estimate',
-      'time_spent',
-      'human_time_estimate',
-      'human_time_spent',
-      'docsUrl',
-    ],
-    data() {
-      return {
-        showHelp: false,
-      };
-    },
-    computed: {
-      timeSpent() {
-        return this.time_spent;
-      },
-      timeEstimate() {
-        return this.time_estimate;
-      },
-      timeEstimateHumanReadable() {
-        return this.human_time_estimate;
-      },
-      timeSpentHumanReadable() {
-        return this.human_time_spent;
-      },
-      hasTimeSpent() {
-        return !!this.timeSpent;
-      },
-      hasTimeEstimate() {
-        return !!this.timeEstimate;
-      },
-      showComparisonState() {
-        return this.hasTimeEstimate && this.hasTimeSpent;
-      },
-      showEstimateOnlyState() {
-        return this.hasTimeEstimate && !this.hasTimeSpent;
-      },
-      showSpentOnlyState() {
-        return this.hasTimeSpent && !this.hasTimeEstimate;
-      },
-      showNoTimeTrackingState() {
-        return !this.hasTimeEstimate && !this.hasTimeSpent;
-      },
-      showHelpState() {
-        return !!this.showHelp;
-      },
-    },
-    methods: {
-      toggleHelpState(show) {
-        this.showHelp = show;
-      },
-    },
-    template: `
-      <div class='time_tracker time-tracking-component-wrap' v-cloak>
-        <time-tracking-collapsed-state
-          :show-comparison-state='showComparisonState'
-          :show-help-state='showHelpState'
-          :show-spent-only-state='showSpentOnlyState'
-          :show-estimate-only-state='showEstimateOnlyState'
-          :time-spent-human-readable='timeSpentHumanReadable'
-          :time-estimate-human-readable='timeEstimateHumanReadable'>
-        </time-tracking-collapsed-state>
-        <div class='title hide-collapsed'>
-          Time tracking
-          <div class='help-button pull-right'
-            v-if='!showHelpState'
-            @click='toggleHelpState(true)'>
-            <i class='fa fa-question-circle' aria-hidden='true'></i>
-          </div>
-          <div class='close-help-button pull-right'
-            v-if='showHelpState'
-            @click='toggleHelpState(false)'>
-            <i class='fa fa-close' aria-hidden='true'></i>
-          </div>
-        </div>
-        <div class='time-tracking-content hide-collapsed'>
-          <time-tracking-estimate-only-pane
-            v-if='showEstimateOnlyState'
-            :time-estimate-human-readable='timeEstimateHumanReadable'>
-          </time-tracking-estimate-only-pane>
-          <time-tracking-spent-only-pane
-            v-if='showSpentOnlyState'
-            :time-spent-human-readable='timeSpentHumanReadable'>
-          </time-tracking-spent-only-pane>
-          <time-tracking-no-tracking-pane
-            v-if='showNoTimeTrackingState'>
-          </time-tracking-no-tracking-pane>
-          <time-tracking-comparison-pane
-            v-if='showComparisonState'
-            :time-estimate='timeEstimate'
-            :time-spent='timeSpent'
-            :time-spent-human-readable='timeSpentHumanReadable'
-            :time-estimate-human-readable='timeEstimateHumanReadable'>
-          </time-tracking-comparison-pane>
-          <transition name='help-state-toggle'>
-            <time-tracking-help-state
-              v-if='showHelpState'
-              :docs-url='docsUrl'>
-            </time-tracking-help-state>
-          </transition>
-        </div>
-      </div>
-    `,
-  });
-})();
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
deleted file mode 100644
index 1689a69e1ed945b94fea2f953c67399868c90031..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-require('./components/time_tracker');
-require('../../smart_interval');
-require('../../subbable_resource');
-
-Vue.use(VueResource);
-
-(() => {
-  /* This Vue instance represents what will become the parent instance for the
-    * sidebar. It will be responsible for managing `issuable` state and propagating
-    * changes to sidebar components. We will want to create a separate service to
-    * interface with the server at that point.
-   */
-
-  class IssuableTimeTracking {
-    constructor(issuableJSON) {
-      const parsedIssuable = JSON.parse(issuableJSON);
-      return this.initComponent(parsedIssuable);
-    }
-
-    initComponent(parsedIssuable) {
-      this.parentInstance = new Vue({
-        el: '#issuable-time-tracker',
-        data: {
-          issuable: parsedIssuable,
-        },
-        methods: {
-          fetchIssuable() {
-            return gl.IssuableResource.get.call(gl.IssuableResource, {
-              type: 'GET',
-              url: gl.IssuableResource.endpoint,
-            });
-          },
-          updateState(data) {
-            this.issuable = data;
-          },
-          subscribeToUpdates() {
-            gl.IssuableResource.subscribe(data => this.updateState(data));
-          },
-          listenForSlashCommands() {
-            $(document).on('ajax:success', '.gfm-form', (e, data) => {
-              const subscribedCommands = ['spend_time', 'time_estimate'];
-              const changedCommands = data.commands_changes
-                ? Object.keys(data.commands_changes)
-                : [];
-              if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
-                this.fetchIssuable();
-              }
-            });
-          },
-        },
-        created() {
-          this.fetchIssuable();
-        },
-        mounted() {
-          this.subscribeToUpdates();
-          this.listenForSlashCommands();
-        },
-      });
-    }
-  }
-
-  gl.IssuableTimeTracking = IssuableTimeTracking;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index b2cfd3ef2a3cba652c32e525e731173ee570658c..56cb536dcde7dfa4612a3f29757754900bfb1ffc 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -19,8 +19,8 @@
               return label;
             };
           })(this),
-          clicked: function(item, $el, e) {
-            return e.preventDefault();
+          clicked: function(options) {
+            return options.e.preventDefault();
           },
           id: function(obj, el) {
             return $(el).data("id");
diff --git a/app/assets/javascripts/issues_bulk_assignment.js b/app/assets/javascripts/issues_bulk_assignment.js
index e0ebd36a65ce807c434c5dab0d4c62584be79156..fee3429e2b84601d26291dfea6bd21e64b77a39e 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js
+++ b/app/assets/javascripts/issues_bulk_assignment.js
@@ -88,7 +88,10 @@
       const formData = {
         update: {
           state_event: this.form.find('input[name="update[state_event]"]').val(),
+          // For Merge Requests
           assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
+          // For Issues
+          assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
           milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
           issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
           subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 9a60f5464df952782a15c08712e0d1980adb95b3..ac5ce84e31b1f80085075ce54d6119e8d40ee32c 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -330,7 +330,10 @@
           },
           multiSelect: $dropdown.hasClass('js-multiselect'),
           vue: $dropdown.hasClass('js-issue-board-sidebar'),
-          clicked: function(label, $el, e, isMarking) {
+          clicked: function(options) {
+            const { $el, e, isMarking } = options;
+            const label = options.selectedObj;
+
             var isIssueIndex, isMRIndex, page, boardsModel;
             var fadeOutLoader = () => {
               $loading.fadeOut();
@@ -352,7 +355,7 @@
 
             if ($dropdown.hasClass('js-filter-bulk-update')) {
               _this.enableBulkLabelDropdown();
-              _this.setDropdownData($dropdown, isMarking, this.id(label));
+              _this.setDropdownData($dropdown, isMarking, label.id);
               return;
             }
 
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index be3c2c9fbb1023a658400776717aa36855881008..1b0d5fc92e38bf91d184ee62283f38d7d07891ac 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -158,7 +158,6 @@ import './single_file_diff';
 import './smart_interval';
 import './snippets_list';
 import './star';
-import './subbable_resource';
 import './subscription';
 import './subscription_select';
 import './syntax_highlight';
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index e3f367a11eb94637229e1ac77c12f1e2272a1930..8291b8c4a709d4a26bf2538cc7e7cc6628196b97 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -31,8 +31,8 @@
           toggleLabel(selected, $el) {
             return $el.text();
           },
-          clicked: (selected, $link) => {
-            this.formSubmit(null, $link);
+          clicked: (options) => {
+            this.formSubmit(null, options.$el);
           },
         });
       });
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index bebd0aa357e0e8f79c249d99ab678c6d3983e0a5..11e68c0a3be80e1594436e035d482e38d5ccfddd 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -121,7 +121,10 @@
             return $value.css('display', '');
           },
           vue: $dropdown.hasClass('js-issue-board-sidebar'),
-          clicked: function(selected, $el, e) {
+          clicked: function(options) {
+            const { $el, e } = options;
+            let selected = options.selectedObj;
+
             var data, isIssueIndex, isMRIndex, page, boardsStore;
             page = $('body').data('page');
             isIssueIndex = page === 'projects:issues:index';
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index b98e6121967a7336281f840ca7cf7b4f6d846882..36bc1257cefd2002d9c535027d9a7e09b0a78497 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -58,7 +58,8 @@
       });
     }
 
-    NamespaceSelect.prototype.onSelectItem = function(item, el, e) {
+    NamespaceSelect.prototype.onSelectItem = function(options) {
+      const { e } = options;
       return e.preventDefault();
     };
 
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index f944fcc5a58c4739d53c9583c08a2041d2715bf9..738e710deb9abbb9a7a7bf6ba0ba918f58e5aace 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -112,7 +112,8 @@ import Cookies from 'js-cookie';
           toggleLabel: function(obj, $el) {
             return $el.text().trim();
           },
-          clicked: function(selected, $el, e) {
+          clicked: function(options) {
+            const { e } = options;
             e.preventDefault();
             if ($('input[name="ref"]').length) {
               var $form = $dropdown.closest('form');
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
index e7fff57ff452c0991f3b21c0bec4d3f2024af8a4..42993a252c33dd5acc2f7ab3d51fb422414b58d7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
@@ -19,7 +19,9 @@
             return 'Select';
           }
         },
-        clicked(item, $el, e) {
+        clicked(opts) {
+          const { e } = opts;
+
           e.preventDefault();
           onSelect();
         }
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
index 1d4bb8a13d67770c5371a0355dc4109925fb7894..bc6110fcd4e4fefabfd6d00e9273ab1cfd55dabe 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
@@ -35,7 +35,8 @@ class ProtectedBranchDropdown {
         return _.escape(protectedBranch.id);
       },
       onFilter: this.toggleCreateNewButton.bind(this),
-      clicked: (item, $el, e) => {
+      clicked: (options) => {
+        const { $el, e } = options;
         e.preventDefault();
         this.onSelect();
       }
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
index fff83f3af3bb3c034d12328645e12b8cc3e83e7f..d4c9a91a74a420ef475cd95d42907dc31c313b8a 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -17,8 +17,8 @@ export default class ProtectedTagAccessDropdown {
         }
         return 'Select';
       },
-      clicked(item, $el, e) {
-        e.preventDefault();
+      clicked(options) {
+        options.e.preventDefault();
         onSelect();
       },
     });
diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js
index 5ff4e4432622b0b4e38af1d5497177748f1df9d6..068e9698e1d025acf483d479139c9f1d7271d4d4 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js
@@ -39,8 +39,8 @@ export default class ProtectedTagDropdown {
         return _.escape(protectedTag.id);
       },
       onFilter: this.toggleCreateNewButton.bind(this),
-      clicked: (item, $el, e) => {
-        e.preventDefault();
+      clicked: (options) => {
+        options.e.preventDefault();
         this.onSelect();
       },
     });
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9ad3708514b70303f1db21fabbc18515b587530
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
@@ -0,0 +1,41 @@
+export default {
+  name: 'AssigneeTitle',
+  props: {
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    numberOfAssignees: {
+      type: Number,
+      required: true,
+    },
+    editable: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  computed: {
+    assigneeTitle() {
+      const assignees = this.numberOfAssignees;
+      return assignees > 1 ? `${assignees} Assignees` : 'Assignee';
+    },
+  },
+  template: `
+    <div class="title hide-collapsed">
+      {{assigneeTitle}}
+      <i
+        v-if="loading"
+        aria-hidden="true"
+        class="fa fa-spinner fa-spin block-loading"
+      />
+      <a
+        v-if="editable"
+        class="edit-link pull-right"
+        href="#"
+      >
+        Edit
+      </a>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js
new file mode 100644
index 0000000000000000000000000000000000000000..7e5feac622c7ad5b8f950e12ef6e642ebf01b252
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.js
@@ -0,0 +1,224 @@
+export default {
+  name: 'Assignees',
+  data() {
+    return {
+      defaultRenderCount: 5,
+      defaultMaxCounter: 99,
+      showLess: true,
+    };
+  },
+  props: {
+    rootPath: {
+      type: String,
+      required: true,
+    },
+    users: {
+      type: Array,
+      required: true,
+    },
+    editable: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  computed: {
+    firstUser() {
+      return this.users[0];
+    },
+    hasMoreThanTwoAssignees() {
+      return this.users.length > 2;
+    },
+    hasMoreThanOneAssignee() {
+      return this.users.length > 1;
+    },
+    hasAssignees() {
+      return this.users.length > 0;
+    },
+    hasNoUsers() {
+      return !this.users.length;
+    },
+    hasOneUser() {
+      return this.users.length === 1;
+    },
+    renderShowMoreSection() {
+      return this.users.length > this.defaultRenderCount;
+    },
+    numberOfHiddenAssignees() {
+      return this.users.length - this.defaultRenderCount;
+    },
+    isHiddenAssignees() {
+      return this.numberOfHiddenAssignees > 0;
+    },
+    hiddenAssigneesLabel() {
+      return `+ ${this.numberOfHiddenAssignees} more`;
+    },
+    collapsedTooltipTitle() {
+      const maxRender = Math.min(this.defaultRenderCount, this.users.length);
+      const renderUsers = this.users.slice(0, maxRender);
+      const names = renderUsers.map(u => u.name);
+
+      if (this.users.length > maxRender) {
+        names.push(`+ ${this.users.length - maxRender} more`);
+      }
+
+      return names.join(', ');
+    },
+    sidebarAvatarCounter() {
+      let counter = `+${this.users.length - 1}`;
+
+      if (this.users.length > this.defaultMaxCounter) {
+        counter = `${this.defaultMaxCounter}+`;
+      }
+
+      return counter;
+    },
+  },
+  methods: {
+    assignSelf() {
+      this.$emit('assign-self');
+    },
+    toggleShowLess() {
+      this.showLess = !this.showLess;
+    },
+    renderAssignee(index) {
+      return !this.showLess || (index < this.defaultRenderCount && this.showLess);
+    },
+    avatarUrl(user) {
+      return user.avatar || user.avatar_url;
+    },
+    assigneeUrl(user) {
+      return `${this.rootPath}${user.username}`;
+    },
+    assigneeAlt(user) {
+      return `${user.name}'s avatar`;
+    },
+    assigneeUsername(user) {
+      return `@${user.username}`;
+    },
+    shouldRenderCollapsedAssignee(index) {
+      const firstTwo = this.users.length <= 2 && index <= 2;
+
+      return index === 0 || firstTwo;
+    },
+  },
+  template: `
+    <div>
+      <div
+        class="sidebar-collapsed-icon sidebar-collapsed-user"
+        :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+        data-container="body"
+        data-placement="left"
+        :title="collapsedTooltipTitle"
+      >
+        <i
+          v-if="hasNoUsers"
+          aria-label="No Assignee"
+          class="fa fa-user"
+        />
+        <button
+          type="button"
+          class="btn-link"
+          v-for="(user, index) in users"
+          v-if="shouldRenderCollapsedAssignee(index)"
+        >
+          <img
+            width="24"
+            class="avatar avatar-inline s24"
+            :alt="assigneeAlt(user)"
+            :src="avatarUrl(user)"
+          />
+          <span class="author">
+            {{ user.name }}
+          </span>
+        </button>
+        <button
+          v-if="hasMoreThanTwoAssignees"
+          class="btn-link"
+          type="button"
+        >
+          <span
+            class="avatar-counter sidebar-avatar-counter"
+          >
+            {{ sidebarAvatarCounter }}
+          </span>
+        </button>
+      </div>
+      <div class="value hide-collapsed">
+        <template v-if="hasNoUsers">
+          <span class="assign-yourself no-value">
+            No assignee
+            <template v-if="editable">
+             -
+              <button
+                type="button"
+                class="btn-link"
+                @click="assignSelf"
+              >
+                assign yourself
+              </button>
+            </template>
+          </span>
+        </template>
+        <template v-else-if="hasOneUser">
+          <a
+            class="author_link bold"
+            :href="assigneeUrl(firstUser)"
+          >
+            <img
+              width="32"
+              class="avatar avatar-inline s32"
+              :alt="assigneeAlt(firstUser)"
+              :src="avatarUrl(firstUser)"
+            />
+            <span class="author">
+              {{ firstUser.name }}
+            </span>
+            <span class="username">
+              {{ assigneeUsername(firstUser) }}
+            </span>
+          </a>
+        </template>
+        <template v-else>
+          <div class="user-list">
+            <div
+              class="user-item"
+              v-for="(user, index) in users"
+              v-if="renderAssignee(index)"
+            >
+              <a
+                class="user-link has-tooltip"
+                data-placement="bottom"
+                :href="assigneeUrl(user)"
+                :data-title="user.name"
+              >
+                <img
+                  width="32"
+                  class="avatar avatar-inline s32"
+                  :alt="assigneeAlt(user)"
+                  :src="avatarUrl(user)"
+                />
+              </a>
+            </div>
+          </div>
+          <div
+            v-if="renderShowMoreSection"
+            class="user-list-more"
+          >
+            <button
+              type="button"
+              class="btn-link"
+              @click="toggleShowLess"
+            >
+              <template v-if="showLess">
+                {{ hiddenAssigneesLabel }}
+              </template>
+              <template v-else>
+                - show less
+              </template>
+            </button>
+          </div>
+        </template>
+      </div>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
new file mode 100644
index 0000000000000000000000000000000000000000..1488a66c695ad1061440ffa8267c6e9e3d977939
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
@@ -0,0 +1,84 @@
+/* global Flash */
+
+import AssigneeTitle from './assignee_title';
+import Assignees from './assignees';
+
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+
+import eventHub from '../../event_hub';
+
+export default {
+  name: 'SidebarAssignees',
+  data() {
+    return {
+      mediator: new Mediator(),
+      store: new Store(),
+      loading: false,
+      field: '',
+    };
+  },
+  components: {
+    'assignee-title': AssigneeTitle,
+    assignees: Assignees,
+  },
+  methods: {
+    assignSelf() {
+      // Notify gl dropdown that we are now assigning to current user
+      this.$el.parentElement.dispatchEvent(new Event('assignYourself'));
+
+      this.mediator.assignYourself();
+      this.saveAssignees();
+    },
+    saveAssignees() {
+      this.loading = true;
+
+      function setLoadingFalse() {
+        this.loading = false;
+      }
+
+      this.mediator.saveAssignees(this.field)
+        .then(setLoadingFalse.bind(this))
+        .catch(() => {
+          setLoadingFalse();
+          return new Flash('Error occurred when saving assignees');
+        });
+    },
+  },
+  created() {
+    this.removeAssignee = this.store.removeAssignee.bind(this.store);
+    this.addAssignee = this.store.addAssignee.bind(this.store);
+    this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store);
+
+    // Get events from glDropdown
+    eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
+    eventHub.$on('sidebar.addAssignee', this.addAssignee);
+    eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
+    eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
+  },
+  beforeDestroy() {
+    eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
+    eventHub.$off('sidebar.addAssignee', this.addAssignee);
+    eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
+    eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
+  },
+  beforeMount() {
+    this.field = this.$el.dataset.field;
+  },
+  template: `
+    <div>
+      <assignee-title
+        :number-of-assignees="store.assignees.length"
+        :loading="loading"
+        :editable="store.editable"
+      />
+      <assignees
+        class="value"
+        :root-path="store.rootPath"
+        :users="store.assignees"
+        :editable="store.editable"
+        @assign-self="assignSelf"
+      />
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
new file mode 100644
index 0000000000000000000000000000000000000000..0da265053bd0e586126318955d7b39d12abd17d1
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
@@ -0,0 +1,97 @@
+import stopwatchSvg from 'icons/_icon_stopwatch.svg';
+
+import '../../../lib/utils/pretty_time';
+
+export default {
+  name: 'time-tracking-collapsed-state',
+  props: {
+    showComparisonState: {
+      type: Boolean,
+      required: true,
+    },
+    showSpentOnlyState: {
+      type: Boolean,
+      required: true,
+    },
+    showEstimateOnlyState: {
+      type: Boolean,
+      required: true,
+    },
+    showNoTimeTrackingState: {
+      type: Boolean,
+      required: true,
+    },
+    timeSpentHumanReadable: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    timeEstimateHumanReadable: {
+      type: String,
+      required: false,
+      default: '',
+    },
+  },
+  computed: {
+    timeSpent() {
+      return this.abbreviateTime(this.timeSpentHumanReadable);
+    },
+    timeEstimate() {
+      return this.abbreviateTime(this.timeEstimateHumanReadable);
+    },
+    divClass() {
+      if (this.showComparisonState) {
+        return 'compare';
+      } else if (this.showEstimateOnlyState) {
+        return 'estimate-only';
+      } else if (this.showSpentOnlyState) {
+        return 'spend-only';
+      } else if (this.showNoTimeTrackingState) {
+        return 'no-tracking';
+      }
+
+      return '';
+    },
+    spanClass() {
+      if (this.showComparisonState) {
+        return '';
+      } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
+        return 'bold';
+      } else if (this.showNoTimeTrackingState) {
+        return 'no-value';
+      }
+
+      return '';
+    },
+    text() {
+      if (this.showComparisonState) {
+        return `${this.timeSpent} / ${this.timeEstimate}`;
+      } else if (this.showEstimateOnlyState) {
+        return `-- / ${this.timeEstimate}`;
+      } else if (this.showSpentOnlyState) {
+        return `${this.timeSpent} / --`;
+      } else if (this.showNoTimeTrackingState) {
+        return 'None';
+      }
+
+      return '';
+    },
+  },
+  methods: {
+    abbreviateTime(timeStr) {
+      return gl.utils.prettyTime.abbreviateTime(timeStr);
+    },
+  },
+  template: `
+    <div class="sidebar-collapsed-icon">
+      ${stopwatchSvg}
+      <div class="time-tracking-collapsed-summary">
+        <div :class="divClass">
+          <span :class="spanClass">
+            {{ text }}
+          </span>
+        </div>
+      </div>
+    </div>
+    `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
new file mode 100644
index 0000000000000000000000000000000000000000..40f5c89c5bbcfb6616b148f33bb74174eabba979
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
@@ -0,0 +1,98 @@
+import '../../../lib/utils/pretty_time';
+
+const prettyTime = gl.utils.prettyTime;
+
+export default {
+  name: 'time-tracking-comparison-pane',
+  props: {
+    timeSpent: {
+      type: Number,
+      required: true,
+    },
+    timeEstimate: {
+      type: Number,
+      required: true,
+    },
+    timeSpentHumanReadable: {
+      type: String,
+      required: true,
+    },
+    timeEstimateHumanReadable: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    parsedRemaining() {
+      const diffSeconds = this.timeEstimate - this.timeSpent;
+      return prettyTime.parseSeconds(diffSeconds);
+    },
+    timeRemainingHumanReadable() {
+      return prettyTime.stringifyTime(this.parsedRemaining);
+    },
+    timeRemainingTooltip() {
+      const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
+      return `${prefix} ${this.timeRemainingHumanReadable}`;
+    },
+    /* Diff values for comparison meter */
+    timeRemainingMinutes() {
+      return this.timeEstimate - this.timeSpent;
+    },
+    timeRemainingPercent() {
+      return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
+    },
+    timeRemainingStatusClass() {
+      return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
+    },
+    /* Parsed time values */
+    parsedEstimate() {
+      return prettyTime.parseSeconds(this.timeEstimate);
+    },
+    parsedSpent() {
+      return prettyTime.parseSeconds(this.timeSpent);
+    },
+  },
+  template: `
+    <div class="time-tracking-comparison-pane">
+      <div
+        class="compare-meter"
+        data-toggle="tooltip"
+        data-placement="top"
+        role="timeRemainingDisplay"
+        :aria-valuenow="timeRemainingTooltip"
+        :title="timeRemainingTooltip"
+        :data-original-title="timeRemainingTooltip"
+        :class="timeRemainingStatusClass"
+      >
+        <div
+          class="meter-container"
+          role="timeSpentPercent"
+          :aria-valuenow="timeRemainingPercent"
+        >
+          <div
+            :style="{ width: timeRemainingPercent }"
+            class="meter-fill"
+          />
+        </div>
+        <div class="compare-display-container">
+          <div class="compare-display pull-left">
+            <span class="compare-label">
+              Spent
+            </span>
+            <span class="compare-value spent">
+              {{ timeSpentHumanReadable }}
+            </span>
+          </div>
+          <div class="compare-display estimated pull-right">
+            <span class="compare-label">
+              Est
+            </span>
+            <span class="compare-value">
+              {{ timeEstimateHumanReadable }}
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad1b9179db017532dcf8e7439206e7389169941a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
@@ -0,0 +1,17 @@
+export default {
+  name: 'time-tracking-estimate-only-pane',
+  props: {
+    timeEstimateHumanReadable: {
+      type: String,
+      required: true,
+    },
+  },
+  template: `
+    <div class="time-tracking-estimate-only-pane">
+      <span class="bold">
+        Estimated:
+      </span>
+      {{ timeEstimateHumanReadable }}
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js
new file mode 100644
index 0000000000000000000000000000000000000000..b2a77462fe0d45ad608dd77a18f4abe8b96db037
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js
@@ -0,0 +1,44 @@
+export default {
+  name: 'time-tracking-help-state',
+  props: {
+    rootPath: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    href() {
+      return `${this.rootPath}help/workflow/time_tracking.md`;
+    },
+  },
+  template: `
+    <div class="time-tracking-help-state">
+      <div class="time-tracking-info">
+        <h4>
+          Track time with slash commands
+        </h4>
+        <p>
+          Slash commands can be used in the issues description and comment boxes.
+        </p>
+        <p>
+          <code>
+            /estimate
+          </code>
+          will update the estimated time with the latest command.
+        </p>
+        <p>
+          <code>
+            /spend
+          </code>
+          will update the sum of the time spent.
+        </p>
+        <a
+          class="btn btn-default learn-more-button"
+          :href="href"
+        >
+          Learn more
+        </a>
+      </div>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1dd1dcdd277dffb635c4a5dd95a2b1f8aeba5c6
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
@@ -0,0 +1,10 @@
+export default {
+  name: 'time-tracking-no-tracking-pane',
+  template: `
+    <div class="time-tracking-no-tracking-pane">
+      <span class="no-value">
+        No estimate or time spent
+      </span>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
new file mode 100644
index 0000000000000000000000000000000000000000..e2dba1fb0c2e79cdf56faf92fb54974d36c134ee
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
@@ -0,0 +1,45 @@
+import '~/smart_interval';
+
+import timeTracker from './time_tracker';
+
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+
+export default {
+  data() {
+    return {
+      mediator: new Mediator(),
+      store: new Store(),
+    };
+  },
+  components: {
+    'issuable-time-tracker': timeTracker,
+  },
+  methods: {
+    listenForSlashCommands() {
+      $(document).on('ajax:success', '.gfm-form', (e, data) => {
+        const subscribedCommands = ['spend_time', 'time_estimate'];
+        const changedCommands = data.commands_changes
+          ? Object.keys(data.commands_changes)
+          : [];
+        if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
+          this.mediator.fetch();
+        }
+      });
+    },
+  },
+  mounted() {
+    this.listenForSlashCommands();
+  },
+  template: `
+    <div class="block">
+      <issuable-time-tracker
+        :time_estimate="store.timeEstimate"
+        :time_spent="store.totalTimeSpent"
+        :human_time_estimate="store.humanTimeEstimate"
+        :human_time_spent="store.humanTotalTimeSpent"
+        :rootPath="store.rootPath"
+      />
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf9875626475ed024a4d5454a08d934b843813f6
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
@@ -0,0 +1,15 @@
+export default {
+  name: 'time-tracking-spent-only-pane',
+  props: {
+    timeSpentHumanReadable: {
+      type: String,
+      required: true,
+    },
+  },
+  template: `
+    <div class="time-tracking-spend-only-pane">
+      <span class="bold">Spent:</span>
+      {{ timeSpentHumanReadable }}
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed0d71a4f797d6e5cb6e0a5413f0d248e96a84d8
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js
@@ -0,0 +1,163 @@
+import timeTrackingHelpState from './help_state';
+import timeTrackingCollapsedState from './collapsed_state';
+import timeTrackingSpentOnlyPane from './spent_only_pane';
+import timeTrackingNoTrackingPane from './no_tracking_pane';
+import timeTrackingEstimateOnlyPane from './estimate_only_pane';
+import timeTrackingComparisonPane from './comparison_pane';
+
+import eventHub from '../../event_hub';
+
+export default {
+  name: 'issuable-time-tracker',
+  props: {
+    time_estimate: {
+      type: Number,
+      required: true,
+    },
+    time_spent: {
+      type: Number,
+      required: true,
+    },
+    human_time_estimate: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    human_time_spent: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    rootPath: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      showHelp: false,
+    };
+  },
+  components: {
+    'time-tracking-collapsed-state': timeTrackingCollapsedState,
+    'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
+    'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
+    'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
+    'time-tracking-comparison-pane': timeTrackingComparisonPane,
+    'time-tracking-help-state': timeTrackingHelpState,
+  },
+  computed: {
+    timeSpent() {
+      return this.time_spent;
+    },
+    timeEstimate() {
+      return this.time_estimate;
+    },
+    timeEstimateHumanReadable() {
+      return this.human_time_estimate;
+    },
+    timeSpentHumanReadable() {
+      return this.human_time_spent;
+    },
+    hasTimeSpent() {
+      return !!this.timeSpent;
+    },
+    hasTimeEstimate() {
+      return !!this.timeEstimate;
+    },
+    showComparisonState() {
+      return this.hasTimeEstimate && this.hasTimeSpent;
+    },
+    showEstimateOnlyState() {
+      return this.hasTimeEstimate && !this.hasTimeSpent;
+    },
+    showSpentOnlyState() {
+      return this.hasTimeSpent && !this.hasTimeEstimate;
+    },
+    showNoTimeTrackingState() {
+      return !this.hasTimeEstimate && !this.hasTimeSpent;
+    },
+    showHelpState() {
+      return !!this.showHelp;
+    },
+  },
+  methods: {
+    toggleHelpState(show) {
+      this.showHelp = show;
+    },
+    update(data) {
+      this.time_estimate = data.time_estimate;
+      this.time_spent = data.time_spent;
+      this.human_time_estimate = data.human_time_estimate;
+      this.human_time_spent = data.human_time_spent;
+    },
+  },
+  created() {
+    eventHub.$on('timeTracker:updateData', this.update);
+  },
+  template: `
+    <div
+      class="time_tracker time-tracking-component-wrap"
+      v-cloak
+    >
+      <time-tracking-collapsed-state
+        :show-comparison-state="showComparisonState"
+        :show-no-time-tracking-state="showNoTimeTrackingState"
+        :show-help-state="showHelpState"
+        :show-spent-only-state="showSpentOnlyState"
+        :show-estimate-only-state="showEstimateOnlyState"
+        :time-spent-human-readable="timeSpentHumanReadable"
+        :time-estimate-human-readable="timeEstimateHumanReadable"
+      />
+      <div class="title hide-collapsed">
+        Time tracking
+        <div
+          class="help-button pull-right"
+          v-if="!showHelpState"
+          @click="toggleHelpState(true)"
+        >
+            <i
+              class="fa fa-question-circle"
+              aria-hidden="true"
+            />
+        </div>
+        <div
+          class="close-help-button pull-right"
+          v-if="showHelpState"
+          @click="toggleHelpState(false)"
+        >
+          <i
+            class="fa fa-close"
+            aria-hidden="true"
+          />
+        </div>
+      </div>
+      <div class="time-tracking-content hide-collapsed">
+        <time-tracking-estimate-only-pane
+          v-if="showEstimateOnlyState"
+          :time-estimate-human-readable="timeEstimateHumanReadable"
+        />
+        <time-tracking-spent-only-pane
+          v-if="showSpentOnlyState"
+          :time-spent-human-readable="timeSpentHumanReadable"
+        />
+        <time-tracking-no-tracking-pane
+          v-if="showNoTimeTrackingState"
+        />
+        <time-tracking-comparison-pane
+          v-if="showComparisonState"
+          :time-estimate="timeEstimate"
+          :time-spent="timeSpent"
+          :time-spent-human-readable="timeSpentHumanReadable"
+          :time-estimate-human-readable="timeEstimateHumanReadable"
+        />
+        <transition name="help-state-toggle">
+          <time-tracking-help-state
+            v-if="showHelpState"
+            :rootPath="rootPath"
+          />
+        </transition>
+      </div>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/sidebar/event_hub.js b/app/assets/javascripts/sidebar/event_hub.js
new file mode 100644
index 0000000000000000000000000000000000000000..0948c2e53524a736a55c060600868ce89ee7687a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
new file mode 100644
index 0000000000000000000000000000000000000000..5a82d01dc41e7ad72dee79f157aa453a3bc61718
--- /dev/null
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class SidebarService {
+  constructor(endpoint) {
+    if (!SidebarService.singleton) {
+      this.endpoint = endpoint;
+
+      SidebarService.singleton = this;
+    }
+
+    return SidebarService.singleton;
+  }
+
+  get() {
+    return Vue.http.get(this.endpoint);
+  }
+
+  update(key, data) {
+    return Vue.http.put(this.endpoint, {
+      [key]: data,
+    }, {
+      emulateJSON: true,
+    });
+  }
+}
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ce53c2ed30d58c567c3dcee4b07ccc482929447
--- /dev/null
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import sidebarAssignees from './components/assignees/sidebar_assignees';
+
+import Mediator from './sidebar_mediator';
+
+document.addEventListener('DOMContentLoaded', () => {
+  const mediator = new Mediator(gl.sidebarOptions);
+  mediator.fetch();
+
+  const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
+
+  // Only create the sidebarAssignees vue app if it is found in the DOM
+  // We currently do not use sidebarAssignees for the MR page
+  if (sidebarAssigneesEl) {
+    new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
+  }
+
+  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
new file mode 100644
index 0000000000000000000000000000000000000000..c13f3391f0d0e59de35a009175efabd22faae4f6
--- /dev/null
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -0,0 +1,38 @@
+/* global Flash */
+
+import Service from './services/sidebar_service';
+import Store from './stores/sidebar_store';
+
+export default class SidebarMediator {
+  constructor(options) {
+    if (!SidebarMediator.singleton) {
+      this.store = new Store(options);
+      this.service = new Service(options.endpoint);
+      SidebarMediator.singleton = this;
+    }
+
+    return SidebarMediator.singleton;
+  }
+
+  assignYourself() {
+    this.store.addAssignee(this.store.currentUser);
+  }
+
+  saveAssignees(field) {
+    const selected = this.store.assignees.map(u => u.id);
+
+    // If there are no ids, that means we have to unassign (which is id = 0)
+    // And it only accepts an array, hence [0]
+    return this.service.update(field, selected.length === 0 ? [0] : selected);
+  }
+
+  fetch() {
+    this.service.get()
+      .then((response) => {
+        const data = response.json();
+        this.store.processAssigneeData(data);
+        this.store.processTimeTrackingData(data);
+      })
+      .catch(() => new Flash('Error occured when fetching sidebar data'));
+  }
+}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
new file mode 100644
index 0000000000000000000000000000000000000000..94408c4d71547e1535490c36386ef8b339e1ec10
--- /dev/null
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -0,0 +1,52 @@
+export default class SidebarStore {
+  constructor(store) {
+    if (!SidebarStore.singleton) {
+      const { currentUser, rootPath, editable } = store;
+      this.currentUser = currentUser;
+      this.rootPath = rootPath;
+      this.editable = editable;
+      this.timeEstimate = 0;
+      this.totalTimeSpent = 0;
+      this.humanTimeEstimate = '';
+      this.humanTimeSpent = '';
+      this.assignees = [];
+
+      SidebarStore.singleton = this;
+    }
+
+    return SidebarStore.singleton;
+  }
+
+  processAssigneeData(data) {
+    if (data.assignees) {
+      this.assignees = data.assignees;
+    }
+  }
+
+  processTimeTrackingData(data) {
+    this.timeEstimate = data.time_estimate;
+    this.totalTimeSpent = data.total_time_spent;
+    this.humanTimeEstimate = data.human_time_estimate;
+    this.humanTotalTimeSpent = data.human_total_time_spent;
+  }
+
+  addAssignee(assignee) {
+    if (!this.findAssignee(assignee)) {
+      this.assignees.push(assignee);
+    }
+  }
+
+  findAssignee(findAssignee) {
+    return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
+  }
+
+  removeAssignee(removeAssignee) {
+    if (removeAssignee) {
+      this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
+    }
+  }
+
+  removeAllAssignees() {
+    this.assignees = [];
+  }
+}
diff --git a/app/assets/javascripts/subbable_resource.js b/app/assets/javascripts/subbable_resource.js
deleted file mode 100644
index d81916051284986d5a7bd115d093a1b02abe5a8e..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/subbable_resource.js
+++ /dev/null
@@ -1,51 +0,0 @@
-(() => {
-/*
-*   SubbableResource can be extended to provide a pubsub-style service for one-off REST
-*   calls. Subscribe by passing a callback or render method you will use to handle responses.
- *
-* */
-
-  class SubbableResource {
-    constructor(resourcePath) {
-      this.endpoint = resourcePath;
-
-      // TODO: Switch to axios.create
-      this.resource = $.ajax;
-      this.subscribers = [];
-    }
-
-    subscribe(callback) {
-      this.subscribers.push(callback);
-    }
-
-    publish(newResponse) {
-      const responseCopy = _.extend({}, newResponse);
-      this.subscribers.forEach((fn) => {
-        fn(responseCopy);
-      });
-      return newResponse;
-    }
-
-    get(payload) {
-      return this.resource(payload)
-        .then(data => this.publish(data));
-    }
-
-    post(payload) {
-      return this.resource(payload)
-        .then(data => this.publish(data));
-    }
-
-    put(payload) {
-      return this.resource(payload)
-        .then(data => this.publish(data));
-    }
-
-    delete(payload) {
-      return this.resource(payload)
-        .then(data => this.publish(data));
-    }
-  }
-
-  gl.SubbableResource = SubbableResource;
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 8b25f43ffc745d42dd80213aebf9a0eec5ba8ed0..0cd591c73208673730951ea4fccbef19385325fa 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -19,8 +19,8 @@
               return label;
             };
           })(this),
-          clicked: function(item, $el, e) {
-            return e.preventDefault();
+          clicked: function(options) {
+            return options.e.preventDefault();
           },
           id: function(obj, el) {
             return $(el).data("id");
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 68cf9ced3efbf7aa895d1ad9809c65a3d0332f55..be29b08c34345154b9a5a6d623dd3f8b3508f866 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,6 +1,7 @@
 /* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
 /* global Issuable */
-/* global ListUser */
+
+import eventHub from './sidebar/event_hub';
 
 (function() {
   var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
@@ -54,42 +55,115 @@
           selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
           selectedId = $dropdown.data('selected') || selectedIdDefault;
 
-          var updateIssueBoardsIssue = function () {
-            $loading.removeClass('hidden').fadeIn();
-            gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
-              .then(function () {
-                $loading.fadeOut();
-              })
-              .catch(function () {
-                $loading.fadeOut();
-              });
+          const assignYourself = function () {
+            const unassignedSelected = $dropdown.closest('.selectbox')
+              .find(`input[name='${$dropdown.data('field-name')}'][value=0]`);
+
+            if (unassignedSelected) {
+              unassignedSelected.remove();
+            }
+
+            // Save current selected user to the DOM
+            const input = document.createElement('input');
+            input.type = 'hidden';
+            input.name = $dropdown.data('field-name');
+
+            const currentUserInfo = $dropdown.data('currentUserInfo');
+
+            if (currentUserInfo) {
+              input.value = currentUserInfo.id;
+              input.dataset.meta = currentUserInfo.name;
+            } else if (_this.currentUser) {
+              input.value = _this.currentUser.id;
+            }
+
+            if ($selectbox) {
+              $dropdown.parent().before(input);
+            } else {
+              $dropdown.after(input);
+            }
+          };
+
+          if ($block[0]) {
+            $block[0].addEventListener('assignYourself', assignYourself);
+          }
+
+          const getSelectedUserInputs = function() {
+            return $selectbox
+              .find(`input[name="${$dropdown.data('field-name')}"]`);
+          };
+
+          const getSelected = function() {
+            return getSelectedUserInputs()
+              .map((index, input) => parseInt(input.value, 10))
+              .get();
+          };
+
+          const checkMaxSelect = function() {
+            const maxSelect = $dropdown.data('max-select');
+            if (maxSelect) {
+              const selected = getSelected();
+
+              if (selected.length > maxSelect) {
+                const firstSelectedId = selected[0];
+                const firstSelected = $dropdown.closest('.selectbox')
+                  .find(`input[name='${$dropdown.data('field-name')}'][value=${firstSelectedId}]`);
+
+                firstSelected.remove();
+                eventHub.$emit('sidebar.removeAssignee', {
+                  id: firstSelectedId,
+                });
+              }
+            }
+          };
+
+          const getMultiSelectDropdownTitle = function(selectedUser, isSelected) {
+            const selectedUsers = getSelected()
+              .filter(u => u !== 0);
+
+            const firstUser = getSelectedUserInputs()
+              .map((index, input) => ({
+                name: input.dataset.meta,
+                value: parseInt(input.value, 10),
+              }))
+              .filter(u => u.id !== 0)
+              .get(0);
+
+            if (selectedUsers.length === 0) {
+              return 'Unassigned';
+            } else if (selectedUsers.length === 1) {
+              return firstUser.name;
+            } else if (isSelected) {
+              const otherSelected = selectedUsers.filter(s => s !== selectedUser.id);
+              return `${selectedUser.name} + ${otherSelected.length} more`;
+            } else {
+              return `${firstUser.name} + ${selectedUsers.length - 1} more`;
+            }
           };
 
           $('.assign-to-me-link').on('click', (e) => {
             e.preventDefault();
             $(e.currentTarget).hide();
-            const $input = $(`input[name="${$dropdown.data('field-name')}"]`);
-            $input.val(gon.current_user_id);
-            selectedId = $input.val();
-            $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
-          });
 
-          $block.on('click', '.js-assign-yourself', function(e) {
-            e.preventDefault();
-
-            if ($dropdown.hasClass('js-issue-board-sidebar')) {
-              gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
-                id: _this.currentUser.id,
-                username: _this.currentUser.username,
-                name: _this.currentUser.name,
-                avatar_url: _this.currentUser.avatar_url
-              }));
+            if ($dropdown.data('multiSelect')) {
+              assignYourself();
+              checkMaxSelect();
 
-              updateIssueBoardsIssue();
+              const currentUserInfo = $dropdown.data('currentUserInfo');
+              $dropdown.find('.dropdown-toggle-text').text(getMultiSelectDropdownTitle(currentUserInfo)).removeClass('is-default');
             } else {
-              return assignTo(_this.currentUser.id);
+              const $input = $(`input[name="${$dropdown.data('field-name')}"]`);
+              $input.val(gon.current_user_id);
+              selectedId = $input.val();
+              $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default');
             }
           });
+
+          $block.on('click', '.js-assign-yourself', (e) => {
+            e.preventDefault();
+            return assignTo(_this.currentUser.id);
+          });
+
           assignTo = function(selected) {
             var data;
             data = {};
@@ -97,6 +171,7 @@
             data[abilityName].assignee_id = selected != null ? selected : null;
             $loading.removeClass('hidden').fadeIn();
             $dropdown.trigger('loading.gl.dropdown');
+
             return $.ajax({
               type: 'PUT',
               dataType: 'json',
@@ -106,7 +181,6 @@
               var user;
               $dropdown.trigger('loaded.gl.dropdown');
               $loading.fadeOut();
-              $selectbox.hide();
               if (data.assignee) {
                 user = {
                   name: data.assignee.name,
@@ -133,51 +207,90 @@
               var isAuthorFilter;
               isAuthorFilter = $('.js-author-search');
               return _this.users(term, options, function(users) {
-                var anyUser, index, j, len, name, obj, showDivider;
-                if (term.length === 0) {
-                  showDivider = 0;
-                  if (firstUser) {
-                    // Move current user to the front of the list
-                    for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
-                      obj = users[index];
-                      if (obj.username === firstUser) {
-                        users.splice(index, 1);
-                        users.unshift(obj);
-                        break;
-                      }
+                // GitLabDropdownFilter returns this.instance
+                // GitLabDropdownRemote returns this.options.instance
+                const glDropdown = this.instance || this.options.instance;
+                glDropdown.options.processData(term, users, callback);
+              }.bind(this));
+            },
+            processData: function(term, users, callback) {
+              let anyUser;
+              let index;
+              let j;
+              let len;
+              let name;
+              let obj;
+              let showDivider;
+              if (term.length === 0) {
+                showDivider = 0;
+                if (firstUser) {
+                  // Move current user to the front of the list
+                  for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
+                    obj = users[index];
+                    if (obj.username === firstUser) {
+                      users.splice(index, 1);
+                      users.unshift(obj);
+                      break;
                     }
                   }
-                  if (showNullUser) {
-                    showDivider += 1;
-                    users.unshift({
-                      beforeDivider: true,
-                      name: 'Unassigned',
-                      id: 0
-                    });
-                  }
-                  if (showAnyUser) {
-                    showDivider += 1;
-                    name = showAnyUser;
-                    if (name === true) {
-                      name = 'Any User';
-                    }
-                    anyUser = {
-                      beforeDivider: true,
-                      name: name,
-                      id: null
-                    };
-                    users.unshift(anyUser);
+                }
+                if (showNullUser) {
+                  showDivider += 1;
+                  users.unshift({
+                    beforeDivider: true,
+                    name: 'Unassigned',
+                    id: 0
+                  });
+                }
+                if (showAnyUser) {
+                  showDivider += 1;
+                  name = showAnyUser;
+                  if (name === true) {
+                    name = 'Any User';
                   }
+                  anyUser = {
+                    beforeDivider: true,
+                    name: name,
+                    id: null
+                  };
+                  users.unshift(anyUser);
                 }
+
                 if (showDivider) {
-                  users.splice(showDivider, 0, "divider");
+                  users.splice(showDivider, 0, 'divider');
                 }
 
-                callback(users);
-                if (showMenuAbove) {
-                  $dropdown.data('glDropdown').positionMenuAbove();
+                if ($dropdown.hasClass('js-multiselect')) {
+                  const selected = getSelected().filter(i => i !== 0);
+
+                  if (selected.length > 0) {
+                    if ($dropdown.data('dropdown-header')) {
+                      showDivider += 1;
+                      users.splice(showDivider, 0, {
+                        header: $dropdown.data('dropdown-header'),
+                      });
+                    }
+
+                    const selectedUsers = users
+                      .filter(u => selected.indexOf(u.id) !== -1)
+                      .sort((a, b) => a.name > b.name);
+
+                    users = users.filter(u => selected.indexOf(u.id) === -1);
+
+                    selectedUsers.forEach((selectedUser) => {
+                      showDivider += 1;
+                      users.splice(showDivider, 0, selectedUser);
+                    });
+
+                    users.splice(showDivider + 1, 0, 'divider');
+                  }
                 }
-              });
+              }
+
+              callback(users);
+              if (showMenuAbove) {
+                $dropdown.data('glDropdown').positionMenuAbove();
+              }
             },
             filterable: true,
             filterRemote: true,
@@ -186,7 +299,22 @@
             },
             selectable: true,
             fieldName: $dropdown.data('field-name'),
-            toggleLabel: function(selected, el) {
+            toggleLabel: function(selected, el, glDropdown) {
+              const inputValue = glDropdown.filterInput.val();
+
+              if (this.multiSelect && inputValue === '') {
+                // Remove non-users from the fullData array
+                const users = glDropdown.filteredFullData();
+                const callback = glDropdown.parseData.bind(glDropdown);
+
+                // Update the data model
+                this.processData(inputValue, users, callback);
+              }
+
+              if (this.multiSelect) {
+                return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
+              }
+
               if (selected && 'id' in selected && $(el).hasClass('is-active')) {
                 $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
                 if (selected.text) {
@@ -200,22 +328,81 @@
               }
             },
             defaultLabel: defaultLabel,
-            inputId: 'issue_assignee_id',
             hidden: function(e) {
-              $selectbox.hide();
-              // display:block overrides the hide-collapse rule
-              return $value.css('display', '');
+              if ($dropdown.hasClass('js-multiselect')) {
+                eventHub.$emit('sidebar.saveAssignees');
+              }
+
+              if (!$dropdown.data('always-show-selectbox')) {
+                $selectbox.hide();
+
+                // Recalculate where .value is because vue might have changed it
+                $block = $selectbox.closest('.block');
+                $value = $block.find('.value');
+                // display:block overrides the hide-collapse rule
+                $value.css('display', '');
+              }
             },
-            vue: $dropdown.hasClass('js-issue-board-sidebar'),
-            clicked: function(user, $el, e) {
-              var isIssueIndex, isMRIndex, page, selected, isSelecting;
+            multiSelect: $dropdown.hasClass('js-multiselect'),
+            inputMeta: $dropdown.data('input-meta'),
+            clicked: function(options) {
+              const { $el, e, isMarking } = options;
+              const user = options.selectedObj;
+
+              if ($dropdown.hasClass('js-multiselect')) {
+                const isActive = $el.hasClass('is-active');
+                const previouslySelected = $dropdown.closest('.selectbox')
+                    .find("input[name='" + ($dropdown.data('field-name')) + "'][value!=0]");
+
+                // Enables support for limiting the number of users selected
+                // Automatically removes the first on the list if more users are selected
+                checkMaxSelect();
+
+                if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
+                  // Unassigned selected
+                  previouslySelected.each((index, element) => {
+                    const id = parseInt(element.value, 10);
+                    element.remove();
+                  });
+                  eventHub.$emit('sidebar.removeAllAssignees');
+                } else if (isActive) {
+                  // user selected
+                  eventHub.$emit('sidebar.addAssignee', user);
+
+                  // Remove unassigned selection (if it was previously selected)
+                  const unassignedSelected = $dropdown.closest('.selectbox')
+                    .find("input[name='" + ($dropdown.data('field-name')) + "'][value=0]");
+
+                  if (unassignedSelected) {
+                    unassignedSelected.remove();
+                  }
+                } else {
+                  if (previouslySelected.length === 0) {
+                  // Select unassigned because there is no more selected users
+                    this.addInput($dropdown.data('field-name'), 0, {});
+                  }
+
+                  // User unselected
+                  eventHub.$emit('sidebar.removeAssignee', user);
+                }
+
+                if (getSelected().find(u => u === gon.current_user_id)) {
+                  $('.assign-to-me-link').hide();
+                } else {
+                  $('.assign-to-me-link').show();
+                }
+              }
+
+              var isIssueIndex, isMRIndex, page, selected;
               page = $('body').data('page');
               isIssueIndex = page === 'projects:issues:index';
               isMRIndex = (page === page && page === 'projects:merge_requests:index');
-              isSelecting = (user.id !== selectedId);
-              selectedId = isSelecting ? user.id : selectedIdDefault;
               if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
                 e.preventDefault();
+
+                const isSelecting = (user.id !== selectedId);
+                selectedId = isSelecting ? user.id : selectedIdDefault;
+
                 if (selectedId === gon.current_user_id) {
                   $('.assign-to-me-link').hide();
                 } else {
@@ -229,20 +416,7 @@
                 return Issuable.filterResults($dropdown.closest('form'));
               } else if ($dropdown.hasClass('js-filter-submit')) {
                 return $dropdown.closest('form').submit();
-              } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
-                if (user.id && isSelecting) {
-                  gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
-                    id: user.id,
-                    username: user.username,
-                    name: user.name,
-                    avatar_url: user.avatar_url
-                  }));
-                } else {
-                  gl.issueBoards.boardStoreIssueDelete('assignee');
-                }
-
-                updateIssueBoardsIssue();
-              } else {
+              } else if (!$dropdown.hasClass('js-multiselect')) {
                 selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
                 return assignTo(selected);
               }
@@ -256,29 +430,54 @@
                 selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault;
               }
               $el.find('.is-active').removeClass('is-active');
-              $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
+
+              function highlightSelected(id) {
+                $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
+              }
+
+              if ($selectbox[0]) {
+                getSelected().forEach(selectedId => highlightSelected(selectedId));
+              } else {
+                highlightSelected(selectedId);
+              }
             },
+            updateLabel: $dropdown.data('dropdown-title'),
             renderRow: function(user) {
-              var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
+              var avatar, img, listClosingTags, listWithName, listWithUserName, username;
               username = user.username ? "@" + user.username : "";
               avatar = user.avatar_url ? user.avatar_url : false;
-              selected = user.id === parseInt(selectedId, 10) ? "is-active" : "";
+
+              let selected = user.id === parseInt(selectedId, 10);
+
+              if (this.multiSelect) {
+                const fieldName = this.fieldName;
+                const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']");
+
+                if (field.length) {
+                  selected = true;
+                }
+              }
+
               img = "";
               if (user.beforeDivider != null) {
-                "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>";
+                `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${user.name}</a></li>`;
               } else {
                 if (avatar) {
-                  img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />";
+                  img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
                 }
               }
-              // split into three parts so we can remove the username section if nessesary
-              listWithName = "<li data-user-id=" + user.id + "> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
-              listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
-              listClosingTags = "</a> </li>";
-              if (username === '') {
-                listWithUserName = '';
-              }
-              return listWithName + listWithUserName + listClosingTags;
+
+              return `
+                <li data-user-id=${user.id}>
+                  <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
+                    ${img}
+                    <strong class='dropdown-menu-user-full-name'>
+                      ${user.name}
+                    </strong>
+                    ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
+                  </a>
+                </li>
+              `;
             }
           });
         };
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 3f5b78ed445367201fbdebd340dbf4e2fc8e55ec..91c1ebd5a7de3a70a43f6b10c60f74ed8736d700 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -93,3 +93,14 @@
     align-self: center;
   }
 }
+
+.avatar-counter {
+  background-color: $gray-darkest;
+  color: $white-light;
+  border: 1px solid $border-color;
+  border-radius: 1em;
+  font-family: $regular_font;
+  font-size: 9px;
+  line-height: 16px;
+  text-align: center;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 73ded9f30d470347b68a29b3358cfa6aa04dfcf9..5c9b71a452cc0733e529d317930061c53c211637 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -251,14 +251,16 @@
   }
 
   .dropdown-header {
-    color: $gl-text-color;
+    color: $gl-text-color-secondary;
     font-size: 13px;
-    font-weight: 600;
     line-height: 22px;
-    text-transform: capitalize;
     padding: 0 16px;
   }
 
+  &.capitalize-header .dropdown-header {
+    text-transform: capitalize;
+  }
+
   .separator + .dropdown-header {
     padding-top: 2px;
   }
@@ -337,8 +339,8 @@
 .dropdown-menu-user {
   .avatar {
     float: left;
-    width: 30px;
-    height: 30px;
+    width: 2 * $gl-padding;
+    height: 2 * $gl-padding;
     margin: 0 10px 0 0;
   }
 }
@@ -381,6 +383,7 @@
 .dropdown-menu-selectable {
   a {
     padding-left: 26px;
+    position: relative;
 
     &.is-indeterminate,
     &.is-active {
@@ -406,6 +409,9 @@
 
     &.is-active::before {
       content: "\f00c";
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
     }
   }
 }
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 15dc0aa6a528838649d0bf9865eca9c7164650a2..c9a25946ffd5f83070aed472009e050b8757d02d 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -255,6 +255,7 @@ ul.controls {
       .avatar-inline {
         margin-left: 0;
         margin-right: 0;
+        margin-bottom: 0;
       }
     }
   }
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 0be1c215959236e1b99c92f64448aaab962d07f9..68d7ab4bf8411d66d4cd383430034b1b9e9fd996 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -207,8 +207,13 @@
     margin-bottom: 5px;
   }
 
-  &.is-active {
+  &.is-active,
+  &.is-active .card-assignee:hover a {
     background-color: $row-hover;
+
+    &:first-child:not(:only-child) {
+      box-shadow: -10px 0 10px 1px $row-hover;
+    }
   }
 
   .label {
@@ -224,7 +229,7 @@
 }
 
 .card-title {
-  margin: 0;
+  margin: 0 30px 0 0;
   font-size: 1em;
   line-height: inherit;
 
@@ -240,10 +245,69 @@
   min-height: 20px;
 
   .card-assignee {
-    margin-left: auto;
-    margin-right: 5px;
-    padding-left: 10px;
+    display: flex;
+    justify-content: flex-end;
+    position: absolute;
+    right: 15px;
     height: 20px;
+    width: 20px;
+
+    .avatar-counter {
+      display: none;
+      vertical-align: middle;
+      min-width: 20px;
+      line-height: 19px;
+      height: 20px;
+      padding-left: 2px;
+      padding-right: 2px;
+      border-radius: 2em;
+    }
+
+    img {
+      vertical-align: top;
+    }
+
+    a {
+      position: relative;
+      margin-left: -15px;
+    }
+
+    a:nth-child(1) {
+      z-index: 3;
+    }
+
+    a:nth-child(2) {
+      z-index: 2;
+    }
+
+    a:nth-child(3) {
+      z-index: 1;
+    }
+
+    a:nth-child(4) {
+      display: none;
+    }
+
+    &:hover {
+      .avatar-counter {
+        display: inline-block;
+      }
+
+      a {
+        position: static;
+        background-color: $white-light;
+        transition: background-color 0s;
+        margin-left: auto;
+
+        &:nth-child(4) {
+          display: block;
+        }
+
+        &:first-child:not(:only-child) {
+          box-shadow: -10px 0 10px 1px $white-light;
+        }
+      }
+    }
   }
 
   .avatar {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index feefaad8a15c65c0b7e6b8eed5046d25f490ebb5..77f2638683a1d96c3d8b61be77065369ea93d39d 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -570,14 +570,7 @@
 
 .diff-comments-more-count,
 .diff-notes-collapse {
-  background-color: $gray-darkest;
-  color: $white-light;
-  border: 1px solid $white-light;
-  border-radius: 1em;
-  font-family: $regular_font;
-  font-size: 9px;
-  line-height: 17px;
-  text-align: center;
+  @extend .avatar-counter;
 }
 
 .diff-notes-collapse {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ad6eb9f6fe040f88776b70c1c8b1296a7a938c77..c4210ffd8230c14e0ec545a8e8d5bf41b792e1a8 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -95,10 +95,15 @@
 }
 
 .right-sidebar {
-  a {
+  a,
+  .btn-link {
     color: inherit;
   }
 
+  .btn-link {
+    outline: none;
+  }
+
   .issuable-header-text {
     margin-top: 7px;
   }
@@ -215,6 +220,10 @@
       }
     }
 
+    .assign-yourself .btn-link {
+      padding-left: 0;
+    }
+
     .light {
       font-weight: normal;
     }
@@ -239,6 +248,10 @@
       margin-left: 0;
     }
 
+    .assignee .user-list .avatar {
+      margin: 0;
+    }
+
     .username {
       display: block;
       margin-top: 4px;
@@ -301,6 +314,10 @@
         margin-top: 0;
       }
 
+      .sidebar-avatar-counter {
+        padding-top: 2px;
+      }
+
       .todo-undone {
         color: $gl-link-color;
       }
@@ -309,10 +326,15 @@
         display: none;
       }
 
-      .avatar:hover {
+      .avatar:hover,
+      .avatar-counter:hover {
         border-color: $issuable-sidebar-color;
       }
 
+      .avatar-counter:hover {
+        color: $issuable-sidebar-color;
+      }
+
       .btn-clipboard {
         border: none;
         color: $issuable-sidebar-color;
@@ -322,6 +344,17 @@
           color: $gl-text-color;
         }
       }
+
+      &.multiple-users {
+        display: flex;
+        justify-content: center;
+      }
+    }
+
+    .sidebar-avatar-counter {
+      width: 24px;
+      height: 24px;
+      border-radius: 12px;
     }
 
     .sidebar-collapsed-user {
@@ -332,6 +365,37 @@
     .issuable-header-btn {
       display: none;
     }
+
+    .multiple-users {
+      height: 24px;
+      margin-bottom: 17px;
+      margin-top: 4px;
+      padding-bottom: 4px;
+
+      .btn-link {
+        padding: 0;
+        border: 0;
+
+        .avatar {
+          margin: 0;
+        }
+      }
+
+      .btn-link:first-child {
+        position: absolute;
+        left: 10px;
+        z-index: 1;
+      }
+
+      .btn-link:last-child {
+        position: absolute;
+        right: 10px;
+
+        &:hover {
+          text-decoration: none;
+        }
+      }
+    }
   }
 
   a {
@@ -383,6 +447,12 @@
   margin: -5px;
 }
 
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+}
+
 .participants-author {
   display: inline-block;
   padding: 5px;
@@ -400,13 +470,39 @@
   }
 }
 
-.participants-more {
+.user-item {
+  display: inline-block;
+  padding: 5px;
+  flex-basis: 20%;
+
+  .user-link {
+    display: inline-block;
+  }
+}
+
+.participants-more,
+.user-list-more {
   margin-top: 5px;
   margin-left: 5px;
 
-  a {
+  a,
+  .btn-link {
     color: $gl-text-color-secondary;
   }
+
+  .btn-link {
+    outline: none;
+    padding: 0;
+  }
+
+  .btn-link:hover {
+    @extend a:hover;
+    text-decoration: none;
+  }
+
+  .btn-link:focus {
+    text-decoration: none;
+  }
 }
 
 .issuable-form-padding-top {
@@ -499,6 +595,19 @@
   }
 }
 
+.issuable-list li,
+.issue-info-container .controls {
+  .avatar-counter {
+    display: inline-block;
+    vertical-align: middle;
+    min-width: 16px;
+    line-height: 14px;
+    height: 16px;
+    padding-left: 2px;
+    padding-right: 2px;
+  }
+}
+
 .time_tracker {
   padding-bottom: 0;
   border-bottom: 0;
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 3ccf2a9ce33e12acd0cfd3cf847b546abef9c357..b199f18da1e6d38304c6d99cf34cccfdac518f3b 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -66,6 +66,7 @@ module IssuableActions
       :milestone_id,
       :state_event,
       :subscription_event,
+      assignee_ids: [],
       label_ids: [],
       add_label_ids: [],
       remove_label_ids: []
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index c8a501d73195adf2b5f3084083da136da0142900..6df2c06874566202b080a9fd79fb400c18695f86 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -43,7 +43,7 @@ module IssuableCollections
   end
 
   def issues_collection
-    issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
+    issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
   end
 
   def merge_requests_collection
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 28c9646910d2cffdae0e1c0781e22c5597aad06d..da9b789d6171faf768162e6bdfa602d6b7234fd3 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -82,7 +82,7 @@ module Projects
           labels: true,
           only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
           include: {
-            assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+            assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
             milestone: { only: [:id, :title] }
           },
           user: current_user
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b7a55a623b7ae99ae44a03557e905d98db7b03b1..bcd23d61519b3e3456a7ce9376a5bef6121e7b68 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -67,7 +67,7 @@ class Projects::IssuesController < Projects::ApplicationController
 
   def new
     params[:issue] ||= ActionController::Parameters.new(
-      assignee_id: ""
+      assignee_ids: ""
     )
     build_params = issue_params.merge(
       merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
@@ -150,7 +150,7 @@ class Projects::IssuesController < Projects::ApplicationController
         if @issue.valid?
           render json: @issue.to_json(methods: [:task_status, :task_status_short],
                                       include: { milestone: {},
-                                                 assignee: { only: [:name, :username], methods: [:avatar_url] },
+                                                 assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
                                                  labels: { methods: :text_color } })
         else
           render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
@@ -284,7 +284,7 @@ class Projects::IssuesController < Projects::ApplicationController
   def issue_params
     params.require(:issue).permit(
       :title, :assignee_id, :position, :description, :confidential,
-      :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
+      :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: [],
     )
   end
 
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 4cc42b88a2a6acaa96e2f9c666560a34675d0d0b..957ad87585837e4e8268e50323c4b91ab8be843b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -231,7 +231,7 @@ class IssuableFinder
     when 'created-by-me', 'authored'
       items.where(author_id: current_user.id)
     when 'assigned-to-me'
-      items.where(assignee_id: current_user.id)
+      items.assigned_to(current_user)
     else
       items
     end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 76715e5970dac7c1b552e561f63c2c0bd22bd115..b4c074bc69c94e7ffa20eb179b39a8e50c83b029 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -26,17 +26,28 @@ class IssuesFinder < IssuableFinder
     IssuesFinder.not_restricted_by_confidentiality(current_user)
   end
 
+  def by_assignee(items)
+    if assignee
+      items.assigned_to(assignee)
+    elsif no_assignee?
+      items.unassigned
+    elsif assignee_id? || assignee_username? # assignee not found
+      items.none
+    else
+      items
+    end
+  end
+
   def self.not_restricted_by_confidentiality(user)
-    return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
+    return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
 
     return Issue.all if user.admin?
 
     Issue.where('
-      issues.confidential IS NULL
-      OR issues.confidential IS FALSE
+      issues.confidential IS NOT TRUE
       OR (issues.confidential = TRUE
         AND (issues.author_id = :user_id
-          OR issues.assignee_id = :user_id
+          OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
           OR issues.project_id IN(:project_ids)))',
       user_id: user.id,
       project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 1182939f656958db73c42a4673a0b1a4304e5ba8..53962b846185b0d4b31051304bac291f8684d997 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -15,4 +15,36 @@ module FormHelper
         end
     end
   end
+
+  def issue_dropdown_options(issuable, has_multiple_assignees = true)
+    options = {
+      toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
+      title: 'Select assignee',
+      filter: true,
+      dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee',
+      placeholder: 'Search users',
+      data: {
+        first_user: current_user&.username,
+        null_user: true,
+        current_user: true,
+        project_id: issuable.project.try(:id),
+        field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]",
+        default_label: 'Assignee',
+        'max-select': 1,
+        'dropdown-header': 'Assignee',
+        multi_select: true,
+        'input-meta': 'name',
+        'always-show-selectbox': true,
+        current_user_info: current_user.to_json(only: [:id, :name])
+      }
+    }
+
+    if has_multiple_assignees
+      options[:title] = 'Select assignee(s)'
+      options[:data][:'dropdown-header'] = 'Assignee(s)'
+      options[:data].delete(:'max-select')
+    end
+
+    options
+  end
 end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 0b13dbf5f8d260ff32d01bf287a0b6686f172ff9..7656929efe7ade65e085bf2a969f6caee89e4cef 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -63,6 +63,16 @@ module IssuablesHelper
     end
   end
 
+  def users_dropdown_label(selected_users)
+    if selected_users.length == 0
+      "Unassigned"
+    elsif selected_users.length == 1
+      selected_users[0].name
+    else
+      "#{selected_users[0].name} + #{selected_users.length - 1} more"
+    end
+  end
+
   def user_dropdown_label(user_id, default_label)
     return default_label if user_id.nil?
     return "Unassigned" if user_id == "0"
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index d64e48f774b92123ceb195424fcce8e7de268154..0f84784129511bc8843c6b45dc3801b8563010b0 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -11,10 +11,12 @@ module Emails
       mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
     end
 
-    def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
+    def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id)
       setup_issue_mail(issue_id, recipient_id)
 
-      @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+      @previous_assignees = []
+      @previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
+
       mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
     end
 
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 26dbf4d95708fb392090e080c2efbdb52c1278e2..16f04305a434599d0e2fb53f59449471e6ec458b 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -26,7 +26,6 @@ module Issuable
     cache_markdown_field :description, issuable_state_filter_enabled: true
 
     belongs_to :author, class_name: "User"
-    belongs_to :assignee, class_name: "User"
     belongs_to :updated_by, class_name: "User"
     belongs_to :milestone
     has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do
@@ -65,11 +64,8 @@ module Issuable
     validates :title, presence: true, length: { maximum: 255 }
 
     scope :authored, ->(user) { where(author_id: user) }
-    scope :assigned_to, ->(u) { where(assignee_id: u.id)}
     scope :recent, -> { reorder(id: :desc) }
     scope :order_position_asc, -> { reorder(position: :asc) }
-    scope :assigned, -> { where("assignee_id IS NOT NULL") }
-    scope :unassigned, -> { where("assignee_id IS NULL") }
     scope :of_projects, ->(ids) { where(project_id: ids) }
     scope :of_milestones, ->(ids) { where(milestone_id: ids) }
     scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
@@ -92,23 +88,14 @@ module Issuable
     attr_mentionable :description
 
     participant :author
-    participant :assignee
     participant :notes_with_associations
 
     strip_attributes :title
 
     acts_as_paranoid
 
-    after_save :update_assignee_cache_counts, if: :assignee_id_changed?
     after_save :record_metrics, unless: :imported?
 
-    def update_assignee_cache_counts
-      # make sure we flush the cache for both the old *and* new assignees(if they exist)
-      previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
-      previous_assignee&.update_cache_counts
-      assignee&.update_cache_counts
-    end
-
     # We want to use optimistic lock for cases when only title or description are involved
     # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
     def locking_enabled?
@@ -237,10 +224,6 @@ module Issuable
     today? && created_at == updated_at
   end
 
-  def is_being_reassigned?
-    assignee_id_changed?
-  end
-
   def open?
     opened? || reopened?
   end
@@ -269,7 +252,11 @@ module Issuable
       # DEPRECATED
       repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
     }
-    hook_data[:assignee] = assignee.hook_attrs if assignee
+    if self.is_a?(Issue)
+      hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any?
+    else
+      hook_data[:assignee] = assignee.hook_attrs if assignee
+    end
 
     hook_data
   end
@@ -331,11 +318,6 @@ module Issuable
     false
   end
 
-  def assignee_or_author?(user)
-    # We're comparing IDs here so we don't need to load any associations.
-    author_id == user.id || assignee_id == user.id
-  end
-
   def record_metrics
     metrics = self.metrics || create_metrics
     metrics.record!
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index f449229864d44debc9fb5b3e5ff0a177a2141eeb..a3472af5c55c954e721d29bf9030b62f75b30662 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -40,7 +40,7 @@ module Milestoneish
   def issues_visible_to_user(user)
     memoize_per_user(user, :issues_visible_to_user) do
       IssuesFinder.new(user, issues_finder_params)
-        .execute.where(milestone_id: milestoneish_ids)
+        .execute.includes(:assignees).where(milestone_id: milestoneish_ids)
     end
   end
 
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 0afbca2cb325edd0fc2b9fb3e6510d5e88ccc70e..538615130a7623a07b1e9b9a57ba99f7fe21f9a3 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -36,7 +36,7 @@ class GlobalMilestone
     closed = count_by_state(milestones_by_state_and_title, 'closed')
     all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
 
-    { 
+    {
       opened: opened,
       closed: closed,
       all: all
@@ -86,7 +86,7 @@ class GlobalMilestone
   end
 
   def issues
-    @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels)
+    @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignees, :labels)
   end
 
   def merge_requests
@@ -94,7 +94,7 @@ class GlobalMilestone
   end
 
   def participants
-    @participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
+    @participants ||= milestones.map(&:participants).flatten.uniq
   end
 
   def labels
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 78bde6820da9719c417cade0e53a96f05b83ba08..27e3ed9bc7f54dd9de738343f9a1ec94ba386956 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -24,10 +24,17 @@ class Issue < ActiveRecord::Base
 
   has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
 
+  has_many :issue_assignees
+  has_many :assignees, class_name: "User", through: :issue_assignees
+
   validates :project, presence: true
 
   scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
 
+  scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
+  scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
+  scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
+
   scope :without_due_date, -> { where(due_date: nil) }
   scope :due_before, ->(date) { where('issues.due_date < ?', date) }
   scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
@@ -37,13 +44,15 @@ class Issue < ActiveRecord::Base
 
   scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
 
-  scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
+  scope :include_associations, -> { includes(:labels, project: :namespace) }
 
   after_save :expire_etag_cache
 
   attr_spammable :title, spam_title: true
   attr_spammable :description, spam_description: true
 
+  participant :assignees
+
   state_machine :state, initial: :opened do
     event :close do
       transition [:reopened, :opened] => :closed
@@ -63,10 +72,14 @@ class Issue < ActiveRecord::Base
   end
 
   def hook_attrs
+    assignee_ids = self.assignee_ids
+
     attrs = {
       total_time_spent: total_time_spent,
       human_total_time_spent: human_total_time_spent,
-      human_time_estimate: human_time_estimate
+      human_time_estimate: human_time_estimate,
+      assignee_ids: assignee_ids,
+      assignee_id: assignee_ids.first # This key is deprecated
     }
 
     attributes.merge!(attrs)
@@ -114,6 +127,22 @@ class Issue < ActiveRecord::Base
               "id DESC")
   end
 
+  # Returns a Hash of attributes to be used for Twitter card metadata
+  def card_attributes
+    {
+      'Author'   => author.try(:name),
+      'Assignee' => assignee_list
+    }
+  end
+
+  def assignee_or_author?(user)
+    author_id == user.id || assignees.exists?(user.id)
+  end
+
+  def assignee_list
+    assignees.map(&:name).to_sentence
+  end
+
   # `from` argument can be a Namespace or Project.
   def to_reference(from = nil, full: false)
     reference = "#{self.class.reference_prefix}#{iid}"
@@ -248,7 +277,7 @@ class Issue < ActiveRecord::Base
       true
     elsif confidential?
       author == user ||
-        assignee == user ||
+        assignees.include?(user) ||
         project.team.member?(user, Gitlab::Access::REPORTER)
     else
       project.public? ||
diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0663d3aaef8b472ac377c69c3106dc323fc658be
--- /dev/null
+++ b/app/models/issue_assignee.rb
@@ -0,0 +1,13 @@
+class IssueAssignee < ActiveRecord::Base
+  extend Gitlab::CurrentSettings
+
+  belongs_to :issue
+  belongs_to :assignee, class_name: "User", foreign_key: :user_id
+
+  after_create :update_assignee_cache_counts
+  after_destroy :update_assignee_cache_counts
+
+  def update_assignee_cache_counts
+    assignee&.update_cache_counts
+  end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 12c5481cd6d24c7bcd841820652c2bfc27d9579e..35231bab12ee1575ade78df4881e9451f847398b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -17,6 +17,8 @@ class MergeRequest < ActiveRecord::Base
 
   has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
 
+  belongs_to :assignee, class_name: "User"
+
   serialize :merge_params, Hash
 
   after_create :ensure_merge_request_diff, unless: :importing?
@@ -114,8 +116,14 @@ class MergeRequest < ActiveRecord::Base
 
   scope :join_project, -> { joins(:target_project) }
   scope :references_project, -> { references(:target_project) }
+  scope :assigned, -> { where("assignee_id IS NOT NULL") }
+  scope :unassigned, -> { where("assignee_id IS NULL") }
+  scope :assigned_to, ->(u) { where(assignee_id: u.id)}
+
+  participant :assignee
 
   after_save :keep_around_commit
+  after_save :update_assignee_cache_counts, if: :assignee_id_changed?
 
   def self.reference_prefix
     '!'
@@ -177,6 +185,30 @@ class MergeRequest < ActiveRecord::Base
     work_in_progress?(title) ? title : "WIP: #{title}"
   end
 
+  def update_assignee_cache_counts
+    # make sure we flush the cache for both the old *and* new assignees(if they exist)
+    previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
+    previous_assignee&.update_cache_counts
+    assignee&.update_cache_counts
+  end
+
+  # Returns a Hash of attributes to be used for Twitter card metadata
+  def card_attributes
+    {
+      'Author'   => author.try(:name),
+      'Assignee' => assignee.try(:name)
+    }
+  end
+
+  # This method is needed for compatibility with issues to not mess view and other code
+  def assignees
+    Array(assignee)
+  end
+
+  def assignee_or_author?(user)
+    author_id == user.id || assignee_id == user.id
+  end
+
   # `from` argument can be a Namespace or Project.
   def to_reference(from = nil, full: false)
     reference = "#{self.class.reference_prefix}#{iid}"
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 652b15519285a1e7e1136ba13c35d92afa1cd263..c06bfe0ccdd773823f8093ec1469373740767d69 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -21,7 +21,6 @@ class Milestone < ActiveRecord::Base
   has_many :issues
   has_many :labels, -> { distinct.reorder('labels.title') },  through: :issues
   has_many :merge_requests
-  has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee
   has_many :events, as: :target, dependent: :destroy
 
   scope :active, -> { with_state(:active) }
@@ -107,6 +106,10 @@ class Milestone < ActiveRecord::Base
     end
   end
 
+  def participants
+    User.joins(assigned_issues: :milestone).where("milestones.id = ?", id)
+  end
+
   def self.sort(method)
     case method.to_s
     when 'due_date_asc'
diff --git a/app/models/user.rb b/app/models/user.rb
index 43c5fdc038d4761460305e5b1c88ec02052502f5..59f2be3ba9df4bd71b7809c70472d6152cb4c8b3 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -100,6 +100,10 @@ class User < ActiveRecord::Base
   has_many :award_emoji,              dependent: :destroy
   has_many :triggers,                 dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
 
+  has_many :issue_assignees
+  has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
+  has_many :assigned_merge_requests,  dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
+
   # Issues that a user owns are expected to be moved to the "ghost" user before
   # the user is destroyed. If the user owns any issues during deletion, this
   # should be treated as an exceptional condition.
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 29aecb50849fce40bd630b01c80763e8a7ce8cbe..65b204d4dd27bee4450f53147265a1dd9bb70fd0 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -1,7 +1,6 @@
 class IssuableEntity < Grape::Entity
   expose :id
   expose :iid
-  expose :assignee_id
   expose :author_id
   expose :description
   expose :lock_version
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 6429159ebe1521d8c325fd929d7b97b52e787c55..bc4f68710b20deca81c86b3a3fcfe0f90abf4b85 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -1,6 +1,7 @@
 class IssueEntity < IssuableEntity
   expose :branch_name
   expose :confidential
+  expose :assignees, using: API::Entities::UserBasic
   expose :due_date
   expose :moved_to_id
   expose :project_id
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 5f80ab397a9baa7b74649cbe9e2b713d22c4c92d..453ba52b892a046a93227b9a8312e3b76bcda1ca 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -1,4 +1,5 @@
 class MergeRequestEntity < IssuableEntity
+  expose :assignee_id
   expose :in_progress_merge_commit_sha
   expose :locked_at
   expose :merge_commit_sha
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index 60891cbb255fbdf6732647ecc84c733d2b228cc6..40ff9b8b8679a5a093a5b03bf889c6a3d79d1726 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -7,10 +7,14 @@ module Issuable
       ids = params.delete(:issuable_ids).split(",")
       items = model_class.where(id: ids)
 
-      %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
+      %i(state_event milestone_id assignee_id assignee_ids add_label_ids remove_label_ids subscription_event).each do |key|
         params.delete(key) unless params[key].present?
       end
 
+      if params[:assignee_ids] == [IssuableFinder::NONE.to_s]
+        params[:assignee_ids] = []
+      end
+
       items.each do |issuable|
         next unless can?(current_user, :"update_#{type}", issuable)
 
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index b071a3984811eb97a6437eb3462f0b4e26452b46..6c2777a8d2d2edc47799d310e0e15aa797fb4a22 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -1,11 +1,6 @@
 class IssuableBaseService < BaseService
   private
 
-  def create_assignee_note(issuable)
-    SystemNoteService.change_assignee(
-      issuable, issuable.project, current_user, issuable.assignee)
-  end
-
   def create_milestone_note(issuable)
     SystemNoteService.change_milestone(
       issuable, issuable.project, current_user, issuable.milestone)
@@ -53,6 +48,7 @@ class IssuableBaseService < BaseService
       params.delete(:add_label_ids)
       params.delete(:remove_label_ids)
       params.delete(:label_ids)
+      params.delete(:assignee_ids)
       params.delete(:assignee_id)
       params.delete(:due_date)
     end
@@ -77,7 +73,7 @@ class IssuableBaseService < BaseService
   def assignee_can_read?(issuable, assignee_id)
     new_assignee = User.find_by_id(assignee_id)
 
-    return false unless new_assignee.present?
+    return false unless new_assignee
 
     ability_name = :"read_#{issuable.to_ability_name}"
     resource     = issuable.persisted? ? issuable : project
@@ -207,6 +203,7 @@ class IssuableBaseService < BaseService
     filter_params(issuable)
     old_labels = issuable.labels.to_a
     old_mentioned_users = issuable.mentioned_users.to_a
+    old_assignees = issuable.assignees.to_a
 
     label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
     params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
@@ -222,7 +219,13 @@ class IssuableBaseService < BaseService
           handle_common_system_notes(issuable, old_labels: old_labels)
         end
 
-        handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+        handle_changes(
+          issuable,
+          old_labels: old_labels,
+          old_mentioned_users: old_mentioned_users,
+          old_assignees: old_assignees
+        )
+
         after_update(issuable)
         issuable.create_new_cross_references!(current_user)
         execute_hooks(issuable, 'update')
@@ -272,7 +275,7 @@ class IssuableBaseService < BaseService
     end
   end
 
-  def has_changes?(issuable, old_labels: [])
+  def has_changes?(issuable, old_labels: [], old_assignees: [])
     valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
 
     attrs_changed = valid_attrs.any? do |attr|
@@ -281,7 +284,9 @@ class IssuableBaseService < BaseService
 
     labels_changed = issuable.labels != old_labels
 
-    attrs_changed || labels_changed
+    assignees_changed = issuable.assignees != old_assignees
+
+    attrs_changed || labels_changed || assignees_changed
   end
 
   def handle_common_system_notes(issuable, old_labels: [])
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index ee1b40db718911c60ea9854b6d4028af80f7e0a9..34199eb5d1373347188ad664d01a17eb84eaa29c 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -9,11 +9,33 @@ module Issues
 
     private
 
+    def create_assignee_note(issue, old_assignees)
+      SystemNoteService.change_issue_assignees(
+        issue, issue.project, current_user, old_assignees)
+    end
+
     def execute_hooks(issue, action = 'open')
       issue_data  = hook_data(issue, action)
       hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
       issue.project.execute_hooks(issue_data, hooks_scope)
       issue.project.execute_services(issue_data, hooks_scope)
     end
+
+    def filter_assignee(issuable)
+      return if params[:assignee_ids].blank?
+
+      # The number of assignees is limited by one for GitLab CE
+      params[:assignee_ids] = params[:assignee_ids][0, 1]
+
+      assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
+
+      if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
+        params[:assignee_ids] = []
+      elsif assignee_ids.any?
+        params[:assignee_ids] = assignee_ids
+      else
+        params.delete(:assignee_ids)
+      end
+    end
   end
 end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index b7fe5cb168b9738db3acdf627d9522e84e274a82..cd9f9a4a16e02724961b0d6384e4733336650d1c 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -12,8 +12,12 @@ module Issues
       spam_check(issue, current_user)
     end
 
-    def handle_changes(issue, old_labels: [], old_mentioned_users: [])
-      if has_changes?(issue, old_labels: old_labels)
+    def handle_changes(issue, options)
+      old_labels = options[:old_labels] || []
+      old_mentioned_users = options[:old_mentioned_users] || []
+      old_assignees = options[:old_assignees] || []
+
+      if has_changes?(issue, old_labels: old_labels, old_assignees: old_assignees)
         todo_service.mark_pending_todos_as_done(issue, current_user)
       end
 
@@ -26,9 +30,9 @@ module Issues
         create_milestone_note(issue)
       end
 
-      if issue.previous_changes.include?('assignee_id')
-        create_assignee_note(issue)
-        notification_service.reassigned_issue(issue, current_user)
+      if issue.assignees != old_assignees
+        create_assignee_note(issue, old_assignees)
+        notification_service.reassigned_issue(issue, current_user, old_assignees)
         todo_service.reassigned_issue(issue, current_user)
       end
 
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
index 1711be7211c6f566e2ca7457fba7c07e489dcf25..a85b9465c8483b569c500068f7d19dbe75f07764 100644
--- a/app/services/members/authorized_destroy_service.rb
+++ b/app/services/members/authorized_destroy_service.rb
@@ -26,15 +26,22 @@ module Members
 
     def unassign_issues_and_merge_requests(member)
       if member.is_a?(GroupMember)
-        IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
-          execute.
-          update_all(assignee_id: nil)
+        issue_ids = IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
+          execute.pluck(:id)
+
+        IssueAssignee.destroy_all(issue_id: issue_ids, user_id: member.user_id)
+
         MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
           execute.
           update_all(assignee_id: nil)
       else
         project = member.source
-        project.issues.opened.assigned_to(member.user).update_all(assignee_id: nil)
+
+        IssueAssignee.destroy_all(
+          user_id: member.user_id,
+          issue_id: project.issues.opened.assigned_to(member.user).select(:id)
+        )
+
         project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
         member.user.update_cache_counts
       end
diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
index 066efa1acc3dd7dae9b345dc384794424cbae448..8c6c484102039a914a29a185cd5a26f4aa30d227 100644
--- a/app/services/merge_requests/assign_issues_service.rb
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
       @assignable_issues ||= begin
         if current_user == merge_request.author
           closes_issues.select do |issue|
-            !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue)
+            !issue.is_a?(ExternalIssue) && !issue.assignees.present? && can?(current_user, :admin_issue, issue)
           end
         else
           []
@@ -14,7 +14,7 @@ module MergeRequests
 
     def execute
       assignable_issues.each do |issue|
-        Issues::UpdateService.new(issue.project, current_user, assignee_id: current_user.id).execute(issue)
+        Issues::UpdateService.new(issue.project, current_user, assignee_ids: [current_user.id]).execute(issue)
       end
 
       {
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 582d5c47b6603ede25c573b194d4eb89ff076a4d..3542a41ac831b7539746000f1d6b81f96b4f77f3 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,6 +38,11 @@ module MergeRequests
 
     private
 
+    def create_assignee_note(merge_request)
+      SystemNoteService.change_assignee(
+        merge_request, merge_request.project, current_user, merge_request.assignee)
+    end
+
     # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
     def merge_requests_for(source_branch, mr_states: [:opened, :reopened])
       MergeRequest
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index ab7fcf3b6e2b7b49c5a06641f0fde958ce3fdaa0..5c843a258fb3b68f21bb4f81d1f77b8b438f83b7 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -21,7 +21,10 @@ module MergeRequests
       update(merge_request)
     end
 
-    def handle_changes(merge_request, old_labels: [], old_mentioned_users: [])
+    def handle_changes(merge_request, options)
+      old_labels = options[:old_labels] || []
+      old_mentioned_users = options[:old_mentioned_users] || []
+
       if has_changes?(merge_request, old_labels: old_labels)
         todo_service.mark_pending_todos_as_done(merge_request, current_user)
       end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 8bb995158de7e5baf8de86b5ffff098d5067c70d..988bd0a7cdbe9838cf4f45d937a2ff4217781b5e 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -19,9 +19,14 @@ class NotificationRecipientService
     # Re-assign is considered as a mention of the new assignee so we add the
     # new assignee to the list of recipients after we rejected users with
     # the "on mention" notification level
-    if [:reassign_merge_request, :reassign_issue].include?(custom_action)
+    case custom_action
+    when :reassign_merge_request
       recipients << previous_assignee if previous_assignee
       recipients << target.assignee
+    when :reassign_issue
+      previous_assignees = Array(previous_assignee)
+      recipients.concat(previous_assignees)
+      recipients.concat(target.assignees)
     end
 
     recipients = reject_muted_users(recipients)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 6b186263bd103c106c572837ef707779e31811ac..c65c66d715040dbf84289f3223ca1540443148c1 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -66,8 +66,25 @@ class NotificationService
   #  * issue new assignee if their notification level is not Disabled
   #  * users with custom level checked with "reassign issue"
   #
-  def reassigned_issue(issue, current_user)
-    reassign_resource_email(issue, issue.project, current_user, :reassigned_issue_email)
+  def reassigned_issue(issue, current_user, previous_assignees = [])
+    recipients = NotificationRecipientService.new(issue.project).build_recipients(
+      issue,
+      current_user,
+      action: "reassign",
+      previous_assignee: previous_assignees
+    )
+
+    previous_assignee_ids = previous_assignees.map(&:id)
+
+    recipients.each do |recipient|
+      mailer.send(
+        :reassigned_issue_email,
+        recipient.id,
+        issue.id,
+        previous_assignee_ids,
+        current_user.id
+      ).deliver_later
+    end
   end
 
   # When we add labels to an issue we should send an email to:
@@ -367,10 +384,10 @@ class NotificationService
   end
 
   def previous_record(object, attribute)
-    if object && attribute
-      if object.previous_changes.include?(attribute)
-        object.previous_changes[attribute].first
-      end
+    return unless object && attribute
+
+    if object.previous_changes.include?(attribute)
+      object.previous_changes[attribute].first
     end
   end
 end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index f1bbc7032d566a2d92ce6ff93acd925aaa42e721..a7e13648b540f539c9413f6745fde54695d855ca 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -91,32 +91,47 @@ module SlashCommands
     end
 
     desc 'Assign'
-    explanation do |user|
-      "Assigns #{user.to_reference}." if user
+    explanation do |users|
+      "Assigns #{users.map(&:to_reference).to_sentence}." if users.any?
     end
     params '@user'
     condition do
       current_user.can?(:"admin_#{issuable.to_ability_name}", project)
     end
     parse_params do |assignee_param|
-      extract_references(assignee_param, :user).first ||
-        User.find_by(username: assignee_param)
+      users = extract_references(assignee_param, :user)
+
+      if users.empty?
+        users = User.where(username: assignee_param.split(' ').map(&:strip))
+      end
+
+      users
     end
-    command :assign do |user|
-      @updates[:assignee_id] = user.id if user
+    command :assign do |users|
+      next if users.empty?
+
+      if issuable.is_a?(Issue)
+        @updates[:assignee_ids] = users.map(&:id)
+      else
+        @updates[:assignee_id] = users.last.id
+      end
     end
 
     desc 'Remove assignee'
     explanation do
-      "Removes assignee #{issuable.assignee.to_reference}."
+      "Removes assignee #{issuable.assignees.first.to_reference}."
     end
     condition do
       issuable.persisted? &&
-        issuable.assignee_id? &&
+        issuable.assignees.any? &&
         current_user.can?(:"admin_#{issuable.to_ability_name}", project)
     end
     command :unassign do
-      @updates[:assignee_id] = nil
+      if issuable.is_a?(Issue)
+        @updates[:assignee_ids] = []
+      else
+        @updates[:assignee_id] = nil
+      end
     end
 
     desc 'Set milestone'
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index c9e25c7aaa20e9d0d0fc122d2b1148a34786a661..fb1f56c9cc6a9ac4184bb5839bc704f7a67e8547 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -49,6 +49,44 @@ module SystemNoteService
     create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
   end
 
+  # Called when the assignees of an Issue is changed or removed
+  #
+  # issue - Issue object
+  # project  - Project owning noteable
+  # author   - User performing the change
+  # assignees - Users being assigned, or nil
+  #
+  # Example Note text:
+  #
+  #   "removed all assignees"
+  #
+  #   "assigned to @user1 additionally to @user2"
+  #
+  #   "assigned to @user1, @user2 and @user3 and unassigned from @user4 and @user5"
+  #
+  #   "assigned to @user1 and @user2"
+  #
+  # Returns the created Note object
+  def change_issue_assignees(issue, project, author, old_assignees)
+    body =
+      if issue.assignees.any? && old_assignees.any?
+        unassigned_users = old_assignees - issue.assignees
+        added_users = issue.assignees.to_a - old_assignees
+
+        text_parts = []
+        text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
+        text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
+
+        text_parts.join(' and ')
+      elsif old_assignees.any?
+        "removed all assignees"
+      elsif issue.assignees.any?
+        "assigned to #{issue.assignees.map(&:to_reference).to_sentence}"
+      end
+
+    create_note(NoteSummary.new(issue, project, author, body, action: 'assignee'))
+  end
+
   # Called when one or more labels on a Noteable are added and/or removed
   #
   # noteable       - Noteable object
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 8ae61694b503e12fad35dd1245bd68db2d47a572..322c62863655a7d9f84653d8ad564c1255c707ca 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -251,9 +251,9 @@ class TodoService
   end
 
   def create_assignment_todo(issuable, author)
-    if issuable.assignee
+    if issuable.assignees.any?
       attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
-      create_todos(issuable.assignee, attributes)
+      create_todos(issuable.assignees, attributes)
     end
   end
 
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 23a884480552ace1c011d29fedf795d3eeb7f985..2ed78bb3b658640e3b9299a4e0ffb265742e8db2 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -23,10 +23,19 @@ xml.entry do
     end
   end
 
-  if issue.assignee
+  if issue.assignees.any?
+    xml.assignees do
+      issue.assignees.each do |assignee|
+        xml.assignee do
+          xml.name assignee.name
+          xml.email assignee.public_email
+        end
+      end
+    end
+
     xml.assignee do
-      xml.name issue.assignee.name
-      xml.email issue.assignee_public_email
+      xml.name issue.assignees.first.name
+      xml.email issue.assignees.first.public_email
     end
   end
 end
diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb
deleted file mode 100644
index daf20a226dd7b36cd251b9c582c4f2ed10d6747c..0000000000000000000000000000000000000000
--- a/app/views/notify/_reassigned_issuable_email.text.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %>
-
-<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
-
-Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
- to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %>
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index c762578971a5f545120a53ccb352671f97ad964e..eb5157ccac9204208d101d146331b625607367fe 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -2,9 +2,9 @@
   %p.details
     #{link_to @issue.author_name, user_url(@issue.author)} created an issue:
 
-- if @issue.assignee_id.present?
+- if @issue.assignees.any?
   %p
-    Assignee: #{@issue.assignee_name}
+    Assignee: #{@issue.assignee_list}
 
 - if @issue.description
   %div
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index ca5c2f2688c0c9d01444235fddb16564332d14de..13f1ac08e94548282dd93600c75eb8c51265b0a2 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -2,6 +2,6 @@ New Issue was created.
 
 Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
 Author:    <%= @issue.author_name %>
-Assignee:  <%= @issue.assignee_name %>
+Assignee:  <%= @issue.assignee_list %>
 
 <%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
index 457e94b48001b3fd85b39d1095e2a00f6b4935ec..f19ac3adfc7aab9fe8d8ad628afb936ca780711d 100644
--- a/app/views/notify/new_mention_in_issue_email.text.erb
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -2,6 +2,6 @@ You have been mentioned in an issue.
 
 Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
 Author:    <%= @issue.author_name %>
-Assignee:  <%= @issue.assignee_name %>
+Assignee:  <%= @issue.assignee_list %>
 
 <%= @issue.description %>
diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml
index 498ba8b83651b2fd1ecfe1212b96fa7dc9cee651..ee2f40e1683a4bebf787e0539ce49aab9cad0e48 100644
--- a/app/views/notify/reassigned_issue_email.html.haml
+++ b/app/views/notify/reassigned_issue_email.html.haml
@@ -1 +1,10 @@
-= render 'reassigned_issuable_email', issuable: @issue
+%p
+  Assignee changed
+  - if @previous_assignees.any?
+    from
+    %strong= @previous_assignees.map(&:name).to_sentence
+  to
+  - if @issue.assignees.any?
+    %strong= @issue.assignee_list
+  - else
+    %strong Unassigned
diff --git a/app/views/notify/reassigned_issue_email.text.erb b/app/views/notify/reassigned_issue_email.text.erb
index 710253be9842975d9d0e4d27948814d4d5c87768..6c357f1074a4841690b76b9d05d7f4a2271d7c38 100644
--- a/app/views/notify/reassigned_issue_email.text.erb
+++ b/app/views/notify/reassigned_issue_email.text.erb
@@ -1 +1,6 @@
-<%= render 'reassigned_issuable_email', issuable: @issue %>
+Reassigned Issue <%= @issue.iid %>
+
+<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
+
+Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
+ to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml
index 2a650130f59ce19c289bf2014d230e7acf72c195..841df872857fc7d2f2e3475d3c54b571bfb57155 100644
--- a/app/views/notify/reassigned_merge_request_email.html.haml
+++ b/app/views/notify/reassigned_merge_request_email.html.haml
@@ -1 +1,9 @@
-= render 'reassigned_issuable_email', issuable: @merge_request
+Reassigned Merge Request #{ @merge_request.iid }
+
+= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }])
+
+Assignee changed
+- if @previous_assignee
+  from #{@previous_assignee.name}
+to
+= @merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'
diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb
index b5b4f1ff99a6b3128835d48891143643a2a390cc..998a40fefdeb339c8fb684969635eb08c56475e3 100644
--- a/app/views/notify/reassigned_merge_request_email.text.erb
+++ b/app/views/notify/reassigned_merge_request_email.text.erb
@@ -1 +1,6 @@
-<%= render 'reassigned_issuable_email', issuable: @merge_request %>
+Reassigned Merge Request <%= @merge_request.iid %>
+
+<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
+
+Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
+ to <%= "#{@merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'}" %>
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index 0f42433452134a94152b8ced5f0ef38577981db8..642da679f97bed943c1e5f20b0cacb415176e77e 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -1,40 +1,28 @@
-.block.assignee
-  .title.hide-collapsed
-    Assignee
-    - if can?(current_user, :admin_issue, @project)
-      = icon("spinner spin", class: "block-loading")
-      = link_to "Edit", "#", class: "edit-link pull-right"
-  .value.hide-collapsed
-    %span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
-      No assignee
-      - if can?(current_user, :admin_issue, @project)
-        \-
-        %a.js-assign-yourself{ href: "#" }
-          assign yourself
-    %a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username",
-      "v-if" => "issue.assignee" }
-      %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
-        width: "32", alt: "Avatar" }
-      %span.author
-        {{ issue.assignee.name }}
-      %span.username
-        = precede "@" do
-          {{ issue.assignee.username }}
+.block.assignee{ ref: "assigneeBlock" }
+  %template{ "v-if" => "issue.assignees" }
+    %assignee-title{ ":number-of-assignees" => "issue.assignees.length",
+      ":loading" => "loadingAssignees",
+      ":editable" => can?(current_user, :admin_issue, @project) }
+    %assignees.value{ "root-path" => "#{root_url}",
+      ":users" => "issue.assignees",
+      ":editable" => can?(current_user, :admin_issue, @project),
+      "@assign-self" => "assignSelf" }
+
   - if can?(current_user, :admin_issue, @project)
     .selectbox.hide-collapsed
       %input{ type: "hidden",
-        name: "issue[assignee_id]",
-        id: "issue_assignee_id",
-        ":value" => "issue.assignee.id",
-        "v-if" => "issue.assignee" }
+        name: "issue[assignee_ids][]",
+        ":value" => "assignee.id",
+        "v-if" => "issue.assignees",
+        "v-for" => "assignee in issue.assignees" }
       .dropdown
-        %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", null_user_default: "true" },
+        %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } },
           ":data-issuable-id" => "issue.id",
           ":data-selected" => "assigneeId",
           ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
           Select assignee
           = icon("chevron-down")
-        .dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
+        .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
           = dropdown_title("Assign to")
           = dropdown_filter("Search users")
           = dropdown_content
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 0e3902c066ad2e7a113261b78975c2cb43b94896..c184e0e00224bf57be5bfc367c358e25672181d4 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -13,9 +13,9 @@
             %li
               CLOSED
 
-          - if issue.assignee
+          - if issue.assignees.any?
             %li
-              = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
+              = render 'shared/issuable/assignees', project: @project, issue: issue
 
           = render 'shared/issuable_meta_data', issuable: issue
 
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index b8e885b4d9a7a938b9b7dec2151d262996221b52..99bc25163660bf44f56eb601495a836f94b63c1d 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -25,7 +25,7 @@
             .merge_access_levels-container
               = dropdown_tag('Select',
                              options: { toggle_class: 'js-allowed-to-merge wide',
-                             dropdown_class: 'dropdown-menu-selectable',
+                             dropdown_class: 'dropdown-menu-selectable capitalize-header',
                              data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
         .form-group
           %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
@@ -34,7 +34,7 @@
             .push_access_levels-container
               = dropdown_tag('Select',
                              options: { toggle_class: 'js-allowed-to-push wide',
-                             dropdown_class: 'dropdown-menu-selectable',
+                             dropdown_class: 'dropdown-menu-selectable capitalize-header',
                              data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
 
     .panel-footer
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
index d6044aacaec851bb06cbb5f921898e0dbdaccd83..c61b2951e1e8971dfdcc287c5296c8c7ee10e4e2 100644
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -1,10 +1,10 @@
 %td
   = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
   = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
-                 options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container',
+                 options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
                  data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
 %td
   = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
   = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
-                 options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container',
+                 options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
                  data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
diff --git a/app/views/projects/protected_tags/_dropdown.html.haml b/app/views/projects/protected_tags/_dropdown.html.haml
index 748515190779f13960615bafc97dc4e147b2dea9..c50515cfe06e9fb8bc9580e385710a241dcc85cb 100644
--- a/app/views/projects/protected_tags/_dropdown.html.haml
+++ b/app/views/projects/protected_tags/_dropdown.html.haml
@@ -2,7 +2,7 @@
 
 = dropdown_tag('Select tag or create wildcard',
                options: { toggle_class: 'js-protected-tag-select js-filter-submit wide',
-                          filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected tag",
+                          filter: true, dropdown_class: "dropdown-menu-selectable  capitalize-header", placeholder: "Search protected tag",
                           footer_content: true,
                           data: { show_no: true, show_any: true, show_upcoming: true,
                                   selected: params[:protected_tag_name],
diff --git a/app/views/projects/protected_tags/_update_protected_tag.haml b/app/views/projects/protected_tags/_update_protected_tag.haml
index 62823bee46e1fac736fa4de4e1c0fa0a22c04a11..cc80bd04dd0678afe490cee069cd9150d760c938 100644
--- a/app/views/projects/protected_tags/_update_protected_tag.haml
+++ b/app/views/projects/protected_tags/_update_protected_tag.haml
@@ -1,5 +1,5 @@
 %td
   = hidden_field_tag "allowed_to_create_#{protected_tag.id}", protected_tag.create_access_levels.first.access_level
   = dropdown_tag( (protected_tag.create_access_levels.first.humanize || 'Select') ,
-                 options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable js-allowed-to-create-container',
+                 options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable capitalize-header js-allowed-to-create-container',
                  data: { field_name: "allowed_to_create_#{protected_tag.id}", access_level_id: protected_tag.create_access_levels.first.id }})
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..36bbb1148d49b2c90d045d0eefd03a6a5cd1f93a
--- /dev/null
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -0,0 +1,15 @@
+- max_render = 3
+- max = [max_render, issue.assignees.length].min
+
+- issue.assignees.each_with_index do |assignee, index|
+  - if index < max
+    = link_to_member(@project, assignee, name: false, title: "Assigned to :name")
+
+- if issue.assignees.length > max_render
+  - counter = issue.assignees.length - max_render
+
+  %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{counter} more assignees" } }
+    - if counter < 99
+      = "+#{counter}"
+    - else
+      99+
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index 171da89993736f94196b1e57a65066ef354e3a11..db407363a0929e94e2c23b56a6aa11ad96bafe52 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -12,9 +12,9 @@
     - participants.each do |participant|
       .participants-author.js-participants-author
         = link_to_member(@project, participant, name: false, size: 24)
-    - if participants_extra > 0
-      .participants-more
-        %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
-          + #{participants_extra} more
+  - if participants_extra > 0
+    .hide-collapsed.participants-more
+      %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
+        + #{participants_extra} more
 :javascript
   IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index b6fce5e3cd439c16fc4770daea1257a75ffe56e7..f7b87171573b038472b511b9386d93cb383bad79 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -124,8 +124,13 @@
                 %li
                   %a{ href: "#", data: { id: "close" } } Closed
           .filter-item.inline
+            - if type == :issues
+              - field_name = "update[assignee_ids][]"
+            - else
+              - field_name = "update[assignee_id]"
+
             = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
-              placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
+              placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
           .filter-item.inline
             = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
           .filter-item.inline.labels-filter
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index bc638e994f3c4537c3995bf823463c0c92b96860..44e624c15a78dc3007168d00e9dad9e1432ee90d 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,10 +1,10 @@
 - todo = issuable_todo(issuable)
 - content_for :page_specific_javascripts do
   = page_specific_javascript_bundle_tag('common_vue')
-  = page_specific_javascript_bundle_tag('issuable')
+  = page_specific_javascript_bundle_tag('sidebar')
 
 %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
-  .issuable-sidebar
+  .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
     - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
     .block.issuable-sidebar-header
       - if current_user
@@ -20,36 +20,55 @@
         .block.todo.hide-expanded
           = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
       .block.assignee
-        .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
-          - if issuable.assignee
-            = link_to_member(@project, issuable.assignee, size: 24)
-          - else
-            = icon('user', 'aria-hidden': 'true')
-        .title.hide-collapsed
-          Assignee
-          = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
-          - if can_edit_issuable
-            = link_to 'Edit', '#', class: 'edit-link pull-right'
-        .value.hide-collapsed
-          - if issuable.assignee
-            = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
-              - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
-                %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
-                  = icon('exclamation-triangle', 'aria-hidden': 'true')
-              %span.username
-                = issuable.assignee.to_reference
-          - else
-            %span.assign-yourself.no-value
-              No assignee
-              - if can_edit_issuable
-                \-
-                %a.js-assign-yourself{ href: '#' }
-                  assign yourself
+        - if issuable.instance_of?(Issue)
+          #js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]" } }
+        - else
+          .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
+            - if issuable.assignee
+              = link_to_member(@project, issuable.assignee, size: 24)
+            - else
+              = icon('user', 'aria-hidden': 'true')
+          .title.hide-collapsed
+            Assignee
+            = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+            - if can_edit_issuable
+              = link_to 'Edit', '#', class: 'edit-link pull-right'
+          .value.hide-collapsed
+            - if issuable.assignee
+              = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
+                - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
+                  %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+                    = icon('exclamation-triangle', 'aria-hidden': 'true')
+                %span.username
+                  = issuable.assignee.to_reference
+            - else
+              %span.assign-yourself.no-value
+                No assignee
+                - if can_edit_issuable
+                  \-
+                  %a.js-assign-yourself{ href: '#' }
+                    assign yourself
 
         .selectbox.hide-collapsed
-          = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
-          = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, null_user_default: true, selected: issuable.assignee_id } })
+          - issuable.assignees.each do |assignee|
+            = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil
 
+          - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }
+
+          - if issuable.instance_of?(Issue)
+            - if issuable.assignees.length == 0
+              = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
+            - title = 'Select assignee'
+            - options[:toggle_class] += ' js-multiselect js-save-user-data'
+            - options[:data][:field_name] = "#{issuable.to_ability_name}[assignee_ids][]"
+            - options[:data][:multi_select] = true
+            - options[:data]['dropdown-title'] = title
+            - options[:data]['dropdown-header'] = 'Assignee'
+            - options[:data]['max-select'] = 1
+          - else
+            - title = 'Select assignee'
+
+          = dropdown_tag(title, options: options)
       .block.milestone
         .sidebar-collapsed-icon
           = icon('clock-o', 'aria-hidden': 'true')
@@ -75,11 +94,10 @@
           = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
       - if issuable.has_attribute?(:time_estimate)
         #issuable-time-tracker.block
-          %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'docs-url' => help_page_path('workflow/time_tracking.md') }
-            // Fallback while content is loading
-            .title.hide-collapsed
-              Time tracking
-              = icon('spinner spin', 'aria-hidden': 'true')
+          // Fallback while content is loading
+          .title.hide-collapsed
+            Time tracking
+            = icon('spinner spin', 'aria-hidden': 'true')
       - if issuable.has_attribute?(:due_date)
         .block.due_date
           .sidebar-collapsed-icon
@@ -169,8 +187,13 @@
           = clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left")
 
     :javascript
-      gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
-      new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
+      gl.sidebarOptions = {
+        endpoint: "#{issuable_json_path(issuable)}",
+        editable: #{can_edit_issuable ? true : false},
+        currentUser: #{current_user.to_json(only: [:username, :id, :name], methods: :avatar_url)},
+        rootPath: "#{root_path}"
+      };
+
       new MilestoneSelect('{"full_path":"#{@project.full_path}"}');
       new LabelsSelect();
       new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c33474ac3b4e7e273fad06f759a82cd0314db420
--- /dev/null
+++ b/app/views/shared/issuable/form/_issue_assignee.html.haml
@@ -0,0 +1,30 @@
+- issue = issuable
+.block.assignee
+  .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee_list) }
+    - if issue.assignees.any?
+      - issue.assignees.each do |assignee|
+        = link_to_member(@project, assignee, size: 24)
+    - else
+      = icon('user', 'aria-hidden': 'true')
+  .title.hide-collapsed
+    Assignee
+    = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+    - if can_edit_issuable
+      = link_to 'Edit', '#', class: 'edit-link pull-right'
+  .value.hide-collapsed
+    - if issue.assignees.any?
+      - issue.assignees.each do |assignee|
+        = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
+          %span.username
+            = assignee.to_reference
+    - else
+      %span.assign-yourself.no-value
+        No assignee
+        - if can_edit_issuable
+          \-
+          %a.js-assign-yourself{ href: '#' }
+            assign yourself
+
+  .selectbox.hide-collapsed
+    = f.hidden_field 'assignee_ids', value: issuable.assignee_ids, id: 'issue_assignee_ids'
+    = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..18011d528a0bef796815e205cc57f6eae10450ff
--- /dev/null
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -0,0 +1,31 @@
+- merge_request = issuable
+.block.assignee
+  .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) }
+    - if merge_request.assignee
+      = link_to_member(@project, merge_request.assignee, size: 24)
+    - else
+      = icon('user', 'aria-hidden': 'true')
+  .title.hide-collapsed
+    Assignee
+    = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+    - if can_edit_issuable
+      = link_to 'Edit', '#', class: 'edit-link pull-right'
+  .value.hide-collapsed
+    - if merge_request.assignee
+      = link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
+        - unless merge_request.can_be_merged_by?(merge_request.assignee)
+          %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+            = icon('exclamation-triangle', 'aria-hidden': 'true')
+        %span.username
+          = merge_request.assignee.to_reference
+    - else
+      %span.assign-yourself.no-value
+        No assignee
+        - if can_edit_issuable
+          \-
+          %a.js-assign-yourself{ href: '#' }
+            assign yourself
+
+  .selectbox.hide-collapsed
+    = f.hidden_field 'assignee_id', value: merge_request.assignee_id, id: 'issue_assignee_id'
+    = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project&.id, author_id: merge_request.author_id, field_name: 'merge_request[assignee_id]', issue_update: issuable_json_path(merge_request), ability_name: 'merge_request', null_user: true } })
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 9dbfedb84f1a2273b974eb2fd491df5037e14389..9281a51574444e6a78ec4fd2e9f31f450ae8c0e3 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -10,13 +10,27 @@
 .row
   %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
     .form-group.issue-assignee
-      = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
-      .col-sm-10{ class: ("col-lg-8" if has_due_date) }
-        .issuable-form-select-holder
-          = form.hidden_field :assignee_id
-          = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
-            placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
-        = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
+      - if issuable.is_a?(Issue)
+        = form.label :assignee_ids, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+        .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+          .issuable-form-select-holder.selectbox
+            - issuable.assignees.each do |assignee|
+              = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name }
+
+            - if issuable.assignees.length === 0
+              = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
+
+            = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_dropdown_options(issuable,false))
+          = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
+      - else
+        = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+        .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+          .issuable-form-select-holder
+            = form.hidden_field :assignee_id
+
+            = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+              placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+          = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}"
     .form-group.issue-milestone
       = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
       .col-sm-10{ class: ("col-lg-8" if has_due_date) }
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 5247d6a51e64206d3fb4f033b491a7bacc95635e..22547a30cdfb1736678e56163b1acc1c1c5b250b 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -1,7 +1,7 @@
 -# @project is present when viewing Project's milestone
 - project = @project || issuable.project
 - namespace = @project_namespace || project.namespace.becomes(Namespace)
-- assignee = issuable.assignee
+- assignees = issuable.assignees
 - issuable_type = issuable.class.table_name
 - base_url_args = [namespace, project]
 - issuable_type_args = base_url_args + [issuable_type]
@@ -26,7 +26,7 @@
         - render_colored_label(label)
 
     %span.assignee-icon
-      - if assignee
-        = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
+      - assignees.each do |assignee|
+        = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
                   class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
-          - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
+          - image_tag(avatar_icon(assignee, 16), class: "avatar s16", alt: '')
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 160dc9057f2f3365e66d00bbc7ec06ae0e1ade2d..119b1ea9d2e4d6813a0c44991a8812895303e05c 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -38,8 +38,6 @@ var config = {
     graphs:               './graphs/graphs_bundle.js',
     group:                './group.js',
     groups_list:          './groups_list.js',
-    issuable:             './issuable/issuable_bundle.js',
-    locale:               './locale/index.js',
     issue_show:           './issue_show/index.js',
     locale:               './locale/index.js',
     main:                 './main.js',
@@ -54,6 +52,7 @@ var config = {
     profile:              './profile/profile_bundle.js',
     protected_branches:   './protected_branches/protected_branches_bundle.js',
     protected_tags:       './protected_tags',
+    sidebar:              './sidebar/sidebar_bundle.js',
     snippet:              './snippet/snippet_bundle.js',
     sketch_viewer:        './blob/sketch_viewer.js',
     stl_viewer:           './blob/stl_viewer.js',
@@ -139,7 +138,7 @@ var config = {
         'diff_notes',
         'environments',
         'environments_folder',
-        'issuable',
+        'sidebar',
         'issue_show',
         'merge_conflicts',
         'notebook_viewer',
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index d93d133d157678a3d1c422e027d4ee02e2ba4cba..0b32a461d56b1291ee73414dcbb3e9c48c00f48d 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -8,7 +8,7 @@ Gitlab::Seeder.quiet do
         description: FFaker::Lorem.sentence,
         state: ['opened', 'closed'].sample,
         milestone: project.milestones.sample,
-        assignee: project.team.users.sample
+        assignees: [project.team.users.sample]
       }
 
       Issues::CreateService.new(project, project.team.users.sample, issue_params).execute
diff --git a/db/migrate/20170320171632_create_issue_assignees_table.rb b/db/migrate/20170320171632_create_issue_assignees_table.rb
new file mode 100644
index 0000000000000000000000000000000000000000..23b8da37b6d75712c8e82067ee60af74f230f6ba
--- /dev/null
+++ b/db/migrate/20170320171632_create_issue_assignees_table.rb
@@ -0,0 +1,40 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateIssueAssigneesTable < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  INDEX_NAME = 'index_issue_assignees_on_issue_id_and_user_id'
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  # DOWNTIME_REASON = ''
+
+  # When using the methods "add_concurrent_index" or "add_column_with_default"
+  # you must disable the use of transactions as these methods can not run in an
+  # existing transaction. When using "add_concurrent_index" make sure that this
+  # method is the _only_ method called in the migration, any other changes
+  # should go in a separate migration. This ensures that upon failure _only_ the
+  # index creation fails and can be retried or reverted easily.
+  #
+  # To disable transactions uncomment the following line and remove these
+  # comments:
+  # disable_ddl_transaction!
+
+  def up
+    create_table :issue_assignees do |t|
+      t.references :user, foreign_key: { on_delete: :cascade }, index: true, null: false
+      t.references :issue, foreign_key: { on_delete: :cascade }, null: false
+    end
+
+    add_index :issue_assignees, [:issue_id, :user_id], unique: true, name: INDEX_NAME
+  end
+
+  def down
+    drop_table :issue_assignees
+  end
+end
diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba8edbd7d32e2f6ef8aecc53b28da235356c948d
--- /dev/null
+++ b/db/migrate/20170320173259_migrate_assignees.rb
@@ -0,0 +1,52 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateAssignees < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  # DOWNTIME_REASON = ''
+
+  # When using the methods "add_concurrent_index" or "add_column_with_default"
+  # you must disable the use of transactions as these methods can not run in an
+  # existing transaction. When using "add_concurrent_index" make sure that this
+  # method is the _only_ method called in the migration, any other changes
+  # should go in a separate migration. This ensures that upon failure _only_ the
+  # index creation fails and can be retried or reverted easily.
+  #
+  # To disable transactions uncomment the following line and remove these
+  # comments:
+  disable_ddl_transaction!
+
+  def up
+    # Optimisation: this accounts for most of the invalid assignee IDs on GitLab.com
+    update_column_in_batches(:issues, :assignee_id, nil) do |table, query|
+      query.where(table[:assignee_id].eq(0))
+    end
+
+    users = Arel::Table.new(:users)
+
+    update_column_in_batches(:issues, :assignee_id, nil) do |table, query|
+      query.where(table[:assignee_id].not_eq(nil)\
+        .and(
+          users.project("true").where(users[:id].eq(table[:assignee_id])).exists.not
+        ))
+    end
+
+    execute <<-EOF
+      INSERT INTO issue_assignees(issue_id, user_id)
+      SELECT id, assignee_id FROM issues WHERE assignee_id IS NOT NULL
+    EOF
+  end
+
+  def down
+    execute <<-EOF
+      DELETE FROM issue_assignees
+    EOF
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 588e2082ae03862abede740daca7242fc7a7be4e..aad97d5f810e02820f598a31a8c458f9d4a8e938 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -454,6 +454,14 @@ ActiveRecord::Schema.define(version: 20170504102911) do
 
   add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
 
+  create_table "issue_assignees", force: :cascade do |t|
+    t.integer "user_id", null: false
+    t.integer "issue_id", null: false
+  end
+
+  add_index "issue_assignees", ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree
+  add_index "issue_assignees", ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
+
   create_table "issue_metrics", force: :cascade do |t|
     t.integer "issue_id", null: false
     t.datetime "first_mentioned_in_commit_at"
@@ -1388,6 +1396,8 @@ ActiveRecord::Schema.define(version: 20170504102911) do
   add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade
   add_foreign_key "ci_trigger_schedules", "ci_triggers", column: "trigger_id", name: "fk_90a406cc94", on_delete: :cascade
   add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
+  add_foreign_key "issue_assignees", "issues", on_delete: :cascade
+  add_foreign_key "issue_assignees", "users", on_delete: :cascade
   add_foreign_key "container_repositories", "projects"
   add_foreign_key "issue_metrics", "issues", on_delete: :cascade
   add_foreign_key "label_priorities", "labels", on_delete: :cascade
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 6c10b5ab0e71a074bf9505424ae9c35ee1f3dc3c..1d43b1298b9b5a86d8f3242988dd63c5a21d5096 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -70,6 +70,14 @@ Example response:
          "updated_at" : "2016-01-04T15:31:39.996Z"
       },
       "project_id" : 1,
+      "assignees" : [{
+         "state" : "active",
+         "id" : 1,
+         "name" : "Administrator",
+         "web_url" : "https://gitlab.example.com/root",
+         "avatar_url" : null,
+         "username" : "root"
+      }],
       "assignee" : {
          "state" : "active",
          "id" : 1,
@@ -92,6 +100,8 @@ Example response:
 ]
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## List group issues
 
 Get a list of a group's issues.
@@ -153,6 +163,14 @@ Example response:
       "description" : "Omnis vero earum sunt corporis dolor et placeat.",
       "state" : "closed",
       "iid" : 1,
+      "assignees" : [{
+         "avatar_url" : null,
+         "web_url" : "https://gitlab.example.com/lennie",
+         "state" : "active",
+         "username" : "lennie",
+         "id" : 9,
+         "name" : "Dr. Luella Kovacek"
+      }],
       "assignee" : {
          "avatar_url" : null,
          "web_url" : "https://gitlab.example.com/lennie",
@@ -174,6 +192,8 @@ Example response:
 ]
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## List project issues
 
 Get a list of a project's issues.
@@ -235,6 +255,14 @@ Example response:
       "description" : "Omnis vero earum sunt corporis dolor et placeat.",
       "state" : "closed",
       "iid" : 1,
+      "assignees" : [{
+         "avatar_url" : null,
+         "web_url" : "https://gitlab.example.com/lennie",
+         "state" : "active",
+         "username" : "lennie",
+         "id" : 9,
+         "name" : "Dr. Luella Kovacek"
+      }],
       "assignee" : {
          "avatar_url" : null,
          "web_url" : "https://gitlab.example.com/lennie",
@@ -256,6 +284,8 @@ Example response:
 ]
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## Single issue
 
 Get a single project issue.
@@ -300,6 +330,14 @@ Example response:
    "description" : "Omnis vero earum sunt corporis dolor et placeat.",
    "state" : "closed",
    "iid" : 1,
+   "assignees" : [{
+      "avatar_url" : null,
+      "web_url" : "https://gitlab.example.com/lennie",
+      "state" : "active",
+      "username" : "lennie",
+      "id" : 9,
+      "name" : "Dr. Luella Kovacek"
+   }],
    "assignee" : {
       "avatar_url" : null,
       "web_url" : "https://gitlab.example.com/lennie",
@@ -321,6 +359,8 @@ Example response:
 }
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## New issue
 
 Creates a new project issue.
@@ -329,13 +369,13 @@ Creates a new project issue.
 POST /projects/:id/issues
 ```
 
-| Attribute                                 | Type    | Required | Description  |
-|-------------------------------------------|---------|----------|--------------|
+| Attribute                                 | Type           | Required | Description  |
+|-------------------------------------------|----------------|----------|--------------|
 | `id`                                      | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
 | `title`                                   | string  | yes      | The title of an issue |
 | `description`                             | string  | no       | The description of an issue  |
 | `confidential`                            | boolean | no       | Set an issue to be confidential. Default is `false`.  |
-| `assignee_id`                             | integer | no       | The ID of a user to assign issue |
+| `assignee_ids`                            | Array[integer] | no       | The ID of a user to assign issue |
 | `milestone_id`                            | integer | no       | The ID of a milestone to assign issue  |
 | `labels`                                  | string  | no       | Comma-separated label names for an issue  |
 | `created_at`                              | string  | no       | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
@@ -357,6 +397,7 @@ Example response:
    "iid" : 14,
    "title" : "Issues with auth",
    "state" : "opened",
+   "assignees" : [],
    "assignee" : null,
    "labels" : [
       "bug"
@@ -380,6 +421,8 @@ Example response:
 }
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## Edit issue
 
 Updates an existing project issue. This call is also used to mark an issue as
@@ -396,7 +439,7 @@ PUT /projects/:id/issues/:issue_iid
 | `title`        | string  | no       | The title of an issue                                                                                      |
 | `description`  | string  | no       | The description of an issue                                                                                |
 | `confidential` | boolean | no       | Updates an issue to be confidential                                                                        |
-| `assignee_id`  | integer | no       | The ID of a user to assign the issue to                                                                    |
+| `assignee_ids`  | Array[integer] | no       | The ID of a user to assign the issue to                                                                    |
 | `milestone_id` | integer | no       | The ID of a milestone to assign the issue to                                                               |
 | `labels`       | string  | no       | Comma-separated label names for an issue                                                                   |
 | `state_event`  | string  | no       | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it                      |
@@ -430,6 +473,7 @@ Example response:
       "bug"
    ],
    "id" : 85,
+   "assignees" : [],
    "assignee" : null,
    "milestone" : null,
    "subscribed" : true,
@@ -440,6 +484,8 @@ Example response:
 }
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## Delete an issue
 
 Only for admins and project owners. Soft deletes the issue in question.
@@ -494,6 +540,14 @@ Example response:
   "updated_at": "2016-04-07T12:20:17.596Z",
   "labels": [],
   "milestone": null,
+  "assignees": [{
+    "name": "Miss Monserrate Beier",
+    "username": "axel.block",
+    "id": 12,
+    "state": "active",
+    "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+    "web_url": "https://gitlab.example.com/axel.block"
+  }],
   "assignee": {
     "name": "Miss Monserrate Beier",
     "username": "axel.block",
@@ -516,6 +570,8 @@ Example response:
 }
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## Subscribe to an issue
 
 Subscribes the authenticated user to an issue to receive notifications.
@@ -549,6 +605,14 @@ Example response:
   "updated_at": "2016-04-07T12:20:17.596Z",
   "labels": [],
   "milestone": null,
+  "assignees": [{
+    "name": "Miss Monserrate Beier",
+    "username": "axel.block",
+    "id": 12,
+    "state": "active",
+    "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+    "web_url": "https://gitlab.example.com/axel.block"
+  }],
   "assignee": {
     "name": "Miss Monserrate Beier",
     "username": "axel.block",
@@ -571,6 +635,8 @@ Example response:
 }
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## Unsubscribe from an issue
 
 Unsubscribes the authenticated user from the issue to not receive notifications
@@ -652,6 +718,14 @@ Example response:
       "updated_at": "2016-06-17T07:47:33.832Z",
       "due_date": null
     },
+    "assignees": [{
+      "name": "Jarret O'Keefe",
+      "username": "francisca",
+      "id": 14,
+      "state": "active",
+      "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
+      "web_url": "https://gitlab.example.com/francisca"
+    }],
     "assignee": {
       "name": "Jarret O'Keefe",
       "username": "francisca",
@@ -683,6 +757,8 @@ Example response:
 }
 ```
 
+**Note**: `assignee` column is deprecated, it shows the first assignee only.
+
 ## Set a time estimate for an issue
 
 Sets an estimated time of work for this issue.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index dbdc93a77a8eb93a9a2ed727bd0abfafd0d14d30..e15daa2feae1b93582282d09d058b3f7d9ffa537 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -232,6 +232,7 @@ X-Gitlab-Event: Issue Hook
   "object_attributes": {
     "id": 301,
     "title": "New API: create/update/delete file",
+    "assignee_ids": [51],
     "assignee_id": 51,
     "author_id": 51,
     "project_id": 14,
@@ -246,6 +247,11 @@ X-Gitlab-Event: Issue Hook
     "url": "http://example.com/diaspora/issues/23",
     "action": "open"
   },
+  "assignees": [{
+    "name": "User1",
+    "username": "user1",
+    "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+  }],
   "assignee": {
     "name": "User1",
     "username": "user1",
@@ -265,6 +271,9 @@ X-Gitlab-Event: Issue Hook
   }]
 }
 ```
+
+**Note**: `assignee` and `assignee_id` keys are deprecated and now show the first assignee only.
+
 ### Comment events
 
 Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
@@ -544,6 +553,7 @@ X-Gitlab-Event: Note Hook
   "issue": {
     "id": 92,
     "title": "test",
+    "assignee_ids": [],
     "assignee_id": null,
     "author_id": 1,
     "project_id": 5,
@@ -559,6 +569,8 @@ X-Gitlab-Event: Note Hook
 }
 ```
 
+**Note**: `assignee_id` field is deprecated and now shows the first assignee only.
+
 #### Comment on code snippet
 
 **Request header**:
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index c715c85c43c01042f2cd61b60b174e64ffa612d3..bf09d7b7114be86caba6d64515273a92350637e3 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -77,7 +77,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
 
   step 'project "Shop" has issue "Bugfix1" with label "feature"' do
     project = Project.find_by(name: "Shop")
-    issue = create(:issue, title: "Bugfix1", project: project, assignee: current_user)
+    issue = create(:issue, title: "Bugfix1", project: project, assignees: [current_user])
     issue.labels << project.labels.find_by(title: 'feature')
   end
 end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 3225e19995bbe7dc8008b362a115de078445b287..b56558ba0d2aea4f7ad31c1186c70013d35dc61b 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -182,7 +182,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
   end
 
   def issue
-    @issue ||= create(:issue, assignee: current_user, project: project)
+    @issue ||= create(:issue, assignees: [current_user], project: project)
   end
 
   def merge_request
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index 49fcd6f1201430668dde67e12f6b0f7c58281398..0b0983f0d0653d2eedea4ab0bcc0e2b9bc43638e 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -113,7 +113,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
 
       create :issue,
         project: project,
-        assignee: current_user,
+        assignees: [current_user],
         author: current_user,
         milestone: milestone
 
@@ -125,7 +125,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
 
       issue = create :issue,
         project: project,
-        assignee: current_user,
+        assignees: [current_user],
         author: current_user,
         milestone: milestone
 
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 4dc87dc4d9c2915defb81b37849995ffc3196f15..83d8abbab1fae334d9068872713b597e24f8906f 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -61,7 +61,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
   step 'project from group "Owned" has issues assigned to me' do
     create :issue,
       project: project,
-      assignee: current_user,
+      assignees: [current_user],
       author: current_user
   end
 
@@ -123,7 +123,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
   step 'the archived project have some issues' do
     create :issue,
       project: @archived_project,
-      assignee: current_user,
+      assignees: [current_user],
       author: current_user
   end
 
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 302ccd3e5327d7db9750863fee3f55bad6796b24..52cd7cbe3dba345cf921d9213f52c354f7f9893c 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -6,6 +6,7 @@ module API
 
     version 'v3', using: :path do
       helpers ::API::V3::Helpers
+      helpers ::API::Helpers::CommonHelpers
 
       mount ::API::V3::AwardEmoji
       mount ::API::V3::Boards
@@ -80,6 +81,7 @@ module API
     # Ensure the namespace is right, otherwise we might load Grape::API::Helpers
     helpers ::SentryHelper
     helpers ::API::Helpers
+    helpers ::API::Helpers::CommonHelpers
 
     # Keep in alphabetical order
     mount ::API::AccessRequests
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 6d6ccefe8776cfa1266e9a589ba1d3823f79eecf..f8f5548d23da94ee963940fd9486ea6b7c1a6c9c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -256,7 +256,11 @@ module API
     class IssueBasic < ProjectEntity
       expose :label_names, as: :labels
       expose :milestone, using: Entities::Milestone
-      expose :assignee, :author, using: Entities::UserBasic
+      expose :assignees, :author, using: Entities::UserBasic
+
+      expose :assignee, using: ::API::Entities::UserBasic do |issue, options|
+        issue.assignees.first
+      end
 
       expose :user_notes_count
       expose :upvotes, :downvotes
diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6236fdd43ca1e6c8e384f493e294d49f3fbe7045
--- /dev/null
+++ b/lib/api/helpers/common_helpers.rb
@@ -0,0 +1,13 @@
+module API
+  module Helpers
+    module CommonHelpers
+      def convert_parameters_from_legacy_format(params)
+        if params[:assignee_id].present?
+          params[:assignee_ids] = [params.delete(:assignee_id)]
+        end
+
+        params
+      end
+    end
+  end
+end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 522f0f3be929190a65ce0526048b778d5c981a05..78db960ae28fa2b8499306bfcfc88aa5d9b3454b 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -32,7 +32,8 @@ module API
 
       params :issue_params_ce do
         optional :description, type: String, desc: 'The description of an issue'
-        optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
+        optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
+        optional :assignee_id,  type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
         optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
         optional :labels, type: String, desc: 'Comma-separated list of label names'
         optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
@@ -135,6 +136,8 @@ module API
 
         issue_params = declared_params(include_missing: false)
 
+        issue_params = convert_parameters_from_legacy_format(issue_params)
+
         issue = ::Issues::CreateService.new(user_project,
                                             current_user,
                                             issue_params.merge(request: request, api: true)).execute
@@ -159,7 +162,7 @@ module API
                               desc: 'Date time when the issue was updated. Available only for admins and project owners.'
         optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
         use :issue_params
-        at_least_one_of :title, :description, :assignee_id, :milestone_id,
+        at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id,
                         :labels, :created_at, :due_date, :confidential, :state_event
       end
       put ':id/issues/:issue_iid' do
@@ -173,6 +176,8 @@ module API
 
         update_params = declared_params(include_missing: false).merge(request: request, api: true)
 
+        update_params = convert_parameters_from_legacy_format(update_params)
+
         issue = ::Issues::UpdateService.new(user_project,
                                             current_user,
                                             update_params).execute(issue)
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 832b4bdeb4fefce69ad954dc74bbbc2915a768f8..7c8be7e51db56266704a9901698e7ba261e60e5a 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -248,6 +248,13 @@ module API
         expose :project_id, :issues_events, :merge_requests_events
         expose :note_events, :build_events, :pipeline_events, :wiki_page_events
       end
+
+      class Issue < ::API::Entities::Issue
+        unexpose :assignees
+        expose :assignee do |issue, options|
+          ::API::Entities::UserBasic.represent(issue.assignees.first, options)
+        end
+      end
     end
   end
 end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index 715083fc4f80f16ee8f07ff674ea5375732b854f..cb371fdbab8f72db01c8e3d1564712182f7fe501 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -8,6 +8,7 @@ module API
       helpers do
         def find_issues(args = {})
           args = params.merge(args)
+          args = convert_parameters_from_legacy_format(args)
 
           args.delete(:id)
           args[:milestone_title] = args.delete(:milestone)
@@ -51,7 +52,7 @@ module API
 
       resource :issues do
         desc "Get currently authenticated user's issues" do
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -61,7 +62,7 @@ module API
         get do
           issues = find_issues(scope: 'authored')
 
-          present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+          present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
         end
       end
 
@@ -70,7 +71,7 @@ module API
       end
       resource :groups, requirements: { id: %r{[^/]+} } do
         desc 'Get a list of group issues' do
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -82,7 +83,7 @@ module API
 
           issues = find_issues(group_id: group.id, match_all_labels: true)
 
-          present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+          present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user
         end
       end
 
@@ -94,7 +95,7 @@ module API
 
         desc 'Get a list of project issues' do
           detail 'iid filter is deprecated have been removed on V4'
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           optional :state, type: String, values: %w[opened closed all], default: 'all',
@@ -107,22 +108,22 @@ module API
 
           issues = find_issues(project_id: project.id)
 
-          present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+          present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
         end
 
         desc 'Get a single project issue' do
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           requires :issue_id, type: Integer, desc: 'The ID of a project issue'
         end
         get ":id/issues/:issue_id" do
           issue = find_project_issue(params[:issue_id])
-          present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+          present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
         end
 
         desc 'Create a new project issue' do
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           requires :title, type: String, desc: 'The title of an issue'
@@ -140,6 +141,7 @@ module API
 
           issue_params = declared_params(include_missing: false)
           issue_params = issue_params.merge(merge_request_to_resolve_discussions_of: issue_params.delete(:merge_request_for_resolving_discussions))
+          issue_params = convert_parameters_from_legacy_format(issue_params)
 
           issue = ::Issues::CreateService.new(user_project,
                                               current_user,
@@ -147,14 +149,14 @@ module API
           render_spam_error! if issue.spam?
 
           if issue.valid?
-            present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+            present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
           else
             render_validation_error!(issue)
           end
         end
 
         desc 'Update an existing issue' do
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           requires :issue_id, type: Integer, desc: 'The ID of a project issue'
@@ -176,6 +178,7 @@ module API
           end
 
           update_params = declared_params(include_missing: false).merge(request: request, api: true)
+          update_params = convert_parameters_from_legacy_format(update_params)
 
           issue = ::Issues::UpdateService.new(user_project,
                                               current_user,
@@ -184,14 +187,14 @@ module API
           render_spam_error! if issue.spam?
 
           if issue.valid?
-            present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+            present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
           else
             render_validation_error!(issue)
           end
         end
 
         desc 'Move an existing issue' do
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           requires :issue_id, type: Integer, desc: 'The ID of a project issue'
@@ -206,7 +209,7 @@ module API
 
           begin
             issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
-            present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+            present issue, with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
           rescue ::Issues::MoveService::MoveError => error
             render_api_error!(error.message, 400)
           end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 1616142a619db6955c02f74619025b50820303b9..b6b7254ae2956ecdebbafd7ad40d90238c3ce5aa 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -34,7 +34,7 @@ module API
             if project.has_external_issue_tracker?
               ::API::Entities::ExternalIssue
             else
-              ::API::Entities::Issue
+              ::API::V3::Entities::Issue
             end
           end
 
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
index be90cec4afcbc866c700cc24dca4fb518fdadcc6..4c7061d493948b0965163b1535977dfee977e38a 100644
--- a/lib/api/v3/milestones.rb
+++ b/lib/api/v3/milestones.rb
@@ -39,7 +39,7 @@ module API
         end
 
         desc 'Get all issues for a single project milestone' do
-          success ::API::Entities::Issue
+          success ::API::V3::Entities::Issue
         end
         params do
           requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
@@ -56,7 +56,7 @@ module API
           }
 
           issues = IssuesFinder.new(current_user, finder_params).execute
-          present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+          present paginate(issues), with: ::API::V3::Entities::Issue, current_user: current_user, project: user_project
         end
       end
     end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index e02b360924ae2287e8896071cf9f2f492ea269fa..89ec715ddf6d91afbca086e4fa3f81f37da87a81 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -28,7 +28,7 @@ module Banzai
           nodes,
           Issue.all.includes(
             :author,
-            :assignee,
+            :assignees,
             {
               # These associations are primarily used for checking permissions.
               # Eager loading these ensures we don't end up running dozens of
diff --git a/lib/github/import.rb b/lib/github/import.rb
index d49761fd6c608a70738bf3d49c383949dbc8233d..06beb607a3eca832ae17fadeceea0712babc56bb 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -245,7 +245,7 @@ module Github
               issue.label_ids    = label_ids(representation.labels)
               issue.milestone_id = milestone_id(representation.milestone)
               issue.author_id    = author_id
-              issue.assignee_id  = user_id(representation.assignee)
+              issue.assignee_ids = [user_id(representation.assignee)]
               issue.created_at   = representation.created_at
               issue.updated_at   = representation.updated_at
               issue.save!(validate: false)
diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb
index 054f7f4be0ce6679f86a17ed9d52a74396c4aba1..25bc82994baa5095a7df469422dd0ea4a32daf93 100644
--- a/lib/gitlab/chat_commands/presenters/issue_base.rb
+++ b/lib/gitlab/chat_commands/presenters/issue_base.rb
@@ -22,7 +22,7 @@ module Gitlab
           [
             {
               title: "Assignee",
-              value: @resource.assignee ? @resource.assignee.name : "_None_",
+              value: @resource.assignees.any? ? @resource.assignees.first.name : "_None_",
               short: true
             },
             {
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 222bcdcbf9c8a569bc666c20d02db599e17760ea..3dcee681c72f4c26e1cf42cf2f18497cb56b8d70 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -122,15 +122,15 @@ module Gitlab
           author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
 
           issue = Issue.create!(
-            iid:         bug['ixBug'],
-            project_id:  project.id,
-            title:       bug['sTitle'],
-            description: body,
-            author_id:   author_id,
-            assignee_id: assignee_id,
-            state:       bug['fOpen'] == 'true' ? 'opened' : 'closed',
-            created_at:  date,
-            updated_at:  DateTime.parse(bug['dtLastUpdated'])
+            iid:          bug['ixBug'],
+            project_id:   project.id,
+            title:        bug['sTitle'],
+            description:  body,
+            author_id:    author_id,
+            assignee_ids: [assignee_id],
+            state:        bug['fOpen'] == 'true' ? 'opened' : 'closed',
+            created_at:   date,
+            updated_at:   DateTime.parse(bug['dtLastUpdated'])
           )
 
           issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 6f5ac4dac0d3885f152644696dc30f85dcab0422..977cd0423ba532b4d085609b43acebc92e87b23e 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -10,7 +10,7 @@ module Gitlab
           description: description,
           state: state,
           author_id: author_id,
-          assignee_id: assignee_id,
+          assignee_ids: Array(assignee_id),
           created_at: raw_data.created_at,
           updated_at: raw_data.updated_at
         }
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 5ca3e6a95cab8db5fac6f64e97df500e00f0c3a3..1b43440673c11bdfd0d2f84ee6e7a14804bd1ee5 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -108,13 +108,13 @@ module Gitlab
           end
 
           issue = Issue.create!(
-            iid:         raw_issue['id'],
-            project_id:  project.id,
-            title:       raw_issue['title'],
-            description: body,
-            author_id:   project.creator_id,
-            assignee_id: assignee_id,
-            state:       raw_issue['state'] == 'closed' ? 'closed' : 'opened'
+            iid:          raw_issue['id'],
+            project_id:   project.id,
+            title:        raw_issue['title'],
+            description:  body,
+            author_id:    project.creator_id,
+            assignee_ids: [assignee_id],
+            state:        raw_issue['state'] == 'closed' ? 'closed' : 'opened'
           )
 
           issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 762e90f4a1672fc482e3de90572100fdf0123077..085f3fd8543d505c93ff8a1b93686000f485808f 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -14,7 +14,7 @@ describe Dashboard::TodosController do
   describe 'GET #index' do
     context 'when using pagination' do
       let(:last_page) { user.todos.page.total_pages }
-      let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
+      let!(:issues) { create_list(:issue, 2, project: project, assignees: [user]) }
 
       before do
         issues.each { |issue| todo_service.new_issue(issue, user) }
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 15667e8d4b11611159fc7f33d0fae5fb6c09f2e8..dc3b72c6de4e20485d9fd3c7d9ed594fb7864e1e 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -34,7 +34,7 @@ describe Projects::Boards::IssuesController do
           issue = create(:labeled_issue, project: project, labels: [planning])
           create(:labeled_issue, project: project, labels: [planning])
           create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
-          create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+          create(:labeled_issue, project: project, labels: [development], assignees: [johndoe])
           issue.subscribe(johndoe, project)
 
           list_issues user: user, board: board, list: list2
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 5f1f892821a059aaef858e6e83c2d91f607cbec9..1f79e72495ac7c0c8f5b50cc567fad28d034f5f7 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -173,12 +173,12 @@ describe Projects::IssuesController do
           namespace_id: project.namespace.to_param,
           project_id: project,
           id: issue.iid,
-          issue: { assignee_id: assignee.id },
+          issue: { assignee_ids: [assignee.id] },
           format: :json
         body = JSON.parse(response.body)
 
-        expect(body['assignee'].keys)
-          .to match_array(%w(name username avatar_url))
+        expect(body['assignees'].first.keys)
+          .to match_array(%w(id name username avatar_url))
       end
     end
 
@@ -348,7 +348,7 @@ describe Projects::IssuesController do
     let(:admin) { create(:admin) }
     let!(:issue) { create(:issue, project: project) }
     let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
-    let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
+    let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignees: [assignee]) }
 
     describe 'GET #index' do
       it 'does not list confidential issues for guests' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index a793da4162a8d184510cebd51001e502118d0990..0483c6b7879542447d7e177ecfa5346d27df8c5c 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1067,7 +1067,7 @@ describe Projects::MergeRequestsController do
     end
 
     it 'correctly pluralizes flash message on success' do
-      issue2.update!(assignee: user)
+      issue2.assignees = [user]
 
       post_assign_issues
 
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 58b14e09740fab59db25c3ab518625f997e1cbe9..9ea325ab41b0566cb736d294067036486d6d8943 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -32,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true  do
       end
 
       context "issue with basic fields" do
-        let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') }
+        let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') }
 
         it "renders issue fields" do
           visit issues_dashboard_path(:atom, private_token: user.private_token)
@@ -41,7 +41,7 @@ describe "Dashboard Issues Feed", feature: true  do
 
           expect(entry).to be_present
           expect(entry).to have_selector('author email', text: issue2.author_public_email)
-          expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email)
+          expect(entry).to have_selector('assignees email', text: assignee.public_email)
           expect(entry).not_to have_selector('labels')
           expect(entry).not_to have_selector('milestone')
           expect(entry).to have_selector('description', text: issue2.description)
@@ -51,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true  do
       context "issue with label and milestone" do
         let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
         let!(:label1)     { create(:label, project: project1, title: 'label1') }
-        let!(:issue1)     { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) }
+        let!(:issue1)     { create(:issue, author: user, assignees: [assignee], project: project1, milestone: milestone1) }
 
         before do
           issue1.labels << label1
@@ -64,7 +64,7 @@ describe "Dashboard Issues Feed", feature: true  do
 
           expect(entry).to be_present
           expect(entry).to have_selector('author email', text: issue1.author_public_email)
-          expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email)
+          expect(entry).to have_selector('assignees email', text: assignee.public_email)
           expect(entry).to have_selector('labels label', text: label1.title)
           expect(entry).to have_selector('milestone', text: milestone1.title)
           expect(entry).not_to have_selector('description')
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index b3903ec2faf92705f45f12126c13ea1c0779f992..4f6754ad54105f608eac4f07b6d73242b8d9b37f 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Issues Feed', feature: true  do
     let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
     let!(:group)    { create(:group) }
     let!(:project)  { create(:project) }
-    let!(:issue)    { create(:issue, author: user, assignee: assignee, project: project) }
+    let!(:issue)    { create(:issue, author: user, assignees: [assignee], project: project) }
 
     before do
       project.team << [user, :developer]
@@ -22,7 +22,8 @@ describe 'Issues Feed', feature: true  do
           to have_content('application/atom+xml')
         expect(body).to have_selector('title', text: "#{project.name} issues")
         expect(body).to have_selector('author email', text: issue.author_public_email)
-        expect(body).to have_selector('assignee email', text: issue.author_public_email)
+        expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
+        expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
         expect(body).to have_selector('entry summary', text: issue.title)
       end
     end
@@ -36,7 +37,8 @@ describe 'Issues Feed', feature: true  do
           to have_content('application/atom+xml')
         expect(body).to have_selector('title', text: "#{project.name} issues")
         expect(body).to have_selector('author email', text: issue.author_public_email)
-        expect(body).to have_selector('assignee email', text: issue.author_public_email)
+        expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
+        expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
         expect(body).to have_selector('entry summary', text: issue.title)
       end
     end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index a172ce1e8c022be4e6b632cf7e15d98185a2b423..18585488e2660fcff24843159755e9ffaf1030dc 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -71,7 +71,7 @@ describe 'Issue Boards', feature: true, js: true do
     let!(:list2) { create(:list, board: board, label: development, position: 1) }
 
     let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
-    let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) }
+    let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) }
     let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
     let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
     let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 4a4c13e79c8a966ad8ce950e7580d433233cf27e..e1367c675e58bee431bba31f101b2224720dbd00 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -98,7 +98,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
   end
 
   context 'assignee' do
-    let!(:issue) { create(:issue, project: project, assignee: user2) }
+    let!(:issue) { create(:issue, project: project, assignees: [user2]) }
 
     before do
       project.team << [user2, :developer]
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index bafa4f05937105461686cef7a37e9b58a2f02853..7c53d2b47d9c83dd342009e3983a89af4f650321 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -4,13 +4,14 @@ describe 'Issue Boards', feature: true, js: true do
   include WaitForVueResource
 
   let(:user)         { create(:user) }
+  let(:user2)        { create(:user) }
   let(:project)      { create(:empty_project, :public) }
   let!(:milestone)   { create(:milestone, project: project) }
   let!(:development) { create(:label, project: project, name: 'Development') }
   let!(:bug)         { create(:label, project: project, name: 'Bug') }
   let!(:regression)  { create(:label, project: project, name: 'Regression') }
   let!(:stretch)     { create(:label, project: project, name: 'Stretch') }
-  let!(:issue1)      { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) }
+  let!(:issue1)      { create(:labeled_issue, project: project, assignees: [user], milestone: milestone, labels: [development], relative_position: 2) }
   let!(:issue2)      { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
   let(:board)        { create(:board, project: project) }
   let!(:list)        { create(:list, board: board, label: development, position: 0) }
@@ -112,10 +113,11 @@ describe 'Issue Boards', feature: true, js: true do
 
         page.within('.dropdown-menu-user') do
           click_link 'Unassigned'
-
-          wait_for_vue_resource
         end
 
+        find('.dropdown-menu-toggle').click
+        wait_for_vue_resource
+
         expect(page).to have_content('No assignee')
       end
 
@@ -128,7 +130,7 @@ describe 'Issue Boards', feature: true, js: true do
       page.within(find('.assignee')) do
         expect(page).to have_content('No assignee')
 
-        click_link 'assign yourself'
+        click_button 'assign yourself'
 
         wait_for_vue_resource
 
@@ -138,7 +140,7 @@ describe 'Issue Boards', feature: true, js: true do
       expect(card).to have_selector('.avatar')
     end
 
-    it 'resets assignee dropdown' do
+    it 'updates assignee dropdown' do
       click_card(card)
 
       page.within('.assignee') do
@@ -162,7 +164,7 @@ describe 'Issue Boards', feature: true, js: true do
       page.within('.assignee') do
         click_link 'Edit'
 
-        expect(page).not_to have_selector('.is-active')
+        expect(page).to have_selector('.is-active')
       end
     end
   end
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index 4fca7577e7431aa07b63986211bb288abd2be4c7..6f7bf0eba6ebfa1e6ff03fe3ac113a6f85a5e738 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -7,7 +7,7 @@ describe 'Navigation bar counter', feature: true, caching: true do
   let(:merge_request) { create(:merge_request, source_project: project) }
 
   before do
-    issue.update(assignee: user)
+    issue.assignees = [user]
     merge_request.update(assignee: user)
     login_as(user)
   end
@@ -17,7 +17,9 @@ describe 'Navigation bar counter', feature: true, caching: true do
 
     expect_counters('issues', '1')
 
-    issue.update(assignee: nil)
+    issue.assignees = []
+
+    user.update_cache_counts
 
     Timecop.travel(3.minutes.from_now) do
       visit issues_path
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index f4420814c3a1bc94b951408c13e312f44d40a749..86c7954e60cf0cc899c3c19c5d7542522f9afdf5 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Dashboard Issues', feature: true do
 
   let!(:authored_issue) { create :issue, author: current_user, project: project }
   let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
-  let!(:assigned_issue) { create :issue, assignee: current_user, project: project }
+  let!(:assigned_issue) { create :issue, assignees: [current_user], project: project }
   let!(:other_issue) { create :issue, project: project }
 
   before do
@@ -30,6 +30,11 @@ RSpec.describe 'Dashboard Issues', feature: true do
     find('#assignee_id', visible: false).set('')
     find('.js-author-search', match: :first).click
     find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click
+    find('.js-author-search', match: :first).click
+
+    page.within '.dropdown-menu-user' do
+      expect(find('.dropdown-menu-author li a.is-active', match: :first, text: current_user.to_reference)).to be_visible
+    end
 
     expect(page).to have_content(authored_issue.title)
     expect(page).to have_content(authored_issue_on_public_project.title)
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index b6b879052315c7f030bdddfd2c08f9e7c7bddf54..ad60fb2c74f515b8b5130cedc2bec00da2339194 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -10,8 +10,8 @@ describe "Dashboard Issues filtering", feature: true, js: true do
       project.team << [user, :master]
       login_as(user)
 
-      create(:issue, project: project, author: user, assignee: user)
-      create(:issue, project: project, author: user, assignee: user, milestone: milestone)
+      create(:issue, project: project, author: user, assignees: [user])
+      create(:issue, project: project, author: user, assignees: [user], milestone: milestone)
 
       visit_issues
     end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index f5b54463df8ffe08987de94f324ceef472c22bcc..005a029a39319308561b7f5dc4234db25c747641 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -54,11 +54,11 @@ describe "GitLab Flavored Markdown", feature: true do
     before do
       @other_issue = create(:issue,
                             author: @user,
-                            assignee: @user,
+                            assignees: [@user],
                             project: project)
       @issue = create(:issue,
                       author: @user,
-                      assignee: @user,
+                      assignees: [@user],
                       project: project,
                       title: "fix #{@other_issue.to_reference}",
                       description: "ask #{fred.to_reference} for details")
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 71df3c949db4b05745d10da2e4b32ffae0e27f1a..853632614c4476266d465c557192001dca33beb9 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -7,7 +7,7 @@ describe 'Awards Emoji', feature: true do
   let!(:user)      { create(:user) }
   let(:issue) do
     create(:issue,
-           assignee: @user,
+           assignees: [user],
            project: project)
   end
 
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index c824aa6a414505d138bfda413794154b436c8d12..a8f4e2d7e10adfc042e2a85e4316222aa148e251 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -51,15 +51,15 @@ describe 'Filter issues', js: true, feature: true do
     create(:issue, project: project, title: "issue with 'single quotes'")
     create(:issue, project: project, title: "issue with \"double quotes\"")
     create(:issue, project: project, title: "issue with !@\#{$%^&*()-+")
-    create(:issue, project: project, title: "issue by assignee", milestone: milestone, author: user, assignee: user)
-    create(:issue, project: project, title: "issue by assignee with searchTerm", milestone: milestone, author: user, assignee: user)
+    create(:issue, project: project, title: "issue by assignee", milestone: milestone, author: user, assignees: [user])
+    create(:issue, project: project, title: "issue by assignee with searchTerm", milestone: milestone, author: user, assignees: [user])
 
     issue = create(:issue,
       title: "Bug 2",
       project: project,
       milestone: milestone,
       author: user,
-      assignee: user)
+      assignees: [user])
     issue.labels << bug_label
 
     issue_with_caps_label = create(:issue,
@@ -67,7 +67,7 @@ describe 'Filter issues', js: true, feature: true do
       project: project,
       milestone: milestone,
       author: user,
-      assignee: user)
+      assignees: [user])
     issue_with_caps_label.labels << caps_sensitive_label
 
     issue_with_everything = create(:issue,
@@ -75,7 +75,7 @@ describe 'Filter issues', js: true, feature: true do
       project: project,
       milestone: milestone,
       author: user,
-      assignee: user)
+      assignees: [user])
     issue_with_everything.labels << bug_label
     issue_with_everything.labels << caps_sensitive_label
 
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 21b8cf3add5c52f7c625ce14f255dc63c3791fdf..87adce3cdddf4522ebfff52650ee05414c1b0172 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -10,7 +10,7 @@ describe 'New/edit issue', feature: true, js: true do
   let!(:milestone) { create(:milestone, project: project) }
   let!(:label)     { create(:label, project: project) }
   let!(:label2)    { create(:label, project: project) }
-  let!(:issue)     { create(:issue, project: project, assignee: user, milestone: milestone) }
+  let!(:issue)     { create(:issue, project: project, assignees: [user], milestone: milestone) }
 
   before do
     project.team << [user, :master]
@@ -23,23 +23,62 @@ describe 'New/edit issue', feature: true, js: true do
       visit new_namespace_project_issue_path(project.namespace, project)
     end
 
+    describe 'multiple assignees' do
+      before do
+        click_button 'Unassigned'
+      end
+
+      it 'unselects other assignees when unassigned is selected' do
+        page.within '.dropdown-menu-user' do
+          click_link user2.name
+        end
+
+        page.within '.dropdown-menu-user' do
+          click_link 'Unassigned'
+        end
+
+        page.within '.js-assignee-search' do
+          expect(page).to have_content 'Unassigned'
+        end
+
+        expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match('0')
+      end
+
+      it 'toggles assign to me when current user is selected and unselected' do
+        page.within '.dropdown-menu-user' do
+          click_link user.name
+        end
+
+        expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
+
+        page.within '.dropdown-menu-user' do
+          click_link user.name
+        end
+
+        expect(find('a', text: 'Assign to me')).to be_visible
+      end
+    end
+
     it 'allows user to create new issue' do
       fill_in 'issue_title', with: 'title'
       fill_in 'issue_description', with: 'title'
 
       expect(find('a', text: 'Assign to me')).to be_visible
-      click_button 'Assignee'
+      click_button 'Unassigned'
       page.within '.dropdown-menu-user' do
         click_link user2.name
       end
-      expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+      expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
       page.within '.js-assignee-search' do
         expect(page).to have_content user2.name
       end
       expect(find('a', text: 'Assign to me')).to be_visible
 
       click_link 'Assign to me'
-      expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+      assignee_ids = page.all('input[name="issue[assignee_ids][]"]', visible: false)
+
+      expect(assignee_ids[0].value).to match(user.id.to_s)
+
       page.within '.js-assignee-search' do
         expect(page).to have_content user.name
       end
@@ -69,7 +108,7 @@ describe 'New/edit issue', feature: true, js: true do
 
       page.within '.issuable-sidebar' do
         page.within '.assignee' do
-          expect(page).to have_content user.name
+          expect(page).to have_content "Assignee"
         end
 
         page.within '.milestone' do
@@ -108,12 +147,12 @@ describe 'New/edit issue', feature: true, js: true do
     end
 
     it 'correctly updates the selected user when changing assignee' do
-      click_button 'Assignee'
+      click_button 'Unassigned'
       page.within '.dropdown-menu-user' do
         click_link user.name
       end
 
-      expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+      expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
 
       click_button user.name
 
@@ -127,7 +166,7 @@ describe 'New/edit issue', feature: true, js: true do
         click_link user2.name
       end
 
-      expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+      expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
 
       click_button user2.name
 
@@ -141,7 +180,7 @@ describe 'New/edit issue', feature: true, js: true do
     end
 
     it 'allows user to update issue' do
-      expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+      expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
       expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
       expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
 
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 82b80a69bed5f73f826210163475aea2075746e5..e9a05f56543b02d27d7274fafa0fa1ceb3b58f07 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -42,6 +42,21 @@ feature 'Issue Sidebar', feature: true do
         expect(page).to have_content(user2.name)
       end
     end
+
+    it 'assigns yourself' do
+      find('.block.assignee .dropdown-menu-toggle').click
+
+      click_button 'assign yourself'
+
+      wait_for_ajax
+
+      find('.block.assignee .edit-link').click
+
+      page.within '.dropdown-menu-user' do
+        expect(page.find('.dropdown-header')).to be_visible
+        expect(page.find('.dropdown-menu-user-link.is-active')).to have_content(user.name)
+      end
+    end
   end
 
   context 'as a allowed user' do
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 7fa83c1fcf7644bc25c35d5d3d47f97aa6796c4a..b250fa2ed3c804944a4264f590924e480a288f24 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -99,7 +99,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
   end
 
   def create_assigned
-    create(:issue, project: project, assignee: user)
+    create(:issue, project: project, assignees: [user])
   end
 
   def create_with_milestone
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 81cc8513454ef779d41cb796c20cbf01c98055b9..5285dda361b4e715f0385454a82b1f8123249a6a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -18,7 +18,7 @@ describe 'Issues', feature: true do
     let!(:issue) do
       create(:issue,
              author: @user,
-             assignee: @user,
+             assignees: [@user],
              project: project)
     end
 
@@ -43,7 +43,7 @@ describe 'Issues', feature: true do
     let!(:issue) do
       create(:issue,
              author: @user,
-             assignee: @user,
+             assignees: [@user],
              project: project)
     end
 
@@ -61,7 +61,7 @@ describe 'Issues', feature: true do
         expect(page).to have_content 'No assignee - assign yourself'
       end
 
-      expect(issue.reload.assignee).to be_nil
+      expect(issue.reload.assignees).to be_empty
     end
   end
 
@@ -138,7 +138,7 @@ describe 'Issues', feature: true do
 
   describe 'Issue info' do
     it 'excludes award_emoji from comment count' do
-      issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
+      issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'foobar')
       create(:award_emoji, awardable: issue)
 
       visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
@@ -153,14 +153,14 @@ describe 'Issues', feature: true do
       %w(foobar barbaz gitlab).each do |title|
         create(:issue,
                author: @user,
-               assignee: @user,
+               assignees: [@user],
                project: project,
                title: title)
       end
 
       @issue = Issue.find_by(title: 'foobar')
       @issue.milestone = create(:milestone, project: project)
-      @issue.assignee = nil
+      @issue.assignees = []
       @issue.save
     end
 
@@ -351,9 +351,9 @@ describe 'Issues', feature: true do
       let(:user2) { create(:user) }
 
       before do
-        foo.assignee = user2
+        foo.assignees << user2
         foo.save
-        bar.assignee = user2
+        bar.assignees << user2
         bar.save
       end
 
@@ -396,7 +396,7 @@ describe 'Issues', feature: true do
   end
 
   describe 'update labels from issue#show', js: true do
-    let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+    let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
     let!(:label) { create(:label, project: project) }
 
     before do
@@ -415,7 +415,7 @@ describe 'Issues', feature: true do
   end
 
   describe 'update assignee from issue#show' do
-    let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+    let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
 
     context 'by authorized user' do
       it 'allows user to select unassigned', js: true do
@@ -426,10 +426,14 @@ describe 'Issues', feature: true do
 
           click_link 'Edit'
           click_link 'Unassigned'
+          first('.title').click
           expect(page).to have_content 'No assignee'
         end
 
-        expect(issue.reload.assignee).to be_nil
+        # wait_for_ajax does not work with vue-resource at the moment
+        sleep 1
+
+        expect(issue.reload.assignees).to be_empty
       end
 
       it 'allows user to select an assignee', js: true do
@@ -461,14 +465,18 @@ describe 'Issues', feature: true do
           click_link 'Edit'
           click_link @user.name
 
-          page.within '.value' do
+          find('.dropdown-menu-toggle').click
+
+          page.within '.value .author' do
             expect(page).to have_content @user.name
           end
 
           click_link 'Edit'
           click_link @user.name
 
-          page.within '.value' do
+          find('.dropdown-menu-toggle').click
+
+          page.within '.value .assign-yourself' do
             expect(page).to have_content "No assignee"
           end
         end
@@ -487,7 +495,7 @@ describe 'Issues', feature: true do
         login_with guest
 
         visit namespace_project_issue_path(project.namespace, project, issue)
-        expect(page).to have_content issue.assignee.name
+        expect(page).to have_content issue.assignees.first.name
       end
     end
   end
@@ -558,7 +566,7 @@ describe 'Issues', feature: true do
       let(:user2) { create(:user) }
 
       before do
-        issue.assignee = user2
+        issue.assignees << user2
         issue.save
       end
     end
@@ -655,7 +663,7 @@ describe 'Issues', feature: true do
 
   describe 'due date' do
     context 'update due on issue#show', js: true do
-      let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+      let(:issue) { create(:issue, project: project, author: @user, assignees: [@user]) }
 
       before do
         visit namespace_project_issue_path(project.namespace, project, issue)
@@ -702,7 +710,7 @@ describe 'Issues', feature: true do
     include WaitForVueResource
 
     it 'updates the title', js: true do
-      issue = create(:issue, author: @user, assignee: @user, project: project, title: 'new title')
+      issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'new title')
 
       visit namespace_project_issue_path(project.namespace, project, issue)
 
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
index 43cc6f2a2a7d7ea3bebde280b94b629784e47189..ec49003772b82df02d7362b0120717cc07fe7edc 100644
--- a/spec/features/merge_requests/assign_issues_spec.rb
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -33,7 +33,7 @@ feature 'Merge request issue assignment', js: true, feature: true do
     end
 
     it "doesn't display if related issues are already assigned" do
-      [issue1, issue2].each { |issue| issue.update!(assignee: user) }
+      [issue1, issue2].each { |issue| issue.update!(assignees: [user]) }
 
       visit_merge_request
 
diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb
index 40b4dc6369734f7ae35d422dc369e75c2c9fdf73..227eb04ba7282828d58c9f00ac1b897c280abb32 100644
--- a/spec/features/milestones/show_spec.rb
+++ b/spec/features/milestones/show_spec.rb
@@ -5,7 +5,7 @@ describe 'Milestone show', feature: true do
   let(:project) { create(:empty_project) }
   let(:milestone) { create(:milestone, project: project) }
   let(:labels) { create_list(:label, 2, project: project) }
-  let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } }
+  let(:issue_params) { { project: project, assignees: [user], author: user, milestone: milestone, labels: labels } }
 
   before do
     project.add_user(user, :developer) 
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index d28a853bbc2fc8c9da3ace42ac4bfe8c9af7d3ed..fa5e30075e3050a22c64076023f29648b42d7c61 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -12,7 +12,7 @@ feature 'issuable templates', feature: true, js: true do
   context 'user creates an issue using templates' do
     let(:template_content) { 'this is a test "bug" template' }
     let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
-    let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+    let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
     let(:description_addition) { ' appending to description' }
 
     background do
@@ -72,7 +72,7 @@ feature 'issuable templates', feature: true, js: true do
   context 'user creates an issue using templates, with a prior description' do
     let(:prior_description) { 'test issue description' }
     let(:template_content) { 'this is a test "bug" template' }
-    let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+    let(:issue) { create(:issue, author: user, assignees: [user], project: project) }
 
     background do
       project.repository.create_file(
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index da6388dcdf20d92228262cb8e52e8459e2c4fc46..498a4a5cba04ce78b96660141a63b32771b7b53b 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -5,7 +5,7 @@ describe "Search", feature: true  do
 
   let(:user) { create(:user) }
   let(:project) { create(:empty_project, namespace: user.namespace) }
-  let!(:issue) { create(:issue, project: project, assignee: user) }
+  let!(:issue) { create(:issue, project: project, assignees: [user]) }
   let!(:issue2) { create(:issue, project: project, author: user) }
 
   before do
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index e2d9cfdd0b0374b13a77be1d708f163020944659..a23c4ca2b92f6d274d6e227813953fb212b8ab2b 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -6,7 +6,7 @@ describe 'Unsubscribe links', feature: true do
   let(:recipient) { create(:user) }
   let(:author) { create(:user) }
   let(:project) { create(:empty_project, :public) }
-  let(:params) { { title: 'A bug!', description: 'Fix it!', assignee: recipient } }
+  let(:params) { { title: 'A bug!', description: 'Fix it!', assignees: [recipient] } }
   let(:issue) { Issues::CreateService.new(project, author, params).execute }
 
   let(:mail) { ActionMailer::Base.deliveries.last }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index a5f717e62338981f1fddc3f92e6d2aca56ac6bd4..9615168935989bdaa6cf80ebb700580c65d5aca6 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -7,12 +7,12 @@ describe IssuesFinder do
   set(:project2) { create(:empty_project) }
   set(:milestone) { create(:milestone, project: project1) }
   set(:label) { create(:label, project: project2) }
-  set(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
-  set(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
-  set(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') }
+  set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab') }
+  set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') }
+  set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki') }
 
   describe '#execute' do
-    set(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
+    set(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
     set(:label_link) { create(:label_link, label: label, target: issue2) }
     let(:search_user) { user }
     let(:params) { {} }
@@ -91,7 +91,7 @@ describe IssuesFinder do
 
         before do
           milestones.each do |milestone|
-            create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+            create(:issue, project: milestone.project, milestone: milestone, author: user, assignees: [user])
           end
         end
 
@@ -126,7 +126,7 @@ describe IssuesFinder do
 
         before do
           milestones.each do |milestone|
-            create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+            create(:issue, project: milestone.project, milestone: milestone, author: user, assignees: [user])
           end
         end
 
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 21c078e0f44ec0e1de82e260f6412180c5ba9a76..ff86437fdd5d4564775bcf892398626db4ddf4e1 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -46,6 +46,24 @@
       "username": { "type": "string" },
       "avatar_url": { "type": "uri" }
     },
+    "assignees": {
+      "type": "array",
+      "items": {
+        "type": ["object", "null"],
+        "required": [
+          "id",
+          "name",
+          "username",
+          "avatar_url"
+        ],
+        "properties": {
+          "id": { "type": "integer" },
+          "name": { "type": "string" },
+          "username": { "type": "string" },
+          "avatar_url": { "type": "uri" }
+        }
+      }
+    },
     "subscribed": { "type": ["boolean", "null"] }
   },
   "additionalProperties": false
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index 52199e757342076ba8ab3cfbd12615fd21a7b9fb..2d1c84ee93d2130cbf8e689fb7cb564cd22f189c 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -33,6 +33,21 @@
         },
         "additionalProperties": false
       },
+      "assignees": {
+        "type": "array",
+        "items": {
+          "type": ["object", "null"],
+          "properties": {
+            "name": { "type": "string" },
+            "username": { "type": "string" },
+            "id": { "type": "integer" },
+            "state": { "type": "string" },
+            "avatar_url": { "type": "uri" },
+            "web_url": { "type": "uri" }
+          },
+          "additionalProperties": false
+        }
+      },
       "assignee": {
         "type": ["object", "null"],
         "properties": {
@@ -67,7 +82,7 @@
     "required": [
       "id", "iid", "project_id", "title", "description",
       "state", "created_at", "updated_at", "labels",
-      "milestone", "assignee", "author", "user_notes_count",
+      "milestone", "assignees", "author", "user_notes_count",
       "upvotes", "downvotes", "due_date", "confidential",
       "web_url"
     ],
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 93bb711f29ab3a117abecbfd61e1871cc6e3e5f3..c1ecb46aece8434bbae81fa4bf425a9c313f4ad8 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -4,6 +4,23 @@ describe IssuablesHelper do
   let(:label)  { build_stubbed(:label) }
   let(:label2) { build_stubbed(:label) }
 
+  describe '#users_dropdown_label' do
+    let(:user)  { build_stubbed(:user) }
+    let(:user2)  { build_stubbed(:user) }
+
+    it 'returns unassigned' do
+      expect(users_dropdown_label([])).to eq('Unassigned')
+    end
+
+    it 'returns selected user\'s name' do
+      expect(users_dropdown_label([user])).to eq(user.name)
+    end
+
+    it 'returns selected user\'s name and counter' do
+      expect(users_dropdown_label([user, user2])).to eq("#{user.name} + 1 more")
+    end
+  end
+
   describe '#issuable_labels_tooltip' do
     it 'returns label text' do
       expect(issuable_labels_tooltip([label])).to eq(label.title)
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index de072e7e470bf15e6af4f321d7f03b28972a8068..376e706d1db82b11a97f35f087e26d5f009dab9c 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -1,12 +1,12 @@
 /* global List */
-/* global ListUser */
+/* global ListAssignee */
 /* global ListLabel */
 /* global listObj */
 /* global boardsMockInterceptor */
 /* global BoardService */
 
 import Vue from 'vue';
-import '~/boards/models/user';
+import '~/boards/models/assignee';
 
 require('~/boards/models/list');
 require('~/boards/models/label');
@@ -133,12 +133,12 @@ describe('Issue card', () => {
     });
 
     it('does not set detail issue if img is clicked', (done) => {
-      vm.issue.assignee = new ListUser({
+      vm.issue.assignees = [new ListAssignee({
         id: 1,
         name: 'testing 123',
         username: 'test',
         avatar: 'test_image',
-      });
+      })];
 
       Vue.nextTick(() => {
         triggerEvent('mouseup', vm.$el.querySelector('img'));
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 3f598887603affc966004be4adcee5df092d563b..a89be911667004576a623ca0989de2a469db7016 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -35,6 +35,7 @@ describe('Board list component', () => {
       iid: 1,
       confidential: false,
       labels: [],
+      assignees: [],
     });
     list.issuesSize = 1;
     list.issues.push(issue);
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index b55ff2f473a23c2121c1e6df4b5a46f18051174b..5ea160b7790c4db677ced2dffcb763f82fc77f01 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -8,14 +8,14 @@
 import Vue from 'vue';
 import Cookies from 'js-cookie';
 
-require('~/lib/utils/url_utility');
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/services/board_service');
-require('~/boards/stores/boards_store');
-require('./mock_data');
+import '~/lib/utils/url_utility';
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/services/board_service';
+import '~/boards/stores/boards_store';
+import './mock_data';
 
 describe('Store', () => {
   beforeEach(() => {
@@ -212,7 +212,8 @@ describe('Store', () => {
         title: 'Testing',
         iid: 2,
         confidential: false,
-        labels: []
+        labels: [],
+        assignees: [],
       });
       const list = gl.issueBoards.BoardsStore.addList(listObj);
 
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index ef567635d48c19c693a8cf7992e40e8dd1e55549..fddde799d01acfbf90fff0be7c4cbb8dbc92f1e6 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -1,20 +1,20 @@
-/* global ListUser */
+/* global ListAssignee */
 /* global ListLabel */
 /* global listObj */
 /* global ListIssue */
 
 import Vue from 'vue';
 
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/stores/boards_store');
-require('~/boards/components/issue_card_inner');
-require('./mock_data');
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/stores/boards_store';
+import '~/boards/components/issue_card_inner';
+import './mock_data';
 
 describe('Issue card component', () => {
-  const user = new ListUser({
+  const user = new ListAssignee({
     id: 1,
     name: 'testing 123',
     username: 'test',
@@ -40,6 +40,7 @@ describe('Issue card component', () => {
       iid: 1,
       confidential: false,
       labels: [list.label],
+      assignees: [],
     });
 
     component = new Vue({
@@ -92,12 +93,12 @@ describe('Issue card component', () => {
   it('renders confidential icon', (done) => {
     component.issue.confidential = true;
 
-    setTimeout(() => {
+    Vue.nextTick(() => {
       expect(
         component.$el.querySelector('.confidential-icon'),
       ).not.toBeNull();
       done();
-    }, 0);
+    });
   });
 
   it('renders issue ID with #', () => {
@@ -109,34 +110,32 @@ describe('Issue card component', () => {
   describe('assignee', () => {
     it('does not render assignee', () => {
       expect(
-        component.$el.querySelector('.card-assignee'),
+        component.$el.querySelector('.card-assignee .avatar'),
       ).toBeNull();
     });
 
     describe('exists', () => {
       beforeEach((done) => {
-        component.issue.assignee = user;
+        component.issue.assignees = [user];
 
-        setTimeout(() => {
-          done();
-        }, 0);
+        Vue.nextTick(() => done());
       });
 
       it('renders assignee', () => {
         expect(
-          component.$el.querySelector('.card-assignee'),
+          component.$el.querySelector('.card-assignee .avatar'),
         ).not.toBeNull();
       });
 
       it('sets title', () => {
         expect(
-          component.$el.querySelector('.card-assignee').getAttribute('title'),
+          component.$el.querySelector('.card-assignee a').getAttribute('title'),
         ).toContain(`Assigned to ${user.name}`);
       });
 
       it('sets users path', () => {
         expect(
-          component.$el.querySelector('.card-assignee').getAttribute('href'),
+          component.$el.querySelector('.card-assignee a').getAttribute('href'),
         ).toBe('/test');
       });
 
@@ -149,11 +148,11 @@ describe('Issue card component', () => {
 
     describe('assignee default avatar', () => {
       beforeEach((done) => {
-        component.issue.assignee = new ListUser({
+        component.issue.assignees = [new ListAssignee({
           id: 1,
           name: 'testing 123',
           username: 'test',
-        }, 'default_avatar');
+        }, 'default_avatar')];
 
         Vue.nextTick(done);
       });
@@ -169,6 +168,75 @@ describe('Issue card component', () => {
     });
   });
 
+  describe('multiple assignees', () => {
+    beforeEach((done) => {
+      component.issue.assignees = [
+        user,
+        new ListAssignee({
+          id: 2,
+          name: 'user2',
+          username: 'user2',
+          avatar: 'test_image',
+        }),
+        new ListAssignee({
+          id: 3,
+          name: 'user3',
+          username: 'user3',
+          avatar: 'test_image',
+        }),
+        new ListAssignee({
+          id: 4,
+          name: 'user4',
+          username: 'user4',
+          avatar: 'test_image',
+        })];
+
+      Vue.nextTick(() => done());
+    });
+
+    it('renders all four assignees', () => {
+      expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(4);
+    });
+
+    describe('more than four assignees', () => {
+      beforeEach((done) => {
+        component.issue.assignees.push(new ListAssignee({
+          id: 5,
+          name: 'user5',
+          username: 'user5',
+          avatar: 'test_image',
+        }));
+
+        Vue.nextTick(() => done());
+      });
+
+      it('renders more avatar counter', () => {
+        expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('+2');
+      });
+
+      it('renders three assignees', () => {
+        expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(3);
+      });
+
+      it('renders 99+ avatar counter', (done) => {
+        for (let i = 5; i < 104; i += 1) {
+          const u = new ListAssignee({
+            id: i,
+            name: 'name',
+            username: 'username',
+            avatar: 'test_image',
+          });
+          component.issue.assignees.push(u);
+        }
+
+        Vue.nextTick(() => {
+          expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('99+');
+          done();
+        });
+      });
+    });
+  });
+
   describe('labels', () => {
     it('does not render any', () => {
       expect(
@@ -180,9 +248,7 @@ describe('Issue card component', () => {
       beforeEach((done) => {
         component.issue.addLabel(label1);
 
-        setTimeout(() => {
-          done();
-        }, 0);
+        Vue.nextTick(() => done());
       });
 
       it('does not render list label', () => {
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index c96dfe94a4a5d35b1338c35809fcab87236d92f7..cd1497bc5e61580648803d3d7accc7018c30eca3 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -2,14 +2,15 @@
 /* global BoardService */
 /* global ListIssue */
 
-require('~/lib/utils/url_utility');
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/services/board_service');
-require('~/boards/stores/boards_store');
-require('./mock_data');
+import Vue from 'vue';
+import '~/lib/utils/url_utility';
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/services/board_service';
+import '~/boards/stores/boards_store';
+import './mock_data';
 
 describe('Issue model', () => {
   let issue;
@@ -27,7 +28,13 @@ describe('Issue model', () => {
         title: 'test',
         color: 'red',
         description: 'testing'
-      }]
+      }],
+      assignees: [{
+        id: 1,
+        name: 'name',
+        username: 'username',
+        avatar_url: 'http://avatar_url',
+      }],
     });
   });
 
@@ -80,6 +87,33 @@ describe('Issue model', () => {
     expect(issue.labels.length).toBe(0);
   });
 
+  it('adds assignee', () => {
+    issue.addAssignee({
+      id: 2,
+      name: 'Bruce Wayne',
+      username: 'batman',
+      avatar_url: 'http://batman',
+    });
+
+    expect(issue.assignees.length).toBe(2);
+  });
+
+  it('finds assignee', () => {
+    const assignee = issue.findAssignee(issue.assignees[0]);
+    expect(assignee).toBeDefined();
+  });
+
+  it('removes assignee', () => {
+    const assignee = issue.findAssignee(issue.assignees[0]);
+    issue.removeAssignee(assignee);
+    expect(issue.assignees.length).toBe(0);
+  });
+
+  it('removes all assignees', () => {
+    issue.removeAllAssignees();
+    expect(issue.assignees.length).toBe(0);
+  });
+
   it('sets position to infinity if no position is stored', () => {
     expect(issue.position).toBe(Infinity);
   });
@@ -90,9 +124,31 @@ describe('Issue model', () => {
       iid: 1,
       confidential: false,
       relative_position: 1,
-      labels: []
+      labels: [],
+      assignees: [],
     });
 
     expect(relativePositionIssue.position).toBe(1);
   });
+
+  describe('update', () => {
+    it('passes assignee ids when there are assignees', (done) => {
+      spyOn(Vue.http, 'patch').and.callFake((url, data) => {
+        expect(data.issue.assignee_ids).toEqual([1]);
+        done();
+      });
+
+      issue.update('url');
+    });
+
+    it('passes assignee ids of [0] when there are no assignees', (done) => {
+      spyOn(Vue.http, 'patch').and.callFake((url, data) => {
+        expect(data.issue.assignee_ids).toEqual([0]);
+        done();
+      });
+
+      issue.removeAllAssignees();
+      issue.update('url');
+    });
+  });
 });
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 24a2da9f6b629f24b647c290cf201ea1bf257b06..8e3d9fd77a073af592cc776389b5f830750ced50 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -8,14 +8,14 @@
 
 import Vue from 'vue';
 
-require('~/lib/utils/url_utility');
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/services/board_service');
-require('~/boards/stores/boards_store');
-require('./mock_data');
+import '~/lib/utils/url_utility';
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/services/board_service';
+import '~/boards/stores/boards_store';
+import './mock_data';
 
 describe('List model', () => {
   let list;
@@ -94,7 +94,8 @@ describe('List model', () => {
       title: 'Testing',
       iid: _.random(10000),
       confidential: false,
-      labels: [list.label, listDup.label]
+      labels: [list.label, listDup.label],
+      assignees: [],
     });
 
     list.issues.push(issue);
@@ -119,7 +120,8 @@ describe('List model', () => {
           title: 'Testing',
           iid: _.random(10000) + i,
           confidential: false,
-          labels: [list.label]
+          labels: [list.label],
+          assignees: [],
         }));
       }
       list.issuesSize = 50;
@@ -137,7 +139,8 @@ describe('List model', () => {
         title: 'Testing',
         iid: _.random(10000),
         confidential: false,
-        labels: [list.label]
+        labels: [list.label],
+        assignees: [],
       }));
       list.issuesSize = 2;
 
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index a4fa694eebec24942e438b61d87d30def895b6fd..a64c3964ee3e356a54c28d7ca78b4c5f1b835f97 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -33,7 +33,8 @@ const BoardsMockData = {
         title: 'Testing',
         iid: 1,
         confidential: false,
-        labels: []
+        labels: [],
+        assignees: [],
       }],
       size: 1
     }
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
index 80db816aff85250246adef5078319670094ccb22..32e6d04df9fed2c73a3e4d9355aad4f7714f1c5b 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -1,10 +1,10 @@
 /* global ListIssue */
 
-require('~/boards/models/issue');
-require('~/boards/models/label');
-require('~/boards/models/list');
-require('~/boards/models/user');
-require('~/boards/stores/modal_store');
+import '~/boards/models/issue';
+import '~/boards/models/label';
+import '~/boards/models/list';
+import '~/boards/models/assignee';
+import '~/boards/stores/modal_store';
 
 describe('Modal store', () => {
   let issue;
@@ -21,12 +21,14 @@ describe('Modal store', () => {
       iid: 1,
       confidential: false,
       labels: [],
+      assignees: [],
     });
     issue2 = new ListIssue({
       title: 'Testing',
       iid: 2,
       confidential: false,
       labels: [],
+      assignees: [],
     });
     Store.store.issues.push(issue);
     Store.store.issues.push(issue2);
diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js
index 0a830f25e29edd0857866533458814a341b523b2..8ff93c4f918d2e7fc286bbb3c847802f4eb85f18 100644
--- a/spec/javascripts/issuable_time_tracker_spec.js
+++ b/spec/javascripts/issuable_time_tracker_spec.js
@@ -2,7 +2,7 @@
 
 import Vue from 'vue';
 
-require('~/issuable/time_tracking/components/time_tracker');
+import timeTracker from '~/sidebar/components/time_tracking/time_tracker';
 
 function initTimeTrackingComponent(opts) {
   setFixtures(`
@@ -16,187 +16,185 @@ function initTimeTrackingComponent(opts) {
     time_spent: opts.timeSpent,
     human_time_estimate: opts.timeEstimateHumanReadable,
     human_time_spent: opts.timeSpentHumanReadable,
-    docsUrl: '/help/workflow/time_tracking.md',
+    rootPath: '/',
   };
 
-  const TimeTrackingComponent = Vue.component('issuable-time-tracker');
+  const TimeTrackingComponent = Vue.extend(timeTracker);
   this.timeTracker = new TimeTrackingComponent({
     el: '#mock-container',
     propsData: this.initialData,
   });
 }
 
-((gl) => {
-  describe('Issuable Time Tracker', function() {
-    describe('Initialization', function() {
-      beforeEach(function() {
-        initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
-      });
+describe('Issuable Time Tracker', function() {
+  describe('Initialization', function() {
+    beforeEach(function() {
+      initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+    });
 
-      it('should return something defined', function() {
-        expect(this.timeTracker).toBeDefined();
-      });
+    it('should return something defined', function() {
+      expect(this.timeTracker).toBeDefined();
+    });
 
-      it ('should correctly set timeEstimate', function(done) {
-        Vue.nextTick(() => {
-          expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
-          done();
-        });
+    it ('should correctly set timeEstimate', function(done) {
+      Vue.nextTick(() => {
+        expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
+        done();
       });
-      it ('should correctly set time_spent', function(done) {
-        Vue.nextTick(() => {
-          expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
-          done();
-        });
+    });
+    it ('should correctly set time_spent', function(done) {
+      Vue.nextTick(() => {
+        expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
+        done();
       });
     });
+  });
 
-    describe('Content Display', function() {
-      describe('Panes', function() {
-        describe('Comparison pane', function() {
-          beforeEach(function() {
-            initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+  describe('Content Display', function() {
+    describe('Panes', function() {
+      describe('Comparison pane', function() {
+        beforeEach(function() {
+          initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+        });
+
+        it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
+          Vue.nextTick(() => {
+            const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
+            expect(this.timeTracker.showComparisonState).toBe(true);
+            done();
           });
+        });
 
-          it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
+        describe('Remaining meter', function() {
+          it('should display the remaining meter with the correct width', function(done) {
             Vue.nextTick(() => {
-              const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
-              expect(this.timeTracker.showComparisonState).toBe(true);
+              const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
+              const correctWidth = '5%';
+
+              expect(meterWidth).toBe(correctWidth);
               done();
-            });
+            })
           });
 
-          describe('Remaining meter', function() {
-            it('should display the remaining meter with the correct width', function(done) {
-              Vue.nextTick(() => {
-                const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
-                const correctWidth = '5%';
-
-                expect(meterWidth).toBe(correctWidth);
-                done();
-              })
-            });
-
-            it('should display the remaining meter with the correct background color when within estimate', function(done) {
-              Vue.nextTick(() => {
-                const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
-                expect(styledMeter.length).toBe(1);
-                done()
-              });
+          it('should display the remaining meter with the correct background color when within estimate', function(done) {
+            Vue.nextTick(() => {
+              const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
+              expect(styledMeter.length).toBe(1);
+              done()
             });
+          });
 
-            it('should display the remaining meter with the correct background color when over estimate', function(done) {
-              this.timeTracker.time_estimate = 100000;
-              this.timeTracker.time_spent = 20000000;
-              Vue.nextTick(() => {
-                const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
-                expect(styledMeter.length).toBe(1);
-                done();
-              });
+          it('should display the remaining meter with the correct background color when over estimate', function(done) {
+            this.timeTracker.time_estimate = 100000;
+            this.timeTracker.time_spent = 20000000;
+            Vue.nextTick(() => {
+              const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
+              expect(styledMeter.length).toBe(1);
+              done();
             });
           });
         });
+      });
 
-        describe("Estimate only pane", function() {
-          beforeEach(function() {
-            initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
-          });
+      describe("Estimate only pane", function() {
+        beforeEach(function() {
+          initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
+        });
 
-          it('should display the human readable version of time estimated', function(done) {
-            Vue.nextTick(() => {
-              const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
-              const correctText = 'Estimated: 2h 46m';
+        it('should display the human readable version of time estimated', function(done) {
+          Vue.nextTick(() => {
+            const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
+            const correctText = 'Estimated: 2h 46m';
 
-              expect(estimateText).toBe(correctText);
-              done();
-            });
+            expect(estimateText).toBe(correctText);
+            done();
           });
         });
+      });
 
-        describe('Spent only pane', function() {
-          beforeEach(function() {
-            initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
-          });
+      describe('Spent only pane', function() {
+        beforeEach(function() {
+          initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+        });
 
-          it('should display the human readable version of time spent', function(done) {
-            Vue.nextTick(() => {
-              const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
-              const correctText = 'Spent: 1h 23m';
+        it('should display the human readable version of time spent', function(done) {
+          Vue.nextTick(() => {
+            const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
+            const correctText = 'Spent: 1h 23m';
 
-              expect(spentText).toBe(correctText);
-              done();
-            });
+            expect(spentText).toBe(correctText);
+            done();
           });
         });
+      });
 
-        describe('No time tracking pane', function() {
-          beforeEach(function() {
-            initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 });
-          });
+      describe('No time tracking pane', function() {
+        beforeEach(function() {
+          initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+        });
 
-          it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
-            Vue.nextTick(() => {
-              const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
-              const noTrackingText =$noTrackingPane.innerText;
-              const correctText = 'No estimate or time spent';
+        it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
+          Vue.nextTick(() => {
+            const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
+            const noTrackingText =$noTrackingPane.innerText;
+            const correctText = 'No estimate or time spent';
 
-              expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
-              expect($noTrackingPane).toBeVisible();
-              expect(noTrackingText).toBe(correctText);
-              done();
-            });
+            expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
+            expect($noTrackingPane).toBeVisible();
+            expect(noTrackingText).toBe(correctText);
+            done();
           });
         });
+      });
 
-        describe("Help pane", function() {
-          beforeEach(function() {
-            initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
-          });
+      describe("Help pane", function() {
+        beforeEach(function() {
+          initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
+        });
 
-          it('should not show the "Help" pane by default', function(done) {
-            Vue.nextTick(() => {
-              const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+        it('should not show the "Help" pane by default', function(done) {
+          Vue.nextTick(() => {
+            const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
 
-              expect(this.timeTracker.showHelpState).toBe(false);
-              expect($helpPane).toBeNull();
-              done();
-            });
+            expect(this.timeTracker.showHelpState).toBe(false);
+            expect($helpPane).toBeNull();
+            done();
           });
+        });
 
-          it('should show the "Help" pane when help button is clicked', function(done) {
-            Vue.nextTick(() => {
-              $(this.timeTracker.$el).find('.help-button').click();
+        it('should show the "Help" pane when help button is clicked', function(done) {
+          Vue.nextTick(() => {
+            $(this.timeTracker.$el).find('.help-button').click();
 
-              setTimeout(() => {
-                const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
-                expect(this.timeTracker.showHelpState).toBe(true);
-                expect($helpPane).toBeVisible();
-                done();
-              }, 10);
-            });
+            setTimeout(() => {
+              const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+              expect(this.timeTracker.showHelpState).toBe(true);
+              expect($helpPane).toBeVisible();
+              done();
+            }, 10);
           });
+        });
 
-          it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
-            Vue.nextTick(() => {
-              $(this.timeTracker.$el).find('.help-button').click();
+        it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
+          Vue.nextTick(() => {
+            $(this.timeTracker.$el).find('.help-button').click();
 
-              setTimeout(() => {
+            setTimeout(() => {
 
-                $(this.timeTracker.$el).find('.close-help-button').click();
+              $(this.timeTracker.$el).find('.close-help-button').click();
 
-                setTimeout(() => {
-                  const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+              setTimeout(() => {
+                const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
 
-                  expect(this.timeTracker.showHelpState).toBe(false);
-                  expect($helpPane).toBeNull();
+                expect(this.timeTracker.showHelpState).toBe(false);
+                expect($helpPane).toBeNull();
 
-                  done();
-                }, 1000);
+                done();
               }, 1000);
-            });
+            }, 1000);
           });
         });
       });
     });
   });
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/subbable_resource_spec.js b/spec/javascripts/subbable_resource_spec.js
deleted file mode 100644
index 454386697f54ff23e374610784a12b43e5ebc2af..0000000000000000000000000000000000000000
--- a/spec/javascripts/subbable_resource_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* eslint-disable max-len, arrow-parens, comma-dangle */
-
-require('~/subbable_resource');
-
-/*
-* Test that each rest verb calls the publish and subscribe function and passes the correct value back
-*
-*
-* */
-((global) => {
-  describe('Subbable Resource', function () {
-    describe('PubSub', function () {
-      beforeEach(function () {
-        this.MockResource = new global.SubbableResource('https://example.com');
-      });
-      it('should successfully add a single subscriber', function () {
-        const callback = () => {};
-        this.MockResource.subscribe(callback);
-
-        expect(this.MockResource.subscribers.length).toBe(1);
-        expect(this.MockResource.subscribers[0]).toBe(callback);
-      });
-
-      it('should successfully add multiple subscribers', function () {
-        const callbackOne = () => {};
-        const callbackTwo = () => {};
-        const callbackThree = () => {};
-
-        this.MockResource.subscribe(callbackOne);
-        this.MockResource.subscribe(callbackTwo);
-        this.MockResource.subscribe(callbackThree);
-
-        expect(this.MockResource.subscribers.length).toBe(3);
-      });
-
-      it('should successfully publish an update to a single subscriber', function () {
-        const state = { myprop: 1 };
-
-        const callbacks = {
-          one: (data) => expect(data.myprop).toBe(2),
-          two: (data) => expect(data.myprop).toBe(2),
-          three: (data) => expect(data.myprop).toBe(2)
-        };
-
-        const spyOne = spyOn(callbacks, 'one');
-        const spyTwo = spyOn(callbacks, 'two');
-        const spyThree = spyOn(callbacks, 'three');
-
-        this.MockResource.subscribe(callbacks.one);
-        this.MockResource.subscribe(callbacks.two);
-        this.MockResource.subscribe(callbacks.three);
-
-        state.myprop += 1;
-
-        this.MockResource.publish(state);
-
-        expect(spyOne).toHaveBeenCalled();
-        expect(spyTwo).toHaveBeenCalled();
-        expect(spyThree).toHaveBeenCalled();
-      });
-    });
-  });
-})(window.gl || (window.gl = {}));
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index 8a6fe1ad6a39d646c56fc9ab633782f8c8251900..7c4a0f32c7b738341ccfa8b838683a5428b4d414 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -113,7 +113,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
       it 'allows references for assignee' do
         assignee = create(:user)
         project = create(:empty_project, :public)
-        issue = create(:issue, :confidential, project: project, assignee: assignee)
+        issue = create(:issue, :confidential, project: project, assignees: [assignee])
 
         link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
         doc = filter(link, current_user: assignee)
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index f34d09f2c1d9a6782862bf5887c3c0c88d59f695..a4089592cf2152b97e66d94d3e919fe1484cc7c7 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -43,7 +43,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
           description: "*Created by: octocat*\n\nI'm having a problem with this.",
           state: 'opened',
           author_id: project.creator_id,
-          assignee_id: nil,
+          assignee_ids: [],
           created_at: created_at,
           updated_at: updated_at
         }
@@ -64,7 +64,7 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
           description: "*Created by: octocat*\n\nI'm having a problem with this.",
           state: 'closed',
           author_id: project.creator_id,
-          assignee_id: nil,
+          assignee_ids: [],
           created_at: created_at,
           updated_at: updated_at
         }
@@ -77,19 +77,19 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
       let(:raw_data) { double(base_data.merge(assignee: octocat)) }
 
       it 'returns nil as assignee_id when is not a GitLab user' do
-        expect(issue.attributes.fetch(:assignee_id)).to be_nil
+        expect(issue.attributes.fetch(:assignee_ids)).to be_empty
       end
 
       it 'returns GitLab user id associated with GitHub id as assignee_id' do
         gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
 
-        expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+        expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
       end
 
       it 'returns GitLab user id associated with GitHub email as assignee_id' do
         gl_user = create(:user, email: octocat.email)
 
-        expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+        expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
       end
     end
 
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index ccaa88a5c798f8b55e511cc1c2179afff83f3742..622a0f513f43ac18a98c815cd3ecbb7f4c222f62 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -49,7 +49,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
       expect(issue).not_to be_nil
       expect(issue.iid).to eq(169)
       expect(issue.author).to eq(project.creator)
-      expect(issue.assignee).to eq(mapped_user)
+      expect(issue.assignees).to eq([mapped_user])
       expect(issue.state).to eq("closed")
       expect(issue.label_names).to include("Priority: Medium")
       expect(issue.label_names).to include("Status: Fixed")
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 0abf89d060cec93df2f47872fea1ffbd6e56c523..cd2fa27bb9f888f7ba0a6971ffa472bb6dc91f82 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -3,7 +3,7 @@ issues:
 - subscriptions
 - award_emoji
 - author
-- assignee
+- assignees
 - updated_by
 - milestone
 - notes
@@ -16,6 +16,7 @@ issues:
 - merge_requests_closing_issues
 - metrics
 - timelogs
+- issue_assignees
 events:
 - author
 - project
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 1035428b2e7d38017890b1a2cf253c4692010d96..5aeb29b7fecb72676fa1d4128e38b2208f4bba29 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -203,7 +203,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
   end
 
   def setup_project
-    issue = create(:issue, assignee: user)
+    issue = create(:issue, assignees: [user])
     snippet = create(:project_snippet)
     release = create(:release)
     group = create(:group)
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index e0ebea63eb40e703c94f3c862d1b447777dca6e6..a7c8e7f1f5708840848b64b343470e30469ff912 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -89,7 +89,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
     let(:project) { create(:empty_project, :internal) }
     let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
     let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
-    let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+    let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
 
     it 'does not list project confidential issues for non project members' do
       results = described_class.new(non_member, project, query)
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 847fb977400157c5118c43d70277dee64a580425..31c3cd4d53c8e09b2e2eae8c8473a0c7256bcb0f 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -72,9 +72,9 @@ describe Gitlab::SearchResults do
     let(:admin) { create(:admin) }
     let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') }
     let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) }
-    let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) }
+    let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignees: [assignee]) }
     let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) }
-    let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
+    let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignees: [assignee]) }
     let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
 
     it 'does not list confidential issues for non project members' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 9f12e40d808e5b971542b09ccccf7fde30b05c53..1e6260270fe31c16576d6f4befed47d964c66972 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -36,11 +36,11 @@ describe Notify do
       end
 
       context 'for issues' do
-        let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) }
-        let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: 'My awesome description') }
+        let(:issue) { create(:issue, author: current_user, assignees: [assignee], project: project) }
+        let(:issue_with_description) { create(:issue, author: current_user, assignees: [assignee], project: project, description: 'My awesome description') }
 
         describe 'that are new' do
-          subject { described_class.new_issue_email(issue.assignee_id, issue.id) }
+          subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
 
           it_behaves_like 'an assignee email'
           it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
@@ -69,7 +69,7 @@ describe Notify do
         end
 
         describe 'that are new with a description' do
-          subject { described_class.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) }
+          subject { described_class.new_issue_email(issue_with_description.assignees.first.id, issue_with_description.id) }
 
           it_behaves_like 'it should show Gmail Actions View Issue link'
 
@@ -79,7 +79,7 @@ describe Notify do
         end
 
         describe 'that have been reassigned' do
-          subject { described_class.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
+          subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
 
           it_behaves_like 'a multiple recipients email'
           it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 3ecba2e96870bb14ab47656a7949b5ee606e3ffa..27890e33b49d82bae9b8b9389c57a68674303158 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -10,7 +10,6 @@ describe Issuable do
 
     it { is_expected.to belong_to(:project) }
     it { is_expected.to belong_to(:author) }
-    it { is_expected.to belong_to(:assignee) }
     it { is_expected.to have_many(:notes).dependent(:destroy) }
     it { is_expected.to have_many(:todos).dependent(:destroy) }
 
@@ -66,60 +65,6 @@ describe Issuable do
     end
   end
 
-  describe 'assignee_name' do
-    it 'is delegated to assignee' do
-      issue.update!(assignee: create(:user))
-
-      expect(issue.assignee_name).to eq issue.assignee.name
-    end
-
-    it 'returns nil when assignee is nil' do
-      issue.assignee_id = nil
-      issue.save(validate: false)
-
-      expect(issue.assignee_name).to eq nil
-    end
-  end
-
-  describe "before_save" do
-    describe "#update_cache_counts" do
-      context "when previous assignee exists" do
-        before do
-          assignee = create(:user)
-          issue.project.team << [assignee, :developer]
-          issue.update(assignee: assignee)
-        end
-
-        it "updates cache counts for new assignee" do
-          user = create(:user)
-
-          expect(user).to receive(:update_cache_counts)
-
-          issue.update(assignee: user)
-        end
-
-        it "updates cache counts for previous assignee" do
-          old_assignee = issue.assignee
-          allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
-
-          expect(old_assignee).to receive(:update_cache_counts)
-
-          issue.update(assignee: nil)
-        end
-      end
-
-      context "when previous assignee does not exist" do
-        before{ issue.update(assignee: nil) }
-
-        it "updates cache count for the new assignee" do
-          expect_any_instance_of(User).to receive(:update_cache_counts)
-
-          issue.update(assignee: user)
-        end
-      end
-    end
-  end
-
   describe ".search" do
     let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
 
@@ -307,7 +252,20 @@ describe Issuable do
     end
 
     context "issue is assigned" do
-      before { issue.update_attribute(:assignee, user) }
+      before { issue.assignees << user }
+
+      it "returns correct hook data" do
+        expect(data[:assignees].first).to eq(user.hook_attrs)
+      end
+    end
+
+    context "merge_request is assigned" do
+      let(:merge_request) { create(:merge_request) }
+      let(:data) { merge_request.to_hook_data(user) }
+
+      before do
+        merge_request.update_attribute(:assignee, user)
+      end
 
       it "returns correct hook data" do
         expect(data[:object_attributes]['assignee_id']).to eq(user.id)
@@ -329,24 +287,6 @@ describe Issuable do
     include_examples 'deprecated repository hook data'
   end
 
-  describe '#card_attributes' do
-    it 'includes the author name' do
-      allow(issue).to receive(:author).and_return(double(name: 'Robert'))
-      allow(issue).to receive(:assignee).and_return(nil)
-
-      expect(issue.card_attributes).
-        to eq({ 'Author' => 'Robert', 'Assignee' => nil })
-    end
-
-    it 'includes the assignee name' do
-      allow(issue).to receive(:author).and_return(double(name: 'Robert'))
-      allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
-
-      expect(issue.card_attributes).
-        to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
-    end
-  end
-
   describe '#labels_array' do
     let(:project) { create(:empty_project) }
     let(:bug) { create(:label, project: project, title: 'bug') }
@@ -475,27 +415,6 @@ describe Issuable do
     end
   end
 
-  describe '#assignee_or_author?' do
-    let(:user) { build(:user, id: 1) }
-    let(:issue) { build(:issue) }
-
-    it 'returns true for a user that is assigned to an issue' do
-      issue.assignee = user
-
-      expect(issue.assignee_or_author?(user)).to eq(true)
-    end
-
-    it 'returns true for a user that is the author of an issue' do
-      issue.author = user
-
-      expect(issue.assignee_or_author?(user)).to eq(true)
-    end
-
-    it 'returns false for a user that is not the assignee or author' do
-      expect(issue.assignee_or_author?(user)).to eq(false)
-    end
-  end
-
   describe '#spend_time' do
     let(:user) { create(:user) }
     let(:issue) { create(:issue) }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 68e4c0a522bc891df6c958c6fed49260ee0e70a1..675b730c5575ab80a4a8aa2c4a9d2fe8d9325a85 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -11,13 +11,13 @@ describe Milestone, 'Milestoneish' do
   let(:milestone) { create(:milestone, project: project) }
   let!(:issue) { create(:issue, project: project, milestone: milestone) }
   let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
-  let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) }
+  let!(:security_issue_2) { create(:issue, :confidential, project: project, assignees: [assignee], milestone: milestone) }
   let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
   let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
   let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
-  let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+  let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignees: [assignee], milestone: milestone) }
   let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
-  let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+  let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignees: [assignee], milestone: milestone) }
   let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
 
   before do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index a9c5b604268cdfe1dfbe7dd24fd661c57d68a50e..b8cb967c4cc16098d51b6beab2146548fb0180b4 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -118,8 +118,8 @@ describe Event, models: true do
     let(:author) { create(:author) }
     let(:assignee) { create(:user) }
     let(:admin) { create(:admin) }
-    let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
-    let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+    let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) }
+    let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
     let(:note_on_commit) { create(:note_on_commit, project: project) }
     let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
     let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index d8aed25c041f5a97d63bbdbf8dff83fdf9d02655..93c2c538e1056053c74009a12bce5fda0246d0e4 100644
--- a/spec/models/issue_collection_spec.rb
+++ b/spec/models/issue_collection_spec.rb
@@ -28,7 +28,7 @@ describe IssueCollection do
       end
 
       it 'returns the issues the user is assigned to' do
-        issue1.assignee = user
+        issue1.assignees << user
 
         expect(collection.updatable_by_user(user)).to eq([issue1])
       end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 8748b98a4e357f200c030de19999f1c7c8a831da..725f5c2311fd37212fbc5fe274e2dac855950df7 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
 describe Issue, models: true do
   describe "Associations" do
     it { is_expected.to belong_to(:milestone) }
+    it { is_expected.to have_many(:assignees) }
   end
 
   describe 'modules' do
@@ -37,6 +38,64 @@ describe Issue, models: true do
     end
   end
 
+  describe "before_save" do
+    describe "#update_cache_counts when an issue is reassigned" do
+      let(:issue) { create(:issue) }
+      let(:assignee) { create(:user) }
+
+      context "when previous assignee exists" do
+        before do
+          issue.project.team << [assignee, :developer]
+          issue.assignees << assignee
+        end
+
+        it "updates cache counts for new assignee" do
+          user = create(:user)
+
+          expect(user).to receive(:update_cache_counts)
+
+          issue.assignees << user
+        end
+
+        it "updates cache counts for previous assignee" do
+          issue.assignees.first
+
+          expect_any_instance_of(User).to receive(:update_cache_counts)
+
+          issue.assignees.destroy_all
+        end
+      end
+
+      context "when previous assignee does not exist" do
+        it "updates cache count for the new assignee" do
+          issue.assignees = []
+
+          expect_any_instance_of(User).to receive(:update_cache_counts)
+
+          issue.assignees << assignee
+        end
+      end
+    end
+  end
+
+  describe '#card_attributes' do
+    it 'includes the author name' do
+      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+      allow(subject).to receive(:assignees).and_return([])
+
+      expect(subject.card_attributes).
+        to eq({ 'Author' => 'Robert', 'Assignee' => '' })
+    end
+
+    it 'includes the assignee name' do
+      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+      allow(subject).to receive(:assignees).and_return([double(name: 'Douwe')])
+
+      expect(subject.card_attributes).
+        to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+    end
+  end
+
   describe '#closed_at' do
     after do
       Timecop.return
@@ -124,13 +183,24 @@ describe Issue, models: true do
     end
   end
 
-  describe '#is_being_reassigned?' do
-    it 'returns true if the issue assignee has changed' do
-      subject.assignee = create(:user)
-      expect(subject.is_being_reassigned?).to be_truthy
+  describe '#assignee_or_author?' do
+    let(:user) { create(:user) }
+    let(:issue) { create(:issue) }
+
+    it 'returns true for a user that is assigned to an issue' do
+      issue.assignees << user
+
+      expect(issue.assignee_or_author?(user)).to be_truthy
     end
-    it 'returns false if the issue assignee has not changed' do
-      expect(subject.is_being_reassigned?).to be_falsey
+
+    it 'returns true for a user that is the author of an issue' do
+      issue.update(author: user)
+
+      expect(issue.assignee_or_author?(user)).to be_truthy
+    end
+
+    it 'returns false for a user that is not the assignee or author' do
+      expect(issue.assignee_or_author?(user)).to be_falsey
     end
   end
 
@@ -383,14 +453,14 @@ describe Issue, models: true do
       user1 = create(:user)
       user2 = create(:user)
       project = create(:empty_project)
-      issue = create(:issue, assignee: user1, project: project)
+      issue = create(:issue, assignees: [user1], project: project)
       project.add_developer(user1)
       project.add_developer(user2)
 
       expect(user1.assigned_open_issues_count).to eq(1)
       expect(user2.assigned_open_issues_count).to eq(0)
 
-      issue.assignee = user2
+      issue.assignees = [user2]
       issue.save
 
       expect(user1.assigned_open_issues_count).to eq(0)
@@ -676,6 +746,11 @@ describe Issue, models: true do
       expect(attrs_hash).to include(:human_total_time_spent)
       expect(attrs_hash).to include('time_estimate')
     end
+
+    it 'includes assignee_ids and deprecated assignee_id' do
+      expect(attrs_hash).to include(:assignee_id)
+      expect(attrs_hash).to include(:assignee_ids)
+    end
   end
 
   describe '#check_for_spam' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8b72125dd5d1cf39a4c201fc434451a56c9f5dd2..6cf3dd30ead92247c83fa8ab996adfb3f7443fbc 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,6 +9,7 @@ describe MergeRequest, models: true do
     it { is_expected.to belong_to(:target_project).class_name('Project') }
     it { is_expected.to belong_to(:source_project).class_name('Project') }
     it { is_expected.to belong_to(:merge_user).class_name("User") }
+    it { is_expected.to belong_to(:assignee) }
     it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
   end
 
@@ -86,6 +87,86 @@ describe MergeRequest, models: true do
     end
   end
 
+  describe "before_save" do
+    describe "#update_cache_counts when a merge request is reassigned" do
+      let(:project) { create :project }
+      let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+      let(:assignee) { create :user }
+
+      context "when previous assignee exists" do
+        before do
+          project.team << [assignee, :developer]
+          merge_request.update(assignee: assignee)
+        end
+
+        it "updates cache counts for new assignee" do
+          user = create(:user)
+
+          expect(user).to receive(:update_cache_counts)
+
+          merge_request.update(assignee: user)
+        end
+
+        it "updates cache counts for previous assignee" do
+          old_assignee = merge_request.assignee
+          allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
+
+          expect(old_assignee).to receive(:update_cache_counts)
+
+          merge_request.update(assignee: nil)
+        end
+      end
+
+      context "when previous assignee does not exist" do
+        it "updates cache count for the new assignee" do
+          merge_request.update(assignee: nil)
+
+          expect_any_instance_of(User).to receive(:update_cache_counts)
+
+          merge_request.update(assignee: assignee)
+        end
+      end
+    end
+  end
+
+  describe '#card_attributes' do
+    it 'includes the author name' do
+      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+      allow(subject).to receive(:assignee).and_return(nil)
+
+      expect(subject.card_attributes).
+        to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+    end
+
+    it 'includes the assignee name' do
+      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+      allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))
+
+      expect(subject.card_attributes).
+        to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+    end
+  end
+
+  describe '#assignee_or_author?' do
+    let(:user) { create(:user) }
+
+    it 'returns true for a user that is assigned to a merge request' do
+      subject.assignee = user
+
+      expect(subject.assignee_or_author?(user)).to eq(true)
+    end
+
+    it 'returns true for a user that is the author of a merge request' do
+      subject.author = user
+
+      expect(subject.assignee_or_author?(user)).to eq(true)
+    end
+
+    it 'returns false for a user that is not the assignee or author' do
+      expect(subject.assignee_or_author?(user)).to eq(false)
+    end
+  end
+
   describe '#cache_merge_request_closes_issues!' do
     before do
       subject.project.team << [subject.author, :developer]
@@ -295,16 +376,6 @@ describe MergeRequest, models: true do
     end
   end
 
-  describe '#is_being_reassigned?' do
-    it 'returns true if the merge_request assignee has changed' do
-      subject.assignee = create(:user)
-      expect(subject.is_being_reassigned?).to be_truthy
-    end
-    it 'returns false if the merge request assignee has not changed' do
-      expect(subject.is_being_reassigned?).to be_falsey
-    end
-  end
-
   describe '#for_fork?' do
     it 'returns true if the merge request is for a fork' do
       subject.source_project = build_stubbed(:empty_project, namespace: create(:group))
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 9a870b7fda12f3e5a7f794941beeabfa2fc9f521..4a07c864428c65630aab3506bc7a5bf70ee29774 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -15,7 +15,7 @@ describe IssuePolicy, models: true do
   context 'a private project' do
     let(:non_member) { create(:user) }
     let(:project) { create(:empty_project, :private) }
-    let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+    let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
     let(:issue_no_assignee) { create(:issue, project: project) }
 
     before do
@@ -69,7 +69,7 @@ describe IssuePolicy, models: true do
     end
 
     context 'with confidential issues' do
-      let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+      let(:confidential_issue) { create(:issue, :confidential, project: project, assignees: [assignee], author: author) }
       let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
 
       it 'does not allow non-members to read confidential issues' do
@@ -110,7 +110,7 @@ describe IssuePolicy, models: true do
 
   context 'a public project' do
     let(:project) { create(:empty_project, :public) }
-    let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+    let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
     let(:issue_no_assignee) { create(:issue, project: project) }
 
     before do
@@ -157,7 +157,7 @@ describe IssuePolicy, models: true do
     end
 
     context 'with confidential issues' do
-      let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+      let(:confidential_issue) { create(:issue, :confidential, project: project, assignees: [assignee], author: author) }
       let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
 
       it 'does not allow guests to read confidential issues' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 3ca13111acb0bb898f112c8b0d49c36126b07d41..da2b56c040bc71eecd8822cfbdc8bcd37adf7ebf 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -19,7 +19,7 @@ describe API::Issues do
   let!(:closed_issue) do
     create :closed_issue,
            author: user,
-           assignee: user,
+           assignees: [user],
            project: project,
            state: :closed,
            milestone: milestone,
@@ -31,14 +31,14 @@ describe API::Issues do
            :confidential,
            project: project,
            author: author,
-           assignee: assignee,
+           assignees: [assignee],
            created_at: generate(:past_time),
            updated_at: 2.hours.ago
   end
   let!(:issue) do
     create :issue,
            author: user,
-           assignee: user,
+           assignees: [user],
            project: project,
            milestone: milestone,
            created_at: generate(:past_time),
@@ -265,7 +265,7 @@ describe API::Issues do
     let!(:group_closed_issue) do
       create :closed_issue,
              author: user,
-             assignee: user,
+             assignees: [user],
              project: group_project,
              state: :closed,
              milestone: group_milestone,
@@ -276,13 +276,13 @@ describe API::Issues do
              :confidential,
              project: group_project,
              author: author,
-             assignee: assignee,
+             assignees: [assignee],
              updated_at: 2.hours.ago
     end
     let!(:group_issue) do
       create :issue,
              author: user,
-             assignee: user,
+             assignees: [user],
              project: group_project,
              milestone: group_milestone,
              updated_at: 1.hour.ago,
@@ -687,6 +687,7 @@ describe API::Issues do
       expect(json_response['updated_at']).to be_present
       expect(json_response['labels']).to eq(issue.label_names)
       expect(json_response['milestone']).to be_a Hash
+      expect(json_response['assignees']).to be_a Array
       expect(json_response['assignee']).to be_a Hash
       expect(json_response['author']).to be_a Hash
       expect(json_response['confidential']).to be_falsy
@@ -759,15 +760,41 @@ describe API::Issues do
   end
 
   describe "POST /projects/:id/issues" do
+    context 'support for deprecated assignee_id' do
+      it 'creates a new project issue' do
+        post api("/projects/#{project.id}/issues", user),
+          title: 'new issue', assignee_id: user2.id
+
+        expect(response).to have_http_status(201)
+        expect(json_response['title']).to eq('new issue')
+        expect(json_response['assignee']['name']).to eq(user2.name)
+        expect(json_response['assignees'].first['name']).to eq(user2.name)
+      end
+    end
+
+    context 'CE restrictions' do
+      it 'creates a new project issue with no more than one assignee' do
+        post api("/projects/#{project.id}/issues", user),
+          title: 'new issue', assignee_ids: [user2.id, guest.id]
+
+        expect(response).to have_http_status(201)
+        expect(json_response['title']).to eq('new issue')
+        expect(json_response['assignees'].count).to eq(1)
+      end
+    end
+
     it 'creates a new project issue' do
       post api("/projects/#{project.id}/issues", user),
-        title: 'new issue', labels: 'label, label2'
+        title: 'new issue', labels: 'label, label2', weight: 3,
+        assignee_ids: [user2.id]
 
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('new issue')
       expect(json_response['description']).to be_nil
       expect(json_response['labels']).to eq(%w(label label2))
       expect(json_response['confidential']).to be_falsy
+      expect(json_response['assignee']['name']).to eq(user2.name)
+      expect(json_response['assignees'].first['name']).to eq(user2.name)
     end
 
     it 'creates a new confidential project issue' do
@@ -1057,6 +1084,57 @@ describe API::Issues do
     end
   end
 
+  describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
+    context 'support for deprecated assignee_id' do
+      it 'removes assignee' do
+        put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+          assignee_id: 0
+
+        expect(response).to have_http_status(200)
+
+        expect(json_response['assignee']).to be_nil
+      end
+
+      it 'updates an issue with new assignee' do
+        put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+          assignee_id: user2.id
+
+        expect(response).to have_http_status(200)
+
+        expect(json_response['assignee']['name']).to eq(user2.name)
+      end
+    end
+
+    it 'removes assignee' do
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+        assignee_ids: [0]
+
+      expect(response).to have_http_status(200)
+
+      expect(json_response['assignees']).to be_empty
+    end
+
+    it 'updates an issue with new assignee' do
+      put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+        assignee_ids: [user2.id]
+
+      expect(response).to have_http_status(200)
+
+      expect(json_response['assignees'].first['name']).to eq(user2.name)
+    end
+
+    context 'CE restrictions' do
+      it 'updates an issue with several assignee but only one has been applied' do
+        put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+          assignee_ids: [user2.id, guest.id]
+
+        expect(response).to have_http_status(200)
+
+        expect(json_response['assignees'].size).to eq(1)
+      end
+    end
+  end
+
   describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
     let!(:label) { create(:label, title: 'dummy', project: project) }
     let!(:label_link) { create(:label_link, label: label, target: issue) }
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index ef5b10a161569b1b587d311c8ba4db07a8c533a1..cc81922697ae67d9dd15296ea6ca94c9f608715f 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -14,7 +14,7 @@ describe API::V3::Issues do
   let!(:closed_issue) do
     create :closed_issue,
            author: user,
-           assignee: user,
+           assignees: [user],
            project: project,
            state: :closed,
            milestone: milestone,
@@ -26,14 +26,14 @@ describe API::V3::Issues do
            :confidential,
            project: project,
            author: author,
-           assignee: assignee,
+           assignees: [assignee],
            created_at: generate(:past_time),
            updated_at: 2.hours.ago
   end
   let!(:issue) do
     create :issue,
            author: user,
-           assignee: user,
+           assignees: [user],
            project: project,
            milestone: milestone,
            created_at: generate(:past_time),
@@ -247,7 +247,7 @@ describe API::V3::Issues do
     let!(:group_closed_issue) do
       create :closed_issue,
              author: user,
-             assignee: user,
+             assignees: [user],
              project: group_project,
              state: :closed,
              milestone: group_milestone,
@@ -258,13 +258,13 @@ describe API::V3::Issues do
              :confidential,
              project: group_project,
              author: author,
-             assignee: assignee,
+             assignees: [assignee],
              updated_at: 2.hours.ago
     end
     let!(:group_issue) do
       create :issue,
              author: user,
-             assignee: user,
+             assignees: [user],
              project: group_project,
              milestone: group_milestone,
              updated_at: 1.hour.ago
@@ -737,13 +737,14 @@ describe API::V3::Issues do
   describe "POST /projects/:id/issues" do
     it 'creates a new project issue' do
       post v3_api("/projects/#{project.id}/issues", user),
-        title: 'new issue', labels: 'label, label2'
+        title: 'new issue', labels: 'label, label2', assignee_id: assignee.id
 
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('new issue')
       expect(json_response['description']).to be_nil
       expect(json_response['labels']).to eq(%w(label label2))
       expect(json_response['confidential']).to be_falsy
+      expect(json_response['assignee']['name']).to eq(assignee.name)
     end
 
     it 'creates a new confidential project issue' do
@@ -1140,6 +1141,22 @@ describe API::V3::Issues do
     end
   end
 
+  describe 'PUT /projects/:id/issues/:issue_id to update assignee' do
+    it 'updates an issue with no assignee' do
+      put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: 0
+
+      expect(response).to have_http_status(200)
+      expect(json_response['assignee']).to eq(nil)
+    end
+
+    it 'updates an issue with assignee' do
+      put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: user2.id
+
+      expect(response).to have_http_status(200)
+      expect(json_response['assignee']['name']).to eq(user2.name)
+    end
+  end
+
   describe "DELETE /projects/:id/issues/:issue_id" do
     it "rejects a non member from deleting an issue" do
       delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 7a1ac0273107516b68c8289604643a753c5d581f..5b1639ca0d6e5fe04c174e2a73c2e603e50aabe5 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -4,11 +4,12 @@ describe Issuable::BulkUpdateService, services: true do
   let(:user)    { create(:user) }
   let(:project) { create(:empty_project, namespace: user.namespace) }
 
-  def bulk_update(issues, extra_params = {})
+  def bulk_update(issuables, extra_params = {})
     bulk_update_params = extra_params
-      .reverse_merge(issuable_ids: Array(issues).map(&:id).join(','))
+      .reverse_merge(issuable_ids: Array(issuables).map(&:id).join(','))
 
-    Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue')
+    type = Array(issuables).first.model_name.param_key
+    Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute(type)
   end
 
   describe 'close issues' do
@@ -47,15 +48,15 @@ describe Issuable::BulkUpdateService, services: true do
     end
   end
 
-  describe 'updating assignee' do
-    let(:issue) { create(:issue, project: project, assignee: user) }
+  describe 'updating merge request assignee' do
+    let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) }
 
     context 'when the new assignee ID is a valid user' do
       it 'succeeds' do
         new_assignee = create(:user)
         project.team << [new_assignee, :developer]
 
-        result = bulk_update(issue, assignee_id: new_assignee.id)
+        result = bulk_update(merge_request, assignee_id: new_assignee.id)
 
         expect(result[:success]).to be_truthy
         expect(result[:count]).to eq(1)
@@ -65,22 +66,59 @@ describe Issuable::BulkUpdateService, services: true do
         assignee = create(:user)
         project.team << [assignee, :developer]
 
-        expect { bulk_update(issue, assignee_id: assignee.id) }
-          .to change { issue.reload.assignee }.from(user).to(assignee)
+        expect { bulk_update(merge_request, assignee_id: assignee.id) }
+          .to change { merge_request.reload.assignee }.from(user).to(assignee)
       end
     end
 
     context "when the new assignee ID is #{IssuableFinder::NONE}" do
       it "unassigns the issues" do
-        expect { bulk_update(issue, assignee_id: IssuableFinder::NONE) }
-          .to change { issue.reload.assignee }.to(nil)
+        expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) }
+          .to change { merge_request.reload.assignee }.to(nil)
       end
     end
 
     context 'when the new assignee ID is not present' do
       it 'does not unassign' do
-        expect { bulk_update(issue, assignee_id: nil) }
-          .not_to change { issue.reload.assignee }
+        expect { bulk_update(merge_request, assignee_id: nil) }
+          .not_to change { merge_request.reload.assignee }
+      end
+    end
+  end
+
+  describe 'updating issue assignee' do
+    let(:issue) { create(:issue, project: project, assignees: [user]) }
+
+    context 'when the new assignee ID is a valid user' do
+      it 'succeeds' do
+        new_assignee = create(:user)
+        project.team << [new_assignee, :developer]
+
+        result = bulk_update(issue, assignee_ids: [new_assignee.id])
+
+        expect(result[:success]).to be_truthy
+        expect(result[:count]).to eq(1)
+      end
+
+      it 'updates the assignee to the use ID passed' do
+        assignee = create(:user)
+        project.team << [assignee, :developer]
+        expect { bulk_update(issue, assignee_ids: [assignee.id]) }
+          .to change { issue.reload.assignees.first }.from(user).to(assignee)
+      end
+    end
+
+    context "when the new assignee ID is #{IssuableFinder::NONE}" do
+      it "unassigns the issues" do
+        expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) }
+          .to change { issue.reload.assignees.count }.from(1).to(0)
+      end
+    end
+
+    context 'when the new assignee ID is not present' do
+      it 'does not unassign' do
+        expect { bulk_update(issue, assignee_ids: []) }
+          .not_to change{ issue.reload.assignees }
       end
     end
   end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 7a54373963e54bbba865402155a0f3d8c3012015..5184053171186d549976db34d8ea90478a0e2e4f 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -4,7 +4,7 @@ describe Issues::CloseService, services: true do
   let(:user) { create(:user) }
   let(:user2) { create(:user) }
   let(:guest) { create(:user) }
-  let(:issue) { create(:issue, assignee: user2) }
+  let(:issue) { create(:issue, assignees: [user2]) }
   let(:project) { issue.project }
   let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
 
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 80bfb7315505a9368b57d3b773ed343194ead5bf..01edc46496d97a807cef0c89ff48072d35328b5f 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -6,10 +6,10 @@ describe Issues::CreateService, services: true do
 
   describe '#execute' do
     let(:issue) { described_class.new(project, user, opts).execute }
+    let(:assignee) { create(:user) }
+    let(:milestone) { create(:milestone, project: project) }
 
     context 'when params are valid' do
-      let(:assignee) { create(:user) }
-      let(:milestone) { create(:milestone, project: project) }
       let(:labels) { create_pair(:label, project: project) }
 
       before do
@@ -20,7 +20,7 @@ describe Issues::CreateService, services: true do
       let(:opts) do
         { title: 'Awesome issue',
           description: 'please fix',
-          assignee_id: assignee.id,
+          assignee_ids: [assignee.id],
           label_ids: labels.map(&:id),
           milestone_id: milestone.id,
           due_date: Date.tomorrow }
@@ -29,7 +29,7 @@ describe Issues::CreateService, services: true do
       it 'creates the issue with the given params' do
         expect(issue).to be_persisted
         expect(issue.title).to eq('Awesome issue')
-        expect(issue.assignee).to eq assignee
+        expect(issue.assignees).to eq [assignee]
         expect(issue.labels).to match_array labels
         expect(issue.milestone).to eq milestone
         expect(issue.due_date).to eq Date.tomorrow
@@ -37,6 +37,7 @@ describe Issues::CreateService, services: true do
 
       context 'when current user cannot admin issues in the project' do
         let(:guest) { create(:user) }
+
         before do
           project.team << [guest, :guest]
         end
@@ -47,7 +48,7 @@ describe Issues::CreateService, services: true do
           expect(issue).to be_persisted
           expect(issue.title).to eq('Awesome issue')
           expect(issue.description).to eq('please fix')
-          expect(issue.assignee).to be_nil
+          expect(issue.assignees).to be_empty
           expect(issue.labels).to be_empty
           expect(issue.milestone).to be_nil
           expect(issue.due_date).to be_nil
@@ -136,10 +137,83 @@ describe Issues::CreateService, services: true do
       end
     end
 
-    it_behaves_like 'issuable create service'
+    context 'issue create service' do
+      context 'assignees' do
+        before { project.team << [user, :master] }
+
+        it 'removes assignee when user id is invalid' do
+          opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
+
+          issue = described_class.new(project, user, opts).execute
+
+          expect(issue.assignees).to be_empty
+        end
+
+        it 'removes assignee when user id is 0' do
+          opts = { title: 'Title', description: 'Description',  assignee_ids: [0] }
+
+          issue = described_class.new(project, user, opts).execute
+
+          expect(issue.assignees).to be_empty
+        end
+
+        it 'saves assignee when user id is valid' do
+          project.team << [assignee, :master]
+          opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
+
+          issue = described_class.new(project, user, opts).execute
+
+          expect(issue.assignees).to eq([assignee])
+        end
+
+        context "when issuable feature is private" do
+          before do
+            project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
+                                           merge_requests_access_level: ProjectFeature::PRIVATE)
+          end
+
+          levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+          levels.each do |level|
+            it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+              project.update(visibility_level: level)
+              opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
+
+              issue = described_class.new(project, user, opts).execute
+
+              expect(issue.assignees).to be_empty
+            end
+          end
+        end
+      end
+    end
 
     it_behaves_like 'new issuable record that supports slash commands'
 
+    context 'Slash commands' do
+      context 'with assignee and milestone in params and command' do
+        let(:opts) do
+          {
+            assignee_ids: [create(:user).id],
+            milestone_id: 1,
+            title: 'Title',
+            description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+          }
+        end
+
+        before do
+          project.team << [user, :master]
+          project.team << [assignee, :master]
+        end
+
+        it 'assigns and sets milestone to issuable from command' do
+          expect(issue).to be_persisted
+          expect(issue.assignees).to eq([assignee])
+          expect(issue.milestone).to eq(milestone)
+        end
+      end
+    end
+
     context 'resolving discussions' do
       let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
       let(:merge_request) { discussion.noteable }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 5b324f3c706d6bf1c99f84fac93a7fc835bd2e2e..6633ac10236e4ddcfcb8108805c37daac6dbdc1b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -14,7 +14,7 @@ describe Issues::UpdateService, services: true do
   let(:issue) do
     create(:issue, title: 'Old title',
                    description: "for #{user2.to_reference}",
-                   assignee_id: user3.id,
+                   assignee_ids: [user3.id],
                    project: project)
   end
 
@@ -40,7 +40,7 @@ describe Issues::UpdateService, services: true do
         {
           title: 'New title',
           description: 'Also please fix',
-          assignee_id: user2.id,
+          assignee_ids: [user2.id],
           state_event: 'close',
           label_ids: [label.id],
           due_date: Date.tomorrow
@@ -53,15 +53,15 @@ describe Issues::UpdateService, services: true do
         expect(issue).to be_valid
         expect(issue.title).to eq 'New title'
         expect(issue.description).to eq 'Also please fix'
-        expect(issue.assignee).to eq user2
+        expect(issue.assignees).to match_array([user2])
         expect(issue).to be_closed
         expect(issue.labels).to match_array [label]
         expect(issue.due_date).to eq Date.tomorrow
       end
 
       it 'sorts issues as specified by parameters' do
-        issue1 = create(:issue, project: project, assignee_id: user3.id)
-        issue2 = create(:issue, project: project, assignee_id: user3.id)
+        issue1 = create(:issue, project: project, assignees: [user3])
+        issue2 = create(:issue, project: project, assignees: [user3])
 
         [issue, issue1, issue2].each do |issue|
           issue.move_to_end
@@ -87,7 +87,7 @@ describe Issues::UpdateService, services: true do
           expect(issue).to be_valid
           expect(issue.title).to eq 'New title'
           expect(issue.description).to eq 'Also please fix'
-          expect(issue.assignee).to eq user3
+          expect(issue.assignees).to match_array [user3]
           expect(issue.labels).to be_empty
           expect(issue.milestone).to be_nil
           expect(issue.due_date).to be_nil
@@ -137,7 +137,7 @@ describe Issues::UpdateService, services: true do
         {
           title: 'New title',
           description: 'Also please fix',
-          assignee_id: user2.id,
+          assignee_ids: [user2],
           state_event: 'close',
           label_ids: [label.id],
           confidential: true
@@ -163,12 +163,12 @@ describe Issues::UpdateService, services: true do
       it 'does not update assignee_id with unauthorized users' do
         project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
         update_issue(confidential: true)
-        non_member        = create(:user)
-        original_assignee = issue.assignee
+        non_member = create(:user)
+        original_assignees = issue.assignees
 
-        update_issue(assignee_id: non_member.id)
+        update_issue(assignee_ids: [non_member.id])
 
-        expect(issue.reload.assignee_id).to eq(original_assignee.id)
+        expect(issue.reload.assignees).to eq(original_assignees)
       end
     end
 
@@ -205,7 +205,7 @@ describe Issues::UpdateService, services: true do
 
       context 'when is reassigned' do
         before do
-          update_issue(assignee: user2)
+          update_issue(assignees: [user2])
         end
 
         it 'marks previous assignee todos as done' do
@@ -408,6 +408,41 @@ describe Issues::UpdateService, services: true do
       end
     end
 
+    context 'updating asssignee_id' do
+      it 'does not update assignee when assignee_id is invalid' do
+        update_issue(assignee_ids: [-1])
+
+        expect(issue.reload.assignees).to eq([user3])
+      end
+
+      it 'unassigns assignee when user id is 0' do
+        update_issue(assignee_ids: [0])
+
+        expect(issue.reload.assignees).to be_empty
+      end
+
+      it 'does not update assignee_id when user cannot read issue' do
+        update_issue(assignee_ids: [create(:user).id])
+
+        expect(issue.reload.assignees).to eq([user3])
+      end
+
+      context "when issuable feature is private" do
+        levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+        levels.each do |level|
+          it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+            assignee = create(:user)
+            project.update(visibility_level: level)
+            feature_visibility_attr = :"#{issue.model_name.plural}_access_level"
+            project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
+
+            expect{ update_issue(assignee_ids: [assignee.id]) }.not_to change{ issue.assignees }
+          end
+        end
+      end
+    end
+
     context 'updating mentions' do
       let(:mentionable) { issue }
       include_examples 'updating mentions', Issues::UpdateService
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
index 3b35a3b8e3a3a0991e2a295345e3f2b0346c8a5e..ab440d18e9f68f8ee6b1c3c2fa5e7c7cf5eec7de 100644
--- a/spec/services/members/authorized_destroy_service_spec.rb
+++ b/spec/services/members/authorized_destroy_service_spec.rb
@@ -14,8 +14,8 @@ describe Members::AuthorizedDestroyService, services: true do
     it "unassigns issues and merge requests" do
       group.add_developer(member_user)
 
-      issue = create :issue, project: group_project, assignee: member_user
-      create :issue, assignee: member_user
+      issue = create :issue, project: group_project, assignees: [member_user]
+      create :issue, assignees: [member_user]
       merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
       create :merge_request, target_project: project, source_project: project, assignee: member_user
 
@@ -33,7 +33,7 @@ describe Members::AuthorizedDestroyService, services: true do
     it "unassigns issues and merge requests" do
       project.team << [member_user, :developer]
 
-      create :issue, project: project, assignee: member_user
+      create :issue, project: project, assignees: [member_user]
       create :merge_request, target_project: project, source_project: project, assignee: member_user
 
       member = project.members.find_by(user_id: member_user.id)
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index fe75757dd29c48cca675e2da158324dcba3bc909..d3556020d4d06ca4d44c19a6df69302e8d122605 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -15,14 +15,14 @@ describe MergeRequests::AssignIssuesService, services: true do
     expect(service.assignable_issues.map(&:id)).to include(issue.id)
   end
 
-  it 'ignores issues already assigned to any user' do
-    issue.update!(assignee: create(:user))
+  it 'ignores issues the user cannot update assignee on' do
+    project.team.truncate
 
     expect(service.assignable_issues).to be_empty
   end
 
-  it 'ignores issues the user cannot update assignee on' do
-    project.team.truncate
+  it 'ignores issues already assigned to any user' do
+    issue.assignees = [create(:user)]
 
     expect(service.assignable_issues).to be_empty
   end
@@ -44,7 +44,7 @@ describe MergeRequests::AssignIssuesService, services: true do
   end
 
   it 'assigns these to the merge request owner' do
-    expect { service.execute }.to change { issue.reload.assignee }.to(user)
+    expect { service.execute }.to change { issue.assignees.first }.to(user)
   end
 
   it 'ignores external issues' do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 0e16c7cc94bbdbd492b690217aa9313702afbd14..ace82380cc999f439bd9b3dfef8c19698fad501c 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -84,7 +84,87 @@ describe MergeRequests::CreateService, services: true do
       end
     end
 
-    it_behaves_like 'issuable create service'
+    context 'Slash commands' do
+      context 'with assignee and milestone in params and command' do
+        let(:merge_request) { described_class.new(project, user, opts).execute }
+        let(:milestone) { create(:milestone, project: project) }
+
+        let(:opts) do
+          {
+            assignee_id: create(:user).id,
+            milestone_id: 1,
+            title: 'Title',
+            description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"),
+            source_branch: 'feature',
+            target_branch: 'master'
+          }
+        end
+
+        before do
+          project.team << [user, :master]
+          project.team << [assignee, :master]
+        end
+
+        it 'assigns and sets milestone to issuable from command' do
+          expect(merge_request).to be_persisted
+          expect(merge_request.assignee).to eq(assignee)
+          expect(merge_request.milestone).to eq(milestone)
+        end
+      end
+    end
+
+    context 'merge request create service' do
+      context 'asssignee_id' do
+        let(:assignee) { create(:user) }
+
+        before { project.team << [user, :master] }
+
+        it 'removes assignee_id when user id is invalid' do
+          opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+
+          merge_request = described_class.new(project, user, opts).execute
+
+          expect(merge_request.assignee_id).to be_nil
+        end
+
+        it 'removes assignee_id when user id is 0' do
+          opts = { title: 'Title', description: 'Description',  assignee_id: 0 }
+
+          merge_request = described_class.new(project, user, opts).execute
+
+          expect(merge_request.assignee_id).to be_nil
+        end
+
+        it 'saves assignee when user id is valid' do
+          project.team << [assignee, :master]
+          opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+          merge_request = described_class.new(project, user, opts).execute
+
+          expect(merge_request.assignee).to eq(assignee)
+        end
+
+        context "when issuable feature is private" do
+          before do
+            project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
+                                           merge_requests_access_level: ProjectFeature::PRIVATE)
+          end
+
+          levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+          levels.each do |level|
+            it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+              project.update(visibility_level: level)
+              opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+              merge_request = described_class.new(project, user, opts).execute
+
+              expect(merge_request.assignee_id).to be_nil
+            end
+          end
+        end
+      end
+    end
 
     context 'while saving references to issues that the created merge request closes' do
       let(:first_issue) { create(:issue, project: project) }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index f2ca1e6fcbd8080bfa9ef08b30c7cb341d7acc95..694ec3a579f8501c98de66b1bc04bf4025bebff1 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -423,6 +423,54 @@ describe MergeRequests::UpdateService, services: true do
       end
     end
 
+    context 'updating asssignee_id' do
+      it 'does not update assignee when assignee_id is invalid' do
+        merge_request.update(assignee_id: user.id)
+
+        update_merge_request(assignee_id: -1)
+
+        expect(merge_request.reload.assignee).to eq(user)
+      end
+
+      it 'unassigns assignee when user id is 0' do
+        merge_request.update(assignee_id: user.id)
+
+        update_merge_request(assignee_id: 0)
+
+        expect(merge_request.assignee_id).to be_nil
+      end
+
+      it 'saves assignee when user id is valid' do
+        update_merge_request(assignee_id: user.id)
+
+        expect(merge_request.assignee_id).to eq(user.id)
+      end
+
+      it 'does not update assignee_id when user cannot read issue' do
+        non_member        = create(:user)
+        original_assignee = merge_request.assignee
+
+        update_merge_request(assignee_id: non_member.id)
+
+        expect(merge_request.assignee_id).to eq(original_assignee.id)
+      end
+
+      context "when issuable feature is private" do
+        levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+        levels.each do |level|
+          it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+            assignee = create(:user)
+            project.update(visibility_level: level)
+            feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level"
+            project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
+
+            expect{ update_merge_request(assignee_id: assignee) }.not_to change{ merge_request.assignee }
+          end
+        end
+      end
+    end
+
     include_examples 'issuable update service' do
       let(:open_issuable) { merge_request }
       let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
index 1a64c8bbf00df05a396be79b356f8b73afa3e3ef..c9954dc360351ed3397a94c51de6c9ebd8d1b794 100644
--- a/spec/services/notes/slash_commands_service_spec.rb
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -66,7 +66,7 @@ describe Notes::SlashCommandsService, services: true do
           expect(content).to eq ''
           expect(note.noteable).to be_closed
           expect(note.noteable.labels).to match_array(labels)
-          expect(note.noteable.assignee).to eq(assignee)
+          expect(note.noteable.assignees).to eq([assignee])
           expect(note.noteable.milestone).to eq(milestone)
         end
       end
@@ -113,7 +113,7 @@ describe Notes::SlashCommandsService, services: true do
           expect(content).to eq "HELLO\nWORLD"
           expect(note.noteable).to be_closed
           expect(note.noteable.labels).to match_array(labels)
-          expect(note.noteable.assignee).to eq(assignee)
+          expect(note.noteable.assignees).to eq([assignee])
           expect(note.noteable.milestone).to eq(milestone)
         end
       end
@@ -220,4 +220,31 @@ describe Notes::SlashCommandsService, services: true do
       let(:note) { build(:note_on_commit, project: project) }
     end
   end
+
+  context 'CE restriction for issue assignees' do
+    describe '/assign' do
+      let(:project) { create(:empty_project) }
+      let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+      let(:assignee) { create(:user) }
+      let(:master) { create(:user) }
+      let(:service) { described_class.new(project, master) }
+      let(:note) { create(:note_on_issue, note: note_text, project: project) }
+
+      let(:note_text) do
+        %(/assign @#{assignee.username} @#{master.username}\n")
+      end
+
+      before do
+        project.team << [master, :master]
+        project.team << [assignee, :master]
+      end
+
+      it 'adds only one assignee from the list' do
+        _, command_params = service.extract_commands(note)
+        service.execute(command_params, note)
+
+        expect(note.noteable.assignees.count).to eq(1)
+      end
+    end
+  end
 end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 989fd90cda9d97f68c27ffaa231f8aec67a2bd1d..74f96b9790988c95d9d2e3a72799237fc010fb56 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -4,6 +4,7 @@ describe NotificationService, services: true do
   include EmailHelpers
 
   let(:notification) { NotificationService.new }
+  let(:assignee) { create(:user) }
 
   around(:each) do |example|
     perform_enqueued_jobs do
@@ -52,7 +53,11 @@ describe NotificationService, services: true do
 
   shared_examples 'participating by assignee notification' do
     it 'emails the participant' do
-      issuable.update_attribute(:assignee, participant)
+      if issuable.is_a?(Issue)
+        issuable.assignees << participant
+      else
+        issuable.update_attribute(:assignee, participant)
+      end
 
       notification_trigger
 
@@ -103,14 +108,14 @@ describe NotificationService, services: true do
   describe 'Notes' do
     context 'issue note' do
       let(:project) { create(:empty_project, :private) }
-      let(:issue) { create(:issue, project: project, assignee: create(:user)) }
-      let(:mentioned_issue) { create(:issue, assignee: issue.assignee) }
+      let(:issue) { create(:issue, project: project, assignees: [assignee]) }
+      let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
       let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') }
 
       before do
         build_team(note.project)
         project.add_master(issue.author)
-        project.add_master(issue.assignee)
+        project.add_master(assignee)
         project.add_master(note.author)
         create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
         update_custom_notification(:new_note, @u_guest_custom, resource: project)
@@ -130,7 +135,7 @@ describe NotificationService, services: true do
 
           should_email(@u_watcher)
           should_email(note.noteable.author)
-          should_email(note.noteable.assignee)
+          should_email(note.noteable.assignees.first)
           should_email(@u_custom_global)
           should_email(@u_mentioned)
           should_email(@subscriber)
@@ -196,7 +201,7 @@ describe NotificationService, services: true do
           notification.new_note(note)
 
           should_email(note.noteable.author)
-          should_email(note.noteable.assignee)
+          should_email(note.noteable.assignees.first)
           should_email(@u_mentioned)
           should_email(@u_custom_global)
           should_not_email(@u_guest_custom)
@@ -218,7 +223,7 @@ describe NotificationService, services: true do
       let(:member) { create(:user) }
       let(:guest) { create(:user) }
       let(:admin) { create(:admin) }
-      let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+      let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
       let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
       let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") }
 
@@ -244,8 +249,8 @@ describe NotificationService, services: true do
 
     context 'issue note mention' do
       let(:project) { create(:empty_project, :public) }
-      let(:issue) { create(:issue, project: project, assignee: create(:user)) }
-      let(:mentioned_issue) { create(:issue, assignee: issue.assignee) }
+      let(:issue) { create(:issue, project: project, assignees: [assignee]) }
+      let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
       let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@all mentioned') }
 
       before do
@@ -269,7 +274,7 @@ describe NotificationService, services: true do
 
           should_email(@u_guest_watcher)
           should_email(note.noteable.author)
-          should_email(note.noteable.assignee)
+          should_email(note.noteable.assignees.first)
           should_not_email(note.author)
           should_email(@u_mentioned)
           should_not_email(@u_disabled)
@@ -449,7 +454,7 @@ describe NotificationService, services: true do
     let(:group) { create(:group) }
     let(:project) { create(:empty_project, :public, namespace: group) }
     let(:another_project) { create(:empty_project, :public, namespace: group) }
-    let(:issue) { create :issue, project: project, assignee: create(:user), description: 'cc @participant' }
+    let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant' }
 
     before do
       build_team(issue.project)
@@ -465,7 +470,7 @@ describe NotificationService, services: true do
       it do
         notification.new_issue(issue, @u_disabled)
 
-        should_email(issue.assignee)
+        should_email(assignee)
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
         should_email(@u_guest_custom)
@@ -480,10 +485,10 @@ describe NotificationService, services: true do
       end
 
       it do
-        create_global_setting_for(issue.assignee, :mention)
+        create_global_setting_for(issue.assignees.first, :mention)
         notification.new_issue(issue, @u_disabled)
 
-        should_not_email(issue.assignee)
+        should_not_email(issue.assignees.first)
       end
 
       it "emails the author if they've opted into notifications about their activity" do
@@ -528,7 +533,7 @@ describe NotificationService, services: true do
         let(:member) { create(:user) }
         let(:guest) { create(:user) }
         let(:admin) { create(:admin) }
-        let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+        let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
 
         it "emails subscribers of the issue's labels that can read the issue" do
           project.add_developer(member)
@@ -572,9 +577,9 @@ describe NotificationService, services: true do
       end
 
       it 'emails new assignee' do
-        notification.reassigned_issue(issue, @u_disabled)
+        notification.reassigned_issue(issue, @u_disabled, [assignee])
 
-        should_email(issue.assignee)
+        should_email(issue.assignees.first)
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
         should_email(@u_guest_custom)
@@ -588,9 +593,8 @@ describe NotificationService, services: true do
       end
 
       it 'emails previous assignee even if he has the "on mention" notif level' do
-        issue.update_attribute(:assignee, @u_mentioned)
-        issue.update_attributes(assignee: @u_watcher)
-        notification.reassigned_issue(issue, @u_disabled)
+        issue.assignees = [@u_mentioned]
+        notification.reassigned_issue(issue, @u_disabled, [@u_watcher])
 
         should_email(@u_mentioned)
         should_email(@u_watcher)
@@ -606,11 +610,11 @@ describe NotificationService, services: true do
       end
 
       it 'emails new assignee even if he has the "on mention" notif level' do
-        issue.update_attributes(assignee: @u_mentioned)
-        notification.reassigned_issue(issue, @u_disabled)
+        issue.assignees = [@u_mentioned]
+        notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
 
-        expect(issue.assignee).to be @u_mentioned
-        should_email(issue.assignee)
+        expect(issue.assignees.first).to be @u_mentioned
+        should_email(issue.assignees.first)
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
         should_email(@u_guest_custom)
@@ -624,11 +628,11 @@ describe NotificationService, services: true do
       end
 
       it 'emails new assignee' do
-        issue.update_attribute(:assignee, @u_mentioned)
-        notification.reassigned_issue(issue, @u_disabled)
+        issue.assignees = [@u_mentioned]
+        notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
 
-        expect(issue.assignee).to be @u_mentioned
-        should_email(issue.assignee)
+        expect(issue.assignees.first).to be @u_mentioned
+        should_email(issue.assignees.first)
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
         should_email(@u_guest_custom)
@@ -642,17 +646,17 @@ describe NotificationService, services: true do
       end
 
       it 'does not email new assignee if they are the current user' do
-        issue.update_attribute(:assignee, @u_mentioned)
-        notification.reassigned_issue(issue, @u_mentioned)
+        issue.assignees = [@u_mentioned]
+        notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned])
 
-        expect(issue.assignee).to be @u_mentioned
+        expect(issue.assignees.first).to be @u_mentioned
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
         should_email(@u_guest_custom)
         should_email(@u_participant_mentioned)
         should_email(@subscriber)
         should_email(@u_custom_global)
-        should_not_email(issue.assignee)
+        should_not_email(issue.assignees.first)
         should_not_email(@unsubscriber)
         should_not_email(@u_participating)
         should_not_email(@u_disabled)
@@ -662,7 +666,7 @@ describe NotificationService, services: true do
       it_behaves_like 'participating notifications' do
         let(:participant) { create(:user, username: 'user-participant') }
         let(:issuable) { issue }
-        let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled) }
+        let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
       end
     end
 
@@ -705,7 +709,7 @@ describe NotificationService, services: true do
       it "doesn't send email to anyone but subscribers of the given labels" do
         notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
 
-        should_not_email(issue.assignee)
+        should_not_email(issue.assignees.first)
         should_not_email(issue.author)
         should_not_email(@u_watcher)
         should_not_email(@u_guest_watcher)
@@ -729,7 +733,7 @@ describe NotificationService, services: true do
         let(:member) { create(:user) }
         let(:guest) { create(:user) }
         let(:admin) { create(:admin) }
-        let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+        let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
         let!(:label_1) { create(:label, project: project, issues: [confidential_issue]) }
         let!(:label_2) { create(:label, project: project) }
 
@@ -767,7 +771,7 @@ describe NotificationService, services: true do
       it 'sends email to issue assignee and issue author' do
         notification.close_issue(issue, @u_disabled)
 
-        should_email(issue.assignee)
+        should_email(issue.assignees.first)
         should_email(issue.author)
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
@@ -798,7 +802,7 @@ describe NotificationService, services: true do
       it 'sends email to issue notification recipients' do
         notification.reopen_issue(issue, @u_disabled)
 
-        should_email(issue.assignee)
+        should_email(issue.assignees.first)
         should_email(issue.author)
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
@@ -826,7 +830,7 @@ describe NotificationService, services: true do
       it 'sends email to issue notification recipients' do
         notification.issue_moved(issue, new_issue, @u_disabled)
 
-        should_email(issue.assignee)
+        should_email(issue.assignees.first)
         should_email(issue.author)
         should_email(@u_watcher)
         should_email(@u_guest_watcher)
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 7916c2d957cc068379fc07c3cdc8805d30a8d777..c198c3eedfca23ef4b3d3465bac6cb1f26616275 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -11,7 +11,7 @@ describe Projects::AutocompleteService, services: true do
       let(:project) { create(:empty_project, :public) }
       let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
       let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
-      let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+      let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
 
       it 'does not list project confidential issues for guests' do
         autocomplete = described_class.new(project, nil)
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index 46cfac4a128fed2d35816160c8252e1aed1deddc..e5e400ee281f6f3fabf488df0032431595394158 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
 describe SlashCommands::InterpretService, services: true do
   let(:project) { create(:empty_project, :public) }
   let(:developer) { create(:user) }
+  let(:developer2) { create(:user) }
   let(:issue) { create(:issue, project: project) }
   let(:milestone) { create(:milestone, project: project, title: '9.10') }
   let(:inprogress) { create(:label, project: project, title: 'In Progress') }
@@ -42,23 +43,6 @@ describe SlashCommands::InterpretService, services: true do
       end
     end
 
-    shared_examples 'assign command' do
-      it 'fetches assignee and populates assignee_id if content contains /assign' do
-        _, updates = service.execute(content, issuable)
-
-        expect(updates).to eq(assignee_id: developer.id)
-      end
-    end
-
-    shared_examples 'unassign command' do
-      it 'populates assignee_id: nil if content contains /unassign' do
-        issuable.update!(assignee_id: developer.id)
-        _, updates = service.execute(content, issuable)
-
-        expect(updates).to eq(assignee_id: nil)
-      end
-    end
-
     shared_examples 'milestone command' do
       it 'fetches milestone and populates milestone_id if content contains /milestone' do
         milestone # populate the milestone
@@ -371,14 +355,46 @@ describe SlashCommands::InterpretService, services: true do
       let(:issuable) { issue }
     end
 
-    it_behaves_like 'assign command' do
+    context 'assign command' do
       let(:content) { "/assign @#{developer.username}" }
-      let(:issuable) { issue }
+
+      context 'Issue' do
+        it 'fetches assignee and populates assignee_id if content contains /assign' do
+          _, updates = service.execute(content, issue)
+
+          expect(updates).to eq(assignee_ids: [developer.id])
+        end
+      end
+
+      context 'Merge Request' do
+        it 'fetches assignee and populates assignee_id if content contains /assign' do
+          _, updates = service.execute(content, merge_request)
+
+          expect(updates).to eq(assignee_id: developer.id)
+        end
+      end
     end
 
-    it_behaves_like 'assign command' do
-      let(:content) { "/assign @#{developer.username}" }
-      let(:issuable) { merge_request }
+    context 'assign command with multiple assignees' do
+      let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
+
+      before{ project.team << [developer2, :developer] }
+
+      context 'Issue' do
+        it 'fetches assignee and populates assignee_id if content contains /assign' do
+          _, updates = service.execute(content, issue)
+
+          expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id])
+        end
+      end
+
+      context 'Merge Request' do
+        it 'fetches assignee and populates assignee_id if content contains /assign' do
+          _, updates = service.execute(content, merge_request)
+
+          expect(updates).to eq(assignee_id: developer.id)
+        end
+      end
     end
 
     it_behaves_like 'empty command' do
@@ -391,14 +407,26 @@ describe SlashCommands::InterpretService, services: true do
       let(:issuable) { issue }
     end
 
-    it_behaves_like 'unassign command' do
+    context 'unassign command' do
       let(:content) { '/unassign' }
-      let(:issuable) { issue }
-    end
 
-    it_behaves_like 'unassign command' do
-      let(:content) { '/unassign' }
-      let(:issuable) { merge_request }
+      context 'Issue' do
+        it 'populates assignee_ids: [] if content contains /unassign' do
+          issue.update(assignee_ids: [developer.id])
+          _, updates = service.execute(content, issue)
+
+          expect(updates).to eq(assignee_ids: [])
+        end
+      end
+
+      context 'Merge Request' do
+        it 'populates assignee_id: nil if content contains /unassign' do
+          merge_request.update(assignee_id: developer.id)
+          _, updates = service.execute(content, merge_request)
+
+          expect(updates).to eq(assignee_id: nil)
+        end
+      end
     end
 
     it_behaves_like 'milestone command' do
@@ -846,7 +874,7 @@ describe SlashCommands::InterpretService, services: true do
 
     describe 'unassign command' do
       let(:content) { '/unassign' }
-      let(:issue) { create(:issue, project: project, assignee: developer) }
+      let(:issue) { create(:issue, project: project, assignees: [developer]) }
 
       it 'includes current assignee reference' do
         _, explanations = service.explain(content, issue)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 75d7caf2508cd52ab71567c8862467e2abe83b6a..68816bf36b8337f9de206a20fb7760148b3056c9 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -6,6 +6,7 @@ describe SystemNoteService, services: true do
   let(:project)  { create(:empty_project) }
   let(:author)   { create(:user) }
   let(:noteable) { create(:issue, project: project) }
+  let(:issue)    { noteable }
 
   shared_examples_for 'a system note' do
     let(:expected_noteable) { noteable }
@@ -155,6 +156,52 @@ describe SystemNoteService, services: true do
     end
   end
 
+  describe '.change_issue_assignees' do
+    subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) }
+
+    let(:assignee) { create(:user) }
+    let(:assignee1) { create(:user) }
+    let(:assignee2) { create(:user) }
+    let(:assignee3) { create(:user) }
+
+    it_behaves_like 'a system note' do
+      let(:action) { 'assignee' }
+    end
+
+    def build_note(old_assignees, new_assignees)
+      issue.assignees = new_assignees
+      described_class.change_issue_assignees(issue, project, author, old_assignees).note
+    end
+
+    it 'builds a correct phrase when an assignee is added to a non-assigned issue' do
+      expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}"
+    end
+
+    it 'builds a correct phrase when assignee removed' do
+      expect(build_note([assignee1], [])).to eq 'removed all assignees'
+    end
+
+    it 'builds a correct phrase when assignees changed' do
+      expect(build_note([assignee1], [assignee2])).to eq \
+        "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
+    end
+
+    it 'builds a correct phrase when three assignees removed and one added' do
+      expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \
+        "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}"
+    end
+
+    it 'builds a correct phrase when one assignee changed from a set' do
+      expect(build_note([assignee, assignee1], [assignee, assignee2])).to eq \
+        "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
+    end
+
+    it 'builds a correct phrase when one assignee removed from a set' do
+      expect(build_note([assignee, assignee1, assignee2], [assignee, assignee1])).to eq \
+        "unassigned @#{assignee2.username}"
+    end
+  end
+
   describe '.change_label' do
     subject { described_class.change_label(noteable, project, author, added, removed) }
 
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 89b3b6aad103c32cb008a8c991ef48d08ec93f40..175a42a32d9b707b53007989f09342a595bb82d0 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -25,11 +25,11 @@ describe TodoService, services: true do
   end
 
   describe 'Issues' do
-    let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
-    let(:addressed_issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
-    let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
-    let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
-    let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: directly_addressed) }
+    let(:issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+    let(:addressed_issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+    let(:unassigned_issue) { create(:issue, project: project, assignees: []) }
+    let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee], description: mentions) }
+    let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee], description: directly_addressed) }
 
     describe '#new_issue' do
       it 'creates a todo if assigned' do
@@ -43,7 +43,7 @@ describe TodoService, services: true do
       end
 
       it 'creates a todo if assignee is the current user' do
-        unassigned_issue.update_attribute(:assignee, john_doe)
+        unassigned_issue.assignees = [john_doe]
         service.new_issue(unassigned_issue, john_doe)
 
         should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
@@ -258,20 +258,20 @@ describe TodoService, services: true do
 
     describe '#reassigned_issue' do
       it 'creates a pending todo for new assignee' do
-        unassigned_issue.update_attribute(:assignee, john_doe)
+        unassigned_issue.assignees << john_doe
         service.reassigned_issue(unassigned_issue, author)
 
         should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED)
       end
 
       it 'does not create a todo if unassigned' do
-        issue.update_attribute(:assignee, nil)
+        issue.assignees.destroy_all
 
         should_not_create_any_todo { service.reassigned_issue(issue, author) }
       end
 
       it 'creates a todo if new assignee is the current user' do
-        unassigned_issue.update_attribute(:assignee, john_doe)
+        unassigned_issue.assignees << john_doe
         service.reassigned_issue(unassigned_issue, john_doe)
 
         should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
@@ -361,7 +361,7 @@ describe TodoService, services: true do
     describe '#new_note' do
       let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
       let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
-      let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+      let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
       let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
       let(:addressed_note) { create(:note, project: project, noteable: issue, author: john_doe, note: directly_addressed) }
       let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
@@ -854,7 +854,7 @@ describe TodoService, services: true do
   end
 
   it 'updates cached counts when a todo is created' do
-    issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
+    issue = create(:issue, project: project, assignees: [john_doe], author: author, description: mentions)
 
     expect(john_doe.todos_pending_count).to eq(0)
     expect(john_doe).to receive(:update_todos_count_cache).and_call_original
@@ -866,8 +866,8 @@ describe TodoService, services: true do
   end
 
   describe '#mark_todos_as_done' do
-    let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) }
-    let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+    let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
+    let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
 
     it 'marks a relation of todos as done' do
       create(:todo, :mentioned, user: john_doe, target: issue, project: project)
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 4bc30018ebd5dcd5764a9e658449222d643737ee..de37a61e38880dd3ea10b368410371cdf34b0464 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -47,7 +47,7 @@ describe Users::DestroyService, services: true do
       end
 
       context "for an issue the user was assigned to" do
-        let!(:issue) { create(:issue, project: project, assignee: user) }
+        let!(:issue) { create(:issue, project: project, assignees: [user]) }
 
         before do
           service.execute(user)
@@ -60,7 +60,7 @@ describe Users::DestroyService, services: true do
         it 'migrates the issue so that it is "Unassigned"' do
           migrated_issue = Issue.find_by_id(issue.id)
 
-          expect(migrated_issue.assignee).to be_nil
+          expect(migrated_issue.assignees).to be_empty
         end
       end
     end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 595bb92f5c091199bac93273d11fafd2190c7cb1..ad46b163cd61918589baed6ba607df2c26aceb54 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -63,7 +63,7 @@ shared_examples 'issuable record that supports slash commands in its description
         note = issuable.notes.user.first
 
         expect(note.note).to eq "Awesome!"
-        expect(issuable.assignee).to eq assignee
+        expect(issuable.assignees).to eq [assignee]
         expect(issuable.labels).to eq [label_bug]
         expect(issuable.milestone).to eq milestone
       end
@@ -81,7 +81,7 @@ shared_examples 'issuable record that supports slash commands in its description
         issuable.reload
 
         expect(issuable.notes.user).to be_empty
-        expect(issuable.assignee).to eq assignee
+        expect(issuable.assignees).to eq [assignee]
         expect(issuable.labels).to eq [label_bug]
         expect(issuable.milestone).to eq milestone
       end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 944ea30656fc9c7eb17f1783bf22be478b178c47..57b6abe12b7ca31442e89abea03e4a64860e973b 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -10,7 +10,7 @@ module ExportFileHelper
 
     create(:release, project: project)
 
-    issue = create(:issue, assignee: user, project: project)
+    issue = create(:issue, assignees: [user], project: project)
     snippet = create(:project_snippet, project: project)
     label = create(:label, project: project)
     milestone = create(:milestone, project: project)
diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb
deleted file mode 100644
index 4f0c745b7ee4853cac4c1f65e401ab114b286633..0000000000000000000000000000000000000000
--- a/spec/support/services/issuable_create_service_shared_examples.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-shared_examples 'issuable create service' do
-  context 'asssignee_id' do
-    let(:assignee) { create(:user) }
-
-    before { project.team << [user, :master] }
-
-    it 'removes assignee_id when user id is invalid' do
-      opts = { title: 'Title', description: 'Description', assignee_id: -1 }
-
-      issuable = described_class.new(project, user, opts).execute
-
-      expect(issuable.assignee_id).to be_nil
-    end
-
-    it 'removes assignee_id when user id is 0' do
-      opts = { title: 'Title', description: 'Description',  assignee_id: 0 }
-
-      issuable = described_class.new(project, user, opts).execute
-
-      expect(issuable.assignee_id).to be_nil
-    end
-
-    it 'saves assignee when user id is valid' do
-      project.team << [assignee, :master]
-      opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
-
-      issuable = described_class.new(project, user, opts).execute
-
-      expect(issuable.assignee_id).to eq(assignee.id)
-    end
-
-    context "when issuable feature is private" do
-      before do
-        project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
-                                       merge_requests_access_level: ProjectFeature::PRIVATE)
-      end
-
-      levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
-
-      levels.each do |level|
-        it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
-          project.update(visibility_level: level)
-          opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
-
-          issuable = described_class.new(project, user, opts).execute
-
-          expect(issuable.assignee_id).to be_nil
-        end
-      end
-    end
-  end
-end
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 9e9cdf3e48b4986e0cb5e3f57a1e55c526286434..1dd3663b944fde965a0a8f3786c25fa216760d1c 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -49,23 +49,7 @@ shared_examples 'new issuable record that supports slash commands' do
 
     it 'assigns and sets milestone to issuable' do
       expect(issuable).to be_persisted
-      expect(issuable.assignee).to eq(assignee)
-      expect(issuable.milestone).to eq(milestone)
-    end
-  end
-
-  context 'with assignee and milestone in params and command' do
-    let(:example_params) do
-      {
-        assignee: create(:user),
-        milestone_id: 1,
-        description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
-      }
-    end
-
-    it 'assigns and sets milestone to issuable from command' do
-      expect(issuable).to be_persisted
-      expect(issuable.assignee).to eq(assignee)
+      expect(issuable.assignees).to eq([assignee])
       expect(issuable.milestone).to eq(milestone)
     end
   end
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
index 49cea1e608cd98b7fa4bf478131da99a1d27c32a..8947f20562f8bc9c0cfd61cc236e48cded082dd5 100644
--- a/spec/support/services/issuable_update_service_shared_examples.rb
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -18,52 +18,4 @@ shared_examples 'issuable update service' do
       end
     end
   end
-
-  context 'asssignee_id' do
-    it 'does not update assignee when assignee_id is invalid' do
-      open_issuable.update(assignee_id: user.id)
-
-      update_issuable(assignee_id: -1)
-
-      expect(open_issuable.reload.assignee).to eq(user)
-    end
-
-    it 'unassigns assignee when user id is 0' do
-      open_issuable.update(assignee_id: user.id)
-
-      update_issuable(assignee_id: 0)
-
-      expect(open_issuable.assignee_id).to be_nil
-    end
-
-    it 'saves assignee when user id is valid' do
-      update_issuable(assignee_id: user.id)
-
-      expect(open_issuable.assignee_id).to eq(user.id)
-    end
-
-    it 'does not update assignee_id when user cannot read issue' do
-      non_member        = create(:user)
-      original_assignee = open_issuable.assignee
-
-      update_issuable(assignee_id: non_member.id)
-
-      expect(open_issuable.assignee_id).to eq(original_assignee.id)
-    end
-
-    context "when issuable feature is private" do
-      levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
-
-      levels.each do |level|
-        it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
-          assignee = create(:user)
-          project.update(visibility_level: level)
-          feature_visibility_attr = :"#{open_issuable.model_name.plural}_access_level"
-          project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
-
-          expect{ update_issuable(assignee_id: assignee) }.not_to change{ open_issuable.assignee }
-        end
-      end
-    end
-  end
 end
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
index e94e17da7e5880a7cb55aa9f985c03ebfcce4475..84ef46ffa27bfef38cdde24f9aff0eca5423c822 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -37,8 +37,7 @@ shared_examples 'issuable time tracker' do
     submit_time('/estimate 3w 1d 1h')
     submit_time('/remove_estimate')
 
-    wait_for_ajax
-    page.within '#issuable-time-tracker' do
+    page.within '.time-tracking-component-wrap' do
       expect(page).to have_content 'No estimate or time spent'
     end
   end
@@ -47,14 +46,13 @@ shared_examples 'issuable time tracker' do
     submit_time('/spend 3w 1d 1h')
     submit_time('/remove_time_spent')
 
-    wait_for_ajax
-    page.within '#issuable-time-tracker' do
+    page.within '.time-tracking-component-wrap' do
       expect(page).to have_content 'No estimate or time spent'
     end
   end
 
   it 'shows the help state when icon is clicked' do
-    page.within '#issuable-time-tracker' do
+    page.within '.time-tracking-component-wrap' do
       find('.help-button').click
       expect(page).to have_content 'Track time with slash commands'
       expect(page).to have_content 'Learn more'
@@ -62,7 +60,7 @@ shared_examples 'issuable time tracker' do
   end
 
   it 'hides the help state when close icon is clicked' do
-    page.within '#issuable-time-tracker' do
+    page.within '.time-tracking-component-wrap' do
       find('.help-button').click
       find('.close-help-button').click
 
@@ -72,7 +70,7 @@ shared_examples 'issuable time tracker' do
   end
 
   it 'displays the correct help url' do
-    page.within '#issuable-time-tracker' do
+    page.within '.time-tracking-component-wrap' do
       find('.help-button').click
 
       expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')