diff --git a/Gemfile.lock b/Gemfile.lock index b2518108..1a097c98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -387,7 +387,7 @@ GEM puma (6.5.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.12) + rack (3.1.14) rack-proxy (0.7.7) rack rack-session (2.0.0) diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index f023f021..6f505992 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -21,7 +21,11 @@ module Api blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid) - authorization_check!(blob) if exp.blank? + attachment = blob.attachments.take + + @record = attachment.record + + authorization_check!(attachment) if exp.blank? if request.headers['Range'].present? send_blob_byte_range_data blob, request.headers['Range'] @@ -37,9 +41,7 @@ module Api private - def authorization_check!(blob) - attachment = blob.attachments.take - + def authorization_check!(attachment) is_authorized = attachment.name.in?(%w[logo preview_images]) || (current_user && attachment.record.account.id == current_user.account_id) || (current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') || diff --git a/app/controllers/api/submitter_email_clicks_controller.rb b/app/controllers/api/submitter_email_clicks_controller.rb index 87221680..cef26542 100644 --- a/app/controllers/api/submitter_email_clicks_controller.rb +++ b/app/controllers/api/submitter_email_clicks_controller.rb @@ -6,10 +6,10 @@ module Api skip_authorization_check def create - submitter = Submitter.find_by!(slug: params[:submitter_slug]) + @submitter = Submitter.find_by!(slug: params[:submitter_slug]) - if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email') - SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request) + if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email') + SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request) end render json: {} diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb index 98d7f5b5..e8b52095 100644 --- a/app/controllers/api/submitter_form_views_controller.rb +++ b/app/controllers/api/submitter_form_views_controller.rb @@ -6,15 +6,15 @@ module Api skip_authorization_check def create - submitter = Submitter.find_by!(slug: params[:submitter_slug]) + @submitter = Submitter.find_by!(slug: params[:submitter_slug]) - submitter.opened_at = Time.current - submitter.save + @submitter.opened_at = Time.current + @submitter.save - SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request) + SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request) - WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url| - SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id, + WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url| + SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id, 'webhook_url_id' => webhook_url.id) end diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb index e7e1db7e..7879abfd 100644 --- a/app/controllers/api/templates_clone_controller.rb +++ b/app/controllers/api/templates_clone_controller.rb @@ -5,7 +5,7 @@ module Api load_and_authorize_resource :template def create - authorize!(:manage, @template) + authorize!(:create, @template) ActiveRecord::Associations::Preloader.new( records: [@template], diff --git a/app/controllers/preview_document_page_controller.rb b/app/controllers/preview_document_page_controller.rb index 5bf42138..5e59e74a 100644 --- a/app/controllers/preview_document_page_controller.rb +++ b/app/controllers/preview_document_page_controller.rb @@ -12,6 +12,8 @@ class PreviewDocumentPageController < ActionController::API return head :not_found unless attachment + @template = attachment.record + preview_image = attachment.preview_images.joins(:blob) .find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] }) diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 90c32bde..080b9807 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -14,7 +14,8 @@ class StartFormController < ApplicationController raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true @submitter = @template.submissions.new(account_id: @template.account_id) - .submitters.new(uuid: (filter_undefined_submitters(@template).first || + .submitters.new(account_id: @template.account_id, + uuid: (filter_undefined_submitters(@template).first || @template.submitters.first)['uuid']) end @@ -96,6 +97,7 @@ class StartFormController < ApplicationController submitter.submission ||= Submission.new(template:, account_id: template.account_id, template_submitters: template.submitters, + expire_at: Templates.build_default_expire_at(template), submitters: [submitter], source: :link) diff --git a/app/controllers/submissions_archived_controller.rb b/app/controllers/submissions_archived_controller.rb index 3c3f49b6..f71638c7 100644 --- a/app/controllers/submissions_archived_controller.rb +++ b/app/controllers/submissions_archived_controller.rb @@ -7,9 +7,7 @@ class SubmissionsArchivedController < ApplicationController @submissions = @submissions.joins(:template) @submissions = @submissions.where.not(archived_at: nil) .or(@submissions.where.not(templates: { archived_at: nil })) - .preload(:created_by_user, template: :author) - - @submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin]) + .preload(:template_accesses, :created_by_user, template: :author) @submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions::Filter.call(@submissions, current_user, params) diff --git a/app/controllers/submissions_dashboard_controller.rb b/app/controllers/submissions_dashboard_controller.rb index 3403d22c..3386edd8 100644 --- a/app/controllers/submissions_dashboard_controller.rb +++ b/app/controllers/submissions_dashboard_controller.rb @@ -8,9 +8,7 @@ class SubmissionsDashboardController < ApplicationController @submissions = @submissions.where(archived_at: nil) .where(templates: { archived_at: nil }) - .preload(:created_by_user, template: :author) - - @submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin]) + .preload(:template_accesses, :created_by_user, template: :author) @submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions::Filter.call(@submissions, current_user, params) diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index e4af5f49..62836650 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -8,20 +8,20 @@ class SubmissionsDownloadController < ApplicationController FILES_TTL = 5.minutes def index - submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? + @submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? signature_valid = - if submitter&.slug == params[:submitter_slug] + if @submitter&.slug == params[:submitter_slug] true else - submitter = nil + @submitter = nil end - submitter ||= Submitter.find_by!(slug: params[:submitter_slug]) + @submitter ||= Submitter.find_by!(slug: params[:submitter_slug]) - Submissions::EnsureResultGenerated.call(submitter) + Submissions::EnsureResultGenerated.call(@submitter) - last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last + last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last return head :not_found unless last_submitter @@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController end if params[:combined] == 'true' - url = build_combined_url(submitter) + url = build_combined_url(@submitter) if url render json: [url] diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index 9be5c0ac..07447940 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -7,25 +7,25 @@ class SubmitFormDownloadController < ApplicationController FILES_TTL = 5.minutes def index - submitter = Submitter.find_by!(slug: params[:submit_form_slug]) + @submitter = Submitter.find_by!(slug: params[:submit_form_slug]) - return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at? + return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at? - return head :unprocessable_entity if submitter.declined_at? || - submitter.submission.archived_at? || - submitter.submission.expired? || - submitter.submission.template.archived_at? + return head :unprocessable_entity if @submitter.declined_at? || + @submitter.submission.archived_at? || + @submitter.submission.expired? || + @submitter.submission.template.archived_at? - last_completed_submitter = submitter.submission.submitters - .where.not(id: submitter.id) - .where.not(completed_at: nil) - .max_by(&:completed_at) + last_completed_submitter = @submitter.submission.submitters + .where.not(id: @submitter.id) + .where.not(completed_at: nil) + .max_by(&:completed_at) attachments = if last_completed_submitter Submitters.select_attachments_for_download(last_completed_submitter) else - submitter.submission.template.schema_documents.preload(:blob) + @submitter.submission.template.schema_documents.preload(:blob) end urls = attachments.map do |attachment| diff --git a/app/controllers/templates_clone_and_replace_controller.rb b/app/controllers/templates_clone_and_replace_controller.rb new file mode 100644 index 00000000..0d75416b --- /dev/null +++ b/app/controllers/templates_clone_and_replace_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class TemplatesCloneAndReplaceController < ApplicationController + load_and_authorize_resource :template + + def create + return head :unprocessable_entity if params[:files].blank? + + ActiveRecord::Associations::Preloader.new( + records: [@template], + associations: [schema_documents: :preview_images_attachments] + ).call + + cloned_template = Templates::Clone.call(@template, author: current_user) + cloned_template.name = File.basename(params[:files].first.original_filename, '.*') + 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 + respond_to do |f| + f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) } + f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity } + end + end +end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 3010d4ab..6eb91ed8 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -21,9 +21,7 @@ class TemplatesController < ApplicationController submissions.order(id: :desc) end - submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin]) - - @pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events)) + @pagy, @submissions = pagy(submissions.preload(:template_accesses, submitters: :start_form_submission_events)) rescue ActiveRecord::RecordNotFound redirect_to root_path end diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 0bd9a2be..319b0297 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -45,13 +45,15 @@ class TemplatesDashboardController < ApplicationController rel = templates.active.preload(:author, :template_accesses) if params[:q].blank? - if Docuseal.multitenant? && !current_account.testing? - rel = rel.where(folder_id: current_account.default_template_folder.id) - else - shared_template_ids = - TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id) + if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account + shared_account_ids = [current_user.account_id] + shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing? + + shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id) rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids)) + else + rel = rel.where(folder_id: current_account.default_template_folder.id) end end diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index 07111a16..8f820e3f 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -26,12 +26,20 @@ class TemplatesPreferencesController < ApplicationController completed_notification_email_attach_documents completed_redirect_url validate_unique_submitters submitters_order require_phone_2fa + default_expire_at_duration + default_expire_at completed_notification_email_subject completed_notification_email_body completed_notification_email_enabled completed_notification_email_attach_audit] + [completed_message: %i[title body], submitters: [%i[uuid request_email_subject request_email_body]]] ).tap do |attrs| attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1' + + if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence) + attrs[:preferences][:default_expire_at] = + (ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc + end + attrs[:preferences] = attrs[:preferences].transform_values do |value| if %w[true false].include?(value) value == 'true' diff --git a/app/javascript/application.js b/app/javascript/application.js index 78e542b0..c2e12079 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: true, 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..b99c00ff --- /dev/null +++ b/app/javascript/elements/dashboard_dropzone.js @@ -0,0 +1,206 @@ +import { target, targets, targetable } from '@github/catalyst/lib/targetable' + +const loadingIconHtml = ` + + +` + +export default targetable(class extends HTMLElement { + static [targets.static] = [ + 'hiddenOnDrag', + 'folderCards', + 'templateCards' + ] + + static [target.static] = [ + 'form', + 'fileDropzone', + 'fileDropzoneLoading' + ] + + connectedCallback () { + document.addEventListener('drop', this.onWindowDragdrop) + document.addEventListener('dragover', this.onWindowDropover) + + window.addEventListener('dragleave', this.onWindowDragleave) + + this.fileDropzone?.addEventListener('drop', this.onDropFile) + + this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el))) + this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate)) + this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart)) + + return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => { + el?.addEventListener('dragover', this.onDragover) + el?.addEventListener('dragleave', this.onDragleave) + }) + } + + disconnectedCallback () { + document.removeEventListener('drop', this.onWindowDragdrop) + document.removeEventListener('dragover', this.onWindowDropover) + + window.removeEventListener('dragleave', this.onWindowDragleave) + } + + onTemplateDragStart = (e) => { + const id = e.target.href.split('/').pop() + + e.dataTransfer.effectAllowed = 'move' + + if (id) { + e.dataTransfer.setData('template_id', id) + + const dragPreview = e.target.cloneNode(true) + const rect = e.target.getBoundingClientRect() + + const height = e.target.children[0].getBoundingClientRect().height + 50 + + dragPreview.children[1].remove() + dragPreview.style.width = `${rect.width}px` + dragPreview.style.height = `${height}px` + dragPreview.style.position = 'absolute' + dragPreview.style.top = '-1000px' + dragPreview.style.pointerEvents = 'none' + dragPreview.style.opacity = '0.9' + + document.body.appendChild(dragPreview) + + e.dataTransfer.setDragImage(dragPreview, rect.width / 2, height / 2) + + setTimeout(() => document.body.removeChild(dragPreview), 0) + } + } + + onDropFile = (e) => { + e.preventDefault() + + this.fileDropzoneLoading.classList.remove('hidden') + this.fileDropzoneLoading.previousElementSibling.classList.add('hidden') + this.fileDropzoneLoading.classList.add('opacity-50') + + this.uploadFiles(e.dataTransfer.files, '/templates_upload') + } + + onDropFolder = (e, el) => { + e.preventDefault() + + const templateId = e.dataTransfer.getData('template_id') + + if (e.dataTransfer.files.length || templateId) { + const loading = document.createElement('div') + const svg = el.querySelector('svg') + + loading.innerHTML = loadingIconHtml + loading.children[0].classList.add(...svg.classList) + + el.replaceChild(loading.children[0], svg) + el.classList.add('opacity-50') + + if (e.dataTransfer.files.length) { + const params = new URLSearchParams({ folder_name: el.innerText }).toString() + + this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`) + } else { + const formData = new FormData() + + formData.append('name', el.innerText) + + fetch(`/templates/${templateId}/folder`, { + method: 'PUT', + redirect: 'manual', + body: formData, + headers: { + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + } + }).finally(() => { + window.Turbo.cache.clear() + window.Turbo.visit(location.href) + }) + } + } + } + + onDropTemplate = (e) => { + e.preventDefault() + + if (e.dataTransfer.files.length) { + const loading = document.createElement('div') + loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute') + loading.innerHTML = loadingIconHtml + + e.target.appendChild(loading) + e.target.classList.add('opacity-50') + + const id = e.target.href.split('/').pop() + + this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`) + } + } + + onWindowDragdrop = (e) => { + e.preventDefault() + + if (!this.isLoading) this.hideDraghover() + } + + uploadFiles (files, url) { + this.isLoading = true + + this.form.action = url + + this.form.querySelector('[type="file"]').files = files + + this.form.querySelector('[type="submit"]').click() + } + + onWindowDropover = (e) => { + e.preventDefault() + + if (e.dataTransfer?.types?.includes('Files')) { + this.showDraghover() + } + } + + onDragover (e) { + if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') { + this.style.backgroundColor = '#F7F3F0' + } + } + + onDragleave () { + this.style.backgroundColor = null + } + + onWindowDragleave = (e) => { + if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) { + this.hideDraghover() + } + } + + showDraghover = () => { + if (this.isDrag) return + + this.isDrag = true + + this.fileDropzone?.classList?.remove('hidden') + + this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' }) + + return [...this.folderCards, ...this.templateCards].forEach((el) => { + el.classList.remove('bg-base-200', 'before:hidden') + }) + } + + hideDraghover = () => { + this.isDrag = false + + this.fileDropzone?.classList?.add('hidden') + + this.hiddenOnDrag.forEach((el) => { el.style.display = null }) + + return [...this.folderCards, ...this.templateCards].forEach((el) => { + el.classList.add('bg-base-200', 'before:hidden') + }) + } +}) diff --git a/app/javascript/elements/toggle_visible.js b/app/javascript/elements/toggle_visible.js index b7d29a9c..d93fdaf5 100644 --- a/app/javascript/elements/toggle_visible.js +++ b/app/javascript/elements/toggle_visible.js @@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement { const elementIds = JSON.parse(this.dataset.elementIds) elementIds.forEach((elementId) => { - document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId) + document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId) }) } }) diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index c83c3873..77e1bb96 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -5,7 +5,20 @@ class="mx-auto pl-3 h-full" :class="isMobile ? 'pl-4' : 'md:pl-4'" @dragover="onDragover" + @drop="isDragFile = false" > + import Upload from './upload' import Dropzone from './dropzone' +import HoverDropzone from './hover_dropzone' import DragPlaceholder from './drag_placeholder' import Fields from './fields' import MobileDrawField from './mobile_draw_field' @@ -462,6 +476,7 @@ export default { MobileFields, Logo, Dropzone, + HoverDropzone, DocumentPreview, DocumentControls, IconInnerShadowTop, @@ -665,6 +680,11 @@ export default { required: false, default: true }, + withReplaceAndCloneUpload: { + type: Boolean, + required: false, + default: false + }, withPhone: { type: Boolean, required: false, @@ -724,7 +744,8 @@ export default { copiedArea: null, drawFieldType: null, drawOption: null, - dragField: null + dragField: null, + isDragFile: false } }, computed: { @@ -836,6 +857,7 @@ export default { window.addEventListener('keydown', this.onKeyDown) window.addEventListener('resize', this.onWindowResize) + window.addEventListener('dragleave', this.onWindowDragLeave) this.$nextTick(() => { if (document.location.search?.includes('stripe_connect_success')) { @@ -854,6 +876,7 @@ export default { window.removeEventListener('keydown', this.onKeyDown) window.removeEventListener('resize', this.onWindowResize) + window.removeEventListener('dragleave', this.onWindowDragLeave) }, beforeUpdate () { this.documentRefs = [] @@ -868,6 +891,13 @@ export default { ref.x = e.clientX - ref.offsetX ref.y = e.clientY - ref.offsetY + } else if (e.dataTransfer?.types?.includes('Files')) { + this.isDragFile = true + } + }, + onWindowDragLeave (event) { + if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) { + this.isDragFile = false } }, reorderFields (item) { @@ -1529,6 +1559,9 @@ export default { }) }, 'image/png') }, + onUploadFailed (error) { + if (error) alert(error) + }, updateFromUpload (data) { this.template.schema.push(...data.schema) this.template.documents.push(...data.documents) @@ -1649,6 +1682,29 @@ export default { this.save() }, + onDocumentsReplace (data) { + data.schema.forEach((schemaItem, index) => { + const existingSchemaItem = this.template.schema[index] + + if (this.template.schema[index]) { + this.onDocumentReplace({ + replaceSchemaItem: existingSchemaItem, + schema: [schemaItem], + documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)] + }) + } else { + this.updateFromUpload({ + schema: [schemaItem], + documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)], + fields: data.fields, + submitters: data.submitters + }) + } + }) + }, + onDocumentsReplaceAndTemplateClone (template) { + window.Turbo.visit(`/templates/${template.id}/edit`) + }, moveDocument (item, direction) { const currentIndex = this.template.schema.indexOf(item) diff --git a/app/javascript/template_builder/dropzone.vue b/app/javascript/template_builder/dropzone.vue index 4136e6fe..ce97bcc1 100644 --- a/app/javascript/template_builder/dropzone.vue +++ b/app/javascript/template_builder/dropzone.vue @@ -2,34 +2,42 @@