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