avo 1.3.4 → 1.3.5.pre.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of avo might be problematic. Click here for more details.

Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/components/avo/common/key_value_component.html.erb +53 -0
  4. data/app/components/avo/common/key_value_component.rb +11 -0
  5. data/app/components/avo/edit/fields/key_value_field_component.html.erb +3 -0
  6. data/app/components/avo/edit/fields/key_value_field_component.rb +4 -0
  7. data/app/components/avo/edit/fields/trix_field_component.html.erb +19 -10
  8. data/app/components/avo/show/fields/key_value_field_component.html.erb +3 -0
  9. data/app/components/avo/show/fields/key_value_field_component.rb +4 -0
  10. data/app/controllers/avo/application_controller.rb +5 -1
  11. data/app/controllers/avo/attachments_controller.rb +14 -3
  12. data/app/packs/entrypoints/application.js +14 -11
  13. data/app/packs/js/controllers/fields/code_field_controller.js +1 -0
  14. data/app/packs/js/controllers/fields/date_field_controller.js +1 -1
  15. data/app/packs/js/controllers/fields/key_value_controller.js +137 -0
  16. data/app/packs/js/controllers/fields/trix_field_controller.js +121 -0
  17. data/app/views/avo/partials/_javascript.html.erb +3 -1
  18. data/config/routes.rb +4 -0
  19. data/lib/avo.rb +1 -1
  20. data/lib/avo/fields/key_value_field.rb +24 -1
  21. data/lib/avo/fields/trix_field.rb +4 -0
  22. data/lib/avo/version.rb +1 -1
  23. data/public/avo-packs/css/{application-af3e670d.css → application-5bdca030.css} +92 -18
  24. data/public/avo-packs/css/application-5bdca030.css.br +0 -0
  25. data/public/avo-packs/css/application-5bdca030.css.gz +0 -0
  26. data/public/avo-packs/css/application-5bdca030.css.map +1 -0
  27. data/public/avo-packs/css/application-5bdca030.css.map.br +0 -0
  28. data/public/avo-packs/css/application-5bdca030.css.map.gz +0 -0
  29. data/public/avo-packs/js/application-16a456a2b7cb56b01153.js +26 -0
  30. data/public/avo-packs/js/{application-801f1297670a4c6df1b6.js.LICENSE.txt → application-16a456a2b7cb56b01153.js.LICENSE.txt} +0 -0
  31. data/public/avo-packs/js/application-16a456a2b7cb56b01153.js.br +0 -0
  32. data/public/avo-packs/js/application-16a456a2b7cb56b01153.js.gz +0 -0
  33. data/public/avo-packs/js/application-16a456a2b7cb56b01153.js.map +1 -0
  34. data/public/avo-packs/js/application-16a456a2b7cb56b01153.js.map.br +0 -0
  35. data/public/avo-packs/js/application-16a456a2b7cb56b01153.js.map.gz +0 -0
  36. data/public/avo-packs/manifest.json +15 -15
  37. metadata +25 -17
  38. data/public/avo-packs/css/application-af3e670d.css.br +0 -0
  39. data/public/avo-packs/css/application-af3e670d.css.gz +0 -0
  40. data/public/avo-packs/css/application-af3e670d.css.map +0 -1
  41. data/public/avo-packs/css/application-af3e670d.css.map.br +0 -0
  42. data/public/avo-packs/css/application-af3e670d.css.map.gz +0 -0
  43. data/public/avo-packs/js/application-801f1297670a4c6df1b6.js +0 -26
  44. data/public/avo-packs/js/application-801f1297670a4c6df1b6.js.br +0 -0
  45. data/public/avo-packs/js/application-801f1297670a4c6df1b6.js.gz +0 -0
  46. data/public/avo-packs/js/application-801f1297670a4c6df1b6.js.map +0 -1
  47. data/public/avo-packs/js/application-801f1297670a4c6df1b6.js.map.br +0 -0
  48. data/public/avo-packs/js/application-801f1297670a4c6df1b6.js.map.gz +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f07bd27219373943d20e34298f27988df5b44c40e2fd9747952e41ee9d3595b7
4
- data.tar.gz: e2526041e2318832a501f6862fb045e39332f6ea21c2762354898be7ee715bf6
3
+ metadata.gz: 6212847ad5e28753e23625482067816d88ec6728f3d35656182b3a6e46b021d2
4
+ data.tar.gz: 479c5130dd59104ab944af271b85ba398aec7007568116191d82668ab47b44cc
5
5
  SHA512:
6
- metadata.gz: af5fb87c665732d3a23aab6129ab55724b5c3c7ad55e61fe0526d4715762f6375aaf71c602905481127a0631d1f39a3cd4f6d14630ac5cc3ca48a43bb43e32a9
7
- data.tar.gz: abe7cdf77c7cb9afbdcb1f6f0c2fa5a89d7fe100170d80b33b7454631fae2e502da13990a1bd52265bc7b9561591db2d03795c3affbea661937e45c9f3415be3
6
+ metadata.gz: e1a9663120f63b7458932e061a90d7287c7c7405382d64bcf5fb75c7c64c342a6a461a4ba06ae85478fc8e34efba204ecc99b7a3508541753d124153d4f519ae
7
+ data.tar.gz: 7dcb4d3494ff81a33d793f14e7fbd8d25aa178f6d22293fbaf27c6601c7426c87703f93674a5fb5db20d6c5b1b2b3a1aab24901571bd39a3da138f0d7df92a9a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (1.3.4)
4
+ avo (1.3.5.pre.1)
5
5
  active_link_to
6
6
  addressable
7
7
  breadcrumbs_on_rails
@@ -0,0 +1,53 @@
1
+ <div class="w-full shadow-lg rounded-lg overflow-hidden"
2
+ data-controller="key-value"
3
+ data-key-value-target="controller"
4
+ data-options="<%= @field.options.to_json %>"
5
+ data-input-classes="<%= input_classes %>"
6
+ data-editable="<%= @view.in?([:edit, :create]) %>"
7
+ >
8
+ <div class="w-full flex flex-col">
9
+ <div class="flex w-full">
10
+ <div class="flex w-full bg-gray-800 shadow overflow-hidden">
11
+ <div class="w-1/2 py-3 px-3 uppercase font-semibold text-xs text-white border-gray-600 border-r">
12
+ <%= @field.key_label %>
13
+ </div>
14
+ <div class="w-1/2 py-3 px-3 uppercase font-semibold text-xs text-white">
15
+ <%= @field.value_label %>
16
+ </div>
17
+ <% if @view.in?([:edit, :create]) %>
18
+ <div class="flex items-center justify-center p-2 px-3 border-l border-gray-600">
19
+ <a href="javascript:void(0);"
20
+ title="<%= @field.action_text %>"
21
+ data-tippy="tooltip"
22
+ data-button="add-row"
23
+ data-action="click->key-value#addRow"
24
+ <% if @field.disable_adding_rows %>
25
+ class="cursor-not-allowed"
26
+ <% end %>
27
+ >
28
+ <%= svg 'plus-circle', class: 'text-gray-400 h-5 hover:text-gray-500' %>
29
+ </a>
30
+ </div>
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+ <div data-key-value-target="rows"></div>
35
+ </div>
36
+ <% if @form.present? %>
37
+ <%= @form.text_area @field.id,
38
+ value: @field.parsed_value,
39
+ class: 'hidden',
40
+ placeholder: @field.placeholder,
41
+ 'data-key-value-target': 'input',
42
+ 'data-view': :edit
43
+ %>
44
+ <% else %>
45
+ <%= text_area_tag @field.id,
46
+ @field.parsed_value,
47
+ class: 'hidden',
48
+ placeholder: @field.placeholder,
49
+ 'data-key-value-target': 'input',
50
+ 'data-view': :edit
51
+ %>
52
+ <% end %>
53
+ </div>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::Common::KeyValueComponent < ViewComponent::Base
4
+ include Avo::ApplicationHelper
5
+
6
+ def initialize(field:, form: nil, view: :show)
7
+ @field = field
8
+ @form = form
9
+ @view = view
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal, full_width: true do %>
2
+ <%= render Avo::Common::KeyValueComponent.new field: @field, form: @form, view: :edit %>
3
+ <% end %>
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::Edit::Fields::KeyValueFieldComponent < Avo::Edit::Fields::FieldComponent
4
+ end
@@ -1,11 +1,20 @@
1
- <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
2
- <% trix_id = "trix_#{@resource.name.underscore}_#{@field.id}" %>
3
- <trix-editor input="<%= trix_id %>" placeholder="<%= @field.placeholder %>"><%== @field.value %></trix-editor>
4
- <%= @form.text_area @field.id,
5
- id: trix_id,
6
- class: helpers.input_classes('w-full hidden', has_error: (@resource.model.present? and @resource.model.errors.include?(@field.id))),
7
- placeholder: @field.placeholder,
8
- disabled: @field.readonly,
9
- rows: @field.meta[:rows]
10
- %>
1
+ <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal, full_width: true do %>
2
+ <div
3
+ data-controller="trix-field"
4
+ data-trix-field-target="controller"
5
+ data-resource-name="<%= @resource.model_class.model_name.route_key %>"
6
+ data-resource-id="<%= @resource.model.id %>"
7
+ data-attachments-disabled="<%= @field.attachments_disabled %>"
8
+ data-attachment-key="<%= @field.attachment_key %>"
9
+ >
10
+ <% trix_id = "trix_#{@resource.name.underscore}_#{@field.id}" %>
11
+ <trix-editor data-trix-field-target="editor" input="<%= trix_id %>" placeholder="<%= @field.placeholder %>"><%== @field.value %></trix-editor>
12
+ <%= @form.text_area @field.id,
13
+ id: trix_id,
14
+ class: helpers.input_classes('w-full hidden', has_error: (@resource.model.present? and @resource.model.errors.include?(@field.id))),
15
+ placeholder: @field.placeholder,
16
+ disabled: @field.readonly,
17
+ rows: @field.meta[:rows]
18
+ %>
19
+ </div>
11
20
  <% end %>
