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 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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
@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';
|
||||||
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';
|
||||||
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 {
|
||||||
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 {
|
||||||
'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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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.`;
|
||||||
<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.`;
|
||||||
<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.`;
|
||||||
: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"
|
||||||
|
|
|
||||||
|
|
@ -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';
|
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', () => {
|
||||||
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', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 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', () => {
|
||||||
.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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue