From 9b267d4a0707d0d8a9235c6e4713fc4b95b04215 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Fri, 2 Oct 2026 00:15:28 +0300 Subject: [PATCH] upload inserted images as attachments --- .../template_builder/dynamic_document.vue | 1 + .../template_builder/dynamic_editor.js | 7 ++-- .../template_builder/dynamic_section.vue | 33 ++++++++++++------- config/locales/i18n.yml | 7 ++++ 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/app/javascript/template_builder/dynamic_document.vue b/app/javascript/template_builder/dynamic_document.vue index e41f80461..9674fa6ee 100644 --- a/app/javascript/template_builder/dynamic_document.vue +++ b/app/javascript/template_builder/dynamic_document.vue @@ -50,6 +50,7 @@ @draw="$emit('draw', $event)" @add-custom-field="$emit('add-custom-field', $event)" @set-draw="$emit('set-draw', $event)" + @add-attachment="document.attachments.push($event)" /> diff --git a/app/javascript/template_builder/dynamic_editor.js b/app/javascript/template_builder/dynamic_editor.js index cb1affe26..145284553 100644 --- a/app/javascript/template_builder/dynamic_editor.js +++ b/app/javascript/template_builder/dynamic_editor.js @@ -1470,7 +1470,7 @@ const VariableHighlight = Extension.create({ } }) -export function buildEditor ({ dynamicAreaProps, attachmentsIndex, renderHtmlForSaveRef, onFieldDrop, onFieldDestroy, editorOptions }) { +export function buildEditor ({ dynamicAreaProps, getAttachmentsIndex, renderHtmlForSaveRef, onFieldDrop, onFieldDestroy, editorOptions }) { const FieldNode = Node.create({ name: 'fieldNode', inline: true, @@ -1777,9 +1777,10 @@ export function buildEditor ({ dynamicAreaProps, attachmentsIndex, renderHtmlFor const attrs = { ...node.attrs.htmlAttrs } const blobUuid = attrs.src?.startsWith('blob:') && attrs.src.slice(5) + const attachmentUrl = blobUuid && getAttachmentsIndex()[blobUuid] - if (blobUuid && attachmentsIndex[blobUuid]) { - attrs.src = attachmentsIndex[blobUuid] + if (attachmentUrl) { + attrs.src = attachmentUrl } img.setAttribute('loading', 'lazy') diff --git a/app/javascript/template_builder/dynamic_section.vue b/app/javascript/template_builder/dynamic_section.vue index 9d9e5bd9c..cdca59253 100644 --- a/app/javascript/template_builder/dynamic_section.vue +++ b/app/javascript/template_builder/dynamic_section.vue @@ -107,7 +107,7 @@ export default { FieldContextMenu, AreaTitle }, - inject: ['template', 'save', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor'], + inject: ['template', 'save', 'baseFetch', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'selectedAreasRef', 'getFieldTypeIndex', 'fieldTypes', 'withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor'], props: { section: { type: Object, @@ -177,7 +177,7 @@ export default { default: null } }, - emits: ['update', 'draw', 'set-draw', 'add-custom-field'], + emits: ['update', 'draw', 'set-draw', 'add-custom-field', 'add-attachment'], data () { return { isAreaDrag: false, @@ -290,7 +290,7 @@ export default { onAreaResize: this.onAreaResize, onAreaDragStart: this.onAreaDragStart }, - attachmentsIndex: this.attachmentsIndex, + getAttachmentsIndex: () => this.attachmentsIndex, onFieldDrop: this.onFieldDrop, onFieldDestroy: this.onFieldDestroy, renderHtmlForSaveRef: this.renderHtmlForSaveRef, @@ -569,7 +569,7 @@ export default { .setTextSelection(from + 4) .run() }, - onImageSelected (event) { + async onImageSelected (event) { const file = event.target.files[0] const pos = this.editor.state.selection.head @@ -577,15 +577,26 @@ export default { if (!file) return - const reader = new FileReader() + const formData = new FormData() - reader.onload = () => { - this.editor.chain().focus() - .insertContentAt(pos, { type: 'image', attrs: { htmlAttrs: { src: reader.result, style: 'max-width: 100%' } } }) - .run() - } + formData.append('file', file) - reader.readAsDataURL(file) + const response = await this.baseFetch(`/templates/${this.template.id}/dynamic_documents/${this.attachmentUuid}/attachments`, { + method: 'POST', + body: formData + }) + + const data = await response.json() + + if (!response.ok) return alert(data.error) + + this.$emit('add-attachment', data) + + await this.$nextTick() + + this.editor.chain().focus() + .insertContentAt(pos, { type: 'image', attrs: { htmlAttrs: { src: `blob:${data.uuid}`, style: 'max-width: 100%' } } }) + .run() }, setDynamicMenuCoords (editor) { const { from, to } = editor.state.selection diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index adc5073c7..bb9f5eb65 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -624,6 +624,7 @@ en: &en form_has_been_declined: Form has been declined. form_is_view_only: Form is view only. file_is_missing: File is missing + file_is_not_an_image: File is not an image folder_name_has_been_updated: Folder name has been updated. unable_to_rename_folder: Unable to rename folder. template_has_been_removed: Template has been removed. @@ -1781,6 +1782,7 @@ es: &es form_has_been_declined: El formulario ha sido rechazado. form_is_view_only: El formulario es de solo lectura. file_is_missing: Falta el archivo + file_is_not_an_image: El archivo no es una imagen folder_name_has_been_updated: El nombre de la carpeta ha sido actualizado. unable_to_rename_folder: No se pudo renombrar la carpeta. template_has_been_removed: La plantilla ha sido eliminada. @@ -2941,6 +2943,7 @@ it: &it form_has_been_declined: Il modulo è stato rifiutato. form_is_view_only: Il modulo è di sola lettura. file_is_missing: File mancante + file_is_not_an_image: Il file non è un'immagine folder_name_has_been_updated: Il nome della cartella è stato aggiornato. unable_to_rename_folder: Impossibile rinominare la cartella. template_has_been_removed: Il modello è stato rimosso. @@ -4087,6 +4090,7 @@ fr: &fr form_has_been_declined: Le formulaire a été refusé. form_is_view_only: Le formulaire est en lecture seule. file_is_missing: Fichier manquant + file_is_not_an_image: Le fichier n'est pas une image folder_name_has_been_updated: Le nom du dossier a été mis à jour. unable_to_rename_folder: Impossible de renommer le dossier. template_has_been_removed: Le modèle a été supprimé. @@ -5243,6 +5247,7 @@ pt: &pt form_has_been_declined: O formulário foi recusado. form_is_view_only: O formulário é somente leitura. file_is_missing: O arquivo está ausente + file_is_not_an_image: O arquivo não é uma imagem folder_name_has_been_updated: O nome da pasta foi atualizado. unable_to_rename_folder: Não foi possível renomear a pasta. template_has_been_removed: O modelo foi removido. @@ -6402,6 +6407,7 @@ de: &de form_has_been_declined: Das Formular wurde abgelehnt. form_is_view_only: Das Formular ist schreibgeschützt. file_is_missing: Datei fehlt + file_is_not_an_image: Datei ist kein Bild folder_name_has_been_updated: Der Ordnername wurde aktualisiert. unable_to_rename_folder: Der Ordner konnte nicht umbenannt werden. template_has_been_removed: Die Vorlage wurde entfernt. @@ -8089,6 +8095,7 @@ nl: &nl form_has_been_declined: Formulier is geweigerd. form_is_view_only: Formulier is alleen-lezen. file_is_missing: Bestand ontbreekt + file_is_not_an_image: Bestand is geen afbeelding folder_name_has_been_updated: Mapnaam is bijgewerkt. unable_to_rename_folder: Kan map niet hernoemen. template_has_been_removed: Sjabloon is verwijderd.