diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index f7693dd7102b8ef551b368d9e5de9dd2d1d0777d..9372618fb2a62c2b6509dced423a77dbd885db9b 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -6,8 +6,12 @@ import {
   GlDisclosureDropdownGroup,
   GlFilteredSearchToken,
   GlTooltipDirective,
+  GlDrawer,
+  GlLink,
 } from '@gitlab/ui';
 import * as Sentry from '@sentry/browser';
+
+import produce from 'immer';
 import fuzzaldrinPlus from 'fuzzaldrin-plus';
 import { isEmpty } from 'lodash';
 import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
@@ -63,6 +67,9 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro
 import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
 import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
+import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
+import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants';
 import {
   CREATED_DESC,
   defaultTypeTokenOptions,
@@ -98,6 +105,7 @@ import {
   getSortKey,
   getSortOptions,
   isSortKey,
+  mapWorkItemWidgetsToIssueFields,
 } from '../utils';
 import { hasNewIssueDropdown } from '../has_new_issue_dropdown_mixin';
 import EmptyStateWithAnyIssues from './empty_state_with_any_issues.vue';
@@ -131,12 +139,15 @@ export default {
     EmptyStateWithoutAnyIssues,
     GlButton,
     GlButtonGroup,
+    GlDrawer,
     IssuableByEmail,
     IssuableList,
     IssueCardStatistics,
     IssueCardTimeInfo,
     NewResourceDropdown,
     LocalStorageSync,
+    WorkItemDetail,
+    GlLink,
   },
   directives: {
     GlTooltip: GlTooltipDirective,
@@ -218,6 +229,7 @@ export default {
           },
         ],
       },
