Merge from docusealco/wip

pull/475/head 1.9.7
Alex Turchyn 7 months ago committed by GitHub
commit 1f48be135c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,

@ -23,9 +23,9 @@ class SubmissionsDownloadController < ApplicationController
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
Submissions::EnsureResultGenerated.call(last_submitter)
return head :not_found unless last_submitter
return head :not_found unless last_submitter.completed_at?
Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)

@ -20,7 +20,11 @@ class SubmitFormController < ApplicationController
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return render :awaiting if (@form_configs[:enforce_signing_order] ||
submission.template.preferences['submitters_order'] == 'preserved') &&
!Submitters.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter)
@ -29,8 +33,6 @@ class SubmitFormController < ApplicationController
@attachments_index = build_attachments_index(submission)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return unless @form_configs[:prefill_signature]
if (user_signature = UserConfigs.load_signature(current_user))
@ -70,6 +72,10 @@ class SubmitFormController < ApplicationController
Submitters::SubmitValues.call(submitter, params, request)
head :ok
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity
end

@ -35,8 +35,6 @@ import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import * as TurboInstantClick from './lib/turbo_instant_click'
import './images/preview.png'
TurboInstantClick.start()
document.addEventListener('turbo:before-cache', () => {

@ -1,5 +1,6 @@
import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () {
@ -43,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return response
})
}).catch(error => {
console.log(error)
}).finally(() => {
this.submitButton.disabled = false
})
@ -65,8 +68,13 @@ window.customElements.define('draw-signature', class extends HTMLElement {
}
async submitImage () {
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
if (!isValidSignatureCanvas(this.pad.toData())) {
alert('Signature is too small or simple. Please redraw.')
return Promise.reject(new Error('Image too small or simple'))
}
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
const formData = new FormData()
@ -79,12 +87,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
return resolve(attachment)
})
}).catch((error) => {
return reject(error)
})
}).then(resp => resp.json())
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
function cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d')
const width = canvas.width
@ -33,10 +33,6 @@ function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSma
croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d')
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
return Promise.reject(new Error('Image too small'))
}
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => {

@ -1404,7 +1404,21 @@ export default {
if (response.status === 422 || response.status === 500) {
const data = await response.json()
if (data.error) {
if (data.field_uuid) {
const field = this.fieldsUuidIndex[data.field_uuid]
if (field) {
const step = this.stepFields.findIndex((fields) => fields.includes(field))
if (step !== -1) {
this.goToStep(step, this.autoscrollFields)
this.showFillAllRequiredFields = true
}
}
return Promise.reject(new Error('Required field: ' + data.field_uuid))
} else if (data.error) {
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
@ -1439,11 +1453,7 @@ export default {
this.isSubmittingComplete = false
})
}).catch(error => {
if (error?.message === 'Image too small') {
alert(this.t('signature_is_too_small_please_redraw'))
} else {
console.log(error)
}
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false

@ -93,7 +93,7 @@ const en = {
reupload: 'Reupload',
upload: 'Upload',
files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.',
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
wait_countdown_seconds: 'Wait {countdown} seconds'
}
@ -191,7 +191,7 @@ const es = {
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
wait_countdown_seconds: 'Espera {countdown} segundos'
}
@ -289,7 +289,7 @@ const it = {
reupload: 'Ricarica',
upload: 'Carica',
files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
wait_countdown_seconds: 'Attendi {countdown} secondi'
}
@ -387,7 +387,7 @@ const de = {
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
wait_countdown_seconds: 'Warte {countdown} Sekunden'
}
@ -485,7 +485,7 @@ const fr = {
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
wait_countdown_seconds: 'Attendez {countdown} secondes'
}
@ -583,7 +583,7 @@ const pl = {
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki',
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
}
const uk = {
@ -680,7 +680,7 @@ const uk = {
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.',
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
}
@ -778,7 +778,7 @@ const cs = {
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory',
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.',
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
wait_countdown_seconds: 'Počkejte {countdown} sekund'
}
@ -876,7 +876,7 @@ const pt = {
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.',
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
wait_countdown_seconds: 'Aguarde {countdown} segundos'
}
@ -975,7 +975,7 @@ const he = {
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.',
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
wait_countdown_seconds: 'המתן {countdown} שניות'
}
@ -1074,7 +1074,7 @@ const nl = {
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.',
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
wait_countdown_seconds: 'Wacht {countdown} seconden'
}
@ -1172,7 +1172,7 @@ const ar = {
reupload: 'إعادة التحميل',
upload: 'تحميل',
files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.',
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
wait_countdown_seconds: 'انتظر {countdown} ثانية'
}
@ -1269,7 +1269,7 @@ const ko = {
reupload: '다시 업로드',
upload: '업로드',
files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작니다. 다시 그려주세요.',
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요'
}

@ -118,11 +118,17 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44"
>
<div class="relative">
<div
v-if="!isDrawInitials"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
</div>
<input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input"

@ -292,6 +292,7 @@
<script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature'
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
import FileDropzone from './dropzone'
@ -598,6 +599,7 @@ export default {
},
drawImage (event) {
this.remove()
this.clear()
this.isSignatureStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
@ -680,8 +682,14 @@ export default {
return Promise.resolve({})
}
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return Promise.reject(new Error('Image too small or simple'))
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.dryRun) {
@ -717,12 +725,6 @@ export default {
return resolve(attachment)
})
}
}).catch((error) => {
if (error.message === 'Image too small' && this.field.required === false) {
return resolve({})
} else {
return reject(error)
}
})
})
}

