Compare commits

...
52 Commits
Author SHA1 Message Date
Alex Turchyn 019222ae40 Merge from docusealco/wip 2025-05-19 13:07:42 +03:00
Pete Matsyburka 8a10852fe2 docs 2025-05-18 17:38:51 +03:00
Pete Matsyburka 01232aed5a add account access 2025-05-18 15:16:53 +03:00
Alex Turchyn 82f3ff4824 fix narrow signatures 2025-05-18 09:44:55 +03:00
Pete Matsyburka 7be3f5a931 fix android 2025-05-16 18:28:55 +03:00
Pete Matsyburka 9d013a7384 fix email normalize 2025-05-15 22:16:06 +03:00
Alex Turchyn 0c71d7bff8 fix EU server selection note 2025-05-15 18:05:07 +03:00
Pete Matsyburka fdd030652e adjust border 2025-05-15 14:39:25 +03:00
Pete Matsyburka f2bf1461f0 adjust selector 2025-05-15 13:50:02 +03:00
Alex Turchyn fc6f796f30 adjust server selector 2025-05-15 13:16:43 +03:00
Alex Turchyn 5f44806287 adjust dropoze border style 2025-05-15 13:16:40 +03:00
Pete Matsyburka 318b65af18 fix i18n 2025-05-14 21:28:52 +03:00
Pete Matsyburka 91b182b04a adjust email validate 2025-05-14 20:40:59 +03:00
Pete Matsyburka 15b886d4ff update reminder durations 2025-05-14 20:22:34 +03:00
Pete Matsyburka 0122a237e3 remove request helper 2025-05-14 13:21:57 +03:00
Alex Turchyn 2dca7d8a8e add builder 'replace' button specs 2025-05-14 13:06:03 +03:00
Pete Matsyburka 87265e2f22 fix prefill same name field 2025-05-14 13:04:35 +03:00
Pete Matsyburka 3166defa1a add submitter value var 2025-05-13 12:27:27 +03:00
Pete Matsyburka 93ba17f42e expire at webhook 2025-05-12 18:23:17 +03:00
Pete Matsyburka 751be1dae5 fix template filter 2025-05-12 15:38:13 +03:00
Pete Matsyburka 3f8d2831e2 allow double dash email 2025-05-12 13:22:11 +03:00
Alex Turchyn 4d8942803e Merge from docusealco/wip 2025-05-12 12:31:27 +03:00
Pete Matsyburka 46d9bb2c87 fix optimize query 2025-05-12 11:17:26 +03:00
Pete Matsyburka 34433c0575 adjust conditions 2025-05-12 11:05:48 +03:00
Pete Matsyburka 544306eac4 fix replace 2025-05-11 18:02:24 +03:00
Pete Matsyburka 0eeebfd3cf clear cache 2025-05-10 20:03:35 +03:00
Pete Matsyburka 050224a10d fix refactor expire at 2025-05-10 17:39:53 +03:00
Alex Turchyn 7b6fc7d06f add expiration duration to template preferences 2025-05-10 12:12:15 +03:00
Pete Matsyburka 7fb148c85c drag folders 2025-05-10 12:11:57 +03:00
Alex Turchyn dffa59fecd add translations 2025-05-09 22:20:20 +03:00
Pete Matsyburka 24fe2a1192 fix refactor dropzone 2025-05-09 22:12:20 +03:00
Alex Turchyn 97b8ac4444 add builder dashboard dropzone 2025-05-09 21:35:31 +03:00
Pete Matsyburka e34a763dd9 fix field id 2025-05-09 10:04:22 +03:00
Pete Matsyburka 298b1d3e1e update gem 2025-05-08 22:41:25 +03:00
Pete Matsyburka d9978519cc adjust qes link 2025-05-08 12:32:10 +03:00
Pete Matsyburka b4a68cf79e adjust field types prop 2025-05-08 12:22:18 +03:00
Pete Matsyburka 5663e60f92 fix log 2025-05-06 22:18:53 +03:00
Pete Matsyburka 19e8835809 fix timestamp path 2025-05-06 18:44:29 +03:00
Pete Matsyburka 330fae927f skip bounce from audit 2025-05-06 16:48:50 +03:00
Pete Matsyburka c5a5212947 disable archived template actions 2025-05-06 15:50:58 +03:00
Pete Matsyburka 21d81e38bf add email events 2025-05-06 15:10:02 +03:00
Pete Matsyburka 5b1a4ebffd improve log 2025-05-03 16:30:11 +03:00
Pete Matsyburka 5f5dabd0e5 advanced formats 2025-05-02 19:17:19 +03:00
Pete Matsyburka ff46993de6 fix spec 2025-05-02 12:54:00 +03:00
Pete Matsyburka 7676db2196 hex colors 2025-05-02 12:36:33 +03:00
Alex Turchyn 3ef0d28ffe adjust app tour 2025-05-02 10:27:57 +03:00
Pete Matsyburka 231177f057 add log 2025-05-01 00:14:04 +03:00
Pete Matsyburka e5f7739952 use sha 256 for tsa 2025-04-30 23:48:03 +03:00
Pete Matsyburka 4a8e2a82e5 tsa fallback 2025-04-30 22:58:50 +03:00
Pete Matsyburka 6efe8027f1 disable multiple sign 2025-04-30 16:54:56 +03:00
Pete Matsyburka 96bb4eb6e5 fix standalone radio 2025-04-29 20:15:32 +03:00
Pete Matsyburka 38e68814cf fix color 2025-04-29 18:02:19 +03:00
109 changed files with 36374 additions and 372 deletions

No files matched your search

