mirror of https://github.com/docusealco/docuseal
				
				
				
			
							parent
							
								
									380f553a17
								
							
						
					
					
						commit
						5643094a7a
					
				| @ -0,0 +1,37 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class UserSignaturesController < ApplicationController | ||||||
|  |   before_action :load_user_config | ||||||
|  |   authorize_resource :user_config | ||||||
|  | 
 | ||||||
|  |   def edit; end | ||||||
|  | 
 | ||||||
|  |   def update | ||||||
|  |     file = params[:file] | ||||||
|  | 
 | ||||||
|  |     return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank? | ||||||
|  | 
 | ||||||
|  |     blob = ActiveStorage::Blob.create_and_upload!(io: file.open, | ||||||
|  |                                                   filename: file.original_filename, | ||||||
|  |                                                   content_type: file.content_type) | ||||||
|  | 
 | ||||||
|  |     attachment = ActiveStorage::Attachment.create!( | ||||||
|  |       blob:, | ||||||
|  |       name: 'signature', | ||||||
|  |       record: current_user | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     if @user_config.update(value: attachment.uuid) | ||||||
|  |       redirect_to settings_profile_index_path, notice: 'Signature has been saved' | ||||||
|  |     else | ||||||
|  |       redirect_to settings_profile_index_path, notice: 'Unable to save signature' | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def load_user_config | ||||||
|  |     @user_config = | ||||||
|  |       UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::SIGNATURE_KEY) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -0,0 +1,46 @@ | |||||||
|  | import { target, targetable } from '@github/catalyst/lib/targetable' | ||||||
|  | import { cropCanvasAndExportToPNG } from '../submission_form/crop_canvas' | ||||||
|  | 
 | ||||||
