Commit 53dd2810 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '335114-content-editor-attachments' into 'master'

Add support for adding attachments in Content Editor

See merge request gitlab-org/gitlab!67728
parents 48fc6df5 e28237cf
...@@ -8,8 +8,8 @@ import { ...@@ -8,8 +8,8 @@ import {
GlDropdownItem, GlDropdownItem,
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { acceptedMimes } from '../extensions/image'; import { acceptedMimes } from '../services/upload_helpers';
import { getImageAlt } from '../services/utils'; import { extractFilename } from '../services/utils';
export default { export default {
components: { components: {
...@@ -41,7 +41,7 @@ export default { ...@@ -41,7 +41,7 @@ export default {
.setImage({ .setImage({
src: this.imgSrc, src: this.imgSrc,
canonicalSrc: this.imgSrc, canonicalSrc: this.imgSrc,
alt: getImageAlt(this.imgSrc), alt: extractFilename(this.imgSrc),
}) })
.run(); .run();
...@@ -58,7 +58,7 @@ export default { ...@@ -58,7 +58,7 @@ export default {
this.tiptapEditor this.tiptapEditor
.chain() .chain()
.focus() .focus()
.uploadImage({ .uploadAttachment({
file: e.target.files[0], file: e.target.files[0],
}) })
.run(); .run();
...@@ -67,7 +67,7 @@ export default { ...@@ -67,7 +67,7 @@ export default {
this.emitExecute('upload'); this.emitExecute('upload');
}, },
}, },
acceptedMimes, acceptedMimes: acceptedMimes.image,
}; };
</script> </script>
<template> <template>
......
...@@ -33,6 +33,13 @@ export default { ...@@ -33,6 +33,13 @@ export default {
}; };
}, },
methods: { methods: {
resetFields() {
this.imgSrc = '';
this.$refs.fileSelector.value = '';
},
openFileUpload() {
this.$refs.fileSelector.click();
},
updateLinkState({ editor }) { updateLinkState({ editor }) {
const { canonicalSrc, href } = editor.getAttributes(Link.name); const { canonicalSrc, href } = editor.getAttributes(Link.name);
...@@ -65,6 +72,18 @@ export default { ...@@ -65,6 +72,18 @@ export default {
this.$emit('execute', { contentType: Link.name }); this.$emit('execute', { contentType: Link.name });
}, },
onFileSelect(e) {
this.tiptapEditor
.chain()
.focus()
.uploadAttachment({
file: e.target.files[0],
})
.run();
this.resetFields();
this.$emit('execute', { contentType: Link.name });
},
}, },
}; };
</script> </script>
...@@ -83,14 +102,25 @@ export default { ...@@ -83,14 +102,25 @@ export default {
<gl-dropdown-form class="gl-px-3!"> <gl-dropdown-form class="gl-px-3!">
<gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')"> <gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')">
<template #append> <template #append>
<gl-button variant="confirm" @click="updateLink()">{{ __('Apply') }}</gl-button> <gl-button variant="confirm" @click="updateLink">{{ __('Apply') }}</gl-button>
</template> </template>
</gl-form-input-group> </gl-form-input-group>
</gl-dropdown-form> </gl-dropdown-form>
<gl-dropdown-divider v-if="isActive" /> <gl-dropdown-divider />
<gl-dropdown-item v-if="isActive" @click="removeLink()"> <gl-dropdown-item v-if="isActive" @click="removeLink">
{{ __('Remove link') }} {{ __('Remove link') }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item v-else @click="openFileUpload">
{{ __('Upload file') }}
</gl-dropdown-item>
<input
ref="fileSelector"
type="file"
name="content_editor_attachment"
class="gl-display-none"
@change="onFileSelect"
/>
</gl-dropdown> </gl-dropdown>
</editor-state-observer> </editor-state-observer>
</template> </template>
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state';
import { handleFileEvent } from '../services/upload_helpers';
export default Extension.create({
name: 'attachment',
defaultOptions: {
uploadsPath: null,
renderMarkdown: null,
},
addCommands() {
return {
uploadAttachment: ({ file }) => () => {
const { uploadsPath, renderMarkdown } = this.options;
return handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
},
};
},
addProseMirrorPlugins() {
const { editor } = this;
return [
new Plugin({
key: new PluginKey('attachment'),
props: {
handlePaste: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
return handleFileEvent({
editor,
file: event.clipboardData.files[0],
uploadsPath,
renderMarkdown,
});
},
handleDrop: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
return handleFileEvent({
editor,
file: event.dataTransfer.files[0],
uploadsPath,
renderMarkdown,
});
},
},
}),
];
},
});
import { Image } from '@tiptap/extension-image'; import { Image } from '@tiptap/extension-image';
import { VueNodeViewRenderer } from '@tiptap/vue-2'; import { VueNodeViewRenderer } from '@tiptap/vue-2';
import { Plugin, PluginKey } from 'prosemirror-state';
import { __ } from '~/locale';
import ImageWrapper from '../components/wrappers/image.vue'; import ImageWrapper from '../components/wrappers/image.vue';
import { uploadFile } from '../services/upload_file';
import { getImageAlt, readFileAsDataURL } from '../services/utils';
export const acceptedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
const resolveImageEl = (element) => const resolveImageEl = (element) =>
element.nodeName === 'IMG' ? element : element.querySelector('img'); element.nodeName === 'IMG' ? element : element.querySelector('img');
const startFileUpload = async ({ editor, file, uploadsPath, renderMarkdown }) => {
const encodedSrc = await readFileAsDataURL(file);
const { view } = editor;
editor.commands.setImage({ uploading: true, src: encodedSrc });
const { state } = view;
const position = state.selection.from - 1;
const { tr } = state;
try {
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
view.dispatch(
tr.setNodeMarkup(position, undefined, {
uploading: false,
src: encodedSrc,
alt: getImageAlt(src),
canonicalSrc,
}),
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
}
};
const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
if (acceptedMimes.includes(file?.type)) {
startFileUpload({ editor, file, uploadsPath, renderMarkdown });
return true;
}
return false;
};
export default Image.extend({ export default Image.extend({
defaultOptions: { defaultOptions: {
...Image.options, ...Image.options,
uploadsPath: null,
renderMarkdown: null,
inline: true, inline: true,
}, },
addAttributes() { addAttributes() {
...@@ -108,47 +63,6 @@ export default Image.extend({ ...@@ -108,47 +63,6 @@ export default Image.extend({
}, },
]; ];
}, },
addCommands() {
return {
...this.parent(),
uploadImage: ({ file }) => () => {
const { uploadsPath, renderMarkdown } = this.options;
handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
},
};
},
addProseMirrorPlugins() {
const { editor } = this;
return [
new Plugin({
key: new PluginKey('handleDropAndPasteImages'),
props: {
handlePaste: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
return handleFileEvent({
editor,
file: event.clipboardData.files[0],
uploadsPath,
renderMarkdown,
});
},
handleDrop: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
return handleFileEvent({
editor,
file: event.dataTransfer.files[0],
uploadsPath,
renderMarkdown,
});
},
},
}),
];
},
addNodeView() { addNodeView() {
return VueNodeViewRenderer(ImageWrapper); return VueNodeViewRenderer(ImageWrapper);
}, },
......
...@@ -21,6 +21,10 @@ export const extractHrefFromMarkdownLink = (match) => { ...@@ -21,6 +21,10 @@ export const extractHrefFromMarkdownLink = (match) => {
}; };
export default Link.extend({ export default Link.extend({
defaultOptions: {
...Link.options,
openOnClick: false,
},
addInputRules() { addInputRules() {
return [ return [
markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink), markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink),
...@@ -48,6 +52,4 @@ export default Link.extend({ ...@@ -48,6 +52,4 @@ export default Link.extend({
}, },
}; };
}, },
}).configure({
openOnClick: false,
}); });
import { Node } from '@tiptap/core';
export default Node.create({
name: 'loading',
inline: true,
group: 'inline',
addAttributes() {
return {
label: {
default: null,
},
};
},
renderHTML({ node }) {
return [
'span',
{ class: 'gl-display-inline-flex gl-align-items-center' },
['span', { class: 'gl-spinner gl-mx-2' }],
['span', { class: 'gl-link' }, node.attrs.label],
];
},
});
import { Editor } from '@tiptap/vue-2'; import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash'; import { isFunction } from 'lodash';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants'; import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import Attachment from '../extensions/attachment';
import Blockquote from '../extensions/blockquote'; import Blockquote from '../extensions/blockquote';
import Bold from '../extensions/bold'; import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list'; import BulletList from '../extensions/bullet_list';
...@@ -17,6 +18,7 @@ import Image from '../extensions/image'; ...@@ -17,6 +18,7 @@ import Image from '../extensions/image';
import Italic from '../extensions/italic'; import Italic from '../extensions/italic';
import Link from '../extensions/link'; import Link from '../extensions/link';
import ListItem from '../extensions/list_item'; import ListItem from '../extensions/list_item';
import Loading from '../extensions/loading';
import OrderedList from '../extensions/ordered_list'; import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph'; import Paragraph from '../extensions/paragraph';
import Strike from '../extensions/strike'; import Strike from '../extensions/strike';
...@@ -52,6 +54,7 @@ export const createContentEditor = ({ ...@@ -52,6 +54,7 @@ export const createContentEditor = ({
} }
const builtInContentEditorExtensions = [ const builtInContentEditorExtensions = [
Attachment.configure({ uploadsPath, renderMarkdown }),
Blockquote, Blockquote,
Bold, Bold,
BulletList, BulletList,
...@@ -64,10 +67,11 @@ export const createContentEditor = ({ ...@@ -64,10 +67,11 @@ export const createContentEditor = ({
Heading, Heading,
History, History,
HorizontalRule, HorizontalRule,
Image.configure({ uploadsPath, renderMarkdown }), Image,
Italic, Italic,
Link, Link,
ListItem, ListItem,
Loading,
OrderedList, OrderedList,
Paragraph, Paragraph,
Strike, Strike,
......
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { extractFilename, readFileAsDataURL } from './utils';
export const acceptedMimes = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'],
};
const extractAttachmentLinkUrl = (html) => { const extractAttachmentLinkUrl = (html) => {
const parser = new DOMParser(); const parser = new DOMParser();
...@@ -42,3 +48,72 @@ export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => { ...@@ -42,3 +48,72 @@ export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
return extractAttachmentLinkUrl(rendered); return extractAttachmentLinkUrl(rendered);
}; };
const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
const encodedSrc = await readFileAsDataURL(file);
const { view } = editor;
editor.commands.setImage({ uploading: true, src: encodedSrc });
const { state } = view;
const position = state.selection.from - 1;
const { tr } = state;
try {
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
view.dispatch(
tr.setNodeMarkup(position, undefined, {
uploading: false,
src: encodedSrc,
alt: extractFilename(src),
canonicalSrc,
}),
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
}
};
const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) => {
await Promise.resolve();
const { view } = editor;
const text = extractFilename(file.name);
const { state } = view;
const { from } = state.selection;
editor.commands.insertContent({
type: 'loading',
attrs: { label: text },
});
try {
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
editor.commands.insertContentAt(
{ from, to: from + 1 },
{ type: 'text', text, marks: [{ type: 'link', attrs: { href: src, canonicalSrc } }] },
);
} catch (e) {
editor.commands.deleteRange({ from, to: from + 1 });
editor.emit('error', __('An error occurred while uploading the file. Please try again.'));
}
};
export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
if (!file) return false;
if (acceptedMimes.image.includes(file?.type)) {
uploadImage({ editor, file, uploadsPath, renderMarkdown });
return true;
}
uploadAttachment({ editor, file, uploadsPath, renderMarkdown });
return true;
};
...@@ -4,8 +4,18 @@ export const hasSelection = (tiptapEditor) => { ...@@ -4,8 +4,18 @@ export const hasSelection = (tiptapEditor) => {
return from < to; return from < to;
}; };
export const getImageAlt = (src) => { /**
return src.replace(/^.*\/|\..*$/g, '').replace(/\W+/g, ' '); * Extracts filename from a URL
*
* @example
* > extractFilename('https://gitlab.com/images/logo-full.png')
* < 'logo-full'
*
* @param {string} src The URL to extract filename from
* @returns {string}
*/
export const extractFilename = (src) => {
return src.replace(/^.*\/|\..+?$/g, '');
}; };
export const readFileAsDataURL = (file) => { export const readFileAsDataURL = (file) => {
......
...@@ -3810,6 +3810,9 @@ msgstr "" ...@@ -3810,6 +3810,9 @@ msgstr ""
msgid "An error occurred while updating the notification settings. Please try again." msgid "An error occurred while updating the notification settings. Please try again."
msgstr "" msgstr ""
msgid "An error occurred while uploading the file. Please try again."
msgstr ""
msgid "An error occurred while uploading the image. Please try again." msgid "An error occurred while uploading the image. Please try again."
msgstr "" msgstr ""
......
...@@ -26,8 +26,21 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen ...@@ -26,8 +26,21 @@ exports[`content_editor/components/toolbar_link_button renders dropdown componen
</div> </div>
</form> </form>
</li> </li>
<li role=\\"presentation\\" class=\\"gl-new-dropdown-divider\\">
<hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
</li>
<li role=\\"presentation\\" class=\\"gl-new-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
<!---->
<!---->
<!----> <!---->
<div class=\\"gl-new-dropdown-item-text-wrapper\\">
<p class=\\"gl-new-dropdown-item-text-primary\\">
Upload file
</p>
<!---->
</div>
<!----> <!---->
</button></li> <input type=\\"file\\" name=\\"content_editor_attachment\\" class=\\"gl-display-none\\">
</div> </div>
<!----> <!---->
</div> </div>
......
import { GlButton, GlFormInputGroup } from '@gitlab/ui'; import { GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue'; import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
import Attachment from '~/content_editor/extensions/attachment';
import Image from '~/content_editor/extensions/image'; import Image from '~/content_editor/extensions/image';
import { createTestEditor, mockChainedCommands } from '../test_utils'; import { createTestEditor, mockChainedCommands } from '../test_utils';
...@@ -31,7 +32,8 @@ describe('content_editor/components/toolbar_image_button', () => { ...@@ -31,7 +32,8 @@ describe('content_editor/components/toolbar_image_button', () => {
beforeEach(() => { beforeEach(() => {
editor = createTestEditor({ editor = createTestEditor({
extensions: [ extensions: [
Image.configure({ Image,
Attachment.configure({
renderMarkdown: jest.fn(), renderMarkdown: jest.fn(),
uploadsPath: '/uploads/', uploadsPath: '/uploads/',
}), }),
...@@ -64,13 +66,13 @@ describe('content_editor/components/toolbar_image_button', () => { ...@@ -64,13 +66,13 @@ describe('content_editor/components/toolbar_image_button', () => {
}); });
it('uploads the selected image when file input changes', async () => { it('uploads the selected image when file input changes', async () => {
const commands = mockChainedCommands(editor, ['focus', 'uploadImage', 'run']); const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
const file = new File(['foo'], 'foo.png', { type: 'image/png' }); const file = new File(['foo'], 'foo.png', { type: 'image/png' });
await selectFile(file); await selectFile(file);
expect(commands.focus).toHaveBeenCalled(); expect(commands.focus).toHaveBeenCalled();
expect(commands.uploadImage).toHaveBeenCalledWith({ file }); expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
expect(commands.run).toHaveBeenCalled(); expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]); expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]);
......
import { GlDropdown, GlDropdownDivider, GlButton, GlFormInputGroup } from '@gitlab/ui'; import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue'; import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
import Link from '~/content_editor/extensions/link'; import Link from '~/content_editor/extensions/link';
...@@ -19,11 +19,18 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -19,11 +19,18 @@ describe('content_editor/components/toolbar_link_button', () => {
}); });
}; };
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]'); const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
const findApplyLinkButton = () => wrapper.findComponent(GlButton); const findApplyLinkButton = () => wrapper.findComponent(GlButton);
const findRemoveLinkButton = () => wrapper.findByText('Remove link'); const findRemoveLinkButton = () => wrapper.findByText('Remove link');
const selectFile = async (file) => {
const input = wrapper.find({ ref: 'fileSelector' });
// override the property definition because `input.files` isn't directly modifyable
Object.defineProperty(input.element, 'files', { value: [file], writable: true });
await input.trigger('change');
};
beforeEach(() => { beforeEach(() => {
editor = createTestEditor(); editor = createTestEditor();
}); });
...@@ -51,8 +58,11 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -51,8 +58,11 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(findDropdown().props('toggleClass')).toEqual({ active: true }); expect(findDropdown().props('toggleClass')).toEqual({ active: true });
}); });
it('does not display the upload file option', () => {
expect(wrapper.findByText('Upload file').exists()).toBe(false);
});
it('displays a remove link dropdown option', () => { it('displays a remove link dropdown option', () => {
expect(findDropdownDivider().exists()).toBe(true);
expect(wrapper.findByText('Remove link').exists()).toBe(true); expect(wrapper.findByText('Remove link').exists()).toBe(true);
}); });
...@@ -107,7 +117,7 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -107,7 +117,7 @@ describe('content_editor/components/toolbar_link_button', () => {
}); });
}); });
describe('when there is not an active link', () => { describe('when there is no active link', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(editor, 'isActive'); jest.spyOn(editor, 'isActive');
editor.isActive.mockReturnValueOnce(false); editor.isActive.mockReturnValueOnce(false);
...@@ -118,8 +128,11 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -118,8 +128,11 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(findDropdown().props('toggleClass')).toEqual({ active: false }); expect(findDropdown().props('toggleClass')).toEqual({ active: false });
}); });
it('displays the upload file option', () => {
expect(wrapper.findByText('Upload file').exists()).toBe(true);
});
it('does not display a remove link dropdown option', () => { it('does not display a remove link dropdown option', () => {
expect(findDropdownDivider().exists()).toBe(false);
expect(wrapper.findByText('Remove link').exists()).toBe(false); expect(wrapper.findByText('Remove link').exists()).toBe(false);
}); });
...@@ -138,6 +151,19 @@ describe('content_editor/components/toolbar_link_button', () => { ...@@ -138,6 +151,19 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]); expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
}); });
it('uploads the selected image when file input changes', async () => {
const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
const file = new File(['foo'], 'foo.png', { type: 'image/png' });
await selectFile(file);
expect(commands.focus).toHaveBeenCalled();
expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
});
}); });
describe('when the user displays the dropdown', () => { describe('when the user displays the dropdown', () => {
......
...@@ -52,9 +52,9 @@ describe('content_editor/services/create_editor', () => { ...@@ -52,9 +52,9 @@ describe('content_editor/services/create_editor', () => {
expect(() => createContentEditor()).toThrow(PROVIDE_SERIALIZER_OR_RENDERER_ERROR); expect(() => createContentEditor()).toThrow(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
}); });
it('provides uploadsPath and renderMarkdown function to Image extension', () => { it('provides uploadsPath and renderMarkdown function to Attachment extension', () => {
expect( expect(
editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'image').options, editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'attachment').options,
).toMatchObject({ ).toMatchObject({
uploadsPath, uploadsPath,
renderMarkdown, renderMarkdown,
......
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { uploadFile } from '~/content_editor/services/upload_file'; import { uploadFile } from '~/content_editor/services/upload_helpers';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
describe('content_editor/services/upload_file', () => { describe('content_editor/services/upload_helpers', () => {
const uploadsPath = '/uploads'; const uploadsPath = '/uploads';
const file = new File(['content'], 'file.txt'); const file = new File(['content'], 'file.txt');
// TODO: Replace with automated fixture // TODO: Replace with automated fixture
......
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