@@ -0,0 +1,3 @@
1
+ <%= show_field_wrapper field: @field, index: @index, full_width: true do %>
2
+ <%= render Avo::Common::KeyValueComponent.new field: @field, view: :show %>
3
+ <% end %>
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::Show::Fields::KeyValueFieldComponent < Avo::Show::Fields::FieldComponent
4
+ end
@@ -42,7 +42,7 @@ module Avo
42
42
  end
43
43
 
44
44
  def check_avo_license
45
- unless on_root_path || on_resources_path
45
+ unless on_root_path || on_resources_path || on_api_path
46
46
  if @license.invalid? || @license.lacks(:custom_tools)
47
47
  if Rails.env.development?
48
48
  @custom_tools_alert_visible = true
@@ -272,5 +272,9 @@ module Avo
272
272
  def on_resources_path
273
273
  request.original_url.match?(/.*\/#{Avo.configuration.namespace}\/resources\/.*/)
274
274
  end
275
+
276
+ def on_api_path
277
+ request.original_url.match?(/.*\/#{Avo.configuration.namespace}\/avo_api\/.*/)
278
+ end
275
279
  end
276
280
  end
@@ -2,9 +2,20 @@ require_dependency "avo/application_controller"
2
2
 
3
3
  module Avo
4
4
  class AttachmentsController < ApplicationController
5
- before_action :set_resource_name, only: :destroy
6
- before_action :set_resource, only: :destroy
7
- before_action :set_model, only: :destroy
5
+ before_action :set_resource_name, only: [:destroy, :create]
6
+ before_action :set_resource, only: [:destroy, :create]
7
+ before_action :set_model, only: [:destroy, :create]
8
+
9
+ def create
10
+ blob = ActiveStorage::Blob.create_and_upload! io: params[:file], filename: params[:filename]
11
+
12
+ @model.send(params[:attachment_key]).attach blob
13
+
14
+ render json: {
15
+ url: main_app.url_for(blob),
16
+ href: main_app.url_for(blob)
17
+ }
18
+ end
8
19
 
9
20
  def show
10
21
  end
@@ -2,7 +2,6 @@
2
2
  import 'core-js/stable'
3
3
  // eslint-disable-next-line import/no-extraneous-dependencies
4
4
  import 'regenerator-runtime/runtime'
5
- import 'trix'
6
5
  import * as Mousetrap from 'mousetrap'
7
6
  import { Application } from 'stimulus'
8
7
  import { Turbo } from '@hotwired/turbo-rails'
@@ -19,6 +18,19 @@ window.Turbolinks = Turbo
19
18
 
20
19
  Mousetrap.bind('r r r', () => Turbo.visit(window.location.href, { action: 'replace' }))
21
20
 
21
+ function initTippy() {
22
+ tippy('[data-tippy="tooltip"]', {
23
+ theme: 'light',
24
+ content(reference) {
25
+ const title = reference.getAttribute('title')
26
+ reference.removeAttribute('title')
27
+
28
+ return title
29
+ },
30
+ })
31
+ }
32
+ window.initTippy = initTippy
33
+
22
34
  const application = Application.start()
23
35
 
24
36
  const context = require.context('./../js/controllers', true, /\.js$/)
@@ -29,16 +41,7 @@ application.load(definitionsFromContext(fieldsContext))
29
41
 
30
42
  document.addEventListener('turbo:load', () => {
31
43
  document.body.classList.remove('turbo-loading')
32
-
33
- tippy('[data-tippy="tooltip"]', {
34
- theme: 'light',
35
- content(reference) {
36
- const title = reference.getAttribute('title')
37
- reference.removeAttribute('title')
38
-
39
- return title
40
- },
41
- })
44
+ initTippy()
42
45
  })
43
46
  document.addEventListener('turbo:visit', () => document.body.classList.add('turbo-loading'))
44
47
  document.addEventListener('turbo:submit-start', () => document.body.classList.add('turbo-loading'))
@@ -12,6 +12,7 @@ import 'codemirror/mode/shell/shell'
12
12
  import 'codemirror/mode/sql/sql'
13
13
  import 'codemirror/mode/vue/vue'
14
14
  import 'codemirror/mode/xml/xml'
15
+ import 'codemirror/mode/yaml/yaml'
15
16
 
16
17
  import { Controller } from 'stimulus'
17
18
  import { castBoolean } from '@/js/helpers/cast_boolean'
@@ -33,7 +33,7 @@ export default class extends Controller {
33
33
 
34
34
  // enable timezone display
35
35
  if (enableTime) {
36
- currentValue = DateTime.fromISO(this.inputTarget.value, { zone: window.timezone })
36
+ currentValue = DateTime.fromISO(this.inputTarget.value, { zone: window.Avo.configuration.timezone })
37
37
  currentValue = currentValue.setZone(this.inputTarget.dataset.timezone)
38
38
  currentValue = currentValue.toISO()
39
39
 
@@ -0,0 +1,137 @@
1
+ /* eslint-disable max-len */
2
+ import { Controller } from 'stimulus'
3
+ import { castBoolean } from '@/js/helpers/cast_boolean'
4
+
5
+ export default class extends Controller {
6
+ static targets = ['input', 'controller', 'rows']
7
+
8
+ fieldValue = []
9
+
10
+ options = {}
11
+
12
+ get keyInputDisabled() {
13
+ return !this.options.editable || this.options.disable_editing_keys
14
+ }
15
+
16
+ get valueInputDisabled() {
17
+ return !this.options.editable
18
+ }
19
+
20
+ connect() {
21
+ this.setOptions()
22
+
23
+ try {
24
+ const objectValue = JSON.parse(this.inputTarget.value)
25
+ Object.keys(objectValue).forEach((key) => this.fieldValue.push([key, objectValue[key]]))
26
+ } catch (error) {
27
+ this.fieldValue = []
28
+ }
29
+
30
+ this.updateKeyValueComponent()
31
+ }
32
+
33
+ addRow() {
34
+ if (this.options.disable_adding_rows || !this.options.editable) return
35
+ this.fieldValue.push(['', ''])
36
+ this.updateKeyValueComponent()
37
+ this.focusLastRow()
38
+ }
39
+
40
+ deleteRow(event) {
41
+ if (this.options.disable_deleting_rows || !this.options.editable) return
42
+ const { index } = event.target.dataset
43
+ this.fieldValue.splice(index, 1)
44
+ this.updateTextareaInput()
45
+ this.updateKeyValueComponent()
46
+ }
47
+
48
+ focusLastRow() {
49
+ return this.rowsTarget.querySelector('.flex.key-value-row:last-child .key-value-input-key').focus()
50
+ }
51
+
52
+ valueFieldUpdated(event) {
53
+ const { value } = event.target
54
+ const { index } = event.target.dataset
55
+ this.fieldValue[index][1] = value
56
+
57
+ this.updateTextareaInput()
58
+ }
59
+
60
+ keyFieldUpdated(event) {
61
+ const { value } = event.target
62
+ const { index } = event.target.dataset
63
+ this.fieldValue[index][0] = value
64
+
65
+ this.updateTextareaInput()
66
+ }
67
+
68
+ updateTextareaInput() {
69
+ if (!this.hasInputTarget) return
70
+ let result = {}
71
+ if (this.fieldValue && this.fieldValue.length > 0) {
72
+ result = Object.assign(...this.fieldValue.map(([key, val]) => ({ [key]: val })))
73
+ }
74
+ this.inputTarget.innerText = JSON.stringify(result)
75
+ }
76
+
77
+ updateKeyValueComponent() {
78
+ let result = ''
79
+ let index = 0
80
+ this.fieldValue.forEach((row) => {
81
+ const [key, value] = row
82
+ result += this.interpolatedRow(key, value, index)
83
+ index++
84
+ })
85
+ this.rowsTarget.innerHTML = result
86
+ window.initTippy()
87
+ }
88
+
89
+ interpolatedRow(key, value, index) {
90
+ let result = `<div class="flex key-value-row">
91
+ ${this.inputField('key', index, key, value)}
92
+ ${this.inputField('value', index, key, value)}`
93
+ if (this.options.editable) {
94
+ result += `<a
95
+ href="javascript:void(0);"
96
+ data-index="${index}"
97
+ data-action="click->key-value#deleteRow"
98
+ title="${this.options.delete_text}"
99
+ data-tippy="tooltip"
100
+ data-button="delete-row"
101
+ tabindex="-1"
102
+ ${this.options.disable_deleting_rows ? "disabled='disabled'" : ''}
103
+ class="flex items-center justify-center p-2 px-3 border-none ${this.options.disable_deleting_rows ? 'cursor-not-allowed' : ''}"
104
+ ><svg class="pointer-events-none text-gray-500 h-5 hover:text-gray-500" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg></a>`
105
+ }
106
+ result += '</div>'
107
+
108
+ return result
109
+ }
110
+
111
+ inputField(id = 'key', index, key, value) {
112
+ return `<input
113
+ class="${this.options.inputClasses} !rounded-none border-gray-600 border-r border-l-0 border-b-0 border-t-0 focus:border-gray-300 w-1/2 focus:outline-none outline-none key-value-input-${id}"
114
+ data-action="input->key-value#${id}FieldUpdated"
115
+ placeholder="${this.options[`${id}_label`]}"
116
+ data-index="${index}"
117
+ ${this[`${id}InputDisabled`] ? "disabled='disabled'" : ''}
118
+ value="${id === 'key' ? key : value}"
119
+ autofocus
120
+ />`
121
+ }
122
+
123
+ setOptions() {
124
+ let fieldOptions
125
+
126
+ try {
127
+ fieldOptions = JSON.parse(this.controllerTarget.dataset.options)
128
+ } catch (error) {
129
+ fieldOptions = {}
130
+ }
131
+ this.options = {
132
+ ...fieldOptions,
133
+ inputClasses: this.controllerTarget.dataset.inputClasses,
134
+ editable: castBoolean(this.controllerTarget.dataset.editable),
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,121 @@
1
+ import 'trix'
2
+ import { Controller } from 'stimulus'
3
+ import { castBoolean } from '@/js/helpers/cast_boolean'
4
+
5
+ export default class extends Controller {
6
+ static targets = ['editor', 'controller']
7
+
8
+ get resourceId() {
9
+ return this.controllerTarget.dataset.resourceId
10
+ }
11
+
12
+ get resourceName() {
13
+ return this.controllerTarget.dataset.resourceName
14
+ }
15
+
16
+ get attachmentKey() {
17
+ return this.controllerTarget.dataset.attachmentKey
18
+ }
19
+
20
+ get attachmentsDisabled() {
21
+ return castBoolean(this.controllerTarget.dataset.attachmentsDisabled)
22
+ }
23
+
24
+ get uploadUrl() {
25
+ return `${window.location.origin}${window.Avo.configuration.root_path}/avo_api/resources/${this.resourceName}/${this.resourceId}/attachments`
26
+ }
27
+
28
+ connect() {
29
+ if (this.attachmentsDisabled) {
30
+ // Remove the attachments button
31
+ this.controllerTarget.querySelector('.trix-button-group--file-tools').remove()
32
+ }
33
+
34
+ window.addEventListener('trix-file-accept', (event) => {
35
+ if (event.target === this.editorTarget) {
36
+ // Prevent file uploads for fields that have attachments disabled.
37
+ if (this.attachmentsDisabled) {
38
+ event.preventDefault()
39
+ window.toastr.warning('This field has attachments disabled.')
40
+
41
+ return
42
+ }
43
+
44
+ // Prevent file uploads for resources that haven't been saved yet.
45
+ if (this.resourceId === '') {
46
+ event.preventDefault()
47
+ window.toastr.warning("You can't upload files into the Trix editor until you save the resource.")
48
+
49
+ return
50
+ }
51
+
52
+ // Prevent file uploads for fields without an attachment key.
53
+ if (this.attachmentKey === '') {
54
+ event.preventDefault()
55
+ window.toastr.warning("You haven't set an <a href='https://google.com' class='!text-blue-700 underline'>attachment_key</a> to this Trix field.")
56
+ }
57
+ }
58
+ })
59
+
60
+ window.addEventListener('trix-attachment-add', (event) => {
61
+ if (event.target === this.editorTarget) {
62
+ if (event.attachment.file) {
63
+ this.uploadFileAttachment(event.attachment)
64
+ }
65
+ }
66
+ })
67
+ }
68
+
69
+ uploadFileAttachment(attachment) {
70
+ this.uploadFile(
71
+ attachment.file,
72
+ (progress) => attachment.setUploadProgress(progress),
73
+ (attributes) => attachment.setAttributes(attributes),
74
+ )
75
+ }
76
+
77
+ uploadFile(file, progressCallback, successCallback) {
78
+ const formData = this.createFormData(file)
79
+ const xhr = new XMLHttpRequest()
80
+
81
+ xhr.open('POST', this.uploadUrl, true)
82
+
83
+ xhr.setRequestHeader('X-CSRF-Token', document.querySelector('meta[name="csrf-token"]').content)
84
+
85
+ xhr.upload.addEventListener('progress', (event) => {
86
+ // eslint-disable-next-line no-mixed-operators
87
+ const progress = event.loaded / event.total * 100
88
+ progressCallback(progress)
89
+ })
90
+
91
+ xhr.addEventListener('load', () => {
92
+ if (xhr.status === 200) {
93
+ let response
94
+ try {
95
+ response = JSON.parse(xhr.response)
96
+ } catch (error) {
97
+ response = {}
98
+ }
99
+
100
+ const attributes = {
101
+ url: response.url,
102
+ href: response.href,
103
+ }
104
+
105
+ successCallback(attributes)
106
+ }
107
+ })
108
+
109
+ xhr.send(formData)
110
+ }
111
+
112
+ createFormData(file) {
113
+ const data = new FormData()
114
+ data.append('Content-Type', file.type)
115
+ data.append('file', file)
116
+ data.append('filename', file.name)
117
+ data.append('attachment_key', this.attachmentKey)
118
+
119
+ return data
120
+ }
121
+ }