@ -0,0 +1,38 @@
function isValidSignatureCanvas (data) {
if (data.length === 0) return false
const strokes = data.filter(stroke => Array.isArray(stroke.points) && stroke.points.length > 2)
if (strokes.length === 0) return false
let skippedStraightLine = 0
const validStrokes = strokes.filter(stroke => {
const points = stroke.points
const first = points[0]
const last = points[points.length - 1]
const A = last.y - first.y
const B = first.x - last.x
const C = last.x * first.y - first.x * last.y
const lineLength = Math.sqrt(A * A + B * B)
const totalDeviation = points.reduce((sum, p) => {
const distanceToLine = Math.abs(A * p.x + B * p.y + C) / lineLength
return sum + distanceToLine
}, 0)
const avgDeviation = totalDeviation / points.length
if (avgDeviation < 3 && skippedStraightLine < 2) {
skippedStraightLine++
return false
}
return true
})
return validStrokes.length > 0
}
export { isValidSignatureCanvas }

@ -94,11 +94,27 @@ class ProcessSubmitterCompletionJob
end
end
to = build_to_addresses(submitter)
maybe_enqueue_copy_emails(submitter)
end
def maybe_enqueue_copy_emails(submitter)
return if submitter.template.preferences['documents_copy_email_enabled'] == false
configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
return if configs.value['enabled'] == false
return if to.blank? || submitter.template.preferences['documents_copy_email_enabled'] == false
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name)
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later!
return if to.blank?
if configs.value['bcc_recipients'] == true
to.each { |to| SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! }
else
SubmitterMailer.documents_copy_email(submitter, to: to.join(', ')).deliver_later!
end
end
def build_bcc_addresses(submission)
@ -110,11 +126,6 @@ class ProcessSubmitterCompletionJob
bcc.to_s.scan(User::EMAIL_REGEXP)
end
def build_to_addresses(submitter)
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
end
def enqueue_next_submitter_request_notification(submitter)
next_submitter_item =
submitter.submission.template_submitters.find do |e|

@ -30,6 +30,7 @@ class AccountConfig < ApplicationRecord
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
SUBMITTER_REMINDERS = 'submitter_reminders'
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'

@ -151,6 +151,20 @@
</div>
<% end %>
<% end %>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('always_enforce_signing_order') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
<%= render 'compliances' %>

