diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js index 01316be06a2e7fd9762d11e3171102bd4f182148..9cf817bc5a1a6b67a38437d084614693b97f7eae 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 493beb8cea9e5c2548983323437c4a9e9f9ea11e..394f64d51bdbd8ce821454d50fcc27b52eae03bb 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 eb6764a7937813c9b1f9f62bea78248c37fb5a47..b9d3afb540411ee316506ec29998516162746bf3 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 41da9d9ebf31e66feab4aff8b9dd4a159f5cf1ae..e7a201cc8d4fdfb5fead3173f9f7c34048e9da0b 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();