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