summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/content_editor/services/upload_helpers.js
blob: 1abecb8f41484d5ac992593f755a9b785d3e8827 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { VARIANT_DANGER } from '~/flash';
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 parser = new DOMParser();
  const { body } = parser.parseFromString(html, 'text/html');
  const link = body.querySelector('a');
  const src = link.getAttribute('href');
  const { canonicalSrc } = link.dataset;

  return { src, canonicalSrc };
};

/**
 * Uploads a file with a post request to the URL indicated
 * in the uploadsPath parameter. The expected response of the
 * uploads service is a JSON object that contains, at least, a
 * link property. The link property should contain markdown link
 * definition (i.e. [GitLab](https://gitlab.com)).
 *
 * This Markdown will be rendered to extract its canonical and full
 * URLs using GitLab Flavored Markdown renderer in the backend.
 *
 * @param {Object} params
 * @param {String} params.uploadsPath An absolute URL that points to a service
 * that allows sending a file for uploading via POST request.
 * @param {String} params.renderMarkdown A function that accepts a markdown string
 * and returns a rendered version in HTML format.
 * @param {File} params.file The file to upload
 *
 * @returns Returns an object with two properties:
 *
 * canonicalSrc: The URL as defined in the Markdown
 * src: The absolute URL that points to the resource in the server
 */
export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
  const formData = new FormData();
  formData.append('file', file, file.name);

  const { data } = await axios.post(uploadsPath, formData);
  const { markdown } = data.link;
  const rendered = await renderMarkdown(markdown);

  return extractAttachmentLinkUrl(rendered);
};

const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
  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 });
    eventHub.$emit('alert', {
      message: __('An error occurred while uploading the image. Please try again.'),
      variant: VARIANT_DANGER,
    });
  }
};

const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
  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 });
    eventHub.$emit('alert', {
      message: __('An error occurred while uploading the file. Please try again.'),
      variant: VARIANT_DANGER,
    });
  }
};

export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
  if (!file) return false;

  if (acceptedMimes.image.includes(file?.type)) {
    uploadImage({ editor, file, uploadsPath, renderMarkdown, eventHub });

    return true;
  }

  uploadAttachment({ editor, file, uploadsPath, renderMarkdown, eventHub });

  return true;
};