+1 -1
View File
@@ -387,7 +387,7 @@ GEM
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.12)
rack (3.1.14)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
@@ -21,7 +21,11 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
authorization_check!(blob) if exp.blank?
attachment = blob.attachments.take
@record = attachment.record
authorization_check!(attachment) if exp.blank?
if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range']
@@ -37,9 +41,7 @@ module Api
private
def authorization_check!(blob)
attachment = blob.attachments.take
def authorization_check!(attachment)
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
@@ -6,10 +6,10 @@ module Api
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
end
render json: {}
@@ -6,15 +6,15 @@ module Api
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
submitter.opened_at = Time.current
submitter.save
@submitter.opened_at = Time.current
@submitter.save
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
'webhook_url_id' => webhook_url.id)
end
+1 -1
View File
@@ -162,7 +162,7 @@ module Api
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
maybe_filder_by_completed_at(submitters, params)
@@ -5,7 +5,7 @@ module Api
load_and_authorize_resource :template
def create
authorize!(:manage, @template)
authorize!(:create, @template)
ActiveRecord::Associations::Preloader.new(
records: [@template],
@@ -12,6 +12,8 @@ class PreviewDocumentPageController < ActionController::API
return head :not_found unless attachment
@template = attachment.record
preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
+15 -4
View File
@@ -14,7 +14,8 @@ class StartFormController < ApplicationController
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
end
@@ -42,9 +43,11 @@ class StartFormController < ApplicationController
if @submitter.save
if is_new_record
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
'webhook_url_id' => webhook_url.id)
enqueue_submission_create_webhooks(@submitter)
if @submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
'submission_id' => @submitter.submission_id)
end
end
@@ -63,6 +66,13 @@ class StartFormController < ApplicationController
private
def enqueue_submission_create_webhooks(submitter)
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'webhook_url_id' => webhook_url.id)
end
end
def find_or_initialize_submitter(template, submitter_params)
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
@@ -96,6 +106,7 @@ class StartFormController < ApplicationController
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
expire_at: Templates.build_default_expire_at(template),
submitters: [submitter],
source: :link)
@@ -7,9 +7,7 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.joins(:template)
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
.preload(:template_accesses, :created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@@ -8,9 +8,7 @@ class SubmissionsDashboardController < ApplicationController
@submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
.preload(:template_accesses, :created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@@ -8,20 +8,20 @@ class SubmissionsDownloadController < ApplicationController
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if submitter&.slug == params[:submitter_slug]
if @submitter&.slug == params[:submitter_slug]
true
else
submitter = nil
@submitter = nil
end
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
Submissions::EnsureResultGenerated.call(submitter)
Submissions::EnsureResultGenerated.call(@submitter)
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
@@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController
end
if params[:combined] == 'true'
url = build_combined_url(submitter)
url = build_combined_url(@submitter)
if url
render json: [url]
@@ -7,25 +7,25 @@ class SubmitFormDownloadController < ApplicationController
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_entity if submitter.declined_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
return head :unprocessable_entity if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template.archived_at?
last_completed_submitter = submitter.submission.submitters
.where.not(id: submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
attachments =
if last_completed_submitter
Submitters.select_attachments_for_download(last_completed_submitter)
else
submitter.submission.template.schema_documents.preload(:blob)
@submitter.submission.template.schema_documents.preload(:blob)
end
urls = attachments.map do |attachment|
@@ -0,0 +1,35 @@
# frozen_string_literal: true
class TemplatesCloneAndReplaceController < ApplicationController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:files].blank?
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
).call
cloned_template = Templates::Clone.call(@template, author: current_user)
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
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
respond_to do |f|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
end
end
end
+1 -3
View File
@@ -21,9 +21,7 @@ class TemplatesController < ApplicationController
submissions.order(id: :desc)
end
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
@@ -45,13 +45,15 @@ class TemplatesDashboardController < ApplicationController
rel = templates.active.preload(:author, :template_accesses)
if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing?
rel = rel.where(folder_id: current_account.default_template_folder.id)
else
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
shared_account_ids = [current_user.account_id]
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
else
rel = rel.where(folder_id: current_account.default_template_folder.id)
end
end
@@ -26,12 +26,20 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
submitters_order require_phone_2fa
default_expire_at_duration
default_expire_at
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]]
).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
attrs[:preferences][:default_expire_at] =
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
end
attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value)
value == 'true'
+1 -1
View File
@@ -16,7 +16,7 @@ class UsersController < ApplicationController
@users.active.where.not(role: 'integration')
end
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
end
def new; end
+3
View File
@@ -33,6 +33,7 @@ import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -101,6 +102,7 @@ safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
@@ -125,6 +127,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true',
withReplaceAndCloneUpload: true,
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true'
@@ -0,0 +1,222 @@
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3a9 9 0 1 0 9 9" />
</svg>`
export default targetable(class extends HTMLElement {
static [targets.static] = [
'hiddenOnDrag',
'folderCards',
'templateCards'
]
static [target.static] = [
'form',
'fileDropzone',
'fileDropzoneLoading'
]
connectedCallback () {
document.addEventListener('drop', this.onWindowDragdrop)
document.addEventListener('dragover', this.onWindowDropover)
window.addEventListener('dragleave', this.onWindowDragleave)
this.fileDropzone?.addEventListener('drop', this.onDropFile)
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
el?.addEventListener('dragover', this.onDragover)
el?.addEventListener('dragleave', this.onDragleave)
})
}
disconnectedCallback () {
document.removeEventListener('drop', this.onWindowDragdrop)
document.removeEventListener('dragover', this.onWindowDropover)
window.removeEventListener('dragleave', this.onWindowDragleave)
}
onTemplateDragStart = (e) => {
const id = e.target.href.split('/').pop()
e.dataTransfer.effectAllowed = 'move'
if (id) {
e.dataTransfer.setData('template_id', id)
const dragPreview = e.target.cloneNode(true)
const rect = e.target.getBoundingClientRect()
const height = e.target.children[0].getBoundingClientRect().height + 50
dragPreview.children[1].remove()
dragPreview.style.width = `${rect.width}px`
dragPreview.style.height = `${height}px`
dragPreview.style.position = 'absolute'
dragPreview.style.top = '-1000px'
dragPreview.style.pointerEvents = 'none'
dragPreview.style.opacity = '0.9'
document.body.appendChild(dragPreview)
e.dataTransfer.setDragImage(dragPreview, rect.width / 2, height / 2)
setTimeout(() => document.body.removeChild(dragPreview), 0)
}
}
onDropFile = (e) => {
e.preventDefault()
this.fileDropzoneLoading.classList.remove('hidden')
this.fileDropzoneLoading.previousElementSibling.classList.add('hidden')
this.fileDropzoneLoading.classList.add('opacity-50')
this.uploadFiles(e.dataTransfer.files, '/templates_upload')
}
onDropFolder = (e, el) => {
e.preventDefault()
const templateId = e.dataTransfer.getData('template_id')
if (e.dataTransfer.files.length || templateId) {
const loading = document.createElement('div')
const svg = el.querySelector('svg')
loading.innerHTML = loadingIconHtml
loading.children[0].classList.add(...svg.classList)
el.replaceChild(loading.children[0], svg)
el.classList.add('opacity-50')
if (e.dataTransfer.files.length) {
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
} else {
const formData = new FormData()
formData.append('name', el.innerText)
fetch(`/templates/${templateId}/folder`, {
method: 'PUT',
redirect: 'manual',
body: formData,
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
}).finally(() => {
window.Turbo.cache.clear()
window.Turbo.visit(location.href)
})
}
}
}
onDropTemplate = (e) => {
e.preventDefault()
if (e.dataTransfer.files.length) {
const loading = document.createElement('div')
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
loading.innerHTML = loadingIconHtml
e.target.appendChild(loading)
e.target.classList.add('opacity-50')
const id = e.target.href.split('/').pop()
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
}
}
onWindowDragdrop = (e) => {
e.preventDefault()
if (!this.isLoading) this.hideDraghover()
}
uploadFiles (files, url) {
this.isLoading = true
this.form.action = url
this.form.querySelector('[type="file"]').files = files
this.form.querySelector('[type="submit"]').click()
}
onWindowDropover = (e) => {
e.preventDefault()
if (e.dataTransfer?.types?.includes('Files')) {
this.showDraghover()
}
}
onDragover (e) {
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
this.style.backgroundColor = '#F7F3F0'
if (this.classList.contains('before:border-base-300')) {
this.classList.remove('before:border-base-300')
this.classList.add('before:border-base-content/30')
} else if (this.classList.contains('border-base-300')) {
this.classList.remove('border-base-300')
this.classList.add('border-base-content/30')
}
}
}
onDragleave () {
this.style.backgroundColor = null
if (this.classList.contains('before:border-base-content/30')) {
this.classList.remove('before:border-base-content/30')
this.classList.add('before:border-base-300')
} else if (this.classList.contains('border-base-content/30')) {
this.classList.remove('border-base-content/30')
this.classList.add('border-base-300')
}
}
onWindowDragleave = (e) => {
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
this.hideDraghover()
}
}
showDraghover = () => {
if (this.isDrag) return
this.isDrag = true
this.fileDropzone?.classList?.remove('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
return [...this.folderCards, ...this.templateCards].forEach((el) => {
el.classList.remove('bg-base-200', 'before:hidden')
})
}
hideDraghover = () => {
this.isDrag = false
this.fileDropzone?.classList?.add('hidden')
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
return [...this.folderCards, ...this.templateCards].forEach((el) => {
el.classList.add('bg-base-200', 'before:hidden')
})
}
})
+22 -4
View File
@@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement {
static [target.static] = [
'loading',
'icon',
'input'
'input',
'area'
]
connectedCallback () {
this.addEventListener('drop', this.onDrop)
this.addEventListener('dragover', (e) => e.preventDefault())
this.addEventListener('drop', this.onDrop)
document.addEventListener('turbo:submit-end', this.toggleLoading)
this.area?.addEventListener('dragover', this.onDragover)
this.area?.addEventListener('dragleave', this.onDragleave)
}
disconnectedCallback () {
this.removeEventListener('drop', this.onDrop)
document.removeEventListener('turbo:submit-end', this.toggleLoading)
this.area?.removeEventListener('dragover', this.onDragover)
this.area?.removeEventListener('dragleave', this.onDragleave)
}
onDragover (e) {
if (e.dataTransfer?.types?.includes('Files')) {
this.style.backgroundColor = '#F7F3F0'
this.classList.remove('border-base-300', 'hover:bg-base-200/30')
this.classList.add('border-base-content/30')
}
}
onDragleave () {
this.style.backgroundColor = null
this.classList.remove('border-base-content/30')
this.classList.add('border-base-300', 'hover:bg-base-200/30')
}
onDrop (e) {
+1 -1
View File
@@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement {
const elementIds = JSON.parse(this.dataset.elementIds)
elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId)
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
})
}
})
+9 -3
View File
@@ -55,10 +55,12 @@
>
<div
v-else-if="field.type === 'signature' && signature"
class="flex flex-col justify-between h-full overflow-hidden"
class="flex justify-between h-full gap-1 overflow-hidden"
:class="isNarrow ? 'flex-row' : 'flex-col'"
>
<div
class="flex-grow flex overflow-hidden"
class="flex overflow-hidden"
:class="isNarrow ? 'w-1/2' : 'flex-grow'"
style="min-height: 50%"
>
<img
@@ -68,7 +70,8 @@
</div>
<div
v-if="withSignatureId"
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
:class="isNarrow ? 'w-1/2' : 'w-full'"
>
<div class="truncate uppercase">
ID: {{ signature.uuid }}
@@ -444,6 +447,9 @@ export default {
}
return style
},
isNarrow () {
return this.area.h > 0 && (this.area.w / this.area.h) > 6
}
},
watch: {
+2 -2
View File
@@ -93,7 +93,7 @@
<div
v-show="isFormVisible"
id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }"
>
@@ -1133,7 +1133,7 @@ export default {
parent.style.overflow = 'hidden'
scrollbox.classList.add('h-full', 'overflow-y-auto')
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
scrollbox.parentNode.classList.add('h-screen', 'h-[100dvh]', 'overflow-y-auto')
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
})
}
+56 -1
View File
@@ -5,7 +5,19 @@
class="mx-auto pl-3 h-full"
:class="isMobile ? 'pl-4' : 'md:pl-4'"
@dragover="onDragover"
@drop="isDragFile = false"
>
<HoverDropzone
v-if="sortedDocuments.length && withUploadButton && editable"
:is-dragging="isDragFile"
:template-id="template.id"
:accept-file-types="acceptFileTypes"
:with-replace-and-clone="withReplaceAndCloneUpload"
@add="[updateFromUpload($event), isDragFile = false]"
@replace="[onDocumentsReplace($event), isDragFile = false]"
@replace-and-clone="onDocumentsReplaceAndTemplateClone($event)"
@error="[onUploadFailed($event), isDragFile = false]"
/>
<DragPlaceholder
ref="dragPlaceholder"
:field="fieldsDragFieldRef.value || toRaw(dragField)"
@@ -433,6 +445,7 @@
<script>
import Upload from './upload'
import Dropzone from './dropzone'
import HoverDropzone from './hover_dropzone'
import DragPlaceholder from './drag_placeholder'
import Fields from './fields'
import MobileDrawField from './mobile_draw_field'
@@ -462,6 +475,7 @@ export default {
MobileFields,
Logo,
Dropzone,
HoverDropzone,
DocumentPreview,
DocumentControls,
IconInnerShadowTop,
@@ -665,6 +679,11 @@ export default {
required: false,
default: true
},
withReplaceAndCloneUpload: {
type: Boolean,
required: false,
default: false
},
withPhone: {
type: Boolean,
required: false,
@@ -724,7 +743,8 @@ export default {
copiedArea: null,
drawFieldType: null,
drawOption: null,
dragField: null
dragField: null,
isDragFile: false
}
},
computed: {
@@ -836,6 +856,7 @@ export default {
window.addEventListener('keydown', this.onKeyDown)
window.addEventListener('resize', this.onWindowResize)
window.addEventListener('dragleave', this.onWindowDragLeave)
this.$nextTick(() => {
if (document.location.search?.includes('stripe_connect_success')) {
@@ -854,6 +875,7 @@ export default {
window.removeEventListener('keydown', this.onKeyDown)
window.removeEventListener('resize', this.onWindowResize)
window.removeEventListener('dragleave', this.onWindowDragLeave)
},
beforeUpdate () {
this.documentRefs = []
@@ -868,6 +890,13 @@ export default {
ref.x = e.clientX - ref.offsetX
ref.y = e.clientY - ref.offsetY
} else if (e.dataTransfer?.types?.includes('Files')) {
this.isDragFile = true
}
},
onWindowDragLeave (event) {
if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) {
this.isDragFile = false
}
},
reorderFields (item) {
@@ -1529,6 +1558,9 @@ export default {
})
}, 'image/png')
},
onUploadFailed (error) {
if (error) alert(error)
},
updateFromUpload (data) {
this.template.schema.push(...data.schema)
this.template.documents.push(...data.documents)
@@ -1649,6 +1681,29 @@ export default {
this.save()
},
onDocumentsReplace (data) {
data.schema.forEach((schemaItem, index) => {
const existingSchemaItem = this.template.schema[index]
if (this.template.schema[index]) {
this.onDocumentReplace({
replaceSchemaItem: existingSchemaItem,
schema: [schemaItem],
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)]
})
} else {
this.updateFromUpload({
schema: [schemaItem],
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)],
fields: data.fields,
submitters: data.submitters
})
}
})
},
onDocumentsReplaceAndTemplateClone (template) {
window.Turbo.visit(`/templates/${template.id}/edit`)
},
moveDocument (item, direction) {
const currentIndex = this.template.schema.indexOf(item)
+61 -15
View File
@@ -2,34 +2,42 @@
<div
class="flex h-60 w-full"
@dragover.prevent
@dragenter="isDragEntering = true"
@dragleave="isDragEntering = false"
@drop.prevent="onDropFiles"
>
<label
id="document_dropzone"
class="w-full relative hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
class="w-full relative rounded-md border-2 border-base-content/10 border-dashed"
:for="inputId"
:class="{ 'opacity-50': isLoading || isProcessing }"
:class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': withHoverClass && !isDragEntering, 'bg-base-200/50 border-base-content/30': isDragEntering }"
>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center pointer-events-none">
<div class="flex flex-col items-center">
<IconInnerShadowTop
v-if="isLoading || isProcessing"
v-if="isLoading"
class="animate-spin"
:width="40"
:height="40"
/>
<IconCloudUpload
<component
:is="icon"
v-else
class="stroke-[1.5px]"
:width="40"
:height="40"
/>
<div
v-if="message"
class="font-medium text-lg mb-1"
class="text-lg mb-1"
:class="{ 'mt-1': !withDescription, 'font-medium': withDescription }"
>
{{ message }}
</div>
<div class="text-sm">
<div
v-if="withDescription"
class="text-sm"
>
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
</div>
</div>
@@ -54,13 +62,16 @@
<script>
import Upload from './upload'
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'FileDropzone',
components: {
IconFilePlus,
IconCloudUpload,
IconInnerShadowTop
IconInnerShadowTop,
IconFileSymlink,
IconFiles
},
inject: ['baseFetch', 't'],
props: {
@@ -68,35 +79,70 @@ export default {
type: [Number, String],
required: true
},
withHoverClass: {
type: Boolean,
required: false,
default: true
},
icon: {
type: String,
required: false,
default: 'IconCloudUpload'
},
cloneTemplateOnUpload: {
type: Boolean,
required: false,
default: false
},
withDescription: {
type: Boolean,
required: false,
default: true
},
title: {
type: String,
required: false,
default: ''
},
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
}
},
emits: ['success'],
emits: ['success', 'error', 'loading'],
data () {
return {
isLoading: false,
isProcessing: false
isDragEntering: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
uploadUrl () {
if (this.cloneTemplateOnUpload) {
return `/templates/${this.templateId}/clone_and_replace`
} else {
return `/templates/${this.templateId}/documents`
}
},
message () {
if (this.isLoading) {
return this.t('uploading')
} else if (this.isProcessing) {
return this.t('processing_')
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
return this.t('add_pdf_documents_or_images')
return this.title || this.t('add_pdf_documents_or_images')
} else {
return this.t('add_documents_or_images')
return this.title || this.t('add_documents_or_images')
}
}
},
watch: {
isLoading (value) {
this.$emit('loading', value)
}
},
methods: {
upload: Upload.methods.upload,
onDropFiles (e) {
@@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
<a
href="#"
class="text-sm py-1 px-2"
+2 -2
View File
@@ -112,7 +112,7 @@
:key="type"
>
<button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
:id="`${type}_type_field_button`"
draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
@@ -168,7 +168,7 @@
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
>
<a
href="https://www.docuseal.com/contact"
href="https://www.docuseal.com/qualified-electronic-signature"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
@@ -0,0 +1,89 @@
<template>
<div
v-if="isDragging || isLoading"
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"
icon="IconFilePlus"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
:with-hover-class="false"
:with-description="false"
:title="t('add_a_new_document')"
type="add_files"
@loading="isLoading = $event"
@success="$emit('add', $event)"
@error="$emit('error', $event)"
/>
<div class="flex-1 flex gap-2 w-full">
<Dropzone
class="flex-1 h-full"
icon="IconFileSymlink"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
:with-hover-class="false"
:with-description="false"
:title="t('replace_existing_document')"
@loading="isLoading = $event"
@success="$emit('replace', $event)"
@error="$emit('error', $event)"
/>
<Dropzone
v-if="withReplaceAndClone"
class="flex-1 h-full"
icon="IconFiles"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
:with-hover-class="false"
:with-description="false"
:clone-template-on-upload="true"
:title="t('clone_and_replace_documents')"
@loading="isLoading = $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
}
}
}
</script>
+18
View File
@@ -63,6 +63,9 @@ const en = {
processing_: 'Processing...',
add_pdf_documents_or_images: 'Add PDF documents or images',
add_documents_or_images: 'Add documents or images',
add_a_new_document: 'Add a new document',
replace_existing_document: 'Replace existing document',
clone_and_replace_documents: 'Clone and replace documents',
required: 'Required',
default_value: 'Default value',
format: 'Format',
@@ -225,6 +228,9 @@ const es = {
processing_: 'Procesando...',
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
add_documents_or_images: 'Agregar documentos o imágenes',
add_a_new_document: 'Agregar un nuevo documento',
replace_existing_document: 'Reemplazar documento existente',
clone_and_replace_documents: 'Clonar y reemplazar documentos',
required: 'Requerido',
default_value: 'Valor predeterminado',
format: 'Formato',
@@ -393,6 +399,9 @@ const it = {
processing_: 'Elaborazione...',
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
add_documents_or_images: 'Aggiungi documenti o immagini',
add_a_new_document: 'Aggiungi un nuovo documento',
replace_existing_document: 'Sostituisci documento esistente',
clone_and_replace_documents: 'Clona e sostituisci documenti',
required: 'Obbligatorio',
default_value: 'Valore predefinito',
format: 'Formato',
@@ -555,6 +564,9 @@ const pt = {
processing_: 'Processando...',
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
add_documents_or_images: 'Adicionar documentos ou imagens',
add_a_new_document: 'Ajouter un nouveau document',
replace_existing_document: 'Substituir documento existente',
clone_and_replace_documents: 'Clonar e substituir documentos',
required: 'Obrigatório',
default_value: 'Valor padrão',
format: 'Formato',
@@ -719,6 +731,9 @@ const fr = {
processing_: 'Traitement en cours...',
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
add_documents_or_images: 'Ajoutez des documents ou des images',
add_a_new_document: 'Adicionar um novo documento',
replace_existing_document: 'Remplacer le document existant',
clone_and_replace_documents: 'Cloner et remplacer les documents',
required: 'Requis',
default_value: 'Valeur par défaut',
format: 'Format',
@@ -884,6 +899,9 @@ const de = {
processing_: 'Verarbeitung...',
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
add_a_new_document: 'Neues Dokument hinzufügen',
replace_existing_document: 'Vorhandenes Dokument ersetzen',
clone_and_replace_documents: 'Dokumente klonen und ersetzen',
required: 'Erforderlich',
default_value: 'Standardwert',
format: 'Format',
@@ -54,7 +54,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
<a
href="#"
class="text-sm py-1 px-2"
+5 -5
View File
@@ -2,7 +2,7 @@
<label
:for="inputId"
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
:class="{ 'opacity-100': isLoading || isProcessing }"
:class="{ 'opacity-100': isLoading }"
>
{{ message }}
<form
@@ -41,19 +41,19 @@ export default {
emits: ['success'],
data () {
return {
isLoading: false,
isProcessing: false
isLoading: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
uploadUrl () {
return `/templates/${this.templateId}/documents`
},
message () {
if (this.isLoading) {
return this.t('uploading_')
} else if (this.isProcessing) {
return this.t('processing_')
} else {
return this.t('replace')
}
+24 -11
View File
@@ -4,10 +4,10 @@
id="add_document_button"
:for="inputId"
class="btn btn-outline w-full add-document-button"
:class="{ 'btn-disabled': isLoading || isProcessing }"
:class="{ 'btn-disabled': isLoading }"
>
<IconInnerShadowTop
v-if="isLoading || isProcessing"
v-if="isLoading"
width="20"
class="animate-spin"
/>
@@ -18,9 +18,6 @@
<span v-if="isLoading">
{{ t('uploading_') }}
</span>
<span v-else-if="isProcessing">
{{ t('processing_') }}
</span>
<span v-else>
{{ t('add_document') }}
</span>
@@ -63,30 +60,34 @@ export default {
default: 'image/*, application/pdf'
}
},
emits: ['success'],
emits: ['success', 'error'],
data () {
return {
isLoading: false,
isProcessing: false
isLoading: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
uploadUrl () {
return `/templates/${this.templateId}/documents`
}
},
methods: {
async upload () {
this.isLoading = true
this.baseFetch(`/templates/${this.templateId}/documents`, {
this.baseFetch(this.uploadUrl, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(this.$refs.form)
}).then((resp) => {
if (resp.ok) {
resp.json().then((data) => {
this.$emit('success', data)
this.$refs.input.value = ''
this.isLoading = false
})
} else if (resp.status === 422) {
resp.json().then((data) => {
@@ -95,21 +96,33 @@ export default {
formData.append('password', prompt(this.t('enter_pdf_password')))
this.baseFetch(`/templates/${this.templateId}/documents`, {
this.baseFetch(this.uploadUrl, {
method: 'POST',
body: formData
}).then(async (resp) => {
if (resp.ok) {
this.$emit('success', await resp.json())
this.$refs.input.value = ''
this.isLoading = false
} else {
alert(this.t('wrong_password'))
this.$emit('error', await resp.json().error)
this.isLoading = false
}
})
} else {
this.$emit('error', data.error)
this.isLoading = false
}
})
} else {
resp.json().then((data) => {
this.$emit('error', data.error)
this.isLoading = false
})
}
}).finally(() => {
}).catch(() => {
this.isLoading = false
})
}
+1
View File
@@ -33,6 +33,7 @@ class Account < ApplicationRecord
has_many :account_linked_accounts, dependent: :destroy
has_many :email_events, dependent: :destroy
has_many :webhook_urls, dependent: :destroy
has_many :account_accesses, dependent: :destroy
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
class_name: 'AccountLinkedAccount',
inverse_of: :account
+24
View File
@@ -0,0 +1,24 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_accesses
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_account_accesses_on_account_id_and_user_id (account_id,user_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class AccountAccess < ApplicationRecord
belongs_to :account
belongs_to :user
end
+2
View File
@@ -38,6 +38,8 @@ class SubmissionEvent < ApplicationRecord
enum :event_type, {
send_email: 'send_email',
bounce_email: 'bounce_email',
complaint_email: 'complaint_email',
send_reminder_email: 'send_reminder_email',
send_sms: 'send_sms',
send_2fa_sms: 'send_2fa_sms',
+1 -1
View File
@@ -54,7 +54,7 @@ class Submitter < ApplicationRecord
has_many_attached :documents
has_many_attached :attachments
has_many_attached :preview_documents
has_many :template_accesses, through: :template
has_many :template_accesses, through: :submission
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
has_many :document_generation_events, dependent: :destroy
+51 -11
View File
@@ -1,12 +1,52 @@
<div class="text-center">
<div class="join">
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
<%= svg_icon 'world', class: 'w-5 h-5' %>
Global
</a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe
</a>
<% eu_server = request.host == 'docuseal.eu' %>
<server-selector>
<div id="global_server_selector" class="text-center hidden">
<div class="join">
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-40 <%= 'bg-base-300' unless eu_server %>">
<%= svg_icon 'world', class: 'w-5 h-5' %>
Global
</a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="relative btn bg-base-200 join-item w-40 <%= 'bg-base-300' if eu_server %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe
<% unless eu_server %>
<span id="eu_server_alert" class="absolute flex space-x-0.5 hidden" style="top: -1.5rem;">
<span class="text-xs font-normal leading-none text-base-content normal-case">
<%= t('eu_data_residency') %>
</span>
<%= svg_icon 'corner_right_down', class: 'w-4 h-5 stroke-1 shrink-0 pt-1' %>
</span>
<% end %>
</a>
</div>
</div>
</div>
<div id="us_server_selector" class="flex justify-center hidden">
<div class="dropdown">
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
<% if eu_server %>
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
<span>EU Cloud</span>
<% else %>
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
<span>US Cloud</span>
<% end %>
<%= svg_icon 'chevron_down', class: 'mr-1 w-4 h-4 absolute right-1' %>
</label>
<ul tabindex="0" class="dropdown-content z-[1] menu border border-base-content/20 mt-1 bg-base-100 rounded-box w-36">
<li>
<a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="flex items-center space-x-2 <%= 'bg-base-300' unless eu_server %>">
<%= svg_icon 'usa_flag', class: 'w-5 h-5' %>
US Cloud
</a>
</li>
<li>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="flex items-center space-x-2 <%= 'bg-base-300' if eu_server %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
EU Cloud
</a>
</li>
</ul>
</div>
</div>
</server-selector>
<%= render 'scripts/server_selector' %>
+1 -1
View File
@@ -141,7 +141,7 @@
<%= t('preferences') %>
</h2>
</div>
<% if can?(:manage, account_config) %>
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || !Docuseal.multitenant?) %>
<%= 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">
+1 -1
View File
@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 9l4 -4l4 4m-4 -4v14" />
<path d="M21 15l-4 4l-4 -4m4 4v-14" />

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 362 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M20.986 12.502a9 9 0 1 0 -5.973 7.98" /><path d="M12 7v5l3 3" /><path d="M19 16v3" /><path d="M19 22v.01" />
</svg>

After

Width:  |  Height:  |  Size: 400 B

@@ -0,0 +1,3 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M6 6h6a3 3 0 0 1 3 3v10l-4 -4m8 0l-4 4"></path>
</svg>

After

Width:  |  Height:  |  Size: 285 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" /><path d="M12 11v6" /><path d="M9.5 13.5l2.5 -2.5l2.5 2.5" />
</svg>

After

Width:  |  Height:  |  Size: 462 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 3v4a1 1 0 0 0 1 1h4" />
<path d="M18 17h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h4l5 5v7a2 2 0 0 1 -2 2z" />
<path d="M16 17v2a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h2" />
</svg>

After

Width:  |  Height:  |  Size: 485 B

+1 -7
View File
@@ -1,9 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 8v-2a2 2 0 0 1 2 -2h2"></path>
<path d="M4 16v2a2 2 0 0 0 2 2h2"></path>
<path d="M16 4h2a2 2 0 0 1 2 2v2"></path>
<path d="M16 20h2a2 2 0 0 0 2 -2v-2"></path>
<path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
<path d="M10 11v-2a2 2 0 1 1 4 0v2"></path>
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" /><path d="M10 11v-2a2 2 0 1 1 4 0v2" />
</svg>

Before

Width:  |  Height:  |  Size: 619 B

After

Width:  |  Height:  |  Size: 559 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 640 480">
<path fill="#bd3d44" d="M0 0h640v480H0" />
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640" />
<path fill="#192f5d" d="M0 0h364.8v258.5H0" />
<marker id="um-a" markerHeight="30" markerWidth="30">
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z" />
</marker>
<path fill="none" marker-mid="url(#um-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z" />
</svg>

After

Width:  |  Height:  |  Size: 671 B

@@ -7,19 +7,19 @@
<div class="w-full">
<%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, t('first_reminder_in'), class: 'label truncate' %>
<%= ff.select :first_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<%= ff.select :first_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
<% end %>
</div>
<div class="w-full">
<%= f.fields_for :value, record do |ff| %>
<%= ff.label :second_duration, t('second_reminder_in'), class: 'label truncate' %>
<%= ff.select :second_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<%= ff.select :second_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
<% end %>
</div>
<div class="w-full">
<%= f.fields_for :value, record do |ff| %>
<%= ff.label :third_duration, t('third_reminder_in'), class: 'label truncate' %>
<%= ff.select :third_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<%= ff.select :third_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
<% end %>
</div>
</div>
@@ -0,0 +1,22 @@
<script>
if (!window.customElements.get('server-selector')) {
customElements.define('server-selector', class extends HTMLElement {
connectedCallback() {
const usServerSelector = this.querySelector('#us_server_selector');
const globalServerSelector = this.querySelector('#global_server_selector');
const euServerAlert = this.querySelector('#eu_server_alert');
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
if (usTimezones.test(timezone)) {
usServerSelector.classList.remove('hidden');
} else if (timezone.includes('Europe')) {
globalServerSelector.classList.remove('hidden');
euServerAlert?.classList?.remove('hidden');
} else {
globalServerSelector.classList.remove('hidden');
}
}
});
}
</script>
+1 -1
View File
@@ -1 +1 @@
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open' %>
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open border-base-content/30 before:border-base-content/30' %>
@@ -38,6 +38,8 @@
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
<% elsif event.event_type.include?('send_') %>
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
<% elsif event.event_type.start_with?('bounce_') || event.event_type.start_with?('complaint_') %>
<%= t("submission_event_names.#{event.event_type}_html", submitter_name:) %>
<% else %>
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
<% end %>
+4 -3
View File
@@ -5,12 +5,13 @@
<% font_type = field.dig('preferences', 'font_type') %>
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
<% if field['type'] == 'signature' %>
<div class="flex flex-col justify-between h-full overflow-hidden">
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow ? 'flex-row' : 'flex-col' %>">
<div class="flex overflow-hidden <%= is_narrow ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
</div>
<% if local_assigns[:with_signature_id] && attachment = attachments_index[value] %>
<div class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]">
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
<div class="truncate uppercase">
ID: <%= attachment.uuid %>
</div>
+16 -2
View File
@@ -172,12 +172,26 @@
</div>
<% end %>
<div class="flex items-center space-x-1 mt-1">
<%= svg_icon('writing', class: 'w-5 h-5') %>
<% if @submission.expire_at? && submitter && !submitter.completed_at? %>
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
<% else %>
<%= svg_icon('writing', class: 'w-5 h-5') %>
<% end %>
<span>
<% if submitter&.declined_at? %>
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif submitter %>
<%= submitter.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : t('not_completed_yet') %>
<% if submitter.completed_at? %>
<%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %>
<% elsif @submission.expire_at? %>
<% if @submission.expired? %>
<%= t(:expired) %>
<% else %>
<%= t('expire_on_time', time: l(@submission.expire_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% end %>
<% else %>
<%= t('not_completed_yet') %>
<% end %>
<% else %>
<%= t('not_invited_yet') %>
<% end %>
+1 -1
View File
@@ -1,5 +1,5 @@
<% 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">
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards">
<% if !is_long %>
<%= svg_icon('folder', class: 'w-6 h-6') %>
<% end %>
+49 -40
View File
@@ -4,47 +4,56 @@
<span><%= t('home') %></span>
<% end %>
</div>
<div class="flex justify-between items-center w-full mb-4">
<h1 class="text-4xl font-bold flex flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
<span class="peer truncate">
<%= @template_folder.name %>
</span>
<% if can?(:update, @template_folder) && @template_folder.name != TemplateFolder::DEFAULT_NAME %>
<span class="pl-1 opacity-0 hover:opacity-100 peer-hover:opacity-100">
<a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal">
<%= svg_icon('pencil', class: 'w-7 h-7') %>
</a>
<dashboard-dropzone>
<div class="relative flex justify-between items-center w-full mb-4">
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>
<input name="form_id" value="<%= form_id %>">
<input name="folder_name" value="<%= @template_folder.name %>">
<button type="submit"></button>
<input id="dashboard_dropzone_input" name="files[]" type="file" multiple>
<% end %>
<%= render 'templates/dashboard_dropzone', style: 'height: 137px' %>
<h1 class="text-2xl truncate md:text-3xl font-bold flex items-center flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
<span class="peer truncate">
<%= @template_folder.name %>
</span>
<% end %>
</h1>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button', folder_name: @template_folder.name %>
<%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block"><%= t('create') %></span>
<% if can?(:update, @template_folder) && @template_folder.name != TemplateFolder::DEFAULT_NAME %>
<span class="opacity-0 hover:opacity-100 peer-hover:opacity-100">
<a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal">
<%= svg_icon('pencil', class: 'w-7 h-7') %>
</a>
</span>
<% end %>
</h1>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button', folder_name: @template_folder.name %>
<%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block"><%= t('create') %></span>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% if @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
<% end %>
<% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
<%= t('templates_not_found') %>
</div>
</div>
<% end %>
<% if @pagy.count > 0 %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
</div>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
<% end %>
<% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
<%= t('templates_not_found') %>
</div>
</div>
<% end %>
</dashboard-dropzone>
@@ -0,0 +1,20 @@
<div class="absolute bottom-0 w-full cursor-pointer rounded-xl bg-base-100 border-2 border-base-300 border-dashed hidden z-50" data-target="dashboard-dropzone.fileDropzone" style="<%= local_assigns[:style] %>">
<div class="absolute top-0 right-0 left-0 bottom-0 flex justify-center p-2 items-center pointer-events-none">
<div class="flex flex-col items-center text-center" data-target="dashboard-dropzone.toggleLoading">
<span class="flex flex-col items-center">
<span>
<%= svg_icon('cloud_upload', class: 'w-9 h-9') %>
</span>
<div class="font-medium mb-1">
<%= t('upload_new_document') %>
</div>
</span>
<span class="flex flex-col items-center hidden" data-target="dashboard-dropzone.fileDropzoneLoading">
<%= svg_icon('loader', class: 'w-9 h-9 animate-spin') %>
<div class="font-medium mb-1">
<%= t('uploading') %>...
</div>
</span>
</div>
</div>
</div>
+2 -2
View File
@@ -2,8 +2,8 @@
<input type="hidden" name="form_id" value="<%= form_id %>">
<button type="submit" class="hidden"></button>
<file-dropzone data-submit-on-upload="true" class="w-full">
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2">
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border-2 border-base-300 border-dashed" data-target="file-dropzone.area">
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2 pointer-events-none">
<div class="flex flex-col items-center text-center">
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
<span>
+4 -4
View File
@@ -1,12 +1,12 @@
<div class="h-36 relative group">
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 peer">
<div class="pb-4 text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl" data-targets="dashboard-dropzone.templateCards">
<div class="text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% if template.template_accesses.present? %>
<%= svg_icon('lock', class: 'w-6 h-6 inline -translate-y-[4px]') %>
<% end %>
<% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %>
</div>
<div class="pb-6 pt-1 space-y-1">
<div class="pb-6 pt-1 space-y-1" data-targets="dashboard-dropzone.hiddenOnDrag">
<p class="flex items-center space-x-1 text-xs text-base-content/60">
<%= svg_icon('user', class: 'w-4 h-4') %>
<span><%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %></span>
@@ -28,7 +28,7 @@
</p>
</div>
</a>
<div class="absolute top-0 bottom-0 w-0 flex items-center hidden md:group-hover:flex" style="right: 37px">
<div class="absolute top-0 bottom-0 w-0 items-center hidden md:group-hover:flex" style="right: 37px" data-targets="dashboard-dropzone.hiddenOnDrag">
<div class="space-y-1">
<% if can?(:update, template) && !template.archived_at? && template.account_id == current_account.id %>
<span class="tooltip tooltip-left" data-tip="<%= t('move') %>">
+1 -1
View File
@@ -14,6 +14,6 @@
</span>
</button>
<input type="hidden" name="form_id" value="<%= form_id %>">
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.multitenant? %>" multiple>
<input id="upload_template" name="files[]" class="hidden" onchange="this.form.requestSubmit()" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls, .odt, .rtf' if Docuseal.advanced_formats? %>" multiple>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %>
+3 -1
View File
@@ -61,7 +61,9 @@
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
<%= render 'submissions_filters/applied_filters', filter_params: %>
<%= render 'submissions_filters/filter_button', filter_params: %>
<%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
<% unless @template.archived_at? %>
<%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
<% end %>
</div>
</div>
<% end %>
+76 -66
View File
@@ -1,76 +1,86 @@
<% has_archived = current_account.templates.where.not(archived_at: nil).exists? %>
<% show_dropzone = params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<div class="flex justify-between items-center w-full mb-4">
<div class="flex items-center flex-grow min-w-0">
<% if has_archived || @pagy.count > 0 || @template_folders.present? %>
<div class="mr-2">
<%= render 'dashboard/toggle_view', selected: 'templates' %>
</div>
<% end %>
<h1 class="text-2xl truncate md:text-3xl sm:text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>">
<%= t('document_templates_html') %>
</h1>
</div>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
<span class="hidden sm:block">
<%= render 'templates/upload_button' %>
</span>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block"><%= t('create') %></span>
<% end %>
<% end %>
</div>
</div>
<% view_archived_html = capture do %>
<% if has_archived %>
<div>
<a href="<%= templates_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
</div>
<dashboard-dropzone>
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>
<input name="form_id" value="<%= form_id %>">
<button type="submit"></button>
<input id="dashboard_dropzone_input" name="files[]" type="file" multiple>
<% end %>
<% end %>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
<% end %>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div>
<% end %>
<% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
<% if show_dropzone && current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %>
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
<div class="hidden md:block">
<app-tour id="app_tour" data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="dashboard" data-next-page-path="<%= @templates.first && can?(:edit, @templates.first) ? edit_template_path(@templates.first, params.permit(:tour)) : settings_account_path %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
<% if user_config.new_record? && !params.key?(:tour) %>
<div class="h-36 rounded-2xl pt-3 px-7 w-full border border-dashed border-base-300">
<div class="text-xl text-center font-semibold text-base-content">
<%= t('welcome_to_docuseal') %>
</div>
<div class="my-2 text-center text-xs text-base-content/70">
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %>
</div>
<div class="flex gap-2 mt-3 w-full">
<%= button_to button_title(title: t('skip'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: false } }, class: 'btn btn-sm btn-outline w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.parentNode.remove()' } %>
<%= button_to t('start_tour'), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'btn btn-sm btn-warning w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.start()' } %>
</div>
</div>
<% end %>
<div class="flex justify-between items-center w-full mb-4 relative">
<% unless show_dropzone %>
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
<% end %>
<div class="flex items-center flex-grow min-w-0">
<% if has_archived || @pagy.count > 0 || @template_folders.present? %>
<div class="mr-2">
<%= render 'dashboard/toggle_view', selected: 'templates' %>
</div>
<% end %>
<% end %>
<h1 class="text-2xl truncate md:text-3xl sm:text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>">
<%= t('document_templates_html') %>
</h1>
</div>
<div class="flex space-x-2">
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<%= render 'shared/search_input' %>
<% end %>
<% if can?(:create, ::Template) %>
<span class="hidden sm:block">
<%= render 'templates/upload_button' %>
</span>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block"><%= t('create') %></span>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% view_archived_html = capture do %>
<% if has_archived %>
<div>
<a href="<%= templates_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
</div>
<% end %>
<% end %>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
<%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
<% end %>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div>
<% end %>
<% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %>
<% if show_dropzone && (current_user.created_at > 2.weeks.ago || params[:tour] == 'true') && current_user == true_user %>
<% user_config = current_user.user_configs.find_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
<div class="hidden md:block">
<app-tour id="app_tour" data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="dashboard" data-next-page-path="<%= @templates.first && can?(:edit, @templates.first) ? edit_template_path(@templates.first, params.permit(:tour)) : settings_account_path %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
<% if user_config.new_record? && !params.key?(:tour) %>
<div class="h-36 rounded-2xl pt-3 px-7 w-full border border-dashed border-base-300">
<div class="text-xl text-center font-semibold text-base-content">
<%= t('welcome_to_docuseal') %>
</div>
<div class="my-2 text-center text-xs text-base-content/70">
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %>
</div>
<div class="flex gap-2 mt-3 w-full">
<%= button_to button_title(title: t('skip'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: false } }, class: 'btn btn-sm btn-outline w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.parentNode.remove()' } %>
<%= button_to t('start_tour'), user_configs_path, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'btn btn-sm btn-warning w-full', form_class: 'flex-1', method: :post, form: { onsubmit: 'window.app_tour.start()' } %>
</div>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
</dashboard-dropzone>
<% if show_dropzone %>
<%= render 'templates/dropzone' %>
<% end %>
+14 -2
View File
@@ -31,7 +31,7 @@
</span>
</span>
<% end %>
<%= tag.input type: 'email', multiple: true, name: 'template[preferences][bcc_completed]', autocomplete: 'off', class: 'base-input', value: ff.object.bcc_completed %>
<%= tag.input type: 'email', multiple: true, name: 'template[preferences][bcc_completed]', autocomplete: 'off', class: 'base-input', value: ff.object.bcc_completed, id: ff.field_id(:bcc_completed) %>
</div>
<% end %>
<div class="form-control pt-3">
@@ -41,7 +41,18 @@
</div>
</div>
<% end %>
<%= render 'templates_code_modal/preferences' %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mb-5' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:default_expire_at_duration, :default_expire_at).new(@template.preferences['default_expire_at_duration'], @template.preferences['default_expire_at'] ? Time.zone.parse(@template.preferences['default_expire_at']).in_time_zone(current_account.timezone) : nil) do |ff| %>
<div class="form-control">
<% duration_options = Templates::EXPIRATION_DURATIONS.keys.map { |duration| [t(duration), duration] } + [[t('specified_date'), 'specified_date']] %>
<%= ff.label :default_expire_at_duration, t('default_expiration'), class: 'label pt-0' %>
<div class="flex items-center gap-2">
<%= ff.select :default_expire_at_duration, duration_options, { include_blank: t('none') }, required: false, class: 'base-select flex-1', dir: 'auto', autocomplete: 'off', onchange: "this.value == 'specified_date' ? window.template_preferences_default_expire_at.classList.remove('hidden') : [window.template_preferences_default_expire_at.classList.add('hidden'), window.template_preferences_default_expire_at.value = '', this.form.requestSubmit()]" %>
<%= ff.datetime_field :default_expire_at, required: false, class: ['base-input flex-1', ff.object.default_expire_at.blank? && 'hidden'].compact_blank.join(' '), dir: 'auto', autocomplete: 'off', onchange: 'this.value && this.form.requestSubmit()' %>
</div>
</div>
<% end %>
<% end %>
<div class="collapse collapse-arrow join-item border border-base-300 mb-2">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
@@ -273,6 +284,7 @@
</div>
</div>
</div>
<%= render 'templates_code_modal/preferences', class: 'pt-2' %>
</div>
<% if show_recipients %>
<div id="recipients" class="hidden mt-2 mb-4 px-5">
+2 -2
View File
@@ -55,8 +55,8 @@
<%= user.email %>
</td>
<td>
<span class="badge badge-info badge-outline">
<%= user.role %>
<span class="badge badge-info badge-outline whitespace-nowrap">
<%= t(user.role) %>
</span>
</td>
<td>
+12 -2
View File
@@ -134,7 +134,15 @@ Rails.application.configure do
{}
end
resource = controller.instance_variable_get(:@submitter) ||
controller.instance_variable_get(:@submission) ||
controller.instance_variable_get(:@template) ||
controller.instance_variable_get(:@record)
current_user = controller.instance_variable_get(:@current_user)
{
host: controller.request.host,
fwd: controller.request.remote_ip,
params: {
id: params[:id],
@@ -148,8 +156,10 @@ Rails.application.configure do
params[:submit_form_slug] ||
params[:template_slug]).to_s.first(5)
}.compact_blank,
host: controller.request.host,
uid: controller.instance_variable_get(:@current_user).try(:id)
uid: current_user.try(:id),
aid: current_user.try(:account_id),
rid: resource.try(:id),
raid: resource.try(:account_id)
}
end
end
+207 -44
View File
@@ -21,6 +21,7 @@ en: &en
language_ja: 日本語
hi_there: Hi there
thanks: Thanks
private: Private
bcc_recipients: BCC recipients
resend_pending: Re-send pending
always_enforce_signing_order: Always enforce the signing order
@@ -468,7 +469,9 @@ en: &en
upload_initials: Upload Initials
draw: Draw
upload_signature: Upload Signature
integration: Integration
admin: Admin
tenant_admin: Tenant Admin
editor: Editor
viewer: Viewer
member: Member
@@ -597,6 +600,7 @@ en: &en
not_invited_yet: Not invited yet
not_completed_yet: Not completed yet
declined_on_time: 'Declined on %{time}'
expire_on_time: 'Expire on %{time}'
sign_in_person: Sign In-person
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submit the existing one</a>
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address.
@@ -717,6 +721,26 @@ en: &en
name_a_z: Name A-Z
recently_used: Recently used
newest_first: Newest first
none: None
default_expiration: Default expiration
specified_date: Specified date
one_day: 1 day
two_days: 2 days
three_days: 3 days
four_days: 4 days
five_days: 5 days
six_days: 6 days
seven_days: 7 days
eight_days: 8 days
nine_days: 9 days
ten_days: 10 days
two_weeks: 2 weeks
three_weeks: 3 weeks
four_weeks: 4 weeks
one_month: 1 month
two_months: 2 months
three_months: 3 months
eu_data_residency: EU data residency
submission_sources:
api: API
bulk: Bulk Send
@@ -725,6 +749,8 @@ en: &en
link: Link
submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
bounce_email_html: '<b>Email bounced</b> %{submitter_name}'
complaint_email_html: '<b>Spam complaint</b> %{submitter_name}'
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
send_sms_to_html: '<b>SMS sent</b> to %{submitter_name}'
send_2fa_sms_to_html: '<b>Verification SMS sent</b> to %{submitter_name}'
@@ -760,11 +786,11 @@ en: &en
previous: Previous
next: Next
template_and_submissions: 'Templates and Submissions'
template_and_submissions_description: "You can select the view that best suits your workflow. Choose the 'Templates' view if you create reusable document templates, or the 'Submissions' view when you sign individual documents or review the status of each signing request."
template_and_submissions_description: "You can select the view that best suits your workflow. Choose the 'Templates' view to create reusable document templates, or the 'Submissions' view to sign individual documents or review the status of each signature request."
upload_a_pdf_file: 'Upload a PDF file'
upload_a_pdf_file_description: 'Upload a document to use it for creating a signing form template.'
upload_a_pdf_file_description: 'Upload a PDF document to create a signing form template.'
select_a_signer_party: 'Select a signer party'
select_a_signer_party_description: 'This dropdown allows you to select a signing party role or add a new party to the document when it needs to be signed by two or more parties.'
select_a_signer_party_description: 'This dropdown allows you to select a signing party role or add a new party to the document if it requires 2 or more signatures.'
available_parties: 'Available parties'
available_parties_description: 'This list contains all available parties with their role names. Once selected, you can add and configure fields for the signer party.'
available_field_types: 'Available field types'
@@ -772,7 +798,7 @@ en: &en
text_input_field: 'Text input field'
text_input_field_description: 'This field allows users to enter text-based information, such as names or emails.'
signature_field: 'Signature field'
signature_field_description: 'This field allows for collecting signatures from the signers.'
signature_field_description: 'This field is used to collect signatures from the signers.'
added_fields: 'Added fields'
added_fields_description: 'This list displays all fields added to the document. You can reorder, edit, or remove them.'
open_field_settings: 'Open field settings'
@@ -786,10 +812,10 @@ en: &en
copy_and_share_link: 'Copy & share link'
copy_and_share_link_description: 'Copy this link to share the document. Anyone with the link can sign it after entering their email.'
sign_the_document: 'Sign the document'
sign_the_document_description: 'If you are one of the signers signer, this button allows you to sign the document yourself.'
sign_the_document_description: 'If you are one of the signers, this button allows you to sign the document.'
send_for_signing: 'Send for signing'
add_recipients: 'Add recipients'
add_recipients_description: 'Add new recipients for the document to be signed. You can enter their email addresses or phone numbers.'
add_recipients_description: 'Add new recipients for the document to be signed by entering their email addresses or phone numbers.'
settings: 'Settings'
settings_account_description: 'With comprehensive account settings, you can customize the eSigning experience and invite more users.'
support: 'Support'
@@ -801,6 +827,7 @@ en: &en
read: Read your data
es: &es
private: Privado
resend_pending: Reenviar pendiente
ensure_unique_recipients: Asegurar destinatarios únicos
require_phone_2fa_to_open: Requiere 2FA por teléfono para abrir
@@ -821,7 +848,7 @@ es: &es
select_user: Seleccionar usuario
team_member_permissions: Permisos de miembros del equipo
entire_team: Todo el equipo
admin_only: Solo administrador
admin_only: Solo admin
accessiable_by: Accesible por
team_access: Acceso del equipo
remove_filter: Eliminar filtro
@@ -1250,7 +1277,9 @@ es: &es
upload_initials: Subir iniciales
draw: Dibujar
upload_signature: Subir firma
integration: Integración
admin: Administrador
tenant_admin: Tenant Administrador
editor: Editor
viewer: Visor
member: Miembro
@@ -1379,6 +1408,7 @@ es: &es
not_invited_yet: Aún no invitado
not_completed_yet: Aún no completado
declined_on_time: 'Rechazado el %{time}'
expire_on_time: 'Expira el %{time}'
sign_in_person: Firma en persona
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crear una nueva plantilla</a> de documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">enviar el existente</a>
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada.
@@ -1499,6 +1529,26 @@ es: &es
name_a_z: Nombre A-Z
recently_used: Usado recientemente
newest_first: Más reciente primero
none: Ninguno
default_expiration: Vencimiento predeterminado
specified_date: Fecha especificada
one_day: 1 día
two_days: 2 días
three_days: 3 días
four_days: 4 días
five_days: 5 días
six_days: 6 días
seven_days: 7 días
eight_days: 8 días
nine_days: 9 días
ten_days: 10 días
two_weeks: 2 semanas
three_weeks: 3 semanas
four_weeks: 4 semanas
one_month: 1 mes
two_months: 2 meses
three_months: 3 meses
eu_data_residency: Datos alojados UE
submission_sources:
api: API
bulk: Envío masivo
@@ -1507,6 +1557,8 @@ es: &es
link: Enlace
submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
bounce_email_html: '<b>Correo electrónico rebotado</b> %{submitter_name}'
complaint_email_html: '<b>Queja de spam</b> %{submitter_name}'
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificación enviado</b> a %{submitter_name}'
@@ -1542,11 +1594,11 @@ es: &es
previous: Anterior
next: Siguiente
template_and_submissions: 'Plantillas y Envíos'
template_and_submissions_description: "Puedes seleccionar la vista que mejor se adapte a tu flujo de trabajo. Elige la vista de 'Plantillas' si creas plantillas reutilizables de documentos, o la vista de 'Envíos' cuando firmas documentos individuales o revisas el estado de cada solicitud de firma."
template_and_submissions_description: "Puedes seleccionar la vista que mejor se adapte a tu flujo de trabajo. Elige la vista 'Plantillas' para crear plantillas reutilizables de documentos, o la vista 'Envíos' para firmar documentos individuales o revisar el estado de cada solicitud de firma."
upload_a_pdf_file: 'Subir un archivo PDF'
upload_a_pdf_file_description: 'Sube un documento para usarlo como plantilla de formulario de firma.'
upload_a_pdf_file_description: 'Sube un documento PDF para crear una plantilla de formulario de firma.'
select_a_signer_party: 'Seleccionar parte firmante'
select_a_signer_party_description: 'Este menú desplegable te permite seleccionar un rol de firmante o agregar una nueva parte al documento cuando debe ser firmado por dos o más partes.'
select_a_signer_party_description: 'Este menú desplegable te permite seleccionar un rol de firmante o agregar una nueva parte al documento si requiere 2 o más firmas.'
available_parties: 'Partes disponibles'
available_parties_description: 'Esta lista contiene todas las partes disponibles con sus roles. Una vez seleccionada, puedes añadir y configurar campos para esa parte.'
available_field_types: 'Tipos de campo disponibles'
@@ -1554,7 +1606,7 @@ es: &es
text_input_field: 'Campo de texto'
text_input_field_description: 'Este campo permite a los usuarios introducir información de texto, como nombres o correos electrónicos.'
signature_field: 'Campo de firma'
signature_field_description: 'Este campo permite recopilar firmas de los firmantes.'
signature_field_description: 'Este campo se utiliza para recopilar las firmas de los firmantes.'
added_fields: 'Campos añadidos'
added_fields_description: 'Esta lista muestra todos los campos añadidos al documento. Puedes reordenarlos, editarlos o eliminarlos.'
open_field_settings: 'Abrir configuración del campo'
@@ -1568,10 +1620,10 @@ es: &es
copy_and_share_link: 'Copiar y compartir enlace'
copy_and_share_link_description: 'Copia este enlace para compartir el documento. Cualquiera con el enlace puede firmarlo tras ingresar su correo electrónico.'
sign_the_document: 'Firmar el documento'
sign_the_document_description: 'Si eres uno de los firmantes, este botón te permite firmar el documento tú mismo.'
sign_the_document_description: 'Si eres uno de los firmantes, este botón te permite firmar el documento.'
send_for_signing: 'Enviar para firma'
add_recipients: 'Agregar destinatarios'
add_recipients_description: 'Agrega nuevos destinatarios para que el documento sea firmado. Puedes ingresar sus correos electrónicos o números de teléfono.'
add_recipients_description: 'Agrega nuevos destinatarios para que firmen el documento ingresando sus correos electrónicos o números de teléfono.'
settings: 'Configuración'
settings_account_description: 'Con una configuración completa de la cuenta, puedes personalizar la experiencia de firma y añadir más usuarios.'
support: 'Soporte'
@@ -1583,6 +1635,7 @@ es: &es
read: Leer tus datos
it: &it
private: Privato
resend_pending: Reinvia in sospeso
ensure_unique_recipients: Assicurarsi destinatari unici
require_phone_2fa_to_open: Richiedi l'autenticazione a due fattori tramite telefono per aprire
@@ -1602,7 +1655,7 @@ it: &it
select_user: Seleziona utente
team_member_permissions: Permessi membri del team
entire_team: Intero team
admin_only: Solo amministratore
admin_only: Solo admin
accessiable_by: Accessibile da
team_access: Accesso al team
remove_filter: Rimuovi filtro
@@ -2031,6 +2084,7 @@ it: &it
upload_initials: Carica iniziali
draw: Disegna
upload_signature: Carica firma
integration: Integrazione
admin: Amministratore
editor: Editor
viewer: Visualizzatore
@@ -2160,6 +2214,7 @@ it: &it
not_invited_yet: Non ancora invitato
not_completed_yet: Non ancora completato
declined_on_time: 'Rifiutato il %{time}'
expire_on_time: 'Scade il %{time}'
sign_in_person: Firma di persona
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crea un nuovo modello</a> di documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">invia quello esistente</a>
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato.
@@ -2280,6 +2335,26 @@ it: &it
name_a_z: Nome A-Z
recently_used: Recentemente usato
newest_first: Più recenti prima
none: Nessuno
default_expiration: Scadenza predefinita
specified_date: Data specificata
one_day: 1 giorno
two_days: 2 giorni
three_days: 3 giorni
four_days: 4 giorni
five_days: 5 giorni
six_days: 6 giorni
seven_days: 7 giorni
eight_days: 8 giorni
nine_days: 9 giorni
ten_days: 10 giorni
two_weeks: 2 settimane
three_weeks: 3 settimane
four_weeks: 4 settimane
one_month: 1 mese
two_months: 2 mesi
three_months: 3 mesi
eu_data_residency: "Dati nell'UE"
submission_sources:
api: API
bulk: Invio massivo
@@ -2287,7 +2362,9 @@ it: &it
invite: Invito
link: Link
submission_event_names:
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
send_email_to_html: '<b>Email inviata</b> a %{submitter_name}'
bounce_email_html: '<b>Email respinta</b> %{submitter_name}'
complaint_email_html: '<b>Segnalazione spam</b> %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS inviato</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS di verifica inviato</b> a %{submitter_name}'
@@ -2323,11 +2400,11 @@ it: &it
previous: Precedente
next: Successivo
template_and_submissions: 'Modelli e Invii'
template_and_submissions_description: "Puoi selezionare la vista che meglio si adatta al tuo flusso di lavoro. Scegli la vista 'Modelli' se crei modelli riutilizzabili di documenti, oppure la vista 'Invii' quando firmi documenti singoli o controlli lo stato di ogni richiesta di firma."
template_and_submissions_description: "Puoi selezionare la visualizzazione più adatta al tuo flusso di lavoro. Scegli la visualizzazione 'Modelli' per creare modelli di documenti riutilizzabili o 'Invii' per firmare documenti singoli o controllare lo stato di ciascuna richiesta di firma."
upload_a_pdf_file: 'Carica un file PDF'
upload_a_pdf_file_description: 'Carica un documento da utilizzare per creare un modello di modulo di firma.'
upload_a_pdf_file_description: 'Carica un documento PDF per creare un modello di modulo di firma.'
select_a_signer_party: 'Seleziona una parte firmataria'
select_a_signer_party_description: 'Questo menu a discesa consente di selezionare un ruolo di firmatario o aggiungere una nuova parte al documento quando deve essere firmato da due o più parti.'
select_a_signer_party_description: 'Questo menu a discesa consente di selezionare un ruolo di firmatario o aggiungere una nuova parte al documento se richiede 2 o più firme.'
available_parties: 'Parti disponibili'
available_parties_description: 'Questo elenco contiene tutte le parti disponibili con i relativi ruoli. Una volta selezionata, puoi aggiungere e configurare i campi per la parte firmataria.'
available_field_types: 'Tipi di campo disponibili'
@@ -2335,7 +2412,7 @@ it: &it
text_input_field: 'Campo di testo'
text_input_field_description: 'Questo campo consente agli utenti di inserire informazioni testuali, come nomi o email.'
signature_field: 'Campo firma'
signature_field_description: 'Questo campo consente di raccogliere firme dai firmatari.'
signature_field_description: 'Questo campo viene utilizzato per raccogliere le firme dei firmatari.'
added_fields: 'Campi aggiunti'
added_fields_description: 'Questo elenco mostra tutti i campi aggiunti al documento. Puoi riordinarli, modificarli o rimuoverli.'
open_field_settings: 'Apri le impostazioni del campo'
@@ -2349,10 +2426,10 @@ it: &it
copy_and_share_link: 'Copia e condividi il link'
copy_and_share_link_description: 'Copia questo link per condividere il documento. Chiunque abbia il link potrà firmarlo dopo aver inserito la propria email.'
sign_the_document: 'Firma il documento'
sign_the_document_description: 'Se sei uno dei firmatari, questo pulsante ti consente di firmare il documento personalmente.'
sign_the_document_description: 'Se sei uno dei firmatari, questo pulsante ti consente di firmare il documento.'
send_for_signing: 'Invia per la firma'
add_recipients: 'Aggiungi destinatari'
add_recipients_description: 'Aggiungi nuovi destinatari per la firma del documento. Puoi inserire il loro indirizzo email o numero di telefono.'
add_recipients_description: 'Aggiungi nuovi destinatari inserendo i loro indirizzi email o numeri di telefono per far firmare il documento.'
settings: 'Impostazioni'
settings_account_description: "Con impostazioni complete dell'account, puoi personalizzare l'esperienza di firma elettronica e invitare altri utenti."
support: 'Supporto'
@@ -2364,6 +2441,7 @@ it: &it
read: Leggi i tuoi dati
fr: &fr
private: Privé
resend_pending: Renvoyer en attente
ensure_unique_recipients: Assurer l'unicité des destinataires
require_phone_2fa_to_open: Requiert une 2FA par téléphone pour ouvrir
@@ -2384,7 +2462,7 @@ fr: &fr
select_user: Sélectionner un utilisateur
team_member_permissions: Permissions des membres de l'équipe
entire_team: Équipe entière
admin_only: Administrateur uniquement
admin_only: Admin seul
accessiable_by: Accessible par
team_access: Accès à l'équipe
remove_filter: Supprimer le filtre
@@ -2814,7 +2892,9 @@ fr: &fr
upload_initials: Télécharger les initiales
draw: Dessiner
upload_signature: Télécharger la signature
integration: Intégration
admin: Administrateur
tenant_admin: Tenant Administrateur
editor: Éditeur
viewer: Lecteur
member: Membre
@@ -2943,6 +3023,7 @@ fr: &fr
not_invited_yet: Pas encore invité
not_completed_yet: Pas encore terminé
declined_on_time: 'Refusé le %{time}'
expire_on_time: 'Expire le %{time}'
sign_in_person: Signature en personne
create_a_new_template_document_form_or_submit_the_existing_one_html: "<a href=\"%{new_template_link}\" data-turbo-frame=\"modal\" class=\"inline underline font-medium\">Créer un nouveau modèle</a> de document ou <a href=\"%{start_form_link}\" target=\"_blank\" class=\"inline underline font-medium\">soumettre l'existant</a>"
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie de l'e-mail avec les documents complétés à une adresse BCC spécifiée.
@@ -3056,13 +3137,33 @@ fr: &fr
submission_created_via_source_html: '<b>Soumission créée</b> via %{source}'
pro_user_seats_used: Places utilisateur Pro en cours d'utilisation
manage_plan: Gérer le plan
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Cette soumission comporte plusieurs signataires, ce qui empêche l’utilisation d’un lien de partage, car il n’est pas clair quel signataire est responsable de quels champs. Pour résoudre ce problème, suivez ce <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> pour définir les détails du signataire par défaut.'
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: "Cette soumission comporte plusieurs signataires, ce qui empêche l'utilisation d'un lien de partage, car il n'est pas clair quel signataire est responsable de quels champs. Pour résoudre ce problème, suivez ce <a href=\"https://www.docuseal.com/resources/pre-filling-recipients\" class=\"link font-bold\" rel=\"noopener noreferrer nofollow\" target=\"_blank\">guide</a> pour définir les détails du signataire par défaut."
welcome_to_docuseal: Bienvenue sur DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Lancez une visite rapide pour apprendre à créer et envoyer votre premier document.
start_tour: Démarrer
name_a_z: Nom A-Z
recently_used: Récemment utilisé
newest_first: Le plus récent d'abord
none: Aucun
default_expiration: Expiration par défaut
specified_date: Date spécifiée
one_day: 1 jour
two_days: 2 jours
three_days: 3 jours
four_days: 4 jours
five_days: 5 jours
six_days: 6 jours
seven_days: 7 jours
eight_days: 8 jours
nine_days: 9 jours
ten_days: 10 jours
two_weeks: 2 semaines
three_weeks: 3 semaines
four_weeks: 4 semaines
one_month: 1 mois
two_months: 2 mois
three_months: 3 mois
eu_data_residency: "Données dans l'UE"
submission_sources:
api: API
bulk: Envoi en masse
@@ -3071,6 +3172,8 @@ fr: &fr
link: Lien
submission_event_names:
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
bounce_email_html: '<b>Email non distribué</b> %{submitter_name}'
complaint_email_html: '<b>Réclamation de spam</b> %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
send_sms_to_html: '<b>SMS envoyé</b> à %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de vérification envoyé</b> à %{submitter_name}'
@@ -3106,11 +3209,11 @@ fr: &fr
previous: Précédent
next: Suivant
template_and_submissions: 'Modèles et Soumissions'
template_and_submissions_description: "Vous pouvez sélectionner la vue qui correspond le mieux à votre flux de travail. Choisissez la vue 'Modèles' si vous créez des modèles de documents réutilisables, ou la vue 'Soumissions' lorsque vous signez des documents individuels ou vérifiez l'état de chaque demande de signature."
template_and_submissions_description: "Vous pouvez sélectionner la vue qui correspond le mieux à votre flux de travail. Choisissez la vue 'Modèles' pour créer des modèles de documents réutilisables ou la vue 'Soumissions' pour signer des documents individuels ou vérifier l'état de chaque demande de signature."
upload_a_pdf_file: 'Téléverser un fichier PDF'
upload_a_pdf_file_description: "Téléversez un document pour l'utiliser comme modèle de formulaire de signature."
upload_a_pdf_file_description: 'Téléversez un document PDF pour créer un modèle de formulaire de signature.'
select_a_signer_party: 'Sélectionner une partie signataire'
select_a_signer_party_description: "Ce menu déroulant vous permet de sélectionner un rôle de signataire ou d'ajouter une nouvelle partie au document lorsqu'il doit être signé par deux parties ou plus."
select_a_signer_party_description: "Ce menu déroulant vous permet de sélectionner un rôle de signataire ou d'ajouter une nouvelle partie au document s'il nécessite 2 signatures ou plus."
available_parties: 'Parties disponibles'
available_parties_description: 'Cette liste contient toutes les parties disponibles avec leurs rôles. Une fois sélectionnée, vous pouvez ajouter et configurer les champs pour cette partie.'
available_field_types: 'Types de champs disponibles'
@@ -3118,7 +3221,7 @@ fr: &fr
text_input_field: 'Champ de texte'
text_input_field_description: 'Ce champ permet aux utilisateurs de saisir des informations textuelles telles que des noms ou des adresses e-mail.'
signature_field: 'Champ de signature'
signature_field_description: 'Ce champ permet de collecter les signatures des signataires.'
signature_field_description: 'Ce champ est utilisé pour recueillir les signatures des signataires.'
added_fields: 'Champs ajoutés'
added_fields_description: 'Cette liste affiche tous les champs ajoutés au document. Vous pouvez les réorganiser, les modifier ou les supprimer.'
open_field_settings: 'Ouvrir les paramètres du champ'
@@ -3132,10 +3235,10 @@ fr: &fr
copy_and_share_link: 'Copier et partager le lien'
copy_and_share_link_description: 'Copiez ce lien pour partager le document. Toute personne disposant du lien pourra le signer après avoir saisi son e-mail.'
sign_the_document: 'Signer le document'
sign_the_document_description: 'Si vous êtes l’un des signataires, ce bouton vous permet de signer le document vous-même.'
sign_the_document_description: "Si vous êtes l'n des signataires, ce bouton vous permet de signer le document."
send_for_signing: 'Envoyer pour signature'
add_recipients: 'Ajouter des destinataires'
add_recipients_description: 'Ajoutez de nouveaux destinataires pour que le document soit signé. Vous pouvez saisir leur adresse e-mail ou leur numéro de téléphone.'
add_recipients_description: "Ajoutez de nouveaux destinataires en saisissant leurs adresses e-mail ou numéros de téléphone pour faire signer le document."
settings: 'Paramètres'
settings_account_description: "Grâce à des paramètres de compte complets, vous pouvez personnaliser l'expérience de signature électronique et inviter d'autres utilisateurs."
support: 'Assistance'
@@ -3147,6 +3250,7 @@ fr: &fr
read: Lire vos données
pt: &pt
private: Privado
resend_pending: Re-enviar pendente
ensure_unique_recipients: Garantir destinatários únicos
require_phone_2fa_to_open: Necessário autenticação de dois fatores via telefone para abrir
@@ -3167,7 +3271,7 @@ pt: &pt
select_user: Selecionar usuário
team_member_permissions: Permissões de membro da equipe
entire_team: Toda a equipe
admin_only: Somente administrador
admin_only: Somente admin
accessiable_by: Acessível por
team_access: Acesso à equipe
remove_filter: Remover filtro
@@ -3596,7 +3700,9 @@ pt: &pt
upload_initials: Enviar iniciais
draw: Desenhar
upload_signature: Enviar assinatura
integration: Integração
admin: Administrador
tenant_admin: Tenant Administrador
editor: Editor
viewer: Visualizador
member: Membro
@@ -3725,6 +3831,7 @@ pt: &pt
not_invited_yet: Ainda não convidado
not_completed_yet: Ainda não concluído
declined_on_time: 'Recusado em %{time}'
expire_on_time: 'Expira em %{time}'
sign_in_person: Assinar pessoalmente
create_a_new_template_document_form_or_submit_the_existing_one_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Criar um novo modelo</a> de documento ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submeter o existente</a>'
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado.
@@ -3845,6 +3952,26 @@ pt: &pt
name_a_z: Nome A-Z
recently_used: Recentemente usado
newest_first: Mais recente primeiro
none: Nenhum
default_expiration: Expiração padrão
specified_date: Data especificada
one_day: 1 dia
two_days: 2 dias
three_days: 3 dias
four_days: 4 dias
five_days: 5 dias
six_days: 6 dias
seven_days: 7 dias
eight_days: 8 dias
nine_days: 9 dias
ten_days: 10 dias
two_weeks: 2 semanas
three_weeks: 3 semanas
four_weeks: 4 semanas
one_month: 1 mês
two_months: 2 meses
three_months: 3 meses
eu_data_residency: Dados na UE
submission_sources:
api: API
bulk: Envio em massa
@@ -3852,8 +3979,10 @@ pt: &pt
invite: Convite
link: Link
submission_event_names:
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
send_email_to_html: '<b>Email enviado</b> para %{submitter_name}'
bounce_email_html: '<b>Email não entregue</b> %{submitter_name}'
complaint_email_html: '<b>Reclamação de spam</b> %{submitter_name}'
send_reminder_email_to_html: '<b>Email de lembrete enviado</b> para %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> para %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificação enviado</b> para %{submitter_name}'
open_email_by_html: '<b>E-mail aberto</b> por %{submitter_name}'
@@ -3888,11 +4017,11 @@ pt: &pt
previous: Anterior
next: Próximo
template_and_submissions: 'Modelos e Envios'
template_and_submissions_description: "Você pode selecionar a visualização que melhor se adapta ao seu fluxo de trabalho. Escolha a visualização 'Modelos' se criar modelos de documentos reutilizáveis, ou 'Envios' ao assinar documentos individuais ou revisar o status de cada solicitação de assinatura."
template_and_submissions_description: "Você pode selecionar a visualização que melhor se adapta ao seu fluxo de trabalho. Escolha a visualização 'Modelos' para criar modelos reutilizáveis de documentos ou 'Envios' para assinar documentos individuais ou verificar o status de cada solicitação de assinatura."
upload_a_pdf_file: 'Enviar um arquivo PDF'
upload_a_pdf_file_description: 'Envie um documento para usá-lo como modelo de formulário de assinatura.'
upload_a_pdf_file_description: 'Envie um documento PDF para criar um modelo de formulário de assinatura.'
select_a_signer_party: 'Selecionar parte assinante'
select_a_signer_party_description: 'Este menu suspenso permite selecionar um papel de assinante ou adicionar uma nova parte ao documento quando ele precisa ser assinado por duas ou mais partes.'
select_a_signer_party_description: 'Este menu suspenso permite selecionar um papel de assinante ou adicionar uma nova parte ao documento, se forem necessárias 2 ou mais assinaturas.'
available_parties: 'Partes disponíveis'
available_parties_description: 'Esta lista contém todas as partes disponíveis com seus respectivos papéis. Após selecionar, você poderá adicionar e configurar os campos para essa parte.'
available_field_types: 'Tipos de campo disponíveis'
@@ -3900,7 +4029,7 @@ pt: &pt
text_input_field: 'Campo de texto'
text_input_field_description: 'Este campo permite que os usuários insiram informações baseadas em texto, como nomes ou e-mails.'
signature_field: 'Campo de assinatura'
signature_field_description: 'Este campo permite coletar assinaturas dos signatários.'
signature_field_description: 'Este campo é usado para coletar assinaturas dos signatários.'
added_fields: 'Campos adicionados'
added_fields_description: 'Esta lista exibe todos os campos adicionados ao documento. Você pode reordená-los, editá-los ou removê-los.'
open_field_settings: 'Abrir configurações do campo'
@@ -3917,7 +4046,7 @@ pt: &pt
sign_the_document_description: 'Se você for um dos signatários, este botão permite que você assine o documento.'
send_for_signing: 'Enviar para assinatura'
add_recipients: 'Adicionar destinatários'
add_recipients_description: 'Adicione novos destinatários para que o documento seja assinado. Você pode inserir seus e-mails ou números de telefone.'
add_recipients_description: 'Adicione novos destinatários inserindo seus endereços de e-mail ou números de telefone para assinar o documento.'
settings: 'Configurações'
settings_account_description: 'Com configurações completas de conta, você pode personalizar a experiência de assinatura eletrônica e convidar mais usuários.'
support: 'Suporte'
@@ -3930,6 +4059,7 @@ pt: &pt
read: Ler seus dados
de: &de
private: Privat
resend_pending: Ausstehende erneut senden
ensure_unique_recipients: Stellen Sie einzigartige Empfänger sicher
require_phone_2fa_to_open: Telefon-2FA zum Öffnen erforderlich
@@ -3950,7 +4080,7 @@ de: &de
select_user: Benutzer auswählen
team_member_permissions: Berechtigungen für Teammitglieder
entire_team: Gesamtes Team
admin_only: Nur Administratoren
admin_only: Nur Admins
accessiable_by: Zugänglich für
team_access: Teamzugang
remove_filter: Filter entfernen
@@ -4379,7 +4509,9 @@ de: &de
upload_initials: Initialen hochladen
draw: Zeichnen
upload_signature: Unterschrift hochladen
integration: Integration
admin: Administrator
tenant_admin: Tenant Administrator
editor: Redakteur
viewer: Betrachter
member: Mitglied
@@ -4508,6 +4640,7 @@ de: &de
not_invited_yet: Noch nicht eingeladen
not_completed_yet: Noch nicht abgeschlossen
declined_on_time: 'Abgelehnt am %{time}'
expire_on_time: 'Ablauf am %{time}'
sign_in_person: Persönlich unterschreiben
create_a_new_template_document_form_or_submit_the_existing_one_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Neue Vorlage erstellen</a> oder <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">vorhandene einreichen</a>'
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse.
@@ -4628,6 +4761,26 @@ de: &de
name_a_z: Name A-Z
recently_used: Kürzlich verwendet
newest_first: Neueste zuerst
none: Keine
default_expiration: Standardablauf
specified_date: Angegebenes Datum
one_day: 1 Tag
two_days: 2 Tage
three_days: 3 Tage
four_days: 4 Tage
five_days: 5 Tage
six_days: 6 Tage
seven_days: 7 Tage
eight_days: 8 Tage
nine_days: 9 Tage
ten_days: 10 Tage
two_weeks: 2 Wochen
three_weeks: 3 Wochen
four_weeks: 4 Wochen
one_month: 1 Monat
two_months: 2 Monate
three_months: 3 Monate
eu_data_residency: EU-Datenspeicher
submission_sources:
api: API
bulk: Massenversand
@@ -4636,6 +4789,8 @@ de: &de
link: Link
submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
bounce_email_html: '<b>E-Mail zurückgesendet</b> %{submitter_name}'
complaint_email_html: '<b>Spam-Beschwerde</b> %{submitter_name}'
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
send_sms_to_html: '<b>SMS gesendet</b> an %{submitter_name}'
send_2fa_sms_to_html: '<b>Verifizierungs-SMS gesendet</b> an %{submitter_name}'
@@ -4671,11 +4826,11 @@ de: &de
previous: Zurück
next: Weiter
template_and_submissions: 'Vorlagen und Übermittlungen'
template_and_submissions_description: "Du kannst die Ansicht wählen, die am besten zu deinem Arbeitsablauf passt. Wähle die Ansicht 'Vorlagen', wenn du wiederverwendbare Dokumentenvorlagen erstellst, oder 'Übermittlungen', wenn du einzelne Dokumente unterschreibst oder den Status jeder Signaturanfrage überprüfen möchtest."
template_and_submissions_description: "Du kannst die Ansicht wählen, die am besten zu deinem Arbeitsablauf passt. Wähle die Ansicht 'Vorlagen', um wiederverwendbare Dokumentvorlagen zu erstellen, oder 'Übermittlungen', um einzelne Dokumente zu unterschreiben oder den Status jeder Signaturanfrage zu überprüfen."
upload_a_pdf_file: 'PDF-Datei hochladen'
upload_a_pdf_file_description: 'Lade ein Dokument hoch, um es als Vorlage für ein Signaturformular zu verwenden.'
upload_a_pdf_file_description: 'Lade ein PDF-Dokument hoch, um eine Vorlage für ein Signaturformular zu erstellen.'
select_a_signer_party: 'Signaturpartei auswählen'
select_a_signer_party_description: 'Dieses Dropdown-Menü ermöglicht es dir, eine Signaturrolle auszuwählen oder eine neue Partei zum Dokument hinzuzufügen, wenn es von zwei oder mehr Parteien unterzeichnet werden muss.'
select_a_signer_party_description: 'Dieses Dropdown-Menü ermöglicht es dir, eine Signaturrolle auszuwählen oder eine neue Partei zum Dokument hinzuzufügen, wenn es 2 oder mehr Unterschriften erfordert.'
available_parties: 'Verfügbare Parteien'
available_parties_description: 'Diese Liste enthält alle verfügbaren Parteien mit ihren Rollennamen. Nach der Auswahl kannst du Felder für die Partei hinzufügen und konfigurieren.'
available_field_types: 'Verfügbare Feldtypen'
@@ -4683,7 +4838,7 @@ de: &de
text_input_field: 'Textfeld'
text_input_field_description: 'Dieses Feld ermöglicht es Benutzern, textbasierte Informationen wie Namen oder E-Mails einzugeben.'
signature_field: 'Signaturfeld'
signature_field_description: 'Dieses Feld dient der Erfassung von Signaturen der Unterzeichner.'
signature_field_description: 'Dieses Feld wird verwendet, um die Unterschriften der Unterzeichner zu erfassen.'
added_fields: 'Hinzugefügte Felder'
added_fields_description: 'Diese Liste zeigt alle dem Dokument hinzugefügten Felder. Du kannst sie neu anordnen, bearbeiten oder entfernen.'
open_field_settings: 'Feld-Einstellungen öffnen'
@@ -4697,10 +4852,10 @@ de: &de
copy_and_share_link: 'Link kopieren und teilen'
copy_and_share_link_description: 'Kopiere diesen Link, um das Dokument zu teilen. Jeder mit dem Link kann es unterschreiben, nachdem er seine E-Mail-Adresse eingegeben hat.'
sign_the_document: 'Dokument unterschreiben'
sign_the_document_description: 'Wenn du einer der Unterzeichner bist, kannst du das Dokument mit diesem Button selbst unterschreiben.'
sign_the_document_description: 'Wenn du einer der Unterzeichner bist, kannst du das Dokument mit diesem Button unterschreiben.'
send_for_signing: 'Zum Unterschreiben senden'
add_recipients: 'Empfänger hinzufügen'
add_recipients_description: 'Füge neue Empfänger hinzu, damit das Dokument unterschrieben werden kann. Du kannst deren E-Mail-Adressen oder Telefonnummern eingeben.'
add_recipients_description: 'Füge neue Empfänger hinzu, indem du ihre E-Mail-Adressen oder Telefonnummern eingibst, damit sie das Dokument unterschreiben können.'
settings: 'Einstellungen'
settings_account_description: 'Mit umfangreichen Kontoeinstellungen kannst du das Signiererlebnis anpassen und weitere Benutzer einladen.'
support: 'Support'
@@ -4777,6 +4932,7 @@ pl:
powered_by: 'Napędzany prze'
count_documents_signed_with_html: '<b>%{count}</b> dokumentów podpisanych za pomocą'
open_source_documents_software: 'oprogramowanie do dokumentów open source'
eu_data_residency: Dane w UE
uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@@ -4844,6 +5000,7 @@ uk:
powered_by: 'Працює на базі'
count_documents_signed_with_html: '<b>%{count}</b> документів підписано за допомогою'
open_source_documents_software: 'відкрите програмне забезпечення для документів'
eu_data_residency: 'Зберігання даних в ЄС'
cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@@ -4911,6 +5068,7 @@ cs:
powered_by: 'Poháněno'
count_documents_signed_with_html: '<b>%{count}</b> dokumentů podepsáno pomocí'
open_source_documents_software: 'open source software pro dokumenty'
eu_data_residency: 'Uložení dat v EU'
he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@@ -4978,6 +5136,7 @@ he:
powered_by: 'מופעל על ידי'
count_documents_signed_with_html: '<b>%{count}</b> מסמכים נחתמו באמצעות'
open_source_documents_software: 'תוכנה בקוד פתוח למסמכים'
eu_data_residency: 'נתונים באיחוד האירופי '
nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
@@ -5045,6 +5204,7 @@ nl:
powered_by: 'Aangedreven door'
count_documents_signed_with_html: '<b>%{count}</b> documenten ondertekend met'
open_source_documents_software: 'open source documenten software'
eu_data_residency: Gegevens EU
ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@@ -5112,6 +5272,7 @@ ar:
powered_by: 'مشغل بواسطة'
count_documents_signed_with_html: '<b>%{count}</b> مستندات تم توقيعها باستخدام'
open_source_documents_software: 'برنامج مستندات مفتوح المصدر'
eu_data_residency: 'بيانات في الاتحاد الأوروبي'
ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@@ -5179,6 +5340,7 @@ ko:
powered_by: '제공:'
count_documents_signed_with_html: '<b>%{count}</b>개의 문서가 다음을 통해 서명됨'
open_source_documents_software: '오픈소스 문서 소프트웨어'
eu_data_residency: 'EU 데이터 보관'
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
@@ -5246,6 +5408,7 @@ ja:
powered_by: '提供元:'
count_documents_signed_with_html: '<b>%{count}</b> 件のドキュメントが以下で署名されました'
open_source_documents_software: 'オープンソースのドキュメントソフトウェア'
eu_data_residency: 'EU データ居住'
en-US:
<<: *en
+1
View File
@@ -97,6 +97,7 @@ Rails.application.routes.draw do
resources :templates, only: %i[new create edit update show destroy] do
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
resources :documents, only: %i[create], controller: 'template_documents'
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
resources :restore, only: %i[create], controller: 'templates_restore'
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
resources :submissions, only: %i[new create]
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class AccountAccesses < ActiveRecord::Migration[8.0]
def change
create_table :account_accesses do |t|
t.references :account, null: false, foreign_key: true, index: false
t.references :user, null: false, foreign_key: false, index: false
t.index %i[account_id user_id], unique: true
t.timestamps
end
end
end
+10 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
ActiveRecord::Schema[8.0].define(version: 2025_05_18_070555) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -24,6 +24,14 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
t.index ["user_id"], name: "index_access_tokens_on_user_id"
end
create_table "account_accesses", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "user_id"], name: "index_account_accesses_on_account_id_and_user_id", unique: true
end
create_table "account_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "key", null: false
@@ -415,6 +423,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_25_111255) do
end
add_foreign_key "access_tokens", "users"
add_foreign_key "account_accesses", "accounts"
add_foreign_key "account_configs", "accounts"
add_foreign_key "account_linked_accounts", "accounts"
add_foreign_key "account_linked_accounts", "accounts", column: "linked_account_id"
+2238
View File
File diff suppressed because it is too large. Load diff
+2657
View File
File diff suppressed because it is too large. Load diff
+2219
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2420
View File
File diff suppressed because it is too large. Load diff
+2277
View File
File diff suppressed because it is too large. Load diff
+2334
View File
File diff suppressed because it is too large. Load diff
+2334
View File
File diff suppressed because it is too large. Load diff
+2219
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+513
View File
@@ -0,0 +1,513 @@
# Angular Form Builder
### Example Code
```angular
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DocusealBuilderComponent } from '@docuseal/angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [DocusealBuilderComponent],
template: `
<div class="app">
<ng-container *ngIf="token">
<docuseal-builder [token]="token"></docuseal-builder>
</ng-container>
</div>
`
})
export class AppComponent implements OnInit {
token: string = ''
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.post('/api/docuseal/builder_token', {}).subscribe((data: any) => {
this.token = data.token;
});
}
}
```
```javascript
const jwt = require('jsonwebtoken');
const token = jwt.sign({
user_email: '{{admin_user_email}}',
integration_email: '{{signer_email}}',
external_id: 'TestForm123',
name: 'Integration W-9 Test Form',
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
}, '{{api_key}}');
```
### Attributes
```json
{
"token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
"required": true,
"properties": {
"user_email": {
"type": "string",
"required": true,
"description": "Email of the owner of the API signing key - admin user email."
},
"integration_email": {
"type": "string",
"required": false,
"description": "Email of the user to create a template for.",
"example": "signer@example.com"
},
"template_id": {
"type": "number",
"required": false,
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify this template within your app.",
"required": false
},
"folder_name": {
"type": "string",
"description": "The folder name in which the template should be created.",
"required": false
},
"document_urls": {
"type": "array",
"required": false,
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
},
"name": {
"type": "string",
"required": false,
"description": "New template name when creating a template with document_urls specified.",
"example": "Integration W-9 Test Form"
},
"extract_fields": {
"type": "boolean",
"required": false,
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
}
}
},
"host": {
"type": "string",
"required": false,
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
"example": "yourdomain.com"
},
"customButton": {
"type": "object",
"required": false,
"description": "Custom button will be displayed on the top right corner of the form builder.",
"properties": {
"title": {
"type": "string",
"required": true,
"description": "Custom button title."
},
"url": {
"type": "string",
"required": true,
"description": "Custom button URL. Only absolute URLs are supported."
}
}
},
"roles": {
"type": "array",
"required": false,
"description": "Submitter role names to be used by default in the form."
},
"fieldTypes": {
"type": "array",
"required": false,
"description": "Field type names to be used in the form builder. All field types are used by default."
},
"drawFieldType": {
"type": "string",
"required": false,
"default": "text",
"description": "Field type to be used by default with the field drawing tool.",
"example": "signature"
},
"fields": {
"type": "array",
"required": false,
"description": "An array of default custom field properties with `name` to be added to the document.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"submitters": {
"type": "array",
"required": false,
"description": "An array of default submitters with `role` name to be added to the document.",
"items": {
"type": "object",
"properties": {
"email": {
"type": "string",
"required": false,
"description": "Submitter email."
},
"role": {
"type": "string",
"required": true,
"description": "Submitter role name."
},
"name": {
"type": "string",
"required": false,
"description": "Submitter name."
},
"phone": {
"type": "string",
"required": false,
"description": "Submitter phone number, formatted according to the E.164 standard."
}
}
}
},
"requiredFields": {
"type": "array",
"required": false,
"description": "An array of default required custom field properties with `name` that should be added to the document.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"emailMessage": {
"type": "object",
"required": false,
"description": "Email subject and body for the signature request.",
"properties": {
"subject": {
"type": "string",
"required": true,
"description": "Email subject for the signature request."
},
"body": {
"type": "string",
"required": true,
"description": "Email body for the signature request."
}
}
},
"withTitle": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove document title from the builder."
},
"withSendButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Send\" button."
},
"withUploadButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Upload\" button."
},
"withAddPageButton": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show the \"Add Blank Page\" button."
},
"withSignYourselfButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Sign Yourself\" button."
},
"withDocumentsList": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
},
"withFieldsList": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
},
"withFieldPlaceholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"onlyDefinedFields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow to add fields only defined in the `fields` prop."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show template in preview mode without ability to edit it."
},
"autosave": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable form changes autosaving."
},
"language": {
"type": "string",
"required": false,
"default": "en",
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
},
"i18n": {
"type": "object",
"required": false,
"default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
},
"backgroundColor": {
"type": "string",
"required": false,
"description": "The form builder background color. Only HEX color codes are supported.",
"example": "#ffffff"
},
"customCss": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form builder.",
"example": "#sign_yourself_button { background-color: #FFA500; }"
}
}
```
### Callback
```json
{
"onLoad": {
"type": "event emitter",
"required": false,
"description": "Event emitted on loading the form builder template data.",
"example": "handleLoad($event)"
},
"onUpload": {
"type": "event emitter",
"required": false,
"description": "Event emitted on uploading a document to the template.",
"example": "handleUpload($event)"
},
"onSend": {
"type": "event emitter",
"required": false,
"description": "Event emitted on sending documents for signature to recipients.",
"example": "handleSend($event)"
},
"onChange": {
"type": "function",
"required": false,
"description": "Function to be called when changes are made to the template form.",
"example": "handleChange($event)"
},
"onSave": {
"type": "event emitter",
"required": false,
"description": "Event emitted on saving changes of the template form.",
"example": "handleSave($event)"
}
}
```
+477
View File
@@ -0,0 +1,477 @@
# JavaScript Form Builder
### Example Code
```javascript
<script src="https://docuseal.com/js/builder.js"></script>
<docuseal-builder
data-token="<%= JWT.encode({
user_email: '{{admin_user_email}}',
integration_email: '{{signer_email}}',
name: 'Integration W-9 Test Form',
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
}, '{{api_key}}') %>">
</docuseal-builder>
```
### Attributes
```json
{
"data-token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
"required": true,
"properties": {
"user_email": {
"type": "string",
"required": true,
"description": "Email of the owner of the API signing key - admin user email."
},
"integration_email": {
"type": "string",
"required": false,
"description": "Email of the user to create a template for.",
"example": "signer@example.com"
},
"template_id": {
"type": "number",
"required": false,
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify this template within your app.",
"required": false
},
"folder_name": {
"type": "string",
"description": "The folder name in which the template should be created.",
"required": false
},
"document_urls": {
"type": "array",
"required": false,
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
},
"name": {
"type": "string",
"required": false,
"description": "New template name when creating a template with document_urls specified.",
"example": "Integration W-9 Test Form"
},
"extract_fields": {
"type": "boolean",
"required": false,
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
}
}
},
"data-host": {
"type": "string",
"required": false,
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
"example": "yourdomain.com"
},
"data-roles": {
"type": "string",
"required": false,
"description": "Comma separated submitter role names to be used by default in the form.",
"example": "Company,Customer"
},
"data-fields": {
"type": "string",
"doc_type": "array",
"required": false,
"description": "A list of default custom fields with `name` to be added to the document. Should contain an array of field properties as a JSON encoded string.",
"example": "[{ \"name\": \"FIELD_1\", \"type\": \"date\", \"role\": \"Customer\", \"default_value\": \"2021-01-01\" }]",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"data-submitters": {
"type": "string",
"doc_type": "array",
"required": false,
"description": "A list of default submitters with `role` name to be added to the document. Should contain an array of field properties as a JSON encoded string.",
"example": "[{ \"email\": \"example@company.com\", \"name\": \"John Doe\", \"phone\": \"+1234567890\", \"role\": \"Customer\" }]",
"items": {
"type": "object",
"properties": {
"email": {
"type": "string",
"required": false,
"description": "Submitter email."
},
"role": {
"type": "string",
"required": true,
"description": "Submitter role name."
},
"name": {
"type": "string",
"required": false,
"description": "Submitter name."
},
"phone": {
"type": "string",
"required": false,
"description": "Submitter phone number, formatted according to the E.164 standard."
}
}
}
},
"data-required-fields": {
"type": "string",
"doc_type": "array",
"required": false,
"description": "A list of required default custom fields with `name` that should be added to the document. Should contain an array of field properties as a JSON encoded string.",
"example": "[{ \"name\": \"FIELD_1\", \"type\": \"date\", \"role\": \"Customer\", \"default_value\": \"2021-01-01\" }]",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"data-field-types": {
"type": "string",
"required": false,
"description": "Comma separated field type names to be used in the form builder. All field types are used by default.",
"example": "text,date"
},
"data-draw-field-type": {
"type": "string",
"required": false,
"default": "text",
"description": "Field type to be used by default with the field drawing tool.",
"example": "signature"
},
"data-custom-button-title": {
"type": "string",
"required": false,
"description": "Custom button title. This button will be displayed on the top right corner of the form builder."
},
"data-custom-button-url": {
"type": "string",
"required": false,
"description": "Custom button URL. Only absolute URLs are supported."
},
"data-with-title": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove document title from the builder."
},
"email-subject": {
"type": "string",
"required": false,
"description": "Email subject for the signature request. Required if `email-body` specified"
},
"email-body": {
"type": "string",
"required": false,
"description": "Email body for the signature request. Required if `email-subject` specified"
},
"data-with-send-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Recipients\" button."
},
"data-with-upload-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Upload\" button."
},
"data-with-add-page-button": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show the \"Add Blank Page\" button."
},
"data-with-sign-yourself-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Sign Yourself\" button."
},
"data-with-documents-list": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
},
"data-with-fields-list": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
},
"data-with-field-placeholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"data-preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show template in preview mode without ability to edit it."
},
"data-only-defined-fields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow to add fields only defined in the `data-fields` attribute."
},
"data-autosave": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable form changes autosaving."
},
"data-i18n": {
"type": "string",
"required": false,
"default": "{}",
"description": "JSON encoded string that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
},
"data-language": {
"type": "string",
"required": false,
"default": "en",
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
},
"data-background-color": {
"type": "string",
"required": false,
"description": "The form builder background color. Only HEX color codes are supported.",
"example": "#ffffff"
},
"data-custom-css": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form builder.",
"example": "#sign_yourself_button { background-color: #FFA500; }"
}
}
```
### Callback
```json
{
"load": {
"type": "event",
"required": false,
"description": "Custom event to be triggered on loading the form builder template data.",
"example": "document.querySelector('docuseal-builder').addEventListener('load', (e) => e.detail)"
},
"upload": {
"type": "event",
"required": false,
"description": "Custom event to be triggered on uploading a document to the template.",
"example": "document.querySelector('docuseal-builder').addEventListener('upload', (e) => e.detail)"
},
"send": {
"type": "event",
"required": false,
"description": "Custom event to be triggered on sending documents for signature to recipients.",
"example": "document.querySelector('docuseal-builder').addEventListener('send', (e) => e.detail)"
},
"change": {
"type": "event",
"required": false,
"description": "Custom event to be triggered every time a change to the template is made.",
"example": "document.querySelector('docuseal-builder').addEventListener('change', (e) => e.detail)"
},
"save": {
"type": "event",
"required": false,
"description": "Custom event to be triggered on saving changes of the template form.",
"example": "document.querySelector('docuseal-builder').addEventListener('save', (e) => e.detail)"
}
}
```
+504
View File
@@ -0,0 +1,504 @@
# React Form Builder
### Example Code
```react
import React, { useState, useEffect } from 'react';
import { DocusealBuilder } from '@docuseal/react'
const App = () => {
const [token, setToken] = useState();
useEffect(() => {
fetch('/api/docuseal/builder_token', {
method: 'POST',
})
.then((response) => response.json())
.then((data) => {
setToken(data.token);
});
}, []);
return token && <DocusealBuilder token={token} />;
};
```
```javascript
const jwt = require('jsonwebtoken');
const token = jwt.sign({
user_email: '{{admin_user_email}}',
integration_email: '{{signer_email}}',
external_id: 'TestForm123',
name: 'Integration W-9 Test Form',
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
}, '{{api_key}}');
```
### Attributes
```json
{
"token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
"required": true,
"properties": {
"user_email": {
"type": "string",
"required": true,
"description": "Email of the owner of the API signing key - admin user email."
},
"integration_email": {
"type": "string",
"required": false,
"description": "Email of the user to create a template for.",
"example": "signer@example.com"
},
"template_id": {
"type": "number",
"required": false,
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify this template within your app.",
"required": false
},
"folder_name": {
"type": "string",
"description": "The folder name in which the template should be created.",
"required": false
},
"document_urls": {
"type": "array",
"required": false,
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
},
"name": {
"type": "string",
"required": false,
"description": "New template name when creating a template with document_urls specified.",
"example": "Integration W-9 Test Form"
},
"extract_fields": {
"type": "boolean",
"required": false,
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
}
}
},
"host": {
"type": "string",
"required": false,
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
"example": "yourdomain.com"
},
"customButton": {
"type": "object",
"required": false,
"description": "Custom button will be displayed on the top right corner of the form builder.",
"properties": {
"title": {
"type": "string",
"required": true,
"description": "Custom button title."
},
"url": {
"type": "string",
"required": true,
"description": "Custom button URL. Only absolute URLs are supported."
}
}
},
"roles": {
"type": "array",
"required": false,
"description": "Submitter role names to be used by default in the form."
},
"fieldTypes": {
"type": "array",
"required": false,
"description": "Field type names to be used in the form builder. All field types are used by default."
},
"drawFieldType": {
"type": "string",
"required": false,
"default": "text",
"description": "Field type to be used by default with the field drawing tool.",
"example": "signature"
},
"fields": {
"type": "array",
"required": false,
"description": "An array of default custom field properties with `name` to be added to the document.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"submitters": {
"type": "array",
"required": false,
"description": "An array of default submitters with `role` name to be added to the document.",
"items": {
"type": "object",
"properties": {
"email": {
"type": "string",
"required": false,
"description": "Submitter email."
},
"role": {
"type": "string",
"required": true,
"description": "Submitter role name."
},
"name": {
"type": "string",
"required": false,
"description": "Submitter name."
},
"phone": {
"type": "string",
"required": false,
"description": "Submitter phone number, formatted according to the E.164 standard."
}
}
}
},
"requiredFields": {
"type": "array",
"required": false,
"description": "An array of default required custom field properties with `name` that should be added to the document.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"emailMessage": {
"type": "object",
"required": false,
"description": "Email subject and body for the signature request.",
"properties": {
"subject": {
"type": "string",
"required": true,
"description": "Email subject for the signature request."
},
"body": {
"type": "string",
"required": true,
"description": "Email body for the signature request."
}
}
},
"withTitle": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove document title from the builder."
},
"withSendButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Send\" button."
},
"withUploadButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Upload\" button."
},
"withAddPageButton": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show the \"Add Blank Page\" button."
},
"withSignYourselfButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Sign Yourself\" button."
},
"withDocumentsList": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
},
"withFieldsList": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
},
"withFieldPlaceholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"onlyDefinedFields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow to add fields only defined in the `fields` prop."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show template in preview mode without ability to edit it."
},
"autosave": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable form changes autosaving."
},
"language": {
"type": "string",
"required": false,
"default": "en",
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
},
"i18n": {
"type": "object",
"required": false,
"default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
},
"backgroundColor": {
"type": "string",
"required": false,
"description": "The form builder background color. Only HEX color codes are supported.",
"example": "#ffffff"
},
"customCss": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form builder.",
"example": "#sign_yourself_button { background-color: #FFA500; }"
}
}
```
### Callback
```json
{
"onLoad": {
"type": "function",
"required": false,
"description": "Function to be called on loading the form builder template data.",
"example": "(data) => { console.log(data) }"
},
"onUpload": {
"type": "function",
"required": false,
"description": "Function to be called on uploading a document to the template.",
"example": "(data) => { console.log(data) }"
},
"onSend": {
"type": "function",
"required": false,
"description": "Function to be called on sending documents for signature to recipients.",
"example": "(data) => { console.log(data) }"
},
"onChange": {
"type": "function",
"required": false,
"description": "Function to be called when changes are made to the template form.",
"example": "(data) => { console.log(data) }"
},
"onSave": {
"type": "function",
"required": false,
"description": "Function to be called on saving changes of the template form.",
"example": "(data) => { console.log(data) }"
}
}
```
+513
View File
@@ -0,0 +1,513 @@
# Vue Form Builder
### Example Code
```vue
<template>
<DocusealBuilder
v-if="token"
:token="token"
/>
</template>
<script>
import { DocusealBuilder } from '@docuseal/vue'
export default {
name: 'App',
components: { DocusealBuilder },
data () {
return { token: '' }
},
mounted () {
fetch('/api/docuseal/builder_token', {
method: 'POST'
}).then(async (resp) => {
const data = await resp.json()
this.token = data.token
})
}
}
</script>
```
```javascript
const jwt = require('jsonwebtoken');
const token = jwt.sign({
user_email: '{{admin_user_email}}',
integration_email: '{{signer_email}}',
external_id: 'TestForm123',
name: 'Integration W-9 Test Form',
document_urls: ['https://www.irs.gov/pub/irs-pdf/fw9.pdf'],
}, '{{api_key}}');
```
### Attributes
```json
{
"token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <br><b>Ensure that the JWT token is generated on the backend to prevent unauthorized access to your documents</b>.",
"required": true,
"properties": {
"user_email": {
"type": "string",
"required": true,
"description": "Email of the owner of the API signing key - admin user email."
},
"integration_email": {
"type": "string",
"required": false,
"description": "Email of the user to create a template for.",
"example": "signer@example.com"
},
"template_id": {
"type": "number",
"required": false,
"description": "ID of the template to open in the form builder. Optional when `document_urls` are specified."
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify this template within your app.",
"required": false
},
"folder_name": {
"type": "string",
"description": "The folder name in which the template should be created.",
"required": false
},
"document_urls": {
"type": "array",
"required": false,
"description": "An Array of URLs with PDF files to open in the form builder. Optional when `template_id` is specified.",
"example": "['https://www.irs.gov/pub/irs-pdf/fw9.pdf']"
},
"name": {
"type": "string",
"required": false,
"description": "New template name when creating a template with document_urls specified.",
"example": "Integration W-9 Test Form"
},
"extract_fields": {
"type": "boolean",
"required": false,
"description": "Pass `false` to disable automatic PDF form fields extraction. PDF fields are automatically added by default."
}
}
},
"host": {
"type": "string",
"required": false,
"description": "DocuSeal host domain name. Only use this attribute if you are using the on-premises DocuSeal installation or docuseal.eu Cloud.",
"example": "yourdomain.com"
},
"custom-button": {
"type": "object",
"required": false,
"description": "Custom button will be displayed on the top right corner of the form builder.",
"properties": {
"title": {
"type": "string",
"required": true,
"description": "Custom button title."
},
"url": {
"type": "string",
"required": true,
"description": "Custom button URL. Only absolute URLs are supported."
}
}
},
"only-defined-fields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow to add fields only defined in the `:fields` prop."
},
"with-send-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Recipients\" button."
},
"roles": {
"type": "array",
"required": false,
"description": "Submitter role names to be used by default in the form."
},
"field-types": {
"type": "array",
"required": false,
"description": "Field type names to be used in the form builder. All field types are used by default."
},
"draw-field-type": {
"type": "string",
"required": false,
"default": "text",
"description": "Field type to be used by default with the field drawing tool.",
"example": "signature"
},
"fields": {
"type": "array",
"required": false,
"description": "An array of default custom field properties with `name` to be added to the document.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"submitters": {
"type": "array",
"required": false,
"description": "An array of default submitters with `role` name to be added to the document.",
"items": {
"type": "object",
"properties": {
"email": {
"type": "string",
"required": false,
"description": "Submitter email."
},
"role": {
"type": "string",
"required": true,
"description": "Submitter role name."
},
"name": {
"type": "string",
"required": false,
"description": "Submitter name."
},
"phone": {
"type": "string",
"required": false,
"description": "Submitter phone number, formatted according to the E.164 standard."
}
}
}
},
"required-fields": {
"type": "array",
"required": false,
"description": "An array of default required custom field properties with `name` that should be added to the document.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true,
"description": "Field name."
},
"type": {
"type": "string",
"required": false,
"description": "Field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
},
"role": {
"type": "string",
"required": false,
"description": "Submitter role name for the field."
},
"default_value": {
"type": "string",
"required": false,
"description": "Default value of the field."
},
"title": {
"type": "string",
"required": false,
"description": "Field title displayed to the user instead of the name, shown on the signing form. Supports Markdown."
},
"description": {
"type": "string",
"required": false,
"description": "Field description displayed on the signing form. Supports Markdown."
},
"width": {
"type": "number",
"required": false,
"description": "Field width in pixels."
},
"height": {
"type": "number",
"required": false,
"description": "Field height in pixels."
},
"format": {
"type": "string",
"required": false,
"description": "Field format. Depends on the field type."
},
"options": {
"type": "array",
"required": false,
"description": "Field options. Required for the select field type."
},
"validation": {
"type": "object",
"required": false,
"description": "Field validation rules.",
"properties": {
"pattern": {
"type": "string",
"required": false,
"description": "Field pattern.",
"example": "^[0-9]{5}$"
},
"message": {
"type": "string",
"required": false,
"description": "Validation error message."
}
}
}
}
}
},
"email-message": {
"type": "object",
"required": false,
"description": "Email subject and body for the signature request.",
"properties": {
"subject": {
"type": "string",
"required": true,
"description": "Email subject for the signature request."
},
"body": {
"type": "string",
"required": true,
"description": "Email body for the signature request."
}
}
},
"with-title": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove document title from the builder."
},
"with-upload-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Upload\" button."
},
"with-add-page-button": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show the \"Add Blank Page\" button."
},
"with-sign-yourself-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Show the \"Sign Yourself\" button."
},
"with-documents-list": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default."
},
"with-fields-list": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default."
},
"with-field-placeholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"autosave": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable form changes autosaving."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show template in preview mode without ability to edit it."
},
"language": {
"type": "string",
"required": false,
"default": "en",
"description": "UI language, 'en', 'es', 'de', 'fr', 'pt', 'he', 'ar' languages are available."
},
"i18n": {
"type": "object",
"required": false,
"default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/template_builder/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">template_builder/i18n.js</a> for available i18n keys."
},
"background-color": {
"type": "string",
"required": false,
"description": "The form builder background color. Only HEX color codes are supported.",
"example": "#ffffff"
},
"custom-css": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form builder.",
"example": "#sign_yourself_button { background-color: #FFA500; }"
}
}
```
### Callback
```json
{
"@load": {
"type": "function",
"required": false,
"description": "Function to be called on loading the form builder template data.",
"example": "onBuilderLoad"
},
"@upload": {
"type": "function",
"required": false,
"description": "Function to be called on uploading a document to the template.",
"example": "onBuilderUpload"
},
"@send": {
"type": "function",
"required": false,
"description": "Function to be called on sending documents for signature to recipients.",
"example": "onBuilderSend"
},
"@change": {
"type": "function",
"required": false,
"description": "Function to be called when changes are made to the template form.",
"example": "onBuilderChange"
},
"@save": {
"type": "function",
"required": false,
"description": "Function to be called on saving changes of the template form.",
"example": "onBuilderSave"
}
}
```
+295
View File
@@ -0,0 +1,295 @@
# Angular Signing Form
### Example Code
```angular
import { Component } from '@angular/core';
import { DocusealFormComponent } from '@docuseal/angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [DocusealFormComponent],
template: `
<div class="app">
<docuseal-form
[src]="'https://docuseal.com/d/{{template_slug}}'"
[email]="'{{signer_email}}'">
</docuseal-form>
</div>
`
})
export class AppComponent {}
```
### Attributes
```json
{
"src": {
"type": "string",
"required": true,
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
},
"email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
},
"name": {
"type": "string",
"required": false,
"description": "Name of the signer."
},
"role": {
"type": "string",
"required": false,
"description": "The role name or title of the signer.",
"example": "First Party"
},
"expand": {
"type": "boolean",
"required": false,
"description": "Expand form on open.",
"default": true
},
"minimize": {
"type": "boolean",
"required": false,
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
"default": false
},
"orderAsOnPage": {
"type": "boolean",
"required": false,
"default": false,
"description": "Order form fields based on their position on the pages."
},
"externalId": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"logo": {
"type": "string",
"required": false,
"description": "Public logo image URL to use in the signing form."
},
"language": {
"type": "string",
"required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
},
"i18n": {
"type": "object",
"required": false,
"default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"goToLast": {
"type": "boolean",
"required": false,
"default": true,
"description": "Navigate to the last unfinished step."
},
"withFieldNames": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
},
"withFieldPlaceholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"skipFields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow skipping form fields."
},
"autoscrollFields": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable auto-scrolling to the next document field."
},
"sendCopyEmail": {
"type": "boolean",
"required": false,
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
},
"backgroundColor": {
"type": "string",
"required": false,
"description": "Form background color. Only HEX color codes are supported.",
"example": "#d9d9d9"
},
"completedRedirectUrl": {
"type": "string",
"required": false,
"description": "URL to redirect to after the submission completion.",
"example": "https://docuseal.com/success"
},
"completedMessage": {
"type": "object",
"required": false,
"description": "Message displayed after the form completion.",
"properties": {
"title": {
"type": "string",
"required": false,
"description": "Message title.",
"example": "Documents have been signed!"
},
"body": {
"type": "string",
"required": false,
"description": "Message content.",
"example": "If you have any questions, please contact us."
}
}
},
"completedButton": {
"type": "object",
"required": false,
"description": "Customizable button after form completion.",
"properties": {
"title": {
"type": "string",
"required": true,
"description": "Button label.",
"example": "Go Back"
},
"url": {
"type": "string",
"required": true,
"description": "Button link. Only absolute URLs are supported.",
"example": "https://example.com"
}
}
},
"withTitle": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the document title from the form."
},
"withDecline": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the decline button in the form."
},
"withDownloadButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document download button from the completed form card."
},
"withSendCopyButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document send email button from the completed form card."
},
"withCompleteButton": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the complete button in the form header."
},
"allowToResubmit": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow user to re-submit the form."
},
"signature": {
"type": "string",
"required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
},
"rememberSignature": {
"type": "boolean",
"required": false,
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
},
"reuseSignature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
},
"allowTypedSignature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow users to type their signature."
},
"values": {
"type": "object",
"required": false,
"description": "Pre-assigned values for form fields.",
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
},
"metadata": {
"type": "object",
"required": false,
"description": "Metadata object with additional signer information.",
"example": "{ customData: 'custom value' }"
},
"readonlyFields": {
"type": "array",
"required": false,
"description": "List of read-only fields.",
"example": "['First Name','Last Name']"
},
"customCss": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form.",
"example": "#submit_form_button { background-color: #d9d9d9; }"
}
}
```
### Callback
```json
{
"onInit": {
"type": "event emitter",
"required": false,
"description": "Event emitted on initializing the form component.",
"example": "handleInit($event)"
},
"onLoad": {
"type": "event emitter",
"required": false,
"description": "Event emitted on loading the form data.",
"example": "handleLoad($event)"
},
"onComplete": {
"type": "event emitter",
"required": false,
"description": "Event emitted the form completion.",
"example": "handleComplete($event)"
},
"onDecline": {
"type": "event emitter",
"required": false,
"description": "Event emitted after the form decline.",
"example": "handleDecline($event)"
}
}
```
+275
View File
@@ -0,0 +1,275 @@
# JavaScript Signing Form
### Example Code
```javascript
<script src="https://cdn.docuseal.com/js/form.js"></script>
<docuseal-form
id="docusealForm"
data-src="https://docuseal.com/d/{{template_slug}}"
data-email="{{signer_email}}">
</docuseal-form>
<script>
window.docusealForm.addEventListener('completed', (e) => e.detail)
</script>
```
### Attributes
```json
{
"data-src": {
"type": "string",
"required": true,
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
},
"data-email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
},
"data-name": {
"type": "string",
"required": false,
"description": "Name of the signer."
},
"data-role": {
"type": "string",
"required": false,
"description": "The role name or title of the signer.",
"example": "First Party"
},
"data-expand": {
"type": "boolean",
"required": false,
"description": "Expand form on open.",
"default": true
},
"data-minimize": {
"type": "boolean",
"required": false,
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
"default": false
},
"data-order-as-on-page": {
"type": "boolean",
"required": false,
"default": false,
"description": "Order form fields based on their position on the pages."
},
"data-preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"data-logo": {
"type": "string",
"required": false,
"description": "Public logo image URL to use in the signing form."
},
"data-language": {
"type": "string",
"required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
},
"data-i18n": {
"type": "string",
"required": false,
"default": "{}",
"description": "JSON encoded string that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
},
"data-go-to-last": {
"type": "boolean",
"required": false,
"default": true,
"description": "Navigate to the last unfinished step."
},
"data-skip-fields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow skipping form fields."
},
"data-autoscroll-fields": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable auto-scrolling to the next document field."
},
"data-send-copy-email": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
},
"data-with-title": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the document title from the form."
},
"data-with-decline": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the decline button in the form."
},
"data-with-field-names": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
},
"data-with-field-placeholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"data-with-download-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document download button from the completed form card."
},
"data-with-send-copy-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document send email button from the completed form card."
},
"data-with-complete-button": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the complete button in the form header."
},
"data-allow-to-resubmit": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow users to re-submit the form."
},
"data-allow-typed-signature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow users to type their signature."
},
"data-signature": {
"type": "string",
"required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
},
"data-remember-signature": {
"type": "boolean",
"required": false,
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
},
"data-reuse-signature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
},
"data-background-color": {
"type": "string",
"required": false,
"description": "Form background color. Only HEX color codes are supported.",
"example": "#d9d9d9"
},
"data-values": {
"type": "object",
"required": false,
"description": "Pre-assigned values for form fields.",
"example": "{\"First Name\":\"Jon\",\"Last Name\":\"Doe\"}"
},
"data-external-id": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"data-metadata": {
"type": "object",
"required": false,
"description": "Signer metadata Object in JSON format. ",
"example": "{\"customData\":\"customValue\"}"
},
"data-readonly-fields": {
"type": "string",
"required": false,
"description": "Comma separated read-only field names",
"example": "First Name,Last Name"
},
"data-completed-redirect-url": {
"type": "string",
"required": false,
"description": "URL to redirect to after the submission completion.",
"example": "https://docuseal.com/success"
},
"data-completed-message-title": {
"type": "string",
"required": false,
"description": "Message title displayed after the form completion.",
"example": "Documents have been completed"
},
"data-completed-message-body": {
"type": "string",
"required": false,
"description": "Message body displayed after the form completion.",
"example": "If you have any questions, please contact us."
},
"data-completed-button-title": {
"type": "string",
"required": false,
"description": "Button title displayed after the form completion.",
"example": "Go Back"
},
"data-completed-button-url": {
"type": "string",
"required": false,
"description": "URL of the button displayed after the form completion.",
"example": "https://example.com"
},
"data-custom-css": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form.",
"example": "#submit_form_button { background-color: #d9d9d9; }"
}
}
```
### Callback
```json
{
"init": {
"type": "event",
"required": false,
"description": "Custom event to be triggered on initializing the form component.",
"example": "document.querySelector('docuseal-form').addEventListener('init', () => console.log('init'))"
},
"load": {
"type": "event",
"required": false,
"description": "Custom event to be triggered on loading the form data.",
"example": "document.querySelector('docuseal-form').addEventListener('load', (e) => e.detail)"
},
"completed": {
"type": "event",
"required": false,
"description": "Custom event to be triggered after form completion.",
"example": "document.querySelector('docuseal-form').addEventListener('completed', (e) => e.detail)"
},
"declined": {
"type": "event",
"description": "Custom event to be triggered after form decline.",
"example": "document.querySelector('docuseal-form').addEventListener('declined', (e) => e.detail)"
}
}
```
+292
View File
@@ -0,0 +1,292 @@
# React Signing Form
### Example Code
```react
import React from "react"
import { DocusealForm } from '@docuseal/react'
export function App() {
return (
<div className="app">
<DocusealForm
src="https://docuseal.com/d/{{template_slug}}"
email="{{signer_email}}"
onComplete={(data) => console.log(data)}
/>
</div>
);
}
```
### Attributes
```json
{
"src": {
"type": "string",
"required": true,
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
},
"email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
},
"name": {
"type": "string",
"required": false,
"description": "Name of the signer."
},
"role": {
"type": "string",
"required": false,
"description": "The role name or title of the signer.",
"example": "First Party"
},
"expand": {
"type": "boolean",
"required": false,
"description": "Expand form on open.",
"default": true
},
"minimize": {
"type": "boolean",
"required": false,
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
"default": false
},
"orderAsOnPage": {
"type": "boolean",
"required": false,
"default": false,
"description": "Order form fields based on their position on the pages."
},
"externalId": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"logo": {
"type": "string",
"required": false,
"description": "Public logo image URL to use in the signing form."
},
"language": {
"type": "string",
"required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
},
"i18n": {
"type": "object",
"required": false,
"default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"goToLast": {
"type": "boolean",
"required": false,
"default": true,
"description": "Navigate to the last unfinished step."
},
"withFieldNames": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
},
"withFieldPlaceholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"skipFields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow skipping form fields."
},
"autoscrollFields": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable auto-scrolling to the next document field."
},
"sendCopyEmail": {
"type": "boolean",
"required": false,
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
},
"backgroundColor": {
"type": "string",
"required": false,
"description": "Form background color. Only HEX color codes are supported.",
"example": "#d9d9d9"
},
"completedRedirectUrl": {
"type": "string",
"required": false,
"description": "URL to redirect to after the submission completion.",
"example": "https://docuseal.com/success"
},
"completedMessage": {
"type": "object",
"required": false,
"description": "Message displayed after the form completion.",
"properties": {
"title": {
"type": "string",
"required": false,
"description": "Message title.",
"example": "Documents have been signed!"
},
"body": {
"type": "string",
"required": false,
"description": "Message content.",
"example": "If you have any questions, please contact us."
}
}
},
"completedButton": {
"type": "object",
"required": false,
"description": "Customizable button after form completion.",
"properties": {
"title": {
"type": "string",
"required": true,
"description": "Button label.",
"example": "Go Back"
},
"url": {
"type": "string",
"required": true,
"description": "Button link. Only absolute URLs are supported.",
"example": "https://example.com"
}
}
},
"withTitle": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the document title from the form."
},
"withDecline": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the decline button in the form."
},
"withDownloadButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document download button from the completed form card."
},
"withSendCopyButton": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document send email button from the completed form card."
},
"withCompleteButton": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the complete button in the form header."
},
"allowToResubmit": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow user to re-submit the form."
},
"allowTypedSignature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow users to type their signature."
},
"signature": {
"type": "string",
"required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
},
"rememberSignature": {
"type": "boolean",
"required": false,
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
},
"reuseSignature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
},
"values": {
"type": "object",
"required": false,
"description": "Pre-assigned values for form fields.",
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
},
"metadata": {
"type": "object",
"required": false,
"description": "Metadata object with additional signer information.",
"example": "{ customData: 'custom value' }"
},
"readonlyFields": {
"type": "array",
"required": false,
"description": "List of read-only fields.",
"example": "['First Name','Last Name']"
},
"customCss": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form.",
"example": "#submit_form_button { background-color: #d9d9d9; }"
}
}
```
### Callback
```json
{
"onInit": {
"type": "function",
"required": false,
"description": "Function to be called on initializing the form component.",
"example": "() => { console.log(\"Loaded\") }"
},
"onLoad": {
"type": "function",
"required": false,
"description": "Function to be called on loading the form data.",
"example": "(data) => { console.log(data) }"
},
"onComplete": {
"type": "function",
"required": false,
"description": "Function to be called after the form completion.",
"example": "(data) => { console.log(data) }"
},
"onDecline": {
"type": "function",
"required": false,
"description": "Function to be called after the form decline.",
"example": "(data) => { console.log(data) }"
}
}
```
+302
View File
@@ -0,0 +1,302 @@
# Vue Signing Form
### Example Code
```vue
<template>
<DocusealForm
:src="'https://docuseal.com/d/{{template_slug}}'"
:email="'{{signer_email}}'"
@complete="onFormComplete"
/>
</template>
<script>
import { DocusealForm } from '@docuseal/vue'
export default {
name: 'App',
components: {
DocusealForm
},
methods: {
onFormComplete (data) {
console.log(data)
}
}
}
</script>
```
### Attributes
```json
{
"src": {
"type": "string",
"required": true,
"description": "Public URL of the document signing form. There are two types of URLs: <li><code>/d/{slug}</code> - template form signing URL can be copied from the template page in the admin dashboard. Also template \"slug\" key can be obtained via the <code>/templates</code> API.</li><li><code>/s/{slug}</code> - individual signer URL. Signer \"slug\" key can be obtained via the <code>/submissions</code> API which is used to initiate signature requests for a template form with recipients.</li>"
},
"email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified."
},
"name": {
"type": "string",
"required": false,
"description": "Name of the signer."
},
"role": {
"type": "string",
"required": false,
"description": "The role name or title of the signer.",
"example": "First Party"
},
"external-id": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"expand": {
"type": "boolean",
"required": false,
"description": "Expand form on open.",
"default": true
},
"minimize": {
"type": "boolean",
"required": false,
"description": "Set to `true` to always minimize form fields. Requires to click on the field to expand the form.",
"default": false
},
"order-as-on-page": {
"type": "boolean",
"required": false,
"default": false,
"description": "Order form fields based on their position on the pages."
},
"logo": {
"type": "string",
"required": false,
"description": "Public logo image URL to use in the signing form."
},
"language": {
"type": "string",
"required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
},
"i18n": {
"type": "object",
"required": false,
"default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"go-to-last": {
"type": "boolean",
"required": false,
"default": true,
"description": "Navigate to the last unfinished step."
},
"skip-fields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Allow skipping form fields."
},
"autoscroll-fields": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable auto-scrolling to the next document field."
},
"with-field-names": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
},
"with-field-placeholder": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display field name placeholders instead of the field type icons."
},
"send-copy-email": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disable automatic email sending with signed documents to the signers. Emails with signed documents are sent to the signers by default."
},
"background-color": {
"type": "string",
"required": false,
"description": "Form background color. Only HEX color codes are supported.",
"example": "#d9d9d9"
},
"with-title": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the document title from the form."
},
"with-decline": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the decline button in the form."
},
"with-download-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document download button from the completed form card."
},
"with-send-copy-button": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to remove the signed document send email button from the completed form card."
},
"with-complete-button": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display the complete button in the form header."
},
"allow-to-resubmit": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow user to re-submit the form."
},
"signature": {
"type": "string",
"required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
},
"remember-signature": {
"type": "boolean",
"required": false,
"description": "Allows to specify whether the signature should be remembered for future use. Remembered signatures are stored in the signer's browser local storage and can be automatically reused to prefill signature fields in new forms for the signer when the value is set to `true`."
},
"reuse-signature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to not reuse the signature in the second signature field and collect a new one."
},
"allow-typed-signature": {
"type": "boolean",
"required": false,
"default": true,
"description": "Set `false` to disallow users to type their signature."
},
"completed-redirect-url": {
"type": "string",
"required": false,
"description": "URL to redirect to after the submission completion.",
"example": "https://docuseal.com/success"
},
"completed-message": {
"type": "object",
"required": false,
"description": "Message displayed after the form completion.",
"properties": {
"title": {
"type": "string",
"required": false,
"description": "Message title.",
"example": "Documents have been signed!"
},
"body": {
"type": "string",
"required": false,
"description": "Message content.",
"example": "If you have any questions, please contact us."
}
}
},
"completed-button": {
"type": "object",
"required": false,
"description": "Customizable button after form completion.",
"properties": {
"title": {
"type": "string",
"required": true,
"description": "Button label.",
"example": "Go Back"
},
"url": {
"type": "string",
"required": true,
"description": "Button link. Only absolute URLs are supported.",
"example": "https://example.com"
}
}
},
"values": {
"type": "object",
"required": false,
"description": "Pre-assigned values for form fields.",
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
},
"metadata": {
"type": "object",
"required": false,
"description": "Metadata object with additional signer information.",
"example": "{ customData: 'custom value' }"
},
"readonly-fields": {
"type": "array",
"required": false,
"description": "List of read-only fields.",
"example": "['First Name','Last Name']"
},
"custom-css": {
"type": "string",
"required": false,
"description": "Custom CSS styles to be applied to the form.",
"example": "#submit_form_button { background-color: #d9d9d9; }"
}
}
```
### Callback
```json
{
"@init": {
"type": "function",
"required": false,
"description": "Function to be called on initializing the form component.",
"example": "onFormLoad"
},
"@load": {
"type": "function",
"required": false,
"description": "Function to be called on loading the form data.",
"example": "onFormLoad"
},
"@complete": {
"type": "function",
"required": false,
"description": "Function to be called after the form completion.",
"example": "onFormComplete"
},
"@decline": {
"type": "function",
"required": false,
"description": "Function to be called after the form decline.",
"example": "onFormDecline"
}
}
```
+7568
View File
File diff suppressed because it is too large. Load diff
+247
View File
@@ -0,0 +1,247 @@
# Form Webhook
During the form filling and signing process, 3 types of events may occur and are dispatched at different stages:
- **'form.viewed'** event is triggered when the submitter first opens the form.
- **'form.started'** event is triggered when the submitter initiates filling out the form.
- **'form.completed'** event is triggered upon successful form completion and signing by one of the parties.
- **'form.declined'** event is triggered when a signer declines the submission.
It's important to note that each of these events contain information available at the time of dispatch, so some data may be missing or incomplete depending on the specific event. Failed webhook requests (4xx, 5xx) are automatically retried multiple times within 48 hours (every 2^attempt minutes) for all production accounts.
**Related Guides**
[Download Signed Documents](https://www.docuseal.com/guides/download-signed-documents)
```json
{
"event_type": {
"type": "string",
"description": "The event type.",
"enum": [
"form.viewed",
"form.started",
"form.completed"
]
},
"timestamp": {
"type": "string",
"description": "The event timestamp.",
"example": "2023-09-24T11:20:42Z",
"format": "date-time"
},
"data": {
"type": "object",
"description": "Submitted data object.",
"properties": {
"id": {
"type": "number",
"description": "The submitter's unique identifier."
},
"submission_id": {
"type": "number",
"description": "The unique submission identifier."
},
"email": {
"type": "string",
"description": "The submitter's email address",
"format": "email",
"example": "john.doe@example.com"
},
"ua": {
"type": "string",
"description": "The user agent string that provides information about the submitter's web browser.",
"example": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
},
"ip": {
"type": "string",
"description": "The submitter's IP address."
},
"name": {
"type": "string",
"description": "The submitter's name."
},
"phone": {
"type": "string",
"description": "The submitter's phone number, formatted according to the E.164 standard.",
"example": "+1234567890"
},
"role": {
"type": "string",
"description": "The submitter's role name or title.",
"example": "First Party"
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify submitter within your app."
},
"application_key": {
"type": "string",
"description": "Your application-specific unique string key to identify submitter within your app. Backward compatibility with the previous version of the API. Use external_id instead."
},
"decline_reason": {
"type": "string",
"description": "Submitter provided decline message."
},
"sent_at": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"description": "The submitter status.",
"enum": [
"completed",
"declined",
"opened",
"sent",
"awaiting"
]
},
"opened_at": {
"type": "string",
"format": "date-time"
},
"completed_at": {
"type": "string",
"format": "date-time"
},
"declined_at": {
"type": "string",
"format": "date-time"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"submission": {
"type": "object",
"description": "The submission details.",
"properties": {
"id": {
"type": "number",
"description": "The submission's unique identifier."
},
"audit_log_url": {
"type": "string",
"description": "The audit log PDF URL. Available only if the submission was completed by all submitters."
},
"combined_document_url": {
"type": "string",
"description": "The URL of the combined documents with audit log. Combined documents can be enabled via <a href=\"https://docuseal.com/settings/account\" target=\"_blank\" class=\"link\">/settings/accounts</a>."
},
"status": {
"type": "string",
"description": "The submission status.",
"enum": [
"completed",
"declined",
"expired",
"pending"
]
},
"url": {
"type": "string",
"description": "The submission URL."
},
"created_at": {
"type": "string",
"description": "The submission creation date.",
"format": "date-time"
}
}
},
"template": {
"type": "object",
"description": "Base template details.",
"properties": {
"id": {
"type": "number",
"description": "The template's unique identifier."
},
"name": {
"type": "string",
"description": "The template's name."
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify template within your app."
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"folder_name": {
"type": "string",
"description": "Template folder name."
}
}
},
"preferences": {
"type": "object",
"properties": {
"send_email": {
"type": "boolean",
"description": "The flag indicating whether the submitter has opted to receive an email."
},
"send_sms": {
"type": "boolean",
"description": "The flag indicating whether the submitter has opted to receive an SMS."
}
}
},
"values": {
"type": "array",
"description": "List of the filled values passed by the submitter.",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string",
"description": "The field name."
},
"values": {
"type": "string",
"description": "The field value."
}
}
}
},
"metadata": {
"type": "object",
"description": "Metadata object with additional submitter information."
},
"audit_log_url": {
"type": "string",
"description": "The audit log PDF URL. Available only if the submission was completed by all submitters."
},
"submission_url": {
"type": "string",
"description": "The submission URL."
},
"documents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The document file name."
},
"url": {
"type": "string",
"description": "The document file URL."
}
}
}
}
}
}
}
```
+317
View File
@@ -0,0 +1,317 @@
# Submission Webhook
Get submission creation, completion, expiration, and archiving notifications using these events:
- **'submission.created'** event is triggered when the submission is created.
- **'submission.completed'** event is triggered when the submission is completed by all signing parties.
- **'submission.expired'** event is triggered when the submission expires.
- **'submission.archived'** event is triggered when the submission is archived.
```json
{
"event_type": {
"type": "string",
"description": "The event type.",
"enum": [
"submission.created",
"submission.archived"
]
},
"timestamp": {
"type": "string",
"description": "The event timestamp.",
"example": "2023-09-24T11:20:42Z",
"format": "date-time"
},
"data": {
"type": "object",
"description": "Submitted data object.",
"properties": {
"id": {
"type": "number",
"description": "The submission's unique identifier."
},
"archived_at": {
"type": "string",
"description": "The submission archive date."
},
"created_at": {
"type": "string",
"description": "The submission creation date."
},
"updated_at": {
"type": "string",
"description": "The submission update date."
},
"source": {
"type": "string",
"description": "The submission source.",
"enum": [
"invite",
"bulk",
"api",
"embed",
"link"
]
},
"submitters_order": {
"type": "string",
"description": "The submitters order.",
"enum": [
"random",
"preserved"
]
},
"audit_log_url": {
"type": "string",
"description": "Audit log file URL."
},
"submitters": {
"type": "array",
"description": "The list of submitters for the submission.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The submitter's unique identifier."
},
"submission_id": {
"type": "number",
"description": "The unique submission identifier."
},
"uuid": {
"type": "string",
"description": "The submitter UUID."
},
"email": {
"type": "string",
"description": "The email address of the submitter.",
"format": "email",
"example": "john.doe@example.com"
},
"slug": {
"type": "string",
"description": "The unique slug of the document template."
},
"sent_at": {
"type": "string",
"description": "The date and time when the signing request was sent to the submitter."
},
"opened_at": {
"type": "string",
"description": "The date and time when the submitter opened the signing form."
},
"completed_at": {
"type": "string",
"description": "The date and time when the submitter completed the signing form."
},
"declined_at": {
"type": "string",
"description": "The date and time when the submitter declined the signing form."
},
"created_at": {
"type": "string",
"description": "The date and time when the submitter was created."
},
"updated_at": {
"type": "string",
"description": "The date and time when the submitter was last updated."
},
"name": {
"type": "string",
"description": "The name of the submitter."
},
"phone": {
"type": "string",
"description": "The phone number of the submitter, formatted according to the E.164 standard.",
"example": "+1234567890"
},
"role": {
"type": "string",
"description": "The role name or title of the submitter.",
"example": "First Party"
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify this submitter within your app."
},
"metadata": {
"type": "object",
"description": "Metadata object with additional submitter information.",
"example": "{ 'customField': 'value' }"
},
"status": {
"type": "string",
"description": "The submitter status.",
"enum": [
"completed",
"declined",
"opened",
"sent",
"awaiting"
]
},
"application_key": {
"type": "string",
"description": "Your application-specific unique string key to identify this submitter within your app."
},
"values": {
"type": "object",
"description": "An object with pre-filled values for the submission. Use field names for keys of the object. For more configurations see `fields` param."
},
"documents": {
"type": "array",
"description": "The list of documents for the submission.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The document file name."
},
"url": {
"type": "string",
"description": "The document file URL."
}
}
}
},
"preferences": {
"type": "object",
"description": "The submitter preferences."
}
}
}
},
"template": {
"type": "object",
"description": "Base template details.",
"properties": {
"id": {
"type": "number",
"description": "The template's unique identifier."
},
"name": {
"type": "string",
"description": "The template's name."
},
"external_id": {
"type": "string",
"description": "Your application-specific unique string key to identify template within your app."
},
"folder_name": {
"type": "string",
"description": "The folder name."
},
"created_at": {
"type": "string",
"description": "The date and time when the template was created."
},
"updated_at": {
"type": "string",
"description": "The date and time when the template was last updated."
}
}
},
"created_by_user": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier of the user who created the submission."
},
"first_name": {
"type": "string",
"description": "The first name of the user who created the submission."
},
"last_name": {
"type": "string",
"description": "The last name of the user who created the submission."
},
"email": {
"type": "string",
"description": "The email address of the user who created the submission."
}
}
},
"submission_events": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Submission event unique ID number."
},
"submitter_id": {
"type": "integer",
"description": "Unique identifier of the submitter that triggered the event."
},
"event_type": {
"type": "string",
"description": "Event type.",
"enum": [
"send_email",
"bounce_email",
"complaint_email",
"send_reminder_email",
"send_sms",
"send_2fa_sms",
"open_email",
"click_email",
"click_sms",
"phone_verified",
"start_form",
"start_verification",
"complete_verification",
"view_form",
"invite_party",
"complete_form",
"decline_form",
"api_complete_form"
]
},
"event_timestamp": {
"type": "string",
"description": "Date and time when the event was triggered."
}
}
}
},
"documents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Document name."
},
"url": {
"type": "string",
"description": "Document URL."
}
}
}
},
"status": {
"type": "string",
"description": "The status of the submission.",
"enum": [
"completed",
"declined",
"expired",
"pending"
]
},
"completed_at": {
"type": "string",
"description": "The date and time when the submission was fully completed."
}
}
}
}
```
+235
View File
@@ -0,0 +1,235 @@
# Template Webhook
Get template creation and update notifications using these events:
- **'template.created'** is triggered when the template is created.
- **'tempate.updated'** is triggered when the template is updated.
```json
{
"event_type": {
"type": "string",
"description": "The event type.",
"enum": [
"template.created",
"template.updated"
]
},
"timestamp": {
"type": "string",
"description": "The event timestamp.",
"example": "2023-09-24T11:20:42Z",
"format": "date-time"
},
"data": {
"type": "object",
"description": "Submitted data object.",
"properties": {
"id": {
"type": "number",
"description": "The template's unique identifier."
},
"slug": {
"type": "string",
"description": "The template's unique slug."
},
"name": {
"type": "string",
"description": "The template's name."
},
"schema": {
"type": "array",
"description": "The template document files.",
"items": {
"type": "object",
"properties": {
"attachment_uuid": {
"type": "string",
"description": "The attachment UUID."
},
"name": {
"type": "string",
"description": "The attachment name."
}
}
}
},
"fields": {
"type": "array",
"description": "The template fields.",
"items": {
"type": "object",
"properties": {
"uuid": {
"type": "string",
"description": "The field UUID."
},
"submitter_uuid": {
"type": "string",
"description": "The submitter role UUID."
},
"name": {
"type": "string",
"description": "The field name."
},
"required": {
"type": "boolean",
"description": "The flag indicating whether the field is required."
},
"preferences": {
"type": "object",
"description": "The field preferences."
},
"areas": {
"type": "array",
"description": "List of areas where the field is located in the document.",
"items": {
"type": "object",
"properties": {
"x": {
"type": "number",
"description": "X coordinate of the area where the field is located in the document."
},
"y": {
"type": "number",
"description": "Y coordinate of the area where the field is located in the document."
},
"w": {
"type": "number",
"description": "Width of the area where the field is located in the document."
},
"h": {
"type": "number",
"description": "Height of the area where the field is located in the document."
},
"attachment_uuid": {
"type": "string",
"description": "Unique identifier of the attached document where the field is located."
},
"page": {
"type": "integer",
"description": "Page number of the attached document where the field is located."
}
}
}
}
}
}
},
"submitters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Submitter name."
},
"uuid": {
"type": "string",
"description": "Unique identifier of the submitter."
}
}
}
},
"author_id": {
"type": "integer",
"description": "Unique identifier of the author of the template."
},
"account_id": {
"type": "integer",
"description": "Unique identifier of the account of the template."
},
"archived_at": {
"type": "string",
"description": "Date and time when the template was archived."
},
"created_at": {
"type": "string",
"description": "Date and time when the template was created."
},
"updated_at": {
"type": "string",
"description": "Date and time when the template was updated."
},
"source": {
"type": "string",
"description": "Source of the template.",
"enum": [
"native",
"api",
"embed"
]
},
"external_id": {
"type": "string",
"description": "Identifier of the template in the external system."
},
"folder_id": {
"type": "integer",
"description": "Unique identifier of the folder where the template is placed."
},
"folder_name": {
"type": "string",
"description": "Folder name where the template is placed."
},
"application_key": {
"type": "string",
"description": "Your application-specific unique string key to identify tempate_id within your app."
},
"author": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier of the author."
},
"first_name": {
"type": "string",
"description": "First name of the author."
},
"last_name": {
"type": "string",
"description": "Last name of the author."
},
"email": {
"type": "string",
"description": "Author email."
}
}
},
"documents": {
"type": "array",
"description": "List of documents attached to the template.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier of the document."
},
"uuid": {
"type": "string",
"description": "Unique identifier of the document."
},
"url": {
"type": "string",
"description": "URL of the document."
},
"preview_image_url": {
"type": "string",
"description": "Document preview image URL."
},
"filename": {
"type": "string",
"description": "Document filename."
}
}
}
}
}
}
}
```
+12 -7
View File
@@ -5,26 +5,31 @@ module Abilities
module_function
def collection(user, ability: nil)
template_ids = Template.where(account_id: user.account_id).select(:id)
templates = Template.where(account_id: user.account_id)
return templates unless user.account.testing?
shared_ids =
TemplateSharing.where({ ability:,
account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
TemplateSharing.where({ ability:, account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
.select(:template_id)
join_query = Template.arel_table
.join(Arel::Nodes::TableAlias.new(template_ids.arel.union(shared_ids.arel), 'union_ids'))
.on(Template.arel_table[:id].eq(Arel::Table.new(:union_ids)[:id]))
join_query =
Template.arel_table
.join(Arel::Nodes::TableAlias.new(templates.select(:id).arel.union(shared_ids.arel), 'union_ids'))
.on(Template.arel_table[:id].eq(Arel::Table.new(:union_ids)[:id]))
Template.joins(join_query.join_sources.first)
end
def entity(template, user:, ability: nil)
return true if template.account_id.blank?
return true if template.account_id == user.account_id
return false unless user.account.linked_account_account
return false if template.template_sharings.to_a.blank?
account_ids = [user.account_id, TemplateSharing::ALL_ID]
template.template_sharings.any? do |e|
template.template_sharings.to_a.any? do |e|
e.account_id.in?(account_ids) && (ability.nil? || e.ability == 'manage' || e.ability == ability)
end
end
+2 -4
View File
@@ -4,13 +4,11 @@ class Ability
include CanCan::Ability
def initialize(user)
can :manage, Template, account_id: user.account_id
can %i[read update create], Template,
Abilities::TemplateConditions.collection(user) do |template|
can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template|
Abilities::TemplateConditions.entity(template, user:, ability: 'manage')
end
can :destroy, Template, account_id: user.account_id
can :manage, TemplateFolder, account_id: user.account_id
can :manage, TemplateSharing, template: { account_id: user.account_id }
can :manage, Submission, account_id: user.account_id
+4
View File
@@ -9,7 +9,11 @@ module AccountConfigs
'twelve_hours' => '12 hours',
'twenty_four_hours' => '24 hours',
'two_days' => '2 days',
'three_days' => '3 days',
'four_days' => '4 days',
'five_days' => '5 days',
'six_days' => '6 days',
'seven_days' => '7 days',
'eight_days' => '8 days',
'fifteen_days' => '15 days'
}.freeze
+4
View File
@@ -55,6 +55,10 @@ module Docuseal
ENV['MULTITENANT'] == 'true'
end
def advanced_formats?
multitenant?
end
def demo?
ENV['DEMO'] == 'true'
end
+2 -2
View File
@@ -72,8 +72,8 @@ module Params
return if params[key].blank?
return if params[key].to_s.include?('<')
if params[key].to_s.strip.split(/\s*[;,]\s*/).compact_blank
.all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) }
if params[key].to_s.strip.split(%r{\s*[;,/]\s*}).compact_blank
.all? { |email| EmailTypo::DotCom.call(email).match?(EMAIL_REGEXP) || email.include?('--') }
return
end
+16
View File
@@ -13,8 +13,10 @@ module ReplaceEmailVariables
SUBMITTER_FIRST_NAME = /\{+submitter\.first_name\}+/i
SUBMITTER_ID = /\{+submitter\.id\}+/i
SUBMITTER_SLUG = /\{+submitter\.slug\}+/i
SUBMITTER_FIELD_VALUE = /\{+submitter\.(?<field_name>[^}]+)\}+/i
SUBMISSION_LINK = /\{+submission\.link\}+/i
SUBMISSION_ID = /\{+submission\.id\}+/i
SUBMISSION_EXPIRE_AT = /\{+submission\.expire_at\}+/i
SUBMITTERS = /\{+(?:submission\.)?submitters\}+/i
SUBMITTERS_N_EMAIL = /\{+submitters\[(?<index>\d+)\]\.email\}+/i
SUBMITTERS_N_NAME = /\{+submitters\[(?<index>\d+)\]\.name\}+/i
@@ -48,6 +50,13 @@ module ReplaceEmailVariables
text = replace(text, SENDER_NAME, html_escape:) { submitter.submission.created_by_user&.full_name }
text = replace(text, SENDER_FIRST_NAME, html_escape:) { submitter.submission.created_by_user&.first_name }
text = replace(text, SUBMISSION_EXPIRE_AT, html_escape:) do
if submitter.submission.expire_at
I18n.l(submitter.submission.expire_at.in_time_zone(submitter.submission.account.timezone),
format: :short, locale: submitter.submission.account.locale)
end
end
text = replace(text, SUBMITTERS_N_NAME, html_escape:) do |match|
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :name)
end
@@ -64,6 +73,13 @@ module ReplaceEmailVariables
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :values, match[:field_name].to_s.strip)
end
text = replace(text, SUBMITTER_FIELD_VALUE, html_escape:) do |match|
submitters = submitter.submission.template_submitters || submitter.submission.template.submitters
index = submitters.find_index { |e| e['uuid'] == submitter.uuid }
build_submitters_n_field(submitter.submission, index, :values, match[:field_name].to_s.strip)
end
replace(text, SENDER_EMAIL, html_escape:) { submitter.submission.created_by_user&.email.to_s.sub(/\+\w+@/, '@') }
end
# rubocop:enable Metrics
+17 -5
View File
@@ -59,10 +59,13 @@ module Submissions
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
preferences = Submitters.normalize_preferences(user.account, user, params)
expire_at = params[:expire_at].presence || Templates.build_default_expire_at(template)
parse_emails(emails, user).uniq.map do |email|
submission = template.submissions.new(created_by_user: user,
account_id: user.account_id,
source:,
expire_at:,
template_submitters: template.submitters)
submission.submitters.new(email: normalize_email(email),
@@ -71,7 +74,13 @@ module Submissions
preferences:,
sent_at: mark_as_sent ? Time.current : nil)
submission.tap(&:save!)
submission.save!
if submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(submission.expire_at, 'submission_id' => submission.id)
end
submission
end
end
@@ -109,18 +118,21 @@ module Submissions
return if email.blank?
return if email.is_a?(Numeric)
return email.downcase if email.to_s.include?(',') ||
email.to_s.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
email.to_s.exclude?('.')
email = email.to_s.tr('/', ',')
return email.downcase if email.include?(',') ||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
email.exclude?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<'))
return fixed_email if fixed_email == email
domain = email.to_s.split('@').last.to_s.downcase
domain = email.split('@').last.to_s.downcase
fixed_domain = fixed_email.to_s.split('@').last
return email.downcase if domain == fixed_domain
return email.downcase if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
if DidYouMean::Levenshtein.distance(domain, fixed_domain) > 3
Rails.logger.info("Skipped email fix #{domain}")
+2 -1
View File
@@ -16,11 +16,12 @@ module Submissions
set_submission_preferences = submission_preferences.slice('send_email', 'bcc_completed')
set_submission_preferences['send_email'] = true if params['send_completed_email']
expire_at = attrs[:expire_at] || Templates.build_default_expire_at(template)
submission = template.submissions.new(created_by_user: user, source:,
account_id: user.account_id,
preferences: set_submission_preferences,
expire_at: attrs[:expire_at],
expire_at:,
template_submitters: [], submitters_order:)
template_submitters = template.submitters.deep_dup
+3 -1
View File
@@ -379,7 +379,9 @@ module Submissions
composer.text(I18n.t('event_log'), font_size: 12, padding: [10, 0, 20, 0])
events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event|
events_data = submission.submission_events.sort_by(&:event_timestamp).filter_map do |event|
next if event.event_type.in?(%w[bounce_email complaint_email])
submitter = submission.submitters.find { |e| e.id == event.submitter_id }
submitter_name =
if event.event_type.include?('sms') || event.event_type.include?('phone')
+83 -39
View File
@@ -209,7 +209,7 @@ module Submissions
font_size = preferences_font_size
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
fill_color = field.dig('preferences', 'color').presence
fill_color = field.dig('preferences', 'color').to_s.delete_prefix('#').presence
font_name = field.dig('preferences', 'font')
font_variant = (field.dig('preferences', 'font_type').presence || 'none').to_sym
@@ -254,22 +254,6 @@ module Submissions
image = Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
id_string = "ID: #{attachment.uuid}".upcase
while true
text = HexaPDF::Layout::TextFragment.create(id_string,
font:,
font_size: (font_size / 1.8).to_i)
result = layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
break if result.status == :success
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
break if id_string.length < 8
end
reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
reason_string =
@@ -284,35 +268,95 @@ module Submissions
font:,
font_size: (font_size / 1.8).to_i)
reason_result = layouter.fit([reason_text], area['w'] * width, height)
if area['h']&.positive? && (area['w'].to_f / area['h']) > 6
area_x = area['x'] * width
area_y = area['y'] * height
area_w = area['w'] * width
area_h = area['h'] * height
text_height = result.lines.sum(&:height) + reason_result.lines.sum(&:height)
half_width = area_w / 2.0
scale = [half_width / image.width, area_h / image.height].min
image_width = image.width * scale
image_height = image.height * scale
image_x = area_x + ((half_width - image_width) / 2.0)
image_y = height - area_y - image_height
image_height = (area['h'] * height) - text_height
image_height = (area['h'] * height) / 2 if image_height < (area['h'] * height) / 2
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
scale = [(area['w'] * width) / image.width, image_height / image.height].min
canvas.image(io, at: [image_x, image_y], width: image_width, height: image_height)
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
id_string = "ID: #{attachment.uuid}".upcase
layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
height - (area['y'] * height) - TEXT_TOP_MARGIN - image_height)
while true
text = HexaPDF::Layout::TextFragment.create(id_string,
font:,
font_size: (font_size / 1.8).to_i)
layouter.fit([reason_text], area['w'] * width, reason_result.lines.sum(&:height))
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
height - (area['y'] * height) - TEXT_TOP_MARGIN -
result.lines.sum(&:height) - image_height)
result = layouter.fit([text], half_width, (font_size / 1.8) / 0.65)
canvas.image(
io,
at: [
(area['x'] * width) + (area['w'] * width / 2) - ((image.width * scale) / 2),
height - (area['y'] * height) - (image.height * scale / 2) - (image_height / 2)
],
width: image.width * scale,
height: image.height * scale
)
break if result.status == :success
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
break if id_string.length < 8
end
text_x = area_x + half_width
text_y = height - area_y
reason_result = layouter.fit([reason_text], half_width, height)
layouter.fit([text], half_width, (font_size / 1.8) / 0.65)
.draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y)
layouter.fit([reason_text], half_width, reason_result.lines.sum(&:height))
.draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y - TEXT_TOP_MARGIN - result.lines.sum(&:height))
else
id_string = "ID: #{attachment.uuid}".upcase
loop do
text = HexaPDF::Layout::TextFragment.create(id_string,
font:,
font_size: (font_size / 1.8).to_i)
result = layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
break if result.status == :success
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
break if id_string.length < 8
end
reason_result = layouter.fit([reason_text], area['w'] * width, height)
text_height = result.lines.sum(&:height) + reason_result.lines.sum(&:height)
image_height = (area['h'] * height) - text_height
image_height = (area['h'] * height) / 2 if image_height < (area['h'] * height) / 2
scale = [(area['w'] * width) / image.width, image_height / image.height].min
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
height - (area['y'] * height) - TEXT_TOP_MARGIN - image_height)
layouter.fit([reason_text], area['w'] * width, reason_result.lines.sum(&:height))
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
height - (area['y'] * height) - TEXT_TOP_MARGIN -
result.lines.sum(&:height) - image_height)
canvas.image(
io,
at: [
(area['x'] * width) + (area['w'] * width / 2) - ((image.width * scale) / 2),
height - (area['y'] * height) - (image.height * scale / 2) - (image_height / 2)
],
width: image.width * scale,
height: image.height * scale
)
end
when 'image', 'signature', 'initials', 'stamp'
attachment = submitter.attachments.find { |a| a.uuid == value }
+9 -2
View File
@@ -2,7 +2,7 @@
module Submissions
class TimestampHandler
HASH_ALGORITHM = 'SHA512'
HASH_ALGORITHM = 'SHA256'
TimestampError = Class.new(StandardError)
@@ -20,6 +20,7 @@ module Submissions
signature[:SubFilter] = :'ETSI.RFC3161'
end
# rubocop:disable Metrics
def sign(io, byte_range)
digest = OpenSSL::Digest.new(HASH_ALGORITHM)
@@ -34,7 +35,7 @@ module Submissions
c.basic_auth(uri.user, uri.password) if uri.password.present?
end
response = conn.post(uri.path, build_payload(digest.digest),
response = conn.post(uri.request_uri, build_payload(digest.digest),
'content-type' => 'application/timestamp-query')
if response.status != 200 || response.body.blank?
@@ -49,7 +50,13 @@ module Submissions
end
OpenSSL::Timestamp::Response.new(response.body).token.to_der
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
Rails.logger.error(e)
OpenSSL::ASN1::GeneralizedTime.new(Time.now.utc).to_der
end
# rubocop:enable Metrics
def build_payload(digest)
req = OpenSSL::Timestamp::Request.new
+3 -3
View File
@@ -52,10 +52,10 @@ module Submitters
attachments.push(*new_attachments)
value = new_value
acc[field['uuid']] = normalize_value(field, new_value)
else
acc[field['uuid']] = normalize_value(field, value)
end
acc[field['uuid']] = normalize_value(field, value)
end
end
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more