From dfbec8b4924b37c6fb593d40df96e4477dbc3039 Mon Sep 17 00:00:00 2001
From: Chad Lavimoniere <clavimoniere@gitlab.com>
Date: Wed, 10 Apr 2024 13:48:54 -0400
Subject: [PATCH] When localStorage draft exists for comment reply, show it on
 load

When a user has a comment draft saved in localstorage, we do not
indicate that on page load at all; the user can only see their comment
draft by clicking reply and seeing the draft populated into the markdown
editor.

This MR changes the noteable_discussion component's behavior such that
it exposes the markdown editor with the user's comment draft on page
load if there is a saved draft for the comment in localstorage.

Changelog: fixed
---
 app/assets/javascripts/lib/utils/autosave.js  |  3 ++
 .../notes/components/noteable_discussion.vue  | 14 +++++----
 .../notes/components/notes_app.vue            | 11 ++++++-
 .../components/noteable_discussion_spec.js    | 29 +++++++++++++++++++
 4 files changed, 51 insertions(+), 6 deletions(-)

diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js
index 01316be06a2e..9cf817bc5a1a 100644
--- a/app/assets/javascripts/lib/utils/autosave.js
+++ b/app/assets/javascripts/lib/utils/autosave.js
@@ -63,3 +63,6 @@ export const updateDraft = (autosaveKey, text, lockVersion) => {
 export const getDiscussionReplyKey = (noteableType, discussionId) =>
   /* eslint-disable-next-line @gitlab/require-i18n-strings */
   ['Note', capitalizeFirstCharacter(noteableType), discussionId, 'Reply'].join('/');
+
+export const getAutoSaveKeyFromDiscussion = (discussion) =>
+  getDiscussionReplyKey(discussion.notes.slice(0, 1)[0].noteable_type, discussion.id);
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 493beb8cea9e..394f64d51bdb 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -4,7 +4,7 @@ import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
 import { mapActions, mapGetters } from 'vuex';
 import DraftNote from '~/batch_comments/components/draft_note.vue';
 import { createAlert } from '~/alert';
-import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
+import { clearDraft, getDraft, getAutoSaveKeyFromDiscussion } from '~/lib/utils/autosave';
 import { isLoggedIn } from '~/lib/utils/common_utils';
 import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
 import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
@@ -119,14 +119,11 @@ export default {
       return this.discussion.internal ? __('internal note') : __('comment');
     },
     autosaveKey() {
-      return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
+      return getAutoSaveKeyFromDiscussion(this.discussion);
     },
     newNotePath() {
       return this.getNoteableData.create_note_path;
     },
-    firstNote() {
-      return this.discussion.notes.slice(0, 1)[0];
-    },
     saveButtonTitle() {
       return this.discussion.internal ? __('Reply internally') : __('Reply');
     },
@@ -187,9 +184,15 @@ export default {
         'gl-pt-0!': !this.discussion.diff_discussion && this.isReplying,
       };
     },
+    hasDraft() {
+      return Boolean(getDraft(this.autosaveKey));
+    },
   },
   created() {
     eventHub.$on('startReplying', this.onStartReplying);
+    if (this.hasDraft) {
+      this.showReplyForm();
+    }
   },
   beforeDestroy() {
     eventHub.$off('startReplying', this.onStartReplying);
@@ -360,6 +363,7 @@ export default {
                     :diff-file="diffFile"
                     :line="diffLine"
                     :save-button-title="saveButtonTitle"
+                    :autofocus="!hasDraft"
                     :autosave-key="autosaveKey"
                     @handleFormUpdateAddToReview="addReplyToReview"
                     @handleFormUpdate="saveReply"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index eb6764a79378..b9d3afb54041 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -3,6 +3,7 @@
 import { mapGetters, mapActions } from 'vuex';
 import { v4 as uuidv4 } from 'uuid';
 import { InternalEvents } from '~/tracking';
+import { getDraft, getAutoSaveKeyFromDiscussion } from '~/lib/utils/autosave';
 import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
 import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
 import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
@@ -197,7 +198,11 @@ export default {
       'fetchNotes',
     ]),
     discussionIsIndividualNoteAndNotConverted(discussion) {
-      return discussion.individual_note && !this.convertedDisscussionIds.includes(discussion.id);
+      return (
+        discussion.individual_note &&
+        !this.convertedDisscussionIds.includes(discussion.id) &&
+        !this.hasDraft(discussion)
+      );
     },
     handleHashChanged() {
       const noteId = this.checkLocationHash();
@@ -238,6 +243,10 @@ export default {
         this.trackEvent(types[event.name]);
       }
     },
+    hasDraft(discussion) {
+      const autoSaveKey = getAutoSaveKeyFromDiscussion(discussion);
+      return Boolean(getDraft(autoSaveKey));
+    },
   },
   systemNote: constants.SYSTEM_NOTE,
 };
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 41da9d9ebf31..e7a201cc8d4f 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -27,6 +27,7 @@ import {
   loggedOutnoteableData,
   userDataMock,
 } from '../mock_data';
+import { useLocalStorageSpy } from '../../__helpers__/local_storage_helper';
 
 Vue.use(Vuex);
 
@@ -98,6 +99,34 @@ describe('noteable_discussion component', () => {
     expect(wrapper.vm.canShowReplyActions).toBe(false);
   });
 
+  describe('drafts', () => {
+    useLocalStorageSpy();
+
+    afterEach(() => {
+      localStorage.clear();
+    });
+
+    it.each`
+      show          | exists              | hasDraft
+      ${'show'}     | ${'exists'}         | ${true}
+      ${'not show'} | ${'does not exist'} | ${false}
+    `(
+      'should $show markdown editor on create if reply draft $exists in localStorage',
+      ({ hasDraft }) => {
+        if (hasDraft) {
+          localStorage.setItem(`autosave/Note/Issue/${discussionMock.id}/Reply`, 'draft');
+        }
+        window.gon.current_user_id = userDataMock.id;
+        store.dispatch('setUserData', userDataMock);
+        wrapper = mount(NoteableDiscussion, {
+          store,
+          propsData: { discussion: discussionMock },
+        });
+        expect(wrapper.find('.note-edit-form').exists()).toBe(hasDraft);
+      },
+    );
+  });
+
   describe('actions', () => {
     it('should toggle reply form', async () => {
       await nextTick();
-- 
GitLab