+      activeIssuable: null,
     };
   },
   apollo: {
@@ -230,6 +242,7 @@ export default {
         return data[this.namespace]?.issues.nodes ?? [];
       },
       fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+      nextFetchPolicy: fetchPolicies.CACHE_FIRST,
       // We need this for handling loading state when using frontend cache
       // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106004#note_1217325202 for details
       notifyOnNetworkStatusChange: true,
@@ -536,6 +549,12 @@ export default {
     isGridView() {
       return this.viewType === ISSUES_GRID_VIEW_KEY;
     },
+    isIssuableSelected() {
+      return !isEmpty(this.activeIssuable);
+    },
+    issuesDrawerEnabled() {
+      return this.glFeatures?.issuesListDrawer;
+    },
   },
   watch: {
     $route(newValue, oldValue) {
@@ -805,12 +824,96 @@ export default {
       // The default view is list view
       this.viewType = ISSUES_LIST_VIEW_KEY;
     },
+    handleSelectIssuable(issuable) {
+      this.activeIssuable = issuable;
+    },
+    updateIssuablesCache(workItem) {
+      const client = this.$apollo.provider.clients.defaultClient;
+      const issuesList = client.readQuery({
+        query: getIssuesQuery,
+        variables: this.queryVariables,
+      });
+
+      const activeIssuable = issuesList.project.issues.nodes.find(
+        (issue) => issue.iid === workItem.iid,
+      );
+
+      // when we change issuable state, it's moved to a different tab
+      // to ensure that we show 20 items of the first page, we need to refetch issuables
+      if (!activeIssuable.state.includes(workItem.state.toLowerCase())) {
+        this.refetchIssuables();
+        return;
+      }
+
+      // handle all other widgets
+      const data = mapWorkItemWidgetsToIssueFields(issuesList, workItem);
+
+      client.writeQuery({ query: getIssuesQuery, variables: this.queryVariables, data });
+    },
+    promoteToObjective(workItemIid) {
+      const { cache } = this.$apollo.provider.clients.defaultClient;
+
+      cache.updateQuery({ query: getIssuesQuery, variables: this.queryVariables }, (issuesList) =>
+        produce(issuesList, (draftData) => {
+          const activeItem = draftData.project.issues.nodes.find(
+            (issue) => issue.iid === workItemIid,
+          );
+
+          activeItem.type = WORK_ITEM_TYPE_ENUM_OBJECTIVE;
+        }),
+      );
+    },
+    refetchIssuables() {
+      this.$apollo.queries.issues.refetch();
+      this.$apollo.queries.issuesCounts.refetch();
+    },
+    deleteIssuable({ workItemId }) {
+      this.$apollo
+        .mutate({
+          mutation: deleteWorkItemMutation,
+          variables: { input: { id: workItemId } },
+        })
+        .then(({ data }) => {
+          if (data.workItemDelete.errors?.length) {
+            throw new Error(data.workItemDelete.errors[0]);
+          }
+          this.activeIssuable = null;
+          this.refetchIssuables();
+        })
+        .catch((error) => {
+          this.issuesError = this.$options.i18n.deleteError;
+          Sentry.captureException(error);
+        });
+    },
   },
 };
 </script>
 
 <template>
   <div>
+    <gl-drawer
+      v-if="issuesDrawerEnabled"
+      :open="isIssuableSelected"
+      header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
+      class="gl-w-40p gl-xs-w-full"
+      @close="activeIssuable = null"
+    >
+      <template #title>
+        <gl-link :href="activeIssuable.webUrl" class="gl-text-black-normal">{{
+          __('Open full view')
+        }}</gl-link>
+      </template>
+      <template #default>
+        <work-item-detail
+          :key="activeIssuable.iid"
+          :work-item-iid="activeIssuable.iid"
+          @work-item-updated="updateIssuablesCache"
+          @addChild="refetchIssuables"
+          @deleteWorkItem="deleteIssuable"
+          @promotedToObjective="promoteToObjective"
+        />
+      </template>
+    </gl-drawer>
     <issuable-list
       v-if="hasAnyIssues"
       :namespace="fullPath"
@@ -840,7 +943,9 @@ export default {
       :has-previous-page="pageInfo.hasPreviousPage"
       :show-filtered-search-friendly-text="hasOrFeature"
       :is-grid-view="isGridView"
+      :active-issuable="activeIssuable"
       show-work-item-type-icon
+      :prevent-redirect="issuesDrawerEnabled"
       @click-tab="handleClickTab"
       @dismiss-alert="handleDismissAlert"
       @filter="handleFilter"
@@ -850,6 +955,7 @@ export default {
       @sort="handleSort"
       @update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
       @page-size-change="handlePageSizeChange"
+      @select-issuable="handleSelectIssuable"
     >
       <template #nav-actions>
         <local-storage-sync
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 1a3d97277c787976f6e62f577f90c46eef6c1c12..a7933803ed4a795870ae0ce842aa3b26e7678424 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -115,6 +115,7 @@ export const i18n = {
   noSearchResultsTitle: __('Sorry, your filter produced no results'),
   relatedMergeRequests: __('Related merge requests'),
   reorderError: __('An error occurred while reordering issues.'),
+  deleteError: __('An error occurred while deleting an issuable.'),
   rssLabel: __('Subscribe to RSS feed'),
   searchPlaceholder: __('Search or filter results...'),
   upvotes: __('Upvotes'),
diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js
index e64870152bd1967a3d674c7ec3396d565c745509..6e9a566cb5c445dd90fef13a39567eca69592e84 100644
--- a/app/assets/javascripts/issues/list/graphql.js
+++ b/app/assets/javascripts/issues/list/graphql.js
@@ -1,6 +1,7 @@
 import produce from 'immer';
 import createDefaultClient, { createApolloClientWithCaching } from '~/lib/graphql';
 import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import { config } from '~/graphql_shared/issuable_client';
 
 let client;
 
@@ -27,7 +28,7 @@ const resolvers = {
 export async function gqlClient() {
   if (client) return client;
   client = gon.features?.frontendCaching
-    ? await createApolloClientWithCaching(resolvers, { localCacheKey: 'issues_list' })
-    : createDefaultClient(resolvers);
+    ? await createApolloClientWithCaching(resolvers, { localCacheKey: 'issues_list', ...config })
+    : createDefaultClient(resolvers, config);
   return client;
 }
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index d1b4529402608c975cc6beef2deabcdeb40c0171..511d6e6f4ce439fdf58a9c74055a49cfb26b7914 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -95,6 +95,8 @@ export async function mountIssuesListApp() {
     showNewIssueLink,
     signInPath,
     groupId = '',
+    reportAbusePath,
+    registerPath,
   } = el.dataset;
 
   return new Vue({
@@ -117,7 +119,10 @@ export async function mountIssuesListApp() {
       canReadCrmOrganization: parseBoolean(canReadCrmOrganization),
       emptyStateSvgPath,
       fullPath,
+      projectPath: fullPath,
       groupPath,
+      reportAbusePath,
+      registerPath,
       hasAnyIssues: parseBoolean(hasAnyIssues),
       hasAnyProjects: parseBoolean(hasAnyProjects),
       hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index d053400dd033f568deb7a362ccf48e9b6fa78137..8859021b16ca272db9c72c1ceb4e5bf2a06856bb 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -1,3 +1,4 @@
+import produce from 'immer';
 import { isPositiveInteger } from '~/lib/utils/number_utils';
 import { getParameterByName } from '~/lib/utils/url_utility';
 import { __ } from '~/locale';
@@ -16,6 +17,15 @@ import {
   TOKEN_TYPE_LABEL,
 } from '~/vue_shared/components/filtered_search_bar/constants';
 import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants';
+import {
+  WORK_ITEM_TO_ISSUE_MAP,
+  WIDGET_TYPE_MILESTONE,
+  WIDGET_TYPE_AWARD_EMOJI,
+  EMOJI_THUMBSUP,
+  EMOJI_THUMBSDOWN,
+  WIDGET_TYPE_ASSIGNEES,
+  WIDGET_TYPE_LABELS,
+} from '~/work_items/constants';
 import {
   ALTERNATIVE_FILTER,
   API_PARAM,
@@ -318,3 +328,59 @@ export const convertToSearchQuery = (filterTokens) =>
     .filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data)
     .map((token) => token.value.data)
     .join(' ') || undefined;
+
+function findWidget(type, workItem) {
+  return workItem?.widgets?.find((widget) => widget.type === type);
+}
+
+export function mapWorkItemWidgetsToIssueFields(issuesList, workItem) {
+  return produce(issuesList, (draftData) => {
+    const activeItem = draftData.project.issues.nodes.find((issue) => issue.iid === workItem.iid);
+
+    Object.keys(WORK_ITEM_TO_ISSUE_MAP).forEach((type) => {
+      const currentWidget = findWidget(type, workItem);
+      if (!currentWidget) {
+        return;
+      }
+      const property = WORK_ITEM_TO_ISSUE_MAP[type];
+
+      // handling the case for assignees and labels
+      if (
+        property === WORK_ITEM_TO_ISSUE_MAP[WIDGET_TYPE_ASSIGNEES] ||
+        property === WORK_ITEM_TO_ISSUE_MAP[WIDGET_TYPE_LABELS]
+      ) {
+        activeItem[property] = {
+          ...currentWidget[property],
+          nodes: currentWidget[property].nodes.map((node) => ({
+            __persist: true,
+            ...node,
+          })),
+        };
+        return;
+      }
+
+      // handling the case for milestone
+      if (property === WORK_ITEM_TO_ISSUE_MAP[WIDGET_TYPE_MILESTONE] && currentWidget[property]) {
+        activeItem[property] = { __persist: true, ...currentWidget[property] };
+        return;
+      }
+
+      // handling emojis
+      if (property === WORK_ITEM_TO_ISSUE_MAP[WIDGET_TYPE_AWARD_EMOJI]) {
+        const upvotesCount =
+          currentWidget[property].nodes.filter((emoji) => emoji.name === EMOJI_THUMBSUP)?.length ??
+          0;
+        const downvotesCount =
+          currentWidget[property].nodes.filter((emoji) => emoji.name === EMOJI_THUMBSDOWN)
+            ?.length ?? 0;
+        activeItem.upvotes = upvotesCount;
+        activeItem.downvotes = downvotesCount;
+        return;
+      }
+      activeItem[property] = currentWidget[property];
+    });
+
+    activeItem.title = workItem.title;
+    activeItem.confidential = workItem.confidential;
+  });
+}
diff --git a/app/assets/javascripts/lib/apollo/persistence_mapper.js b/app/assets/javascripts/lib/apollo/persistence_mapper.js
index f8ae180107ce6a3bed5818380d48d809c6ef88ad..56f6606808e87169917c8a8b4c92389dd26663f5 100644
--- a/app/assets/javascripts/lib/apollo/persistence_mapper.js
+++ b/app/assets/javascripts/lib/apollo/persistence_mapper.js
@@ -50,7 +50,7 @@ export const persistenceMapper = async (data) => {
     // we need this to prevent overcaching when we fetch the same entity (e.g. project) more than once
     // with different set of fields
 
-    if (Object.values(rootQuery).some((value) => value.__ref === key)) {
+    if (Object.values(rootQuery).some((value) => value?.__ref === key)) {
       const mappedEntity = {};
       Object.entries(parsedEntity).forEach(([parsedKey, parsedValue]) => {
         if (!parsedValue || typeof parsedValue !== 'object' || parsedValue['__persist']) {
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index ce33d7a9b4b0a2033f9c084ddfae835b9db7384f..37aedc4ff0955f7cdf768f75c5883703bd8d033a 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -57,6 +57,16 @@ export default {
       required: false,
       default: false,
     },
+    isActive: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    preventRedirect: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
   computed: {
     issuableId() {
@@ -177,6 +187,13 @@ export default {
       }
       return '';
     },
+    handleIssuableItemClick(e) {
+      if (e.metaKey || e.ctrlKey || !this.preventRedirect) {
+        return;
+      }
+      e.preventDefault();
+      this.$emit('select-issuable', { iid: this.issuableIid, webUrl: this.webUrl });
+    },
   },
 };
 </script>
@@ -185,9 +202,10 @@ export default {
   <li
     :id="`issuable_${issuableId}`"
     class="issue gl-display-flex! gl-px-5!"
-    :class="{ closed: issuable.closedAt }"
+    :class="{ closed: issuable.closedAt, 'gl-bg-blue-50': isActive }"
     :data-labels="labelIdsString"
     :data-qa-issue-id="issuableId"
+    data-testid="issuable-item-wrapper"
   >
     <gl-form-checkbox
       v-if="showCheckbox"
@@ -226,7 +244,9 @@ export default {
           dir="auto"
           :href="webUrl"
           data-qa-selector="issuable_title_link"
+          data-testid="issuable-title-link"
           v-bind="issuableTitleProps"
+          @click="handleIssuableItemClick"
         >
           {{ issuable.title }}
           <gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 4023337a1cb44b7ca18b83f2c3f460af7ef6a6a5..7a9404e06c7f2f8d620cd20d2a0be85562b0675e 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -203,6 +203,16 @@ export default {
       required: false,
       default: false,
     },
+    activeIssuable: {
+      type: Object,
+      required: false,
+      default: null,
+    },
+    preventRedirect: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
   data() {
     return {
@@ -299,6 +309,9 @@ export default {
     handlePageSizeChange(newPageSize) {
       this.$emit('page-size-change', newPageSize);
     },
+    isIssuableActive(issuable) {
+      return Boolean(issuable.iid === this.activeIssuable?.iid);
+    },
   },
   PAGE_SIZE_STORAGE_KEY,
 };
@@ -373,7 +386,10 @@ export default {
           :show-checkbox="showBulkEditSidebar"
           :checked="issuableChecked(issuable)"
           :show-work-item-type-icon="showWorkItemTypeIcon"
+          :prevent-redirect="preventRedirect"
+          :is-active="isIssuableActive(issuable)"
           @checked-input="handleIssuableCheckedInput(issuable, $event)"
+          @select-issuable="$emit('select-issuable', $event)"
         >
           <template #reference>
             <slot name="reference" :issuable="issuable"></slot>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 76a04bede611bf88d211f34825ef60669fcff98b..6ca487d5427fec3bf8b41d25bfe527aeb965e374 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -275,6 +275,7 @@ export default {
         }
         this.$toast.show(s__('WorkItem|Promoted to objective.'));
         this.track('promote_kr_to_objective');
+        this.$emit('promotedToObjective');
       } catch (error) {
         this.throwConvertError();
         Sentry.captureException(error);
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index bbf1f39e30802643e1244bb7cc31bb412ebae83b..13c4aff09da959c725ad7c5d8ccbc651cda84a58 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -134,6 +134,7 @@ export default {
         if (!res.data) {
           return;
         }
+        this.$emit('work-item-updated', this.workItem);
         if (isEmpty(this.workItem)) {
           this.setEmptyState();
         }
@@ -467,6 +468,7 @@ export default {
             @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
             @toggleWorkItemConfidentiality="toggleConfidentiality"
             @error="updateError = $event"
+            @promotedToObjective="$emit('promotedToObjective', workItemIid)"
           />
           <gl-button
             v-if="isModal"
@@ -542,6 +544,7 @@ export default {
                   "
                   @toggleWorkItemConfidentiality="toggleConfidentiality"
                   @error="updateError = $event"
+                  @promotedToObjective="$emit('promotedToObjective', workItemIid)"
                 />
               </div>
             </div>
@@ -584,6 +587,7 @@ export default {
               :can-update="canUpdate"
               :confidential="workItem.confidential"
               @show-modal="openInModal"
+              @addChild="$emit('addChild')"
             />
             <work-item-notes
               v-if="workItemNotes"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index db649913602f33f191cb4f5a6072811675aaad9f..4960189fb488a748b859935b8b36568e12c3f9a9 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -306,6 +306,7 @@ export default {
             [this.error] = data.workItemCreate.errors;
           } else {
             this.unsetError();
+            this.$emit('addChild');
           }
         })
         .catch(() => {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index 83f3c391769d30d8ba3dc882415731e2ef74b203..246eac82c784115bc78479284cc2be8f6683ac6a 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -145,6 +145,7 @@ export default {
         :children-ids="childrenIds"
         :parent-confidential="confidential"
         @cancel="hideAddForm"
+        @addChild="$emit('addChild')"
       />
       <work-item-children-wrapper
         :children="children"
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index b8324d7d5522c453d5428836bb67c3f4736d34c2..7f03ba7f6d3a28a6c638ace0020f0c303b094c31 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -247,3 +247,13 @@ export const EMOJI_ACTION_ADD = 'ADD';
 export const EMOJI_ACTION_REMOVE = 'REMOVE';
 export const EMOJI_THUMBSUP = 'thumbsup';
 export const EMOJI_THUMBSDOWN = 'thumbsdown';
+
+export const WORK_ITEM_TO_ISSUE_MAP = {
+  [WIDGET_TYPE_ASSIGNEES]: 'assignees',
+  [WIDGET_TYPE_LABELS]: 'labels',
+  [WIDGET_TYPE_MILESTONE]: 'milestone',
+  [WIDGET_TYPE_WEIGHT]: 'weight',
+  [WIDGET_TYPE_START_AND_DUE_DATE]: 'dueDate',
+  [WIDGET_TYPE_HEALTH_STATUS]: 'healthStatus',
+  [WIDGET_TYPE_AWARD_EMOJI]: 'awardEmoji',
+};
diff --git a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
index 5c93370aac9356f1d7666dab344e11ee8c3fd1c8..9828363990b525f4fe21cda6fd9585a396b7a991 100644
--- a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
@@ -5,4 +5,5 @@ fragment MilestoneFragment on Milestone {
   state
   startDate
   dueDate
+  webPath
 }
diff --git a/app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql
index 4c3be007d96643ba6191689e89981d1a66777bec..66ac9dcd8d1800c6ed2865c55c36d56cacb3cdd1 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql
@@ -1,7 +1,7 @@
 #import "./work_item.fragment.graphql"
 
 query workItemByIid($fullPath: ID!, $iid: String) {
-  workspace: project(fullPath: $fullPath) {
+  workspace: project(fullPath: $fullPath) @persist {
     id
     workItems(iid: $iid) {
       nodes {
diff --git a/app/assets/stylesheets/page_bundles/issues_list.scss b/app/assets/stylesheets/page_bundles/issues_list.scss
index f39dee12126739644a663bd2e7eeec9327f0d5ca..b5afb8cdf4dc00dc013656177f2258a1d1e06399 100644
--- a/app/assets/stylesheets/page_bundles/issues_list.scss
+++ b/app/assets/stylesheets/page_bundles/issues_list.scss
@@ -34,3 +34,13 @@
   opacity: 0.3;
   pointer-events: none;
 }
+
+.work-item-labels {
+  .gl-token {
+    padding-left: $gl-spacing-scale-1;
+  }
+
+  .gl-token-close {
+    display: none;
+  }
+}
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 6a45595580f512c90d049ae4ea4edc1fdc87b9d7..045278e370b7646c8723150b44bd14344649bac4 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
     push_frontend_feature_flag(:saved_replies, current_user)
     push_frontend_feature_flag(:issues_grid_view)
     push_frontend_feature_flag(:service_desk_ticket)
+    push_frontend_feature_flag(:issues_list_drawer)
   end
 
   before_action only: [:index, :show] do
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index d9b9b27d16c5d58f49a0d0840024fef90a2940a2..bbd8d2dd0a9256c74dd3c64617029857158d30f6 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -220,7 +220,9 @@ def project_issues_list_data(project, current_user)
       quick_actions_help_path: help_page_path('user/project/quick_actions'),
       releases_path: project_releases_path(project, format: :json),
       reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
-      show_new_issue_link: show_new_issue_link?(project).to_s
+      show_new_issue_link: show_new_issue_link?(project).to_s,
+      report_abuse_path: add_category_abuse_reports_path,
+      register_path: new_user_registration_path(redirect_to_referer: 'yes')
     )
   end
 
diff --git a/config/feature_flags/development/issues_list_drawer.yml b/config/feature_flags/development/issues_list_drawer.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dba17d109a34ce8ec9f2defbaad9d254ecafcd2b
--- /dev/null
+++ b/config/feature_flags/development/issues_list_drawer.yml
@@ -0,0 +1,8 @@
+---
+name: issues_list_drawer
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123019
+rollout_issue_url:
+milestone: '16.3'
+type: development
+group: group::product planning
+default_enabled: false
diff --git a/ee/spec/frontend/external_issues_list/components/__snapshots__/external_issues_list_root_spec.js.snap b/ee/spec/frontend/external_issues_list/components/__snapshots__/external_issues_list_root_spec.js.snap
index 26f099f888775e44f060e230e0107f8f13c9ab2a..c989558017771a1e638e744904d5387b57a66caf 100644
--- a/ee/spec/frontend/external_issues_list/components/__snapshots__/external_issues_list_root_spec.js.snap
+++ b/ee/spec/frontend/external_issues_list/components/__snapshots__/external_issues_list_root_spec.js.snap
@@ -10,6 +10,7 @@ exports[`ExternalIssuesListRoot error handling when request fails displays error
 
 exports[`ExternalIssuesListRoot when request succeeds renders issuable-list component with correct props 1`] = `
 Object {
+  "activeIssuable": null,
   "currentPage": 1,
   "currentTab": "opened",
   "defaultPageSize": 2,
@@ -120,6 +121,7 @@ Object {
   "labelFilterParam": "labels",
   "namespace": "gitlab-org/gitlab-test",
   "nextPage": 2,
+  "preventRedirect": false,
   "previousPage": 0,
   "recentSearchesStorageKey": "jira_issues",
   "searchInputPlaceholder": "Search Jira issues",
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7960d24b762e392f307f3976ba324fb97b5eb230..fc4b58c1b097f2e58729c848699f8a2281443577 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4901,6 +4901,9 @@ msgstr ""
 msgid "An error occurred while decoding the file."
 msgstr ""
 
+msgid "An error occurred while deleting an issuable."
+msgstr ""
+
 msgid "An error occurred while deleting the approvers group"
 msgstr ""
 
@@ -32242,6 +32245,9 @@ msgstr ""
 msgid "Open evidence JSON in new tab"
 msgstr ""
 
+msgid "Open full view"
+msgstr ""
+
 msgid "Open in Gitpod"
 msgstr ""
 
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 72bf48260568d8fa44e5a8b5b31dd3ea8a5a1fd2..31c6df953c6ce9b7ba0a526cedd0d4b6d7ec6ef8 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown, GlDrawer } from '@gitlab/ui';
 import * as Sentry from '@sentry/browser';
 import { mount, shallowMount } from '@vue/test-utils';
 import AxiosMockAdapter from 'axios-mock-adapter';
@@ -70,6 +70,14 @@ import {
   TOKEN_TYPE_SEARCH_WITHIN,
   TOKEN_TYPE_TYPE,
 } from '~/vue_shared/components/filtered_search_bar/constants';
+import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
+import {
+  workItemResponseFactory,
+  mockAssignees,
+  mockLabels,
+  mockMilestone,
+} from 'jest/work_items/mock_data';
 
 import('~/issuable');
 import('~/users_select');
@@ -130,6 +138,10 @@ describe('CE IssuesListApp component', () => {
   const mockIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
   const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
 
+  const deleteWorkItemMutationHandler = jest
+    .fn()
+    .mockResolvedValue({ data: { workItemDelete: { errors: [] } } });
+
   const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
   const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
   const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
@@ -142,6 +154,8 @@ describe('CE IssuesListApp component', () => {
   const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown);
   const findCalendarButton = () => wrapper.findByTestId('subscribe-calendar');
   const findRssButton = () => wrapper.findByTestId('subscribe-rss');
+  const findIssuableDrawer = () => wrapper.findComponent(GlDrawer);
+  const findDrawerWorkItem = () => wrapper.findComponent(WorkItemDetail);
 
   const findLabelsToken = () =>
     findIssuableList()
@@ -156,11 +170,13 @@ describe('CE IssuesListApp component', () => {
     sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
     stubs = {},
     mountFn = shallowMount,
+    deleteMutationHandler = deleteWorkItemMutationHandler,
   } = {}) => {
     const requestHandlers = [
       [getIssuesQuery, issuesQueryResponse],
       [getIssuesCountsQuery, issuesCountsQueryResponse],
       [setSortPreferenceMutation, sortPreferenceMutationResponse],
+      [deleteWorkItemMutation, deleteMutationHandler],
     ];
 
     router = new VueRouter({ mode: 'history' });
@@ -1017,4 +1033,209 @@ describe('CE IssuesListApp component', () => {
       expect(findLabelsToken().fetchLatestLabels).toBe(null);
     });
   });
+
+  describe('when issue drawer is enabled', () => {
+    beforeEach(() => {
+      wrapper = mountComponent({
+        provide: {
+          glFeatures: {
+            issuesListDrawer: true,
+          },
+        },
+        stubs: {
+          GlDrawer,
+        },
+      });
+    });
+
+    it('renders issuable drawer component', () => {
+      expect(findIssuableDrawer().exists()).toBe(true);
+    });
+
+    it('renders issuable drawer closed by default', () => {
+      expect(findIssuableDrawer().props('open')).toBe(false);
+    });
+
+    describe('on selecting an issuable', () => {
+      beforeEach(() => {
+        findIssuableList().vm.$emit(
+          'select-issuable',
+          getIssuesQueryResponse.data.project.issues.nodes[0],
+        );
+        return nextTick();
+      });
+
+      it('opens issuable drawer', () => {
+        expect(findIssuableDrawer().props('open')).toBe(true);
+      });
+
+      it('selects active issuable', () => {
+        expect(findIssuableList().props('activeIssuable')).toEqual(
+          getIssuesQueryResponse.data.project.issues.nodes[0],
+        );
+      });
+
+      describe('when closing the drawer', () => {
+        it('closes the drawer on drawer `close` event', async () => {
+          findIssuableDrawer().vm.$emit('close');
+          await nextTick();
+
+          expect(findIssuableDrawer().props('open')).toBe(false);
+        });
+
+        it('removes active issuable', async () => {
+          findIssuableDrawer().vm.$emit('close');
+          await nextTick();
+
+          expect(findIssuableList().props('activeIssuable')).toBe(null);
+        });
+      });
+
+      describe('when updating an issuable', () => {
+        it('refetches the list if the issuable changed state', async () => {
+          const {
+            data: { workItem },
+          } = workItemResponseFactory({ iid: '789', state: 'CLOSED' });
+          findDrawerWorkItem().vm.$emit('work-item-updated', workItem);
+
+          await waitForPromises();
+
+          expect(mockIssuesQueryResponse).toHaveBeenCalledTimes(2);
+          expect(mockIssuesCountsQueryResponse).toHaveBeenCalledTimes(2);
+        });
+
+        it('updates the assignees field of active issuable', async () => {
+          const {
+            data: { workItem },
+          } = workItemResponseFactory({ iid: '789' });
+          findDrawerWorkItem().vm.$emit('work-item-updated', workItem);
+
+          await waitForPromises();
+
+          expect(findIssuableList().props('issuables')[0].assignees.nodes).toEqual(
+            mockAssignees.map((assignee) => ({
+              ...assignee,
+              __persist: true,
+            })),
+          );
+        });
+
+        it('updates the labels field of active issuable', async () => {
+          const {
+            data: { workItem },
+          } = workItemResponseFactory({ iid: '789' });
+          findDrawerWorkItem().vm.$emit('work-item-updated', workItem);
+
+          await waitForPromises();
+
+          expect(findIssuableList().props('issuables')[0].labels.nodes).toEqual(
+            mockLabels.map((label) => ({
+              ...label,
+              __persist: true,
+              textColor: undefined,
+            })),
+          );
+        });
+
+        it('updates the milestone field of active issuable', async () => {
+          const {
+            data: { workItem },
+          } = workItemResponseFactory({ iid: '789' });
+          findDrawerWorkItem().vm.$emit('work-item-updated', workItem);
+
+          await waitForPromises();
+
+          expect(findIssuableList().props('issuables')[0].milestone).toEqual({
+            ...mockMilestone,
+            __persist: true,
+            expired: undefined,
+            state: undefined,
+          });
+        });
+
+        it('updates the title and confidential state of active issuable', async () => {
+          const {
+            data: { workItem },
+          } = workItemResponseFactory({ iid: '789', confidential: true });
+          findDrawerWorkItem().vm.$emit('work-item-updated', workItem);
+
+          await waitForPromises();
+
+          expect(findIssuableList().props('issuables')[0].title).toBe('Updated title');
+          expect(findIssuableList().props('issuables')[0].confidential).toBe(true);
+        });
+
+        it('refetches the list if new child was added to active issuable', async () => {
+          findDrawerWorkItem().vm.$emit('addChild');
+
+          await waitForPromises();
+
+          expect(mockIssuesQueryResponse).toHaveBeenCalledTimes(2);
+          expect(mockIssuesCountsQueryResponse).toHaveBeenCalledTimes(2);
+        });
+
+        it('updates issuable type to objective if promoted to objective', async () => {
+          findDrawerWorkItem().vm.$emit('promotedToObjective', '789');
+
+          await waitForPromises();
+          // required for cache updates
+          jest.runOnlyPendingTimers();
+          await nextTick();
+
+          expect(findIssuableList().props('issuables')[0].type).toBe('OBJECTIVE');
+        });
+      });
+
+      describe('when deleting an issuable from the drawer', () => {
+        beforeEach(async () => {
+          const {
+            data: { workItem },
+          } = workItemResponseFactory({ iid: '789' });
+          findDrawerWorkItem().vm.$emit('deleteWorkItem', workItem);
+
+          await waitForPromises();
+        });
+
+        it('should refetch issues and issues count', () => {
+          expect(mockIssuesQueryResponse).toHaveBeenCalledTimes(2);
+          expect(mockIssuesCountsQueryResponse).toHaveBeenCalledTimes(2);
+        });
+
+        it('should close the issue drawer', () => {
+          expect(findIssuableDrawer().props('open')).toBe(false);
+        });
+      });
+    });
+  });
+
+  it('shows an error when deleting from the drawer fails', async () => {
+    const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
+    const {
+      data: { workItem },
+    } = workItemResponseFactory({ iid: '789' });
+
+    wrapper = mountComponent({
+      provide: {
+        glFeatures: {
+          issuesListDrawer: true,
+        },
+      },
+      stubs: {
+        GlDrawer,
+      },
+      deleteMutationHandler: errorHandler,
+    });
+
+    findIssuableList().vm.$emit(
+      'select-issuable',
+      getIssuesQueryResponse.data.project.issues.nodes[0],
+    );
+    await nextTick();
+
+    findDrawerWorkItem().vm.$emit('deleteWorkItem', workItem);
+    await waitForPromises();
+
+    expect(Sentry.captureException).toHaveBeenCalled();
+    expect(findIssuableList().props('error')).toBe('An error occurred while deleting an issuable.');
+  });
 });
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 502fa609ebca32f59740e975613998fc0dc8e02a..77333a878d173343d12f3f7977c78aa2bfe095ae 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -15,6 +15,8 @@ const createComponent = ({
   showCheckbox = true,
   slots = {},
   showWorkItemTypeIcon = false,
+  isActive = false,
+  preventRedirect = false,
 } = {}) =>
   shallowMount(IssuableItem, {
     propsData: {
@@ -24,6 +26,8 @@ const createComponent = ({
       showDiscussions: true,
       showCheckbox,
       showWorkItemTypeIcon,
+      isActive,
+      preventRedirect,
     },
     slots,
     stubs: {
@@ -43,6 +47,8 @@ describe('IssuableItem', () => {
 
   const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]');
   const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
+  const findIssuableTitleLink = () => wrapper.findComponentByTestId('issuable-title-link');
+  const findIssuableItemWrapper = () => wrapper.findByTestId('issuable-item-wrapper');
 
   beforeEach(() => {
     gon.gitlab_url = MOCK_GITLAB_URL;
@@ -553,4 +559,35 @@ describe('IssuableItem', () => {
       });
     });
   });
+
+  describe('when preventing redirect on clicking the link', () => {
+    it('emits an event on item click', () => {
+      const { iid, webUrl } = mockIssuable;
+
+      wrapper = createComponent({
+        preventRedirect: true,
+      });
+
+      findIssuableTitleLink().vm.$emit('click', new MouseEvent('click'));
+
+      expect(wrapper.emitted('select-issuable')).toEqual([[{ iid, webUrl }]]);
+    });
+
+    it('does not apply highlighted class when item is not active', () => {
+      wrapper = createComponent({
+        preventRedirect: true,
+      });
+
+      expect(findIssuableItemWrapper().classes('gl-bg-blue-50')).toBe(false);
+    });
+
+    it('applies highlghted class when item is active', () => {
+      wrapper = createComponent({
+        isActive: true,
+        preventRedirect: true,
+      });
+
+      expect(findIssuableItemWrapper().classes('gl-bg-blue-50')).toBe(true);
+    });
+  });
 });
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 68904603f408a30e3c64bfdfd28f481c6bd6c6d3..51aae9b45123617c99330b37b284fbf37c85cc9b 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -530,4 +530,28 @@ describe('IssuableListRoot', () => {
       expect(findIssuableGrid().exists()).toBe(true);
     });
   });
+
+  it('passes `isActive` prop as false if there is no active issuable', () => {
+    wrapper = createComponent({});
+
+    expect(findIssuableItem().props('isActive')).toBe(false);
+  });
+
+  it('passes `isActive` prop as true if active issuable matches issuable item', () => {
+    wrapper = createComponent({
+      props: {
+        activeIssuable: mockIssuableListProps.issuables[0],
+      },
+    });
+
+    expect(findIssuableItem().props('isActive')).toBe(true);
+  });
+
+  it('emits `select-issuable` event on emitting `select-issuable` from issuable item', () => {
+    const mockIssuable = mockIssuableListProps.issuables[0];
+    wrapper = createComponent({});
+    findIssuableItem().vm.$emit('select-issuable', mockIssuable);
+
+    expect(wrapper.emitted('select-issuable')).toEqual([[mockIssuable]]);
+  });
 });
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index e03c6a7e28d9a95f9832a541ced706b8fc5d7b58..cbe026cd014f244c8062c37343e793bbb736191e 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -359,6 +359,7 @@ describe('WorkItemActions component', () => {
 
       expect(convertWorkItemMutationSuccessHandler).toHaveBeenCalled();
       expect($toast.show).toHaveBeenCalledWith('Promoted to objective.');
+      expect(wrapper.emitted('promotedToObjective')).toEqual([[]]);
     });
 
     it('emits error when promote mutation fails', async () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 5f7f56d7063546e2c03dc1fb87b18ca9511635d1..8caacc2dc972c043c01204f0dd556abd0182ed32 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -94,7 +94,7 @@ describe('WorkItemLinksForm', () => {
         preventDefault: jest.fn(),
       });
       await waitForPromises();
-      expect(wrapper.vm.childWorkItemType).toEqual('gid://gitlab/WorkItems::Type/3');
+
       expect(createMutationResolver).toHaveBeenCalledWith({
         input: {
           title: 'Create task test',
@@ -106,6 +106,7 @@ describe('WorkItemLinksForm', () => {
           confidential: false,
         },
       });
+      expect(wrapper.emitted('addChild')).toEqual([[]]);
     });
 
     it('creates child task in confidential parent', async () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index e90775a52400cb95ae595bf08920ba3dd46b05bf..01fa4591cde5ee9fe7dc3efe2cfc22d8ca5b285a 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -118,4 +118,14 @@ describe('WorkItemTree', () => {
       expect(findWorkItemLinkChildrenWrapper().props('canUpdate')).toBe(false);
     });
   });
+
+  it('emits `addChild` event when form emits `addChild` event', async () => {
+    createComponent();
+
+    findToggleFormSplitButton().vm.$emit('showCreateObjectiveForm');
+    await nextTick();
+    findForm().vm.$emit('addChild');
+
+    expect(wrapper.emitted('addChild')).toEqual([[]]);
+  });
 });
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index f88e69a7ffe3efc9063f08b85ac861e85c2fd3e6..d7e5c02ffbe52411412b38389c347fb1f667c868 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -44,6 +44,7 @@ export const mockMilestone = {
   expired: false,
   startDate: '2022-10-17',
   dueDate: '2022-10-24',
+  webPath: '123',
 };
 
 export const mockAwardEmojiThumbsUp = {
@@ -451,6 +452,7 @@ export const objectiveType = {
 };
 
 export const workItemResponseFactory = ({
+  iid = '1',
   canUpdate = false,
   canDelete = false,
   canCreateNote = false,
@@ -482,14 +484,15 @@ export const workItemResponseFactory = ({
   createdAt = '2022-08-03T12:41:54Z',
   updatedAt = '2022-08-08T12:32:54Z',
   awardEmoji = mockAwardsWidget,
+  state = 'OPEN',
 } = {}) => ({
   data: {
     workItem: {
       __typename: 'WorkItem',
       id: 'gid://gitlab/WorkItem/1',
-      iid: '1',
+      iid,
       title: 'Updated title',
-      state: 'OPEN',
+      state,
       description: 'description',
       confidential,
       createdAt,