@ -8,7 +8,7 @@
<div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents')) do |ff| %>
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
@ -24,24 +24,42 @@
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="flex items-center justify-between pt-2.5 mx-1">
<div class="space-y-3.5">
<div class="flex items-center justify-between mx-1">
<span>
<%= t('attach_documents') %>
</span>
<%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %>
</div>
<div class="flex items-center justify-between pb-2.5 mx-1">
<div class="flex items-center justify-between mx-1">
<span>
<%= t('attach_audit_log_pdf') %>
</span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% unless Docuseal.multitenant? %>
<div class="flex items-center justify-between mx-1">
<span>
<%= t('bcc_recipients') %>
</span>
<%= ff.check_box :bcc_recipients, { checked: ff.object.bcc_recipients == true, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %>
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
<div class="flex items-center justify-between mx-1">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :enabled, { checked: ff.object.enabled != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>

@ -13,10 +13,10 @@
<meta property="og:image" content="">
<meta name="twitter:image" content="">
<% else %>
<meta property="og:image" content="<%= image_pack_url('images/preview.png') %>">
<meta property="og:image" content="<%= root_url %>preview.png">
<meta property="og:image:width" content="800">
<meta property="og:image:height" content="800">
<meta name="twitter:image" content="<%= image_pack_url('images/preview.png') %>">
<meta name="twitter:image" content="<%= root_url %>preview.png">
<% end %>
<meta name="twitter:card" content="summary">
<meta name="twitter:creator" content="@docusealco">

@ -206,7 +206,7 @@
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['enabled'] == false }, 'true', 'false' %>
</div>
<% end %>
<div class="form-control pt-2">
@ -322,6 +322,7 @@
<% end %>
</div>
<% end %>
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
<span>
@ -332,6 +333,7 @@
<% end %>
</div>
<% end %>
<% end %>
<div class="form-control mt-5 pb-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
</div>

@ -1,7 +1,7 @@
<div class="flex-wrap space-y-4 md:flex md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="md:flex-grow">
<div class="flex flex-col md:flex-row gap-2 md:justify-between md:items-end mb-4 min-h-12">
<div class="flex flex-col md:flex-row md:flex-wrap gap-2 md:justify-between md:items-end mb-4 min-h-12">
<h1 class="text-4xl font-bold">
<% if params[:status] == 'archived' %>
<%= t('archived_users') %>

@ -20,6 +20,8 @@ en: &en
language_ko: 한국어
hi_there: Hi there
thanks: Thanks
bcc_recipients: BCC recipients
always_enforce_signing_order: Always enforce the signing order
edit_per_party: Edit per party
reply_to: Reply to
pending_by_me: Pending by me
@ -432,7 +434,7 @@ en: &en
share_template_with_test_mode: Share template with Test mode
share_template_with_all_tenants: Share template with all Tenants
use_following_placeholders_text_: 'Use following placeholders text:'
upgrade_plan_to_add_more_users: Upgrade plan to add more users
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.'
move_into_folder: Move Into Folder
new_folder_name: New Folder Name
exit_preview: Exit Preview
@ -473,8 +475,6 @@ en: &en
team_accounts: Team Accounts
tenant_account: Tenant account
tenant_accounts: Tenant Accounts
upgrade_plan: Upgrade Plan
add_user: Add User
impersonate: Impersonate
loading: Loading
documents: Documents
@ -745,6 +745,8 @@ en: &en
read: Read your data
es: &es
always_enforce_signing_order: Siempre imponer el orden de firma
bcc_recipients: Destinatarios CCO
edit_per_party: Editar por parte
signed: Firmado
reply_to: Responder a
@ -1159,7 +1161,7 @@ es: &es
share_template_with_test_mode: Compartir plantilla con el modo de prueba
share_template_with_all_tenants: Compartir plantilla con todos los inquilinos
use_following_placeholders_text_: 'Usa los siguientes marcadores de posición:'
upgrade_plan_to_add_more_users: Actualiza el plan para agregar más usuarios
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".'
move_into_folder: Mover a la carpeta
new_folder_name: Nuevo nombre de la carpeta
exit_preview: Salir de la vista previa
@ -1200,8 +1202,6 @@ es: &es
team_accounts: Cuentas de equipo
tenant_account: Cuenta de inquilino
tenant_accounts: Cuentas de inquilino
upgrade_plan: Actualizar plan
add_user: Agregar usuario
impersonate: Suplantar
loading: Cargando
documents: Documentos
@ -1472,6 +1472,8 @@ es: &es
read: Leer tus datos
it: &it
always_enforce_signing_order: Applicare sempre l'ordine di firma
bcc_recipients: Destinatari BCC
edit_per_party: Modifica per partito
signed: Firmato
reply_to: Rispondi a
@ -1885,7 +1887,7 @@ it: &it
share_template_with_test_mode: Condividi modello con la modalità di test
share_template_with_all_tenants: Condividi il modello con tutti i tenant
use_following_placeholders_text_: 'Usa i seguenti segnaposto:'
upgrade_plan_to_add_more_users: Aggiorna il piano per aggiungere più utenti
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".'
move_into_folder: Sposta nella cartella
new_folder_name: Nuovo nome della cartella
exit_preview: "Esci dall'anteprima"
@ -1926,8 +1928,6 @@ it: &it
team_accounts: Account di squadra
tenant_account: Account tenant
tenant_accounts: Account tenant
upgrade_plan: Aggiorna piano
add_user: Aggiungi utente
impersonate: Impersona
loading: Caricamento in corso
documents: Documenti
@ -2198,6 +2198,8 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
always_enforce_signing_order: Toujours appliquer l'ordre de signature
bcc_recipients: Destinataires en CCI
edit_per_party: Éditer par partie
signed: Signé
reply_to: Répondre à
@ -2613,7 +2615,7 @@ fr: &fr
share_template_with_test_mode: Partager le modèle avec le mode test
share_template_with_all_tenants: Partager le modèle avec tous les locataires
use_following_placeholders_text_: 'Utilisez les espaces réservés suivants:'
upgrade_plan_to_add_more_users: "Mettez à jour le plan pour ajouter plus d'utilisateurs"
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".'
move_into_folder: Déplacer dans le dossier
new_folder_name: Nouveau nom du dossier
exit_preview: "Quitter l'aperçu"
@ -2654,8 +2656,6 @@ fr: &fr
team_accounts: "Comptes d'équipe"
tenant_account: Compte locataire
tenant_accounts: Comptes locataires
upgrade_plan: Mettre à jour le plan
add_user: Ajouter un utilisateur
impersonate: Usurper
loading: Chargement en cours
documents: Documents
@ -2926,6 +2926,8 @@ fr: &fr
read: Lire vos données
pt: &pt
always_enforce_signing_order: Sempre impor a ordem de assinatura
bcc_recipients: Destinatários BCC
edit_per_party: Edita por festa
signed: Assinado
reply_to: Responder a
@ -3340,7 +3342,7 @@ pt: &pt
share_template_with_test_mode: Compartilhar modelo com o modo de teste
share_template_with_all_tenants: Compartilhar modelo com todos os locatários
use_following_placeholders_text_: 'Use os seguintes textos de substituição:'
upgrade_plan_to_add_more_users: Faça upgrade do plano para adicionar mais usuários
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".'
move_into_folder: Mover para pasta
new_folder_name: Novo nome da pasta
exit_preview: Sair da pré-visualização
@ -3381,8 +3383,6 @@ pt: &pt
team_accounts: Contas de equipe
tenant_account: Conta de locatário
tenant_accounts: Contas de locatário
upgrade_plan: Fazer upgrade do plano
add_user: Adicionar usuário
impersonate: Usurpar
loading: Carregando
documents: Documentos
@ -3653,6 +3653,8 @@ pt: &pt
read: Ler seus dados
de: &de
always_enforce_signing_order: Immer die Reihenfolge der Unterschriften erzwingen
bcc_recipients: BCC-Empfänger
edit_per_party: Bearbeiten pro Partei
signed: Unterschrieben
reply_to: Antworten auf
@ -4067,7 +4069,7 @@ de: &de
share_template_with_test_mode: Vorlage mit dem Testmodus teilen
share_template_with_all_tenants: Vorlage mit allen Mietern teilen
use_following_placeholders_text_: 'Verwende die folgenden Platzhaltertexte:'
upgrade_plan_to_add_more_users: Upgrade des Plans, um weitere Benutzer hinzuzufügen
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".'
move_into_folder: In Ordner verschieben
new_folder_name: Neuer Ordnername
exit_preview: Vorschau beenden
@ -4108,8 +4110,6 @@ de: &de
team_accounts: Teamkonten
tenant_account: Mieterkonto
tenant_accounts: Mieterkonten
upgrade_plan: Plan upgraden
add_user: Benutzer hinzufügen
impersonate: Nachahmen
loading: Wird geladen
documents: Dokumente

@ -28,7 +28,7 @@ module ActionMailerConfigsInterceptor
end
unless Docuseal.multitenant?
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
email_configs = EncryptedConfig.order(:account_id).find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
if email_configs
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))

