mirror of https://github.com/docusealco/docuseal
parent
e34a763dd9
commit
97b8ac4444
@ -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
|
||||||
@ -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 })
|
||||||
|
}
|
||||||
|
}))
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isDragging || isLoading || isProcessing"
|
||||||
|
class="modal modal-open"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
|
||||||
|
<Dropzone
|
||||||
|
class="flex-1 h-full"
|
||||||
|
hover-class="bg-base-200/50"
|
||||||
|
icon="IconFilePlus"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-description="false"
|
||||||
|
:header="{ documents_or_images: t('upload_new_document') }"
|
||||||
|
type="add_files"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@processing="isProcessing = $event"
|
||||||
|
@success="$emit('add', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 flex gap-2 w-full">
|
||||||
|
<Dropzone
|
||||||
|
class="flex-1 h-full"
|
||||||
|
hover-class="bg-base-200/50"
|
||||||
|
icon="IconFileSymlink"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-description="false"
|
||||||
|
:header="{ documents_or_images: t('replace_existing_document') }"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@processing="isProcessing = $event"
|
||||||
|
@success="$emit('replace', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
<Dropzone
|
||||||
|
v-if="withReplaceAndClone"
|
||||||
|
class="flex-1 h-full"
|
||||||
|
hover-class="bg-base-200/50"
|
||||||
|
icon="IconFiles"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-description="false"
|
||||||
|
:clone-template-on-upload="true"
|
||||||
|
:header="{ documents_or_images: t('clone_template_and_replace_documents') }"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@processing="isProcessing = $event"
|
||||||
|
@success="$emit('replace-and-clone', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropzone from './dropzone'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HoverDropzone',
|
||||||
|
components: {
|
||||||
|
Dropzone
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
isDragging: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
templateId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withReplaceAndClone: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
isProcessing: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
After Width: | Height: | Size: 485 B |
@ -1,12 +1,15 @@
|
|||||||
<% is_long = folder.name.size > 32 %>
|
<% is_long = folder.name.size > 32 %>
|
||||||
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200">
|
<dashboard-dropzone class="relative rounded-2xl bg-base-200 border-2 border-transparent">
|
||||||
<% if !is_long %>
|
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between py-5 px-6 w-full">
|
||||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
<% if !is_long %>
|
||||||
<% end %>
|
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||||
<div class="text-lg font-semibold mt-1" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: <%= is_long ? 2 : 1 %>;">
|
|
||||||
<% if is_long %>
|
|
||||||
<%= svg_icon('folder', class: 'w-6 h-6 inline') %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= folder.name %>
|
<div class="text-lg font-semibold mt-1" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: <%= is_long ? 2 : 1 %>;">
|
||||||
</div>
|
<% if is_long %>
|
||||||
</a>
|
<%= svg_icon('folder', class: 'w-6 h-6 inline') %>
|
||||||
|
<% end %>
|
||||||
|
<%= folder.name %>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<%= render 'templates/dashboard_dropzone_form', url: templates_upload_path(folder_name: folder.name) %>
|
||||||
|
</dashboard-dropzone>
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
<%= form_for '', url: local_assigns.fetch(:url, templates_upload_path), id: form_id = SecureRandom.uuid, method: :post, class: 'block hidden', html: { enctype: 'multipart/form-data' }, data: { target: 'dashboard-dropzone.fileDropzone' } do %>
|
||||||
|
<input type="hidden" name="form_id" value="<%= form_id %>">
|
||||||
|
<button type="submit" class="hidden"></button>
|
||||||
|
<label for="dashboard_dropzone_input_<%= form_id %>">
|
||||||
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex justify-center p-2 items-<%= local_assigns.fetch(:position, 'center') %>">
|
||||||
|
<div class="flex flex-col items-center text-center">
|
||||||
|
<% if local_assigns[:icon] %>
|
||||||
|
<span data-target="dashboard-dropzone.icon" class="flex flex-col items-center">
|
||||||
|
<span>
|
||||||
|
<%= svg_icon(local_assigns[:icon], class: 'w-10 h-10') %>
|
||||||
|
</span>
|
||||||
|
<% if local_assigns[:title] %>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
<%= local_assigns[:title] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<span data-target="dashboard-dropzone.loading" class="flex flex-col items-center hidden">
|
||||||
|
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
<%= t('uploading') %>...
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input id="dashboard_dropzone_input_<%= form_id %>" name="files[]" class="hidden" data-action="change:dashboard-dropzone#onSelectFiles" data-target="dashboard-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.multitenant? %>" multiple>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Templates
|
||||||
|
module ReplaceAttachments
|
||||||
|
module_function
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def call(template, params = {}, extract_fields: false)
|
||||||
|
documents = Templates::CreateAttachments.call(template, params, extract_fields:)
|
||||||
|
submitter = template.submitters.first
|
||||||
|
|
||||||
|
documents.each_with_index do |document, index|
|
||||||
|
replaced_document_schema = template.schema[index]
|
||||||
|
|
||||||
|
template.schema[index] = { attachment_uuid: document.uuid, name: document.filename.base }
|
||||||
|
|
||||||
|
if replaced_document_schema
|
||||||
|
template.fields.each do |field|
|
||||||
|
next if field['areas'].blank?
|
||||||
|
|
||||||
|
field['areas'].each do |area|
|
||||||
|
if area['attachment_uuid'] == replaced_document_schema['attachment_uuid']
|
||||||
|
area['attachment_uuid'] = document.uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
next if template.fields.any? { |f| f['areas']&.any? { |a| a['attachment_uuid'] == document.uuid } }
|
||||||
|
next unless submitter && document.metadata.dig('pdf', 'fields').present?
|
||||||
|
|
||||||
|
pdf_fields = document.metadata['pdf'].delete('fields').to_a
|
||||||
|
pdf_fields.each { |f| f['submitter_uuid'] = submitter['uuid'] }
|
||||||
|
|
||||||
|
if index.positive? && pdf_fields.present?
|
||||||
|
preview_document = template.schema[index - 1]
|
||||||
|
preview_document_last_field = template.fields.reverse.find do |f|
|
||||||
|
f['areas']&.any? do |a|
|
||||||
|
a['attachment_uuid'] == preview_document[:attachment_uuid]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if preview_document_last_field
|
||||||
|
last_preview_document_field_index = template.fields.find_index do |f|
|
||||||
|
f['uuid'] == preview_document_last_field['uuid']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if last_preview_document_field_index
|
||||||
|
template.fields.insert(index, *pdf_fields)
|
||||||
|
else
|
||||||
|
template.fields += pdf_fields
|
||||||
|
end
|
||||||
|
elsif pdf_fields.present?
|
||||||
|
template.fields += pdf_fields
|
||||||
|
|
||||||
|
template.schema[index]['pending_fields'] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
documents
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in new issue