Merge branch 'winh-toggle-comment-draft' into 'master'
Display draft when toggling replies Closes #48211 and #56364 See merge request gitlab-org/gitlab-ce!25563
This commit is contained in:
		
						commit
						9618f419f2
					
				|  | @ -0,0 +1,32 @@ | |||
| import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; | ||||
| 
 | ||||
| export const clearDraft = autosaveKey => { | ||||
|   try { | ||||
|     window.localStorage.removeItem(`autosave/${autosaveKey}`); | ||||
|   } catch (e) { | ||||
|     // eslint-disable-next-line no-console
 | ||||
|     console.error(e); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const getDraft = autosaveKey => { | ||||
|   try { | ||||
|     return window.localStorage.getItem(`autosave/${autosaveKey}`); | ||||
|   } catch (e) { | ||||
|     // eslint-disable-next-line no-console
 | ||||
|     console.error(e); | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const updateDraft = (autosaveKey, text) => { | ||||
|   try { | ||||
|     window.localStorage.setItem(`autosave/${autosaveKey}`, text); | ||||
|   } catch (e) { | ||||
|     // eslint-disable-next-line no-console
 | ||||
|     console.error(e); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const getDiscussionReplyKey = (noteableType, discussionId) => | ||||
|   ['Note', capitalizeFirstCharacter(noteableType), discussionId, 'Reply'].join('/'); | ||||
|  | @ -7,6 +7,7 @@ import markdownField from '../../vue_shared/components/markdown/field.vue'; | |||
| import issuableStateMixin from '../mixins/issuable_state'; | ||||
| import resolvable from '../mixins/resolvable'; | ||||
| import { __ } from '~/locale'; | ||||
| import { getDraft, updateDraft } from '~/lib/utils/autosave'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'NoteForm', | ||||
|  | @ -65,10 +66,21 @@ export default { | |||
|       required: false, | ||||
|       default: '', | ||||
|     }, | ||||
|     autosaveKey: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     let updatedNoteBody = this.noteBody; | ||||
| 
 | ||||
|     if (!updatedNoteBody && this.autosaveKey) { | ||||
|       updatedNoteBody = getDraft(this.autosaveKey) || ''; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       updatedNoteBody: this.noteBody, | ||||
|       updatedNoteBody, | ||||
|       conflictWhileEditing: false, | ||||
|       isSubmitting: false, | ||||
|       isResolving: this.resolveDiscussion, | ||||
|  | @ -175,6 +187,12 @@ export default { | |||
|       // Sends information about confirm message and if the textarea has changed | ||||
|       this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody); | ||||
|     }, | ||||
|     onInput() { | ||||
|       if (this.autosaveKey) { | ||||
|         const { autosaveKey, updatedNoteBody: text } = this; | ||||
|         updateDraft(autosaveKey, text); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | @ -218,6 +236,7 @@ export default { | |||
|           @keydown.ctrl.enter="handleKeySubmit()" | ||||
|           @keydown.up="editMyLastNote()" | ||||
|           @keydown.esc="cancelHandler(true)" | ||||
|           @input="onInput" | ||||
|         ></textarea> | ||||
|       </markdown-field> | ||||
|       <div class="note-form-actions clearfix"> | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { mapActions, mapGetters } from 'vuex'; | |||
| import { GlTooltipDirective } from '@gitlab/ui'; | ||||
| import { truncateSha } from '~/lib/utils/text_utility'; | ||||
| import { s__, __, sprintf } from '~/locale'; | ||||
| import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; | ||||
| import systemNote from '~/vue_shared/components/notes/system_note.vue'; | ||||
| import icon from '~/vue_shared/components/icon.vue'; | ||||
| import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; | ||||
|  | @ -21,7 +22,6 @@ import noteForm from './note_form.vue'; | |||
| import diffWithNote from './diff_with_note.vue'; | ||||
| import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; | ||||
| import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; | ||||
| import autosave from '../mixins/autosave'; | ||||
| import noteable from '../mixins/noteable'; | ||||
| import resolvable from '../mixins/resolvable'; | ||||
| import discussionNavigation from '../mixins/discussion_navigation'; | ||||
|  | @ -54,7 +54,7 @@ export default { | |||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   mixins: [autosave, noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], | ||||
|   mixins: [noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], | ||||
|   props: { | ||||
|     discussion: { | ||||
|       type: Object, | ||||
|  | @ -106,7 +106,10 @@ export default { | |||
|       'showJumpToNextDiscussion', | ||||
|     ]), | ||||
|     author() { | ||||
|       return this.initialDiscussion.author; | ||||
|       return this.firstNote.author; | ||||
|     }, | ||||
|     autosaveKey() { | ||||
|       return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id); | ||||
|     }, | ||||
|     canReply() { | ||||
|       return this.getNoteableData.current_user.can_create_note; | ||||
|  | @ -117,7 +120,7 @@ export default { | |||
|     hasReplies() { | ||||
|       return this.discussion.notes.length > 1; | ||||
|     }, | ||||
|     initialDiscussion() { | ||||
|     firstNote() { | ||||
|       return this.discussion.notes.slice(0, 1)[0]; | ||||
|     }, | ||||
|     replies() { | ||||
|  | @ -242,18 +245,6 @@ export default { | |||
|       return !this.discussionResolved && this.discussion.resolve_with_issue_path; | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     isReplying() { | ||||
|       if (this.isReplying) { | ||||
|         this.$nextTick(() => { | ||||
|           // Pass an extra key to separate reply and note edit forms | ||||
|           this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']); | ||||
|         }); | ||||
|       } else { | ||||
|         this.disposeAutoSave(); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   created() { | ||||
|     eventHub.$on('startReplying', this.onStartReplying); | ||||
|   }, | ||||
|  | @ -312,7 +303,7 @@ export default { | |||
|       } | ||||
| 
 | ||||
|       this.isReplying = false; | ||||
|       this.resetAutoSave(); | ||||
|       clearDraft(this.autosaveKey); | ||||
|     }, | ||||
|     saveReply(noteText, form, callback) { | ||||
|       const postData = { | ||||
|  | @ -338,7 +329,7 @@ export default { | |||
|       this.isReplying = false; | ||||
|       this.saveNote(replyData) | ||||
|         .then(() => { | ||||
|           this.resetAutoSave(); | ||||
|           clearDraft(this.autosaveKey); | ||||
|           callback(); | ||||
|         }) | ||||
|         .catch(err => { | ||||
|  | @ -390,8 +381,8 @@ Please check your network connection and try again.`; | |||
|           <div class="timeline-content"> | ||||
|             <note-header | ||||
|               :author="author" | ||||
|               :created-at="initialDiscussion.created_at" | ||||
|               :note-id="initialDiscussion.id" | ||||
|               :created-at="firstNote.created_at" | ||||
|               :note-id="firstNote.id" | ||||
|               :include-toggle="true" | ||||
|               :expanded="discussion.expanded" | ||||
|               @toggleHandler="toggleDiscussionHandler" | ||||
|  | @ -424,8 +415,8 @@ Please check your network connection and try again.`; | |||
|               <ul class="notes"> | ||||
|                 <template v-if="shouldGroupReplies"> | ||||
|                   <component | ||||
|                     :is="componentName(initialDiscussion)" | ||||
|                     :note="componentData(initialDiscussion)" | ||||
|                     :is="componentName(firstNote)" | ||||
|                     :note="componentData(firstNote)" | ||||
|                     :line="line" | ||||
|                     :commit="commit" | ||||
|                     :help-page-path="helpPagePath" | ||||
|  | @ -512,6 +503,7 @@ Please check your network connection and try again.`; | |||
|                   :is-editing="false" | ||||
|                   :line="diffLine" | ||||
|                   save-button-title="Comment" | ||||
|                   :autosave-key="autosaveKey" | ||||
|                   @handleFormUpdateAddToReview="addReplyToReview" | ||||
|                   @handleFormUpdate="saveReply" | ||||
|                   @cancelForm="cancelReplyForm" | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Display draft when toggling replies | ||||
| merge_request: 25563 | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,64 @@ | |||
| import { clearDraft, getDraft, updateDraft } from '~/lib/utils/autosave'; | ||||
| 
 | ||||
| describe('autosave utils', () => { | ||||
|   const autosaveKey = 'dummy-autosave-key'; | ||||
|   const text = 'some dummy text'; | ||||
| 
 | ||||
|   describe('clearDraft', () => { | ||||
|     beforeEach(() => { | ||||
|       localStorage.setItem(`autosave/${autosaveKey}`, text); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|       localStorage.removeItem(`autosave/${autosaveKey}`); | ||||
|     }); | ||||
| 
 | ||||
|     it('removes the draft from localStorage', () => { | ||||
|       clearDraft(autosaveKey); | ||||
| 
 | ||||
|       expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(null); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getDraft', () => { | ||||
|     beforeEach(() => { | ||||
|       localStorage.setItem(`autosave/${autosaveKey}`, text); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|       localStorage.removeItem(`autosave/${autosaveKey}`); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns the draft from localStorage', () => { | ||||
|       const result = getDraft(autosaveKey); | ||||
| 
 | ||||
|       expect(result).toBe(text); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns null if no entry exists in localStorage', () => { | ||||
|       localStorage.removeItem(`autosave/${autosaveKey}`); | ||||
| 
 | ||||
|       const result = getDraft(autosaveKey); | ||||
| 
 | ||||
|       expect(result).toBe(null); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('updateDraft', () => { | ||||
|     beforeEach(() => { | ||||
|       localStorage.setItem(`autosave/${autosaveKey}`, text); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|       localStorage.removeItem(`autosave/${autosaveKey}`); | ||||
|     }); | ||||
| 
 | ||||
|     it('removes the draft from localStorage', () => { | ||||
|       const newText = 'new text'; | ||||
| 
 | ||||
|       updateDraft(autosaveKey, newText); | ||||
| 
 | ||||
|       expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(newText); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -5,11 +5,33 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; | |||
| import { noteableDataMock, notesDataMock } from '../mock_data'; | ||||
| 
 | ||||
| describe('issue_note_form component', () => { | ||||
|   const dummyAutosaveKey = 'some-autosave-key'; | ||||
|   const dummyDraft = 'dummy draft content'; | ||||
| 
 | ||||
|   let store; | ||||
|   let wrapper; | ||||
|   let props; | ||||
| 
 | ||||
|   const createComponentWrapper = () => { | ||||
|     const localVue = createLocalVue(); | ||||
|     return shallowMount(NoteForm, { | ||||
|       store, | ||||
|       propsData: props, | ||||
|       // see https://gitlab.com/gitlab-org/gitlab-ce/issues/56317 for the following
 | ||||
|       localVue, | ||||
|       sync: false, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     spyOnDependency(NoteForm, 'getDraft').and.callFake(key => { | ||||
|       if (key === dummyAutosaveKey) { | ||||
|         return dummyDraft; | ||||
|       } | ||||
| 
 | ||||
|       return null; | ||||
|     }); | ||||
| 
 | ||||
|     store = createStore(); | ||||
|     store.dispatch('setNoteableData', noteableDataMock); | ||||
|     store.dispatch('setNotesData', notesDataMock); | ||||
|  | @ -20,14 +42,7 @@ describe('issue_note_form component', () => { | |||
|       noteId: '545', | ||||
|     }; | ||||
| 
 | ||||
|     const localVue = createLocalVue(); | ||||
|     wrapper = shallowMount(NoteForm, { | ||||
|       store, | ||||
|       propsData: props, | ||||
|       // see https://gitlab.com/gitlab-org/gitlab-ce/issues/56317 for the following
 | ||||
|       localVue, | ||||
|       sync: false, | ||||
|     }); | ||||
|     wrapper = createComponentWrapper(); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|  | @ -181,4 +196,67 @@ describe('issue_note_form component', () => { | |||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('with autosaveKey', () => { | ||||
|     beforeEach(() => { | ||||
|       wrapper.destroy(); | ||||
|     }); | ||||
| 
 | ||||
|     describe('with draft', () => { | ||||
|       beforeEach(done => { | ||||
|         Object.assign(props, { | ||||
|           noteBody: '', | ||||
|           autosaveKey: dummyAutosaveKey, | ||||
|         }); | ||||
|         wrapper = createComponentWrapper(); | ||||
| 
 | ||||
|         wrapper.vm | ||||
|           .$nextTick() | ||||
|           .then(done) | ||||
|           .catch(done.fail); | ||||
|       }); | ||||
| 
 | ||||
|       it('displays the draft in textarea', () => { | ||||
|         const textarea = wrapper.find('textarea'); | ||||
| 
 | ||||
|         expect(textarea.element.value).toBe(dummyDraft); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('without draft', () => { | ||||
|       beforeEach(done => { | ||||
|         Object.assign(props, { | ||||
|           noteBody: '', | ||||
|           autosaveKey: 'some key without draft', | ||||
|         }); | ||||
|         wrapper = createComponentWrapper(); | ||||
| 
 | ||||
|         wrapper.vm | ||||
|           .$nextTick() | ||||
|           .then(done) | ||||
|           .catch(done.fail); | ||||
|       }); | ||||
| 
 | ||||
|       it('leaves the textarea empty', () => { | ||||
|         const textarea = wrapper.find('textarea'); | ||||
| 
 | ||||
|         expect(textarea.element.value).toBe(''); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('updates the draft if textarea content changes', () => { | ||||
|       const updateDraftSpy = spyOnDependency(NoteForm, 'updateDraft').and.stub(); | ||||
|       Object.assign(props, { | ||||
|         noteBody: '', | ||||
|         autosaveKey: dummyAutosaveKey, | ||||
|       }); | ||||
|       wrapper = createComponentWrapper(); | ||||
|       const textarea = wrapper.find('textarea'); | ||||
|       const dummyContent = 'some new content'; | ||||
| 
 | ||||
|       textarea.setValue(dummyContent); | ||||
| 
 | ||||
|       expect(updateDraftSpy).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import createStore from '~/notes/stores'; | |||
| import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; | ||||
| import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; | ||||
| import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue'; | ||||
| import NoteForm from '~/notes/components/note_form.vue'; | ||||
| import '~/behaviors/markdown/render_gfm'; | ||||
| import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; | ||||
| import mockDiffFile from '../../diffs/mock_data/diff_file'; | ||||
|  | @ -72,7 +73,18 @@ describe('noteable_discussion component', () => { | |||
|         .then(() => wrapper.vm.$nextTick()) | ||||
|         .then(() => { | ||||
|           expect(wrapper.vm.isReplying).toEqual(true); | ||||
|           expect(wrapper.vm.$refs.noteForm).not.toBeNull(); | ||||
| 
 | ||||
|           const noteForm = wrapper.find(NoteForm); | ||||
| 
 | ||||
|           expect(noteForm.exists()).toBe(true); | ||||
| 
 | ||||
|           const noteFormProps = noteForm.props(); | ||||
| 
 | ||||
|           expect(noteFormProps.discussion).toBe(discussionMock); | ||||
|           expect(noteFormProps.isEditing).toBe(false); | ||||
|           expect(noteFormProps.line).toBe(null); | ||||
|           expect(noteFormProps.saveButtonTitle).toBe('Comment'); | ||||
|           expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`); | ||||
|         }) | ||||
|         .then(done) | ||||
|         .catch(done.fail); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue