diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index a350198d692f63e3b05fe45ce0f45bdf4fb015f7..1f7728e440b82ec771ab565b844b4da5192146c9 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -1,6 +1,7 @@ <script> import axios from '~/lib/utils/axios_utils'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { updateDraft, clearDraft, getDraft } from '~/lib/utils/autosave'; import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants'; import MarkdownField from './field.vue'; @@ -52,10 +53,15 @@ export default { required: false, default: false, }, + autosaveKey: { + type: String, + required: false, + default: null, + }, }, data() { return { - markdown: this.value || '', + markdown: this.value || (this.autosaveKey ? getDraft(this.autosaveKey) : '') || '', editingMode: EDITING_MODE_MARKDOWN_FIELD, autofocused: false, }; @@ -72,19 +78,27 @@ export default { watch: { value(val) { this.markdown = val; + + this.saveDraft(); }, }, mounted() { this.autofocusTextarea(); + + this.saveDraft(); }, methods: { updateMarkdownFromContentEditor({ markdown }) { this.markdown = markdown; this.$emit('input', markdown); + + this.saveDraft(); }, updateMarkdownFromMarkdownField({ target }) { this.markdown = target.value; this.$emit('input', target.value); + + this.saveDraft(); }, renderMarkdown(markdown) { return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body); @@ -110,6 +124,11 @@ export default { setEditorAsAutofocused() { this.autofocused = true; }, + saveDraft() { + if (!this.autosaveKey) return; + if (this.markdown) updateDraft(this.autosaveKey, this.markdown); + else clearDraft(this.autosaveKey); + }, }, }; </script> diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index 3e3dd3bcc8858ce6ec91e091b541eedbf7f7e57d..51afb7c499f02cd8467fb2fdb4ec7581814a91f1 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -9,10 +9,13 @@ import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { stubComponent } from 'helpers/stub_component'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; jest.mock('~/emoji'); describe('vue_shared/component/markdown/markdown_editor', () => { + useLocalStorageSpy(); + let wrapper; const value = 'test markdown'; const renderMarkdownPath = '/api/markdown'; @@ -64,6 +67,8 @@ describe('vue_shared/component/markdown/markdown_editor', () => { afterEach(() => { mock.restore(); + + localStorage.clear(); }); it('displays markdown field by default', () => { @@ -102,6 +107,42 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }); }); + describe('autosave', () => { + it('automatically saves the textarea value to local storage if autosaveKey is defined', () => { + buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: 'This is **markdown**' } }); + + expect(localStorage.getItem('autosave/issue/1234')).toBe('This is **markdown**'); + }); + + it("loads value from local storage if autosaveKey is defined, and value isn't", () => { + localStorage.setItem('autosave/issue/1234', 'This is **markdown**'); + + buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } }); + + expect(findTextarea().element.value).toBe('This is **markdown**'); + }); + + it("doesn't load value from local storage if autosaveKey is defined, and value is", () => { + localStorage.setItem('autosave/issue/1234', 'This is **markdown**'); + + buildWrapper({ propsData: { autosaveKey: 'issue/1234' } }); + + expect(findTextarea().element.value).toBe('test markdown'); + }); + + it('does not save the textarea value to local storage if autosaveKey is not defined', () => { + buildWrapper({ propsData: { value: 'This is **markdown**' } }); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('does not save the textarea value to local storage if value is empty', () => { + buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } }); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + }); + it('renders markdown field textarea', () => { buildWrapper({ propsData: { supportsQuickActions: true } }); @@ -158,6 +199,16 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(wrapper.emitted('input')).toEqual([[newValue]]); }); + it('autosaves the markdown value to local storage', async () => { + buildWrapper({ propsData: { autosaveKey: 'issue/1234' } }); + + const newValue = 'new value'; + + await findTextarea().setValue(newValue); + + expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue); + }); + describe('when autofocus is true', () => { beforeEach(async () => { buildWrapper({ attachTo: document.body, propsData: { autofocus: true } }); @@ -219,7 +270,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => { describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => { beforeEach(() => { - buildWrapper(); + buildWrapper({ propsData: { autosaveKey: 'issue/1234' } }); findMarkdownField().vm.$emit('enableContentEditor'); }); @@ -244,6 +295,14 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(wrapper.emitted('input')).toEqual([[newValue]]); }); + it('autosaves the content editor value to local storage', async () => { + const newValue = 'new value'; + + await findContentEditor().vm.$emit('change', { markdown: newValue }); + + expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue); + }); + it('bubbles up keydown event', () => { const event = new Event('keydown');