|  | export default targetable(class extends HTMLElement { | ||||||
|  |   static [target.static] = ['canvas', 'input', 'clear', 'button'] | ||||||
|  | 
 | ||||||
|  |   async connectedCallback () { | ||||||
|  |     this.canvas.width = this.canvas.parentNode.parentNode.clientWidth | ||||||
|  |     this.canvas.height = this.canvas.parentNode.parentNode.clientWidth / 3 | ||||||
|  | 
 | ||||||
|  |     const { default: SignaturePad } = await import('signature_pad') | ||||||
|  | 
 | ||||||
|  |     this.pad = new SignaturePad(this.canvas) | ||||||
|  | 
 | ||||||
|  |     this.clear.addEventListener('click', (e) => { | ||||||
|  |       e.preventDefault() | ||||||
|  | 
 | ||||||
|  |       this.pad.clear() | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     this.button.addEventListener('click', (e) => { | ||||||
|  |       e.preventDefault() | ||||||
|  | 
 | ||||||
|  |       this.button.disabled = true | ||||||
|  | 
 | ||||||
|  |       this.submit() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async submit () { | ||||||
|  |     const blob = await cropCanvasAndExportToPNG(this.canvas) | ||||||
|  |     const file = new File([blob], 'signature.png', { type: 'image/png' }) | ||||||
|  | 
 | ||||||
|  |     const dataTransfer = new DataTransfer() | ||||||
|  | 
 | ||||||
|  |     dataTransfer.items.add(file) | ||||||
|  | 
 | ||||||
|  |     this.input.files = dataTransfer.files | ||||||
|  | 
 | ||||||
|  |     if (this.input.webkitEntries.length) { | ||||||
|  |       this.input.dataset.file = `${dataTransfer.files[0].name}` | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.closest('form').requestSubmit() | ||||||
|  |   } | ||||||
|  | }) | ||||||
| @ -0,0 +1,49 @@ | |||||||
|  | function cropCanvasAndExportToPNG (canvas) { | ||||||
|  |   const ctx = canvas.getContext('2d') | ||||||
|  | 
 | ||||||
|  |   const width = canvas.width | ||||||
|  |   const height = canvas.height | ||||||
|  | 
 | ||||||
|  |   let topmost = height | ||||||
|  |   let bottommost = 0 | ||||||
|  |   let leftmost = width | ||||||
|  |   let rightmost = 0 | ||||||
|  | 
 | ||||||
|  |   const imageData = ctx.getImageData(0, 0, width, height) | ||||||
|  |   const pixels = imageData.data | ||||||
|  | 
 | ||||||
|  |   for (let y = 0; y < height; y++) { | ||||||
|  |     for (let x = 0; x < width; x++) { | ||||||
|  |       const pixelIndex = (y * width + x) * 4 | ||||||
|  |       const alpha = pixels[pixelIndex + 3] | ||||||
|  |       if (alpha !== 0) { | ||||||
|  |         topmost = Math.min(topmost, y) | ||||||
|  |         bottommost = Math.max(bottommost, y) | ||||||
|  |         leftmost = Math.min(leftmost, x) | ||||||
|  |         rightmost = Math.max(rightmost, x) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const croppedWidth = rightmost - leftmost + 1 | ||||||
|  |   const croppedHeight = bottommost - topmost + 1 | ||||||
|  | 
 | ||||||
|  |   const croppedCanvas = document.createElement('canvas') | ||||||
|  |   croppedCanvas.width = croppedWidth | ||||||
|  |   croppedCanvas.height = croppedHeight | ||||||
|  |   const croppedCtx = croppedCanvas.getContext('2d') | ||||||
|  | 
 | ||||||
|  |   croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight) | ||||||
|  | 
 | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     croppedCanvas.toBlob((blob) => { | ||||||
|  |       if (blob) { | ||||||
|  |         resolve(blob) | ||||||
|  |       } else { | ||||||
|  |         reject(new Error('Failed to create a PNG blob.')) | ||||||
|  |       } | ||||||
|  |     }, 'image/png') | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { cropCanvasAndExportToPNG } | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | # == Schema Information | ||||||
|  | # | ||||||
|  | # Table name: user_configs | ||||||
|  | # | ||||||
|  | #  id         :bigint           not null, primary key | ||||||
|  | #  key        :string           not null | ||||||
|  | #  value      :text             not null | ||||||
|  | #  created_at :datetime         not null | ||||||
|  | #  updated_at :datetime         not null | ||||||
|  | #  user_id    :bigint           not null | ||||||
|  | # | ||||||
|  | # Indexes | ||||||
|  | # | ||||||
|  | #  index_user_configs_on_user_id          (user_id) | ||||||
|  | #  index_user_configs_on_user_id_and_key  (user_id,key) UNIQUE | ||||||
|  | # | ||||||
|  | # Foreign Keys | ||||||
|  | # | ||||||
|  | #  fk_rails_...  (user_id => users.id) | ||||||
|  | # | ||||||
|  | class UserConfig < ApplicationRecord | ||||||
|  |   SIGNATURE_KEY = 'signature' | ||||||
|  | 
 | ||||||
|  |   belongs_to :user | ||||||
|  | 
 | ||||||
|  |   serialize :value, JSON | ||||||
|  | end | ||||||
| @ -1,3 +1,3 @@ | |||||||
| <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %> | <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %> | ||||||
| <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> | <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> | ||||||
| <submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form> | <submission-form data-is-demo="<%= Docuseal.demo? %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form> | ||||||
|  | |||||||
| @ -0,0 +1,55 @@ | |||||||
|  | <%= render 'shared/turbo_modal', title: 'Update Signature' do %> | ||||||
|  |   <% options = [%w[Draw draw], %w[Upload upload]] %> | ||||||
|  |   <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block"> | ||||||
|  |     <div class="join"> | ||||||
|  |       <% options.each_with_index do |(label, value), index| %> | ||||||
|  |         <span> | ||||||
|  |           <%= radio_button_tag 'option', value, value == 'draw', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> | ||||||
|  |           <label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item w-28 peer-checked:btn-active normal-case"> | ||||||
|  |             <%= label %> | ||||||
|  |           </label> | ||||||
|  |         </span> | ||||||
|  |       <% end %> | ||||||
|  |     </div> | ||||||
|  |   </toggle-visible> | ||||||
|  |   <div id="draw" class="mt-3"> | ||||||
|  |     <%= form_for @user_config, url: user_signature_path, method: :put, data: { turbo_frame: :_top }, html: { autocomplete: :off, enctype: 'multipart/form-data' } do |f| %> | ||||||
|  |       <signature-form class="relative block"> | ||||||
|  |         <a class="absolute top-1 right-1 link text-sm" data-target="signature-form.clear" href="#">Clear</a> | ||||||
|  |         <canvas data-target="signature-form.canvas" class="bg-white border border-base-300 rounded"></canvas> | ||||||
|  |         <input name="file" class="hidden" data-target="signature-form.input" type="file" accept="image/png,image/jpeg,image/jpg"> | ||||||
|  |         <div class="form-control mt-4"> | ||||||
|  |           <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button', data: { target: 'signature-form.button' } %> | ||||||
|  |         </div> | ||||||
|  |       </signature-form> | ||||||
|  |     <% end %> | ||||||
|  |   </div> | ||||||
|  |   <div id="upload" class="hidden mt-3"> | ||||||
|  |     <%= form_for @user_config, url: user_signature_path, method: :put, data: { turbo_frame: :_top }, html: { autocomplete: :off, enctype: 'multipart/form-data' } do |f| %> | ||||||
|  |       <file-dropzone data-is-direct-upload="false" data-submit-on-upload="true" class="w-full"> | ||||||
|  |         <label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed"> | ||||||
|  |           <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> | ||||||
|  |             <div class="flex flex-col items-center"> | ||||||
|  |               <span data-target="file-dropzone.icon"> | ||||||
|  |                 <%= svg_icon('cloud_upload', class: 'w-10 h-10') %> | ||||||
|  |               </span> | ||||||
|  |               <span data-target="file-dropzone.loading" class="hidden"> | ||||||
|  |                 <%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %> | ||||||
|  |               </span> | ||||||
|  |               <div class="font-medium mb-1"> | ||||||
|  |                 Upload Signature | ||||||
|  |               </div> | ||||||
|  |               <div class="text-xs"> | ||||||
|  |                 <span class="font-medium">Click to upload</span> or drag and drop | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |             <input id="file" name="file" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/png,image/jpeg,image/jpg"> | ||||||
|  |           </div> | ||||||
|  |         </label> | ||||||
|  |       </file-dropzone> | ||||||
|  |       <div class="form-control mt-4"> | ||||||
|  |         <%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %> | ||||||
|  |       </div> | ||||||
|  |     <% end %> | ||||||
|  |   </div> | ||||||
|  | <% end %> | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class CreateUserConfigs < ActiveRecord::Migration[7.0] | ||||||
|  |   def change | ||||||
|  |     create_table :user_configs do |t| | ||||||
|  |       t.references :user, null: false, foreign_key: true, index: true | ||||||
|  |       t.string :key, null: false | ||||||
|  |       t.text :value, null: false | ||||||
|  | 
 | ||||||
|  |       t.index %i[user_id key], unique: true | ||||||
|  | 
 | ||||||
|  |       t.timestamps | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -0,0 +1,48 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Submitters | ||||||
|  |   module MaybeUpdateDefaultValues | ||||||
|  |     module_function | ||||||
|  | 
 | ||||||
|  |     def call(submitter, current_user) | ||||||
|  |       user = | ||||||
|  |         if current_user && current_user.email == submitter.email | ||||||
|  |           current_user | ||||||
|  |         else | ||||||
|  |           submitter.account.users.find_by(email: submitter.email) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |       return if user.blank? | ||||||
|  | 
 | ||||||
|  |       fields = submitter.submission.template_fields || submitter.submission.template.fields | ||||||
|  | 
 | ||||||
|  |       fields.each do |field| | ||||||
|  |         next if field['submitter_uuid'] != submitter.uuid | ||||||
|  | 
 | ||||||
|  |         submitter.values[field['uuid']] ||= get_default_value_for_field(field, user, submitter) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       submitter.save! | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def get_default_value_for_field(field, user, submitter) | ||||||
|  |       field_name = field['name'].to_s.downcase | ||||||
|  | 
 | ||||||
|  |       if field_name.in?(['full name', 'legal name']) | ||||||
|  |         user.full_name | ||||||
|  |       elsif field_name == 'first name' | ||||||
|  |         user.first_name | ||||||
|  |       elsif field_name == 'last name' | ||||||
|  |         user.last_name | ||||||
|  |       elsif field['type'] == 'signature' && (signature = UserConfigs.load_signature(user)) | ||||||
|  |         attachment = ActiveStorage::Attachment.find_or_create_by!( | ||||||
|  |           blob_id: signature.blob_id, | ||||||
|  |           name: 'attachments', | ||||||
|  |           record: submitter | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         attachment.uuid | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -0,0 +1,13 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module UserConfigs | ||||||
|  |   module_function | ||||||
|  | 
 | ||||||
|  |   def load_signature(user) | ||||||
|  |     return if user.blank? | ||||||
|  | 
 | ||||||
|  |     uuid = user.user_configs.find_or_initialize_by(key: UserConfig::SIGNATURE_KEY).value | ||||||
|  | 
 | ||||||
|  |     ActiveStorage::Attachment.find_by(uuid:, record: user, name: 'signature') if uuid.present? | ||||||
|  |   end | ||||||
|  | end | ||||||
					Loading…
					
					
				
		Reference in new issue