@ -196,4 +196,20 @@ module Submissions
end
end.exclude?(false)
end
def regenerate_documents(submission)
submitters = submission.submitters.where.not(completed_at: nil).preload(:documents_attachments)
submitters.each { |submitter| submitter.documents.each(&:destroy!) }
submission.submitters.where.not(completed_at: nil).order(:completed_at).each do |submitter|
GenerateResultAttachments.call(submitter)
end
return if submission.combined_document_attachment.blank?
submission.combined_document_attachment.destroy!
Submissions::GenerateCombinedAttachment.call(submission.submitters.completed.order(:completed_at).last)
end
end

@ -7,11 +7,15 @@ module Submissions
CHECK_COMPLETE_TIMEOUT = 90.seconds
WaitForCompleteTimeout = Class.new(StandardError)
NotCompletedYet = Class.new(StandardError)
module_function
def call(submitter)
return [] unless submitter
raise NotCompletedYet unless submitter.completed_at?
return submitter.documents if ApplicationRecord.uncached { submitter.document_generation_events.complete.exists? }
events =

@ -8,6 +8,7 @@ module Submitters
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE,
@ -27,6 +28,7 @@ module Submitters
with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
attrs = {
@ -36,6 +38,7 @@ module Submitters
reuse_signature:,
with_decline:,
policy_links:,
enforce_signing_order:,
completed_message:,
require_signing_reason:,
prefill_signature:,

@ -3,12 +3,14 @@
module Submitters
module SubmitValues
ValidationError = Class.new(StandardError)
RequiredFieldError = Class.new(StandardError)
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
NONEDITABLE_FIELD_TYPES = %w[stamp heading].freeze
module_function
def call(submitter, params, request)
def call(submitter, params, request, validate_required: true)
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
unless submitter.submission_events.exists?(event_type: 'start_form')
@ -20,7 +22,7 @@ module Submitters
end
end
update_submitter!(submitter, params, request)
update_submitter!(submitter, params, request, validate_required:)
submitter.submission.save!
@ -29,13 +31,13 @@ module Submitters
submitter
end
def update_submitter!(submitter, params, request)
def update_submitter!(submitter, params, request, validate_required: true)
values = normalized_values(params)
submitter.values.merge!(values)
submitter.opened_at ||= Time.current
assign_completed_attributes(submitter, request) if params[:completed] == 'true'
assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true'
ApplicationRecord.transaction do
maybe_set_signature_reason!(values, submitter, params)
@ -49,25 +51,36 @@ module Submitters
submitter
end
def assign_completed_attributes(submitter, request)
def assign_completed_attributes(submitter, request, validate_required: true)
submitter.completed_at = Time.current
submitter.ip = request.remote_ip
submitter.ua = request.user_agent
submitter.values = merge_default_values(submitter)
submitter.values = maybe_remove_condition_values(submitter)
required_field_uuids_acc = Set.new
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
formula_values = build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
submitter.values = maybe_remove_condition_values(submitter)
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
required_field_uuids_acc.each do |uuid|
next if submitter.values[uuid].present?
raise RequiredFieldError, uuid if validate_required
Rollbar.warning("Required field #{submitter.id}: #{uuid}") if defined?(Rollbar)
end
submitter
end
@ -188,37 +201,67 @@ module Submitters
with_time:)
end
def maybe_remove_condition_values(submitter)
fields_uuid_index = submitter.submission.template_fields.index_by { |e| e['uuid'] }
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
submission = submitter.submission
attachments_index =
Submissions.filtered_conditions_schema(submitter.submission).index_by { |i| i['attachment_uuid'] }
fields_uuid_index = submission.template_fields.index_by { |e| e['uuid'] }
submitter_values = nil
is_other_submitter_conditions = submitter.submission.template_submitters.size > 1
submitters_values = nil
has_other_submitters = submission.template_submitters.size > 1
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
has_document_conditions =
(submission.template_schema || submission.template.schema).any? { |e| e['conditions'].present? }
attachments_index =
if has_document_conditions
Submissions.filtered_conditions_schema(submission).index_by { |i| i['attachment_uuid'] }
end
submitter_values ||= submitter.values
submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
is_other_submitter_conditions &&= field_conditions_other_submitter?(submitter, field, fields_uuid_index)
required_field_uuids_acc.add(field['uuid']) if required_field_uuids_acc && required_editable_field?(field)
if is_other_submitter_conditions
submitter_values = submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
if has_document_conditions && !check_field_areas_attachments(field, attachments_index)
submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid'])
end
submitter.values.delete(field['uuid']) unless check_field_conditions(submitter_values, field, fields_uuid_index)
if has_other_submitters && !submitters_values &&
field_conditions_other_submitter?(submitter, field, fields_uuid_index)
submitters_values = merge_submitters_values(submitter)
end
if field['areas'].present? && field['areas'].none? { |area| attachments_index[area['attachment_uuid']] }
unless check_field_conditions(submitters_values || submitter.values, field, fields_uuid_index)
submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid'])
end
end
submitter.values
end
def required_editable_field?(field)
return false if NONEDITABLE_FIELD_TYPES.include?(field['type'])
field['required'].present? && field['readonly'].blank?
end
def check_field_areas_attachments(field, attachments_index)
return true if field['areas'].blank?
field['areas'].any? { |area| attachments_index[area['attachment_uuid']] }
end
def merge_submitters_values(submitter)
submitter.submission.submitters
.reduce({}) { |acc, sub| acc.merge(sub.values) }
.merge(submitter.values)
end
def field_conditions_other_submitter?(submitter, field, fields_uuid_index)
return false if field['conditions'].blank?
field['conditions'].to_a.any? do |c|
fields_uuid_index.dig(c['field_uuid'], 'submitter_uuid') != submitter.uuid
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@ -4,21 +4,49 @@ module SigningFormHelper
module_function
def draw_canvas
page.find('canvas').click([], { x: 150, y: 100 })
page.execute_script <<~JS
const canvas = document.getElementsByTagName('canvas')[0];
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(150, 100);
ctx.lineTo(450, 100);
ctx.stroke();
const startX = rect.left + 50;
const startY = rect.top + 100;
ctx.beginPath();
ctx.moveTo(150, 100);
ctx.lineTo(150, 150);
ctx.stroke();
const amplitude = 20;
const wavelength = 30;
const length = 300;
function dispatchPointerEvent(type, x, y) {
const event = new PointerEvent(type, {
pointerId: 1,
pointerType: 'pen',
isPrimary: true,
clientX: x,
clientY: y,
bubbles: true,
pressure: 0.5
});
canvas.dispatchEvent(event);
}
dispatchPointerEvent('pointerdown', startX, startY);
let x = 0;
function drawStep() {
if (x > length) {
dispatchPointerEvent('pointerup', startX + x, startY);
return;
}
const y = startY + amplitude * Math.sin((x / wavelength) * 2 * Math.PI);
dispatchPointerEvent('pointermove', startX + x, y);
x += 5;
requestAnimationFrame(drawStep);
}
drawStep();
JS
sleep 0.5
end

@ -360,6 +360,19 @@ RSpec.describe 'Signing Form', type: :system do
expect(field_value(submitter, 'Signature')).to be_present
end
it 'shows an error message if the canvas is not drawn or too simple' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
page.find('canvas').click([], { x: 150, y: 100 })
alert_text = page.accept_alert do
click_button 'Sign and Complete'
end
expect(alert_text).to eq 'Signature is too small or simple. Please redraw.'
end
it 'completes the form if the canvas is typed' do
visit submit_form_path(slug: submitter.slug)

Loading…
Cancel
Save