Commit 9618f419 authored by Kushal Pandya's avatar Kushal Pandya

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
parents 21a8079a 25133a37
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'; ...@@ -7,6 +7,7 @@ import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
export default { export default {
name: 'NoteForm', name: 'NoteForm',
...@@ -65,10 +66,21 @@ export default { ...@@ -65,10 +66,21 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
autosaveKey: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
let updatedNoteBody = this.noteBody;
if (!updatedNoteBody && this.autosaveKey) {
updatedNoteBody = getDraft(this.autosaveKey) || '';
}
return { return {
updatedNoteBody: this.noteBody, updatedNoteBody,
conflictWhileEditing: false, conflictWhileEditing: false,
isSubmitting: false, isSubmitting: false,
isResolving: this.resolveDiscussion, isResolving: this.resolveDiscussion,
...@@ -175,6 +187,12 @@ export default { ...@@ -175,6 +187,12 @@ export default {
// Sends information about confirm message and if the textarea has changed // Sends information about confirm message and if the textarea has changed
this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody); this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody);
}, },
onInput() {
if (this.autosaveKey) {
const { autosaveKey, updatedNoteBody: text } = this;
updateDraft(autosaveKey, text);
}
},
}, },
}; };
</script> </script>
...@@ -218,6 +236,7 @@ export default { ...@@ -218,6 +236,7 @@ export default {
@keydown.ctrl.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()"
@keydown.up="editMyLastNote()" @keydown.up="editMyLastNote()"
@keydown.esc="cancelHandler(true)" @keydown.esc="cancelHandler(true)"
@input="onInput"
></textarea> ></textarea>
</markdown-field> </markdown-field>
<div class="note-form-actions clearfix"> <div class="note-form-actions clearfix">
......
...@@ -4,6 +4,7 @@ import { mapActions, mapGetters } from 'vuex'; ...@@ -4,6 +4,7 @@ import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import systemNote from '~/vue_shared/components/notes/system_note.vue'; import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
...@@ -21,7 +22,6 @@ import noteForm from './note_form.vue'; ...@@ -21,7 +22,6 @@ import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue'; import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation'; import discussionNavigation from '../mixins/discussion_navigation';
...@@ -54,7 +54,7 @@ export default { ...@@ -54,7 +54,7 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [autosave, noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], mixins: [noteable, resolvable, discussionNavigation, diffLineNoteFormMixin],
props: { props: {
discussion: { discussion: {
type: Object, type: Object,
...@@ -106,7 +106,10 @@ export default { ...@@ -106,7 +106,10 @@ export default {
'showJumpToNextDiscussion', 'showJumpToNextDiscussion',
]), ]),
author() { author() {
return this.initialDiscussion.author; return this.firstNote.author;
},
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
}, },
canReply() { canReply() {
return this.getNoteableData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
...@@ -117,7 +120,7 @@ export default { ...@@ -117,7 +120,7 @@ export default {
hasReplies() { hasReplies() {
return this.discussion.notes.length > 1; return this.discussion.notes.length > 1;
}, },
initialDiscussion() { firstNote() {
return this.discussion.notes.slice(0, 1)[0]; return this.discussion.notes.slice(0, 1)[0];
}, },
replies() { replies() {
...@@ -242,18 +245,6 @@ export default { ...@@ -242,18 +245,6 @@ export default {
return !this.discussionResolved && this.discussion.resolve_with_issue_path; 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() { created() {
eventHub.$on('startReplying', this.onStartReplying); eventHub.$on('startReplying', this.onStartReplying);
}, },
...@@ -312,7 +303,7 @@ export default { ...@@ -312,7 +303,7 @@ export default {
} }
this.isReplying = false; this.isReplying = false;
this.resetAutoSave(); clearDraft(this.autosaveKey);
}, },
saveReply(noteText, form, callback) { saveReply(noteText, form, callback) {
const postData = { const postData = {
...@@ -338,7 +329,7 @@ export default { ...@@ -338,7 +329,7 @@ export default {
this.isReplying = false; this.isReplying = false;
this.saveNote(replyData) this.saveNote(replyData)
.then(() => { .then(() => {
this.resetAutoSave(); clearDraft(this.autosaveKey);
callback(); callback();
}) })
.catch(err => { .catch(err => {
...@@ -390,8 +381,8 @@ Please check your network connection and try again.`; ...@@ -390,8 +381,8 @@ Please check your network connection and try again.`;
<div class="timeline-content"> <div class="timeline-content">
<note-header <note-header
:author="author" :author="author"
:created-at="initialDiscussion.created_at" :created-at="firstNote.created_at"
:note-id="initialDiscussion.id" :note-id="firstNote.id"
:include-toggle="true" :include-toggle="true"
:expanded="discussion.expanded" :expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler" @toggleHandler="toggleDiscussionHandler"
...@@ -424,8 +415,8 @@ Please check your network connection and try again.`; ...@@ -424,8 +415,8 @@ Please check your network connection and try again.`;
<ul class="notes"> <ul class="notes">
<template v-if="shouldGroupReplies"> <template v-if="shouldGroupReplies">
<component <component
:is="componentName(initialDiscussion)" :is="componentName(firstNote)"
:note="componentData(initialDiscussion)" :note="componentData(firstNote)"
:line="line" :line="line"
:commit="commit" :commit="commit"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
...@@ -512,6 +503,7 @@ Please check your network connection and try again.`; ...@@ -512,6 +503,7 @@ Please check your network connection and try again.`;
:is-editing="false" :is-editing="false"
:line="diffLine" :line="diffLine"
save-button-title="Comment" save-button-title="Comment"
:autosave-key="autosaveKey"
@handleFormUpdateAddToReview="addReplyToReview" @handleFormUpdateAddToReview="addReplyToReview"
@handleFormUpdate="saveReply" @handleFormUpdate="saveReply"
@cancelForm="cancelReplyForm" @cancelForm="cancelReplyForm"
......
---
title: Display draft when toggling replies
merge_request: 25563
author:
type: fixed
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'; ...@@ -5,11 +5,33 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { noteableDataMock, notesDataMock } from '../mock_data'; import { noteableDataMock, notesDataMock } from '../mock_data';
describe('issue_note_form component', () => { describe('issue_note_form component', () => {
const dummyAutosaveKey = 'some-autosave-key';
const dummyDraft = 'dummy draft content';
let store; let store;
let wrapper; let wrapper;
let props; 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(() => { beforeEach(() => {
spyOnDependency(NoteForm, 'getDraft').and.callFake(key => {
if (key === dummyAutosaveKey) {
return dummyDraft;
}
return null;
});
store = createStore(); store = createStore();
store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock); store.dispatch('setNotesData', notesDataMock);
...@@ -20,14 +42,7 @@ describe('issue_note_form component', () => { ...@@ -20,14 +42,7 @@ describe('issue_note_form component', () => {
noteId: '545', noteId: '545',
}; };
const localVue = createLocalVue(); wrapper = createComponentWrapper();
wrapper = shallowMount(NoteForm, {
store,
propsData: props,
// see https://gitlab.com/gitlab-org/gitlab-ce/issues/56317 for the following
localVue,
sync: false,
});
}); });
afterEach(() => { afterEach(() => {
...@@ -181,4 +196,67 @@ describe('issue_note_form component', () => { ...@@ -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'; ...@@ -3,6 +3,7 @@ import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.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 '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_file'; import mockDiffFile from '../../diffs/mock_data/diff_file';
...@@ -72,7 +73,18 @@ describe('noteable_discussion component', () => { ...@@ -72,7 +73,18 @@ describe('noteable_discussion component', () => {
.then(() => wrapper.vm.$nextTick()) .then(() => wrapper.vm.$nextTick())
.then(() => { .then(() => {
expect(wrapper.vm.isReplying).toEqual(true); 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) .then(done)
.catch(done.fail); .catch(done.fail);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment