diff --git a/app/controllers/template_replace_documents_controller.rb b/app/controllers/template_replace_documents_controller.rb new file mode 100644 index 00000000..b9f28881 --- /dev/null +++ b/app/controllers/template_replace_documents_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class TemplateReplaceDocumentsController < ApplicationController + load_and_authorize_resource :template + + def create + if params[:blobs].blank? && params[:files].blank? + return respond_to do |f| + f.html { redirect_back fallback_location: template_path(@template), alert: I18n.t('file_is_missing') } + f.json { render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity } + end + end + + ActiveRecord::Associations::Preloader.new( + records: [@template], + associations: [schema_documents: :preview_images_attachments] + ).call + + cloned_template = Templates::Clone.call(@template, author: current_user) + cloned_template.save! + + documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true) + + cloned_template.save! + + Templates::CloneAttachments.call(template: cloned_template, original_template: @template, + excluded_attachment_uuids: documents.map(&:uuid)) + + respond_to do |f| + f.html { redirect_to edit_template_path(cloned_template) } + f.json { render json: { id: cloned_template.id } } + end + rescue Templates::CreateAttachments::PdfEncrypted + render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index 78e542b0..7cf2371e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -33,6 +33,7 @@ import MaskedInput from './elements/masked_input' import SetDateButton from './elements/set_date_button' import IndeterminateCheckbox from './elements/indeterminate_checkbox' import AppTour from './elements/app_tour' +import DashboardDropzone from './elements/dashboard_dropzone' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -101,6 +102,7 @@ safeRegisterElement('masked-input', MaskedInput) safeRegisterElement('set-date-button', SetDateButton) safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox) safeRegisterElement('app-tour', AppTour) +safeRegisterElement('dashboard-dropzone', DashboardDropzone) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { @@ -125,6 +127,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { withSendButton: this.dataset.withSendButton !== 'false', withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false', withConditions: this.dataset.withConditions === 'true', + withReplaceAndCloneUpload: this.dataset.withReplaceAndCloneUpload !== 'false', currencies: (this.dataset.currencies || '').split(',').filter(Boolean), acceptFileTypes: this.dataset.acceptFileTypes, showTourStartForm: this.dataset.showTourStartForm === 'true' diff --git a/app/javascript/elements/dashboard_dropzone.js b/app/javascript/elements/dashboard_dropzone.js new file mode 100644 index 00000000..1dc3622f --- /dev/null +++ b/app/javascript/elements/dashboard_dropzone.js @@ -0,0 +1,123 @@ +import { actionable } from '@github/catalyst/lib/actionable' +import { target, targets, targetable } from '@github/catalyst/lib/targetable' + +export default actionable(targetable(class extends HTMLElement { + static [targets.static] = ['hiddenOnHover'] + static [target.static] = [ + 'loading', + 'icon', + 'input', + 'fileDropzone' + ] + + connectedCallback () { + this.showOnlyOnWindowHover = this.dataset.showOnlyOnWindowHover === 'true' + + document.addEventListener('drop', this.onWindowDragdrop) + document.addEventListener('dragover', this.onWindowDropover) + window.addEventListener('dragleave', this.onWindowDragleave) + + this.addEventListener('dragover', this.onDragover) + this.addEventListener('dragleave', this.onDragleave) + + this.fileDropzone.addEventListener('drop', this.onDrop) + this.fileDropzone.addEventListener('turbo:submit-start', this.showDraghover) + this.fileDropzone.addEventListener('turbo:submit-end', this.hideDraghover) + } + + disconnectedCallback () { + document.removeEventListener('drop', this.onWindowDragdrop) + document.removeEventListener('dragover', this.onWindowDropover) + window.removeEventListener('dragleave', this.onWindowDragleave) + + this.removeEventListener('dragover', this.onDragover) + this.removeEventListener('dragleave', this.onDragleave) + + this.fileDropzone.removeEventListener('drop', this.onDrop) + this.fileDropzone.removeEventListener('turbo:submit-start', this.showDraghover) + this.fileDropzone.removeEventListener('turbo:submit-end', this.hideDraghover) + } + + onDrop = (e) => { + e.preventDefault() + + this.input.files = e.dataTransfer.files + + this.uploadFiles(e.dataTransfer.files) + } + + onWindowDragdrop = () => { + if (!this.hovered) this.hideDraghover() + } + + onSelectFiles (e) { + e.preventDefault() + + this.uploadFiles(this.input.files) + } + + toggleLoading = (e) => { + if (e && e.target && (!e.target.contains(this) || !e.detail?.formSubmission?.formElement?.contains(this))) { + return + } + + this.loading?.classList?.toggle('hidden') + this.icon?.classList?.toggle('hidden') + } + + uploadFiles () { + this.toggleLoading() + + this.fileDropzone.querySelector('button[type="submit"]').click() + } + + onWindowDropover = (e) => { + e.preventDefault() + + if (e.dataTransfer?.types?.includes('Files')) { + this.showDraghover() + } + } + + onWindowDragleave = (e) => { + if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) { + this.hideDraghover() + } + } + + onDragover (e) { + e.preventDefault() + + this.hovered = true + this.style.backgroundColor = '#F7F3F0' + } + + onDragleave (e) { + e.preventDefault() + + this.hovered = false + this.style.backgroundColor = null + } + + showDraghover = () => { + if (this.showOnlyOnWindowHover) { + this.classList.remove('hidden') + } + + this.classList.remove('bg-base-200', 'border-transparent') + this.classList.add('bg-base-100', 'border-base-300', 'border-dashed') + this.fileDropzone.classList.remove('hidden') + this.hiddenOnHover.forEach((el) => { el.style.display = 'none' }) + } + + hideDraghover = () => { + if (this.showOnlyOnWindowHover) { + this.classList.add('hidden') + } + + this.classList.add('bg-base-200', 'border-transparent') + this.classList.remove('bg-base-100', 'border-base-300', 'border-dashed') + this.fileDropzone.classList.add('hidden') + this.hiddenOnHover.forEach((el) => { el.style.display = null }) + } +})) diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index c83c3873..2077f9c8 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -1,10 +1,23 @@