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