Compare commits

..
66 Commits
Author SHA1 Message Date
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
Alex Turchyn 786c80bee7 Merge pull from docusealco/wip 2025-04-28 12:46:30 +03:00
Pete Matsyburka 98dcce299e resend 2025-04-28 08:59:41 +03:00
Pete Matsyburka cbff242a5d adjust condition fields 2025-04-25 12:14:49 +03:00
Pete Matsyburka 1876b3efba update readme 2025-04-24 19:44:49 +03:00
Alex Turchyn 51091f05f0 add Japanese language to the signing form 2025-04-24 19:38:46 +03:00
Pete Matsyburka d76eee2ae4 add valign 2025-04-24 19:33:26 +03:00
Pete Matsyburka d7bfa96d57 hide order on mobile 2025-04-24 12:59:31 +03:00
Pete Matsyburka be60608211 fix remove listener 2025-04-23 21:24:18 +03:00
Pete Matsyburka 4e5abb3c09 fix query 2025-04-23 12:30:06 +03:00
Pete Matsyburka 7fee82ded2 fix order queries 2025-04-23 10:58:40 +03:00
Alex Turchyn e1cd62ff40 add folders/templates sorting 2025-04-22 21:16:17 +03:00
Pete Matsyburka b498291b62 update gem 2025-04-22 16:20:35 +03:00
Pete Matsyburka def3009846 fix cert disabled 2025-04-22 15:52:03 +03:00
Pete Matsyburka 9d43fd9aa5 fix fields 2025-04-18 18:18:57 +03:00
Pete Matsyburka 22a21fb037 fix field focus 2025-04-18 13:02:17 +03:00
Pete Matsyburka 2a9c375273 adjust header 2025-04-18 12:57:43 +03:00
Alex Turchyn 847e70ec1b improve signing form header 2025-04-18 12:16:11 +03:00
Pete Matsyburka 7964d6e0f0 improve signature validation 2025-04-18 11:34:10 +03:00
Pete Matsyburka d0259791bf add field value email variable 2025-04-16 21:19:13 +03:00
Pete Matsyburka a9dcaeccbc fix erblint 2025-04-16 11:35:24 +03:00
Pete Matsyburka 501b91a603 removed email 2025-04-16 11:35:20 +03:00
Pete Matsyburka 2b5f772feb update ai link 2025-04-15 21:40:22 +03:00
Pete Matsyburka 7cada0af8b older devises 2025-04-15 19:58:31 +03:00
Pete Matsyburka 67be785e69 add slug filter 2025-04-15 13:22:02 +03:00
Pete Matsyburka d1cfc64adc adjust api response 2025-04-14 12:14:24 +03:00
Pete Matsyburka 4e5157d1ee add text valign 2025-04-11 16:25:32 +03:00
Pete Matsyburka 5e27cc36c6 fix default font 2025-04-10 13:22:10 +03:00
Pete Matsyburka 6e81a3be3f fix i18n 2025-04-10 11:51:40 +03:00
Pete Matsyburka 858b2f33fd ensure unique recipients 2025-04-10 00:56:16 +03:00
Alex Turchyn 6adcc87b2b add translations 2025-04-09 20:45:17 +03:00
Alex Turchyn e1d5a5b6a6 prevent an infinite loop of field conditions 2025-04-09 14:04:42 +03:00
Pete Matsyburka f090e79002 fix required 2025-04-09 13:51:43 +03:00
Pete Matsyburka 56b32e35a2 make last name optional 2025-04-08 19:12:04 +03:00
Pete Matsyburka b40c27cdbf fix specs 2025-04-08 16:21:45 +03:00
Pete Matsyburka c4505e67f0 require 2fa placeholder 2025-04-08 15:24:17 +03:00
116 changed files with 2022 additions and 423 deletions

No files matched your search

+7 -7
View File
@@ -338,18 +338,18 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.5)
nokogiri (1.18.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.5-aarch64-linux-gnu)
nokogiri (1.18.8-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.5-aarch64-linux-musl)
nokogiri (1.18.8-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.5-arm64-darwin)
nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.5-x86_64-linux-gnu)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.5-x86_64-linux-musl)
nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0)
@@ -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)
+1 -1
View File
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- PDF signature verification
- Users management
- Mobile-optimized
- 6 UI languages with signing available in 13 languages
- 6 UI languages with signing available in 14 languages
- API and Webhooks for integrations
- Easy to deploy in minutes
+1 -1
View File
@@ -45,7 +45,7 @@ class AccountsController < ApplicationController
def destroy
authorize!(:manage, current_account)
true_user.update!(locked_at: Time.current)
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
@@ -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') ||
+16 -11
View File
@@ -11,17 +11,7 @@ module Api
def index
submissions = Submissions.search(@submissions, params[:q])
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
if params.key?(:archived)
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
end
submissions = Submissions::Filter.call(submissions, current_user, params)
submissions = filter_submissions(submissions, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder,
@@ -115,6 +105,21 @@ module Api
private
def filter_submissions(submissions, params)
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
if params.key?(:archived)
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
end
Submissions::Filter.call(submissions, current_user, params)
end
def build_create_json(submissions)
json = submissions.flat_map do |submission|
submission.submitters.map do |s|
@@ -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
+14 -9
View File
@@ -7,15 +7,7 @@ module Api
def index
submitters = Submitters.search(@submitters, params[:q])
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
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] })
end
submitters = maybe_filder_by_completed_at(submitters, params)
submitters = filter_submitters(submitters, params)
submitters = paginate(
submitters.preload(:template, :submission, :submission_events,
@@ -163,6 +155,19 @@ module Api
submitter
end
def filter_submitters(submitters, params)
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
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] })
end
maybe_filder_by_completed_at(submitters, params)
end
def assign_external_id(submitter, attrs)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
@@ -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],
@@ -90,6 +90,7 @@ module Api
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.where(slug: params[:slug]) if params[:slug].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
templates
@@ -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"] })
+5 -1
View File
@@ -11,8 +11,11 @@ class StartFormController < ApplicationController
before_action :load_template
def show
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
@@ -94,6 +97,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)
@@ -57,6 +57,11 @@ class SubmissionsController < ApplicationController
Submissions.send_signature_requests(submissions)
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error,
partial: 'submissions/error',
locals: { error: e.message }),
status: :unprocessable_entity
end
def destroy
@@ -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]
+13 -13
View File
@@ -7,11 +7,11 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
before_action :load_submitter, only: %i[show update completed]
CONFIG_KEYS = [].freeze
def show
@submitter = Submitter.find_by!(slug: params[:slug])
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
@@ -50,44 +50,44 @@ class SubmitFormController < ApplicationController
end
def update
submitter = Submitter.find_by!(slug: params[:slug])
if submitter.completed_at?
if @submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end
if submitter.template.archived_at? || submitter.submission.archived_at?
if @submitter.template.archived_at? || @submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end
if submitter.submission.expired?
if @submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
end
if submitter.declined_at?
if @submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity
end
Submitters::SubmitValues.call(submitter, params, request)
Submitters::SubmitValues.call(@submitter, params, request)
head :ok
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar)
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity
end
def completed
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
end
def completed; end
def success; end
private
def load_submitter
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
end
def build_attachments_index(submission)
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@@ -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|
@@ -5,8 +5,9 @@ class TemplateFoldersController < ApplicationController
def show
@templates = @template_folder.templates.active.accessible_by(current_ability)
.preload(:author, :template_accesses).order(id: :desc)
.preload(:author, :template_accesses)
@templates = Templates.search(@templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
@pagy, @templates = pagy(@templates, limit: 12)
end
@@ -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
@@ -9,9 +9,11 @@ class TemplatesDashboardController < ApplicationController
FOLDERS_PER_PAGE = 18
def index
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc)
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = sort_template_folders(@template_folders, current_user,
cookies.permanent[:dashboard_templates_order])
@pagy, @template_folders = pagy(
@template_folders,
@@ -24,6 +26,7 @@ class TemplatesDashboardController < ApplicationController
else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates)
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
limit =
if @template_folders.size < 4
@@ -39,19 +42,55 @@ class TemplatesDashboardController < ApplicationController
private
def filter_templates(templates)
rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
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
Templates.search(rel, params[:q])
end
def sort_template_folders(template_folders, current_user, order)
case order
when 'used_at'
subquery =
Template.left_joins(:submissions)
.group(:folder_id)
.where(account_id: current_user.account_id)
.select(
:folder_id,
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
)
template_folders = template_folders.joins(
Template.arel_table
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
.join_sources
)
template_folders.order(
Arel::Nodes::Case.new
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
.then(Template.arel_table[:submission_created_at_max])
.else(Template.arel_table[:updated_at_max])
.desc
)
when 'name'
template_folders.order(name: :asc)
else
template_folders.order(id: :desc)
end
end
end
@@ -24,14 +24,22 @@ class TemplatesPreferencesController < ApplicationController
documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents
completed_redirect_url
submitters_order
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'
+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,206 @@
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'
}
}
onDragleave () {
this.style.backgroundColor = null
}
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')
})
}
})
+36
View File
@@ -0,0 +1,36 @@
export default class extends HTMLElement {
connectedCallback () {
this.form.addEventListener('submit', (e) => {
e.preventDefault()
this.submit()
})
if (this.dataset.onload === 'true') {
this.form.querySelector('button').click()
}
}
submit () {
fetch(this.form.action, {
method: this.form.method,
body: new FormData(this.form)
}).then(async (resp) => {
if (!resp.ok) {
try {
const data = JSON.parse(await resp.text())
if (data.error) {
alert(data.error)
}
} catch (err) {
console.error(err)
}
}
})
}
get form () {
return this.querySelector('form')
}
}
+62
View File
@@ -0,0 +1,62 @@
export default class extends HTMLElement {
connectedCallback () {
this.header = document.querySelector('#signing_form_header')
window.addEventListener('scroll', this.onScroll)
window.addEventListener('resize', this.onResize)
if (!this.isNarrow() && this.isHeaderNotVisible()) {
this.showButtons({ animate: false })
}
}
disconnectedCallback () {
window.removeEventListener('scroll', this.onScroll)
window.removeEventListener('resize', this.onResize)
}
onResize = () => {
if (this.isNarrow()) {
this.hideButtons(true)
} else if (this.isHeaderNotVisible()) {
this.showButtons()
}
}
isNarrow () {
return window.innerWidth < 1230
}
onScroll = () => {
if (this.isHeaderNotVisible() && !this.isNarrow()) {
this.showButtons()
} else {
this.hideButtons()
}
}
isHeaderNotVisible () {
const rect = this.header.getBoundingClientRect()
return rect.bottom <= 0 || rect.top >= window.innerHeight
}
showButtons ({ animate } = { animate: true }) {
if (animate) {
this.classList.add('transition-transform', 'duration-300')
}
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
this.classList.add('translate-y-0', 'opacity-100')
}
hideButtons () {
this.classList.remove('translate-y-0', 'opacity-100')
this.classList.add('-translate-y-10', 'opacity-0')
setTimeout(() => {
if (this.classList.contains('-translate-y-10')) {
this.classList.add('hidden')
}
}, 300)
}
}
+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)
})
}
})
+4
View File
@@ -3,11 +3,15 @@ import { createApp, reactive } from 'vue'
import Form from './submission_form/form'
import DownloadButton from './elements/download_button'
import ToggleSubmit from './elements/toggle_submit'
import FetchForm from './elements/fetch_form'
import ScrollButtons from './elements/scroll_buttons'
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
safeRegisterElement('download-button', DownloadButton)
safeRegisterElement('toggle-submit', ToggleSubmit)
safeRegisterElement('fetch-form', FetchForm)
safeRegisterElement('scroll-buttons', ScrollButtons)
safeRegisterElement('submission-form', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
+6 -3
View File
@@ -183,7 +183,7 @@
v-else
ref="textContainer"
dir="auto"
class="flex items-center px-0.5 w-full"
class="flex px-0.5 w-full"
:class="{ ...alignClasses, ...fontClasses }"
>
<span
@@ -327,13 +327,16 @@ export default {
},
alignClasses () {
if (!this.field.preferences) {
return {}
return { 'items-center': true }
}
return {
'text-center': this.field.preferences.align === 'center',
'text-left': this.field.preferences.align === 'left',
'text-right': this.field.preferences.align === 'right'
'text-right': this.field.preferences.align === 'right',
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
'items-start': this.field.preferences.valign === 'top',
'items-end': this.field.preferences.valign === 'bottom'
}
},
fontClasses () {
+3 -3
View File
@@ -193,9 +193,9 @@ export default {
})
}
if (window.decline_button) {
window.decline_button.setAttribute('disabled', 'true')
}
document.querySelectorAll('#decline_button').forEach((button) => {
button.setAttribute('disabled', 'true')
})
},
methods: {
sendCopyToEmail () {
@@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas) {
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
const ctx = canvas.getContext('2d')
const width = canvas.width
@@ -33,6 +33,10 @@ function cropCanvasAndExportToPNG (canvas) {
croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d')
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
return Promise.reject(new Error('Image too small'))
}
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => {
+1 -1
View File
@@ -11,7 +11,7 @@
:with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields"
:scroll-padding="scrollPadding"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
/>
<FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])"
+117 -6
View File
@@ -148,6 +148,7 @@ const es = {
sign_now: 'Firmar ahora',
type_here_: 'Escribe aquí...',
optional: 'opcional',
option: 'Opción',
appears_on: 'Aparece en',
page: 'Página',
select_your_option: 'Selecciona tu opción',
@@ -245,6 +246,7 @@ const it = {
sign_now: 'Firma ora',
type_here_: 'Digita qui...',
optional: 'opzionale',
option: 'Opzione',
appears_on: 'Compare su',
page: 'Pagina',
take_photo: 'Scattare una foto',
@@ -265,7 +267,7 @@ const it = {
document_has_been_signed: 'Il documento è stato firmato!',
documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por',
powered_by: 'Fornito da',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono',
@@ -343,6 +345,7 @@ const de = {
sign_now: 'Jetzt unterschreiben',
type_here_: 'Hier eingeben...',
optional: 'optional',
option: 'Option',
appears_on: 'Erscheint auf',
page: 'Seite',
take_photo: 'Foto aufnehmen',
@@ -393,8 +396,8 @@ const de = {
const fr = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité",
verify_id: "Vérification de l'ID",
identity_verification: "Vérification de l'identité",
complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer',
@@ -441,6 +444,7 @@ const fr = {
sign_now: 'Signer maintenant',
type_here_: 'Tapez ici...',
optional: 'facultatif',
option: 'Option',
appears_on: 'Apparaît sur',
page: 'Page',
take_photo: 'Prendre une photo',
@@ -539,6 +543,7 @@ const pl = {
sign_now: 'Podpisz teraz',
type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny',
option: 'Opcja',
appears_on: 'Pojawia się na',
page: 'Strona',
select_your_option: 'Wybierz swoją opcję',
@@ -583,7 +588,8 @@ const pl = {
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki',
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
}
const uk = {
@@ -636,6 +642,7 @@ const uk = {
sign_now: 'Підписати зараз',
type_here_: 'Введіть тут',
optional: 'необов’язково',
option: 'Опція',
appears_on: "З'являється на",
page: 'Сторінка',
take_photo: 'Зробити фото',
@@ -658,7 +665,7 @@ const uk = {
create_a_free_account: 'Створити безкоштовний обліковий запис',
powered_by: 'Працює на базі',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів',
open_source_documents_software: 'відкрите програмне забезпечення для документів',
verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
six_digits_code: '6-значний код',
@@ -734,6 +741,7 @@ const cs = {
sign_now: 'Podepsat nyní',
type_here_: 'Zadejte zde',
optional: 'volitelné',
option: 'Možnost',
appears_on: 'Zobrazuje se na',
page: 'Stránka',
select_your_option: 'Vyberte svou volbu',
@@ -832,6 +840,7 @@ const pt = {
sign_now: 'Assinar agora',
type_here_: 'Digite aqui',
optional: 'opcional',
option: 'Opção',
appears_on: 'Aparece em',
page: 'Página',
take_photo: 'Tirar foto',
@@ -1097,6 +1106,7 @@ const ar = {
select_a_reason: 'اختر سببًا',
value_is_invalid: 'القيمة غير صالحة',
verification_code_is_invalid: 'رمز التحقق غير صالح',
already_paid: 'تم الدفع بالفعل',
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
@@ -1198,6 +1208,8 @@ const ko = {
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
electronic_signature_disclosure: '전자 서명 공개',
esignature_disclosure: '전자 서명 공개',
value_is_invalid: '값이 올바르지 않습니다',
verification_code_is_invalid: '인증 코드가 올바르지 않습니다',
already_paid: '이미 지불됨',
text: '텍스트',
signature: '서명',
@@ -1273,6 +1285,105 @@ const ko = {
wait_countdown_seconds: '{countdown}초 기다리세요'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
const ja = {
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
verify_id: '本人確認',
identity_verification: '本人確認',
complete: '完了',
fill_all_required_fields_to_complete: '完了するにはすべての必須項目を入力してください',
sign_and_complete: '署名して完了',
text: 'テキスト',
by_clicking_you_agree_to_the: '"{button}" をクリックすることで、次に同意したことになります:',
electronic_signature_disclosure: '電子署名に関する開示',
esignature_disclosure: '電子署名開示',
signature: '署名',
initials: 'イニシャル',
drawn_signature_on_a_touchscreen_device: 'タッチスクリーンデバイスで描かれた署名',
approved: '承認済み',
reviewed: '確認済み',
other: 'その他',
authored_by_me: '自分が作成',
invite: '招待',
email: 'メール',
approved_by: '承認者',
reviewed_by: '確認者',
authored_by: '作成者',
select_a_reason: '理由を選択',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'QRコードをカメラアプリで読み取り、モバイルでフォームを開いて署名を描いてください',
date: '日付',
number: '数値',
value_is_invalid: '無効な値です',
verification_code_is_invalid: '認証コードが無効です',
already_paid: 'すでに支払い済み',
image: '画像',
pay: '支払う',
take_photo: '写真を撮る',
number_phone_is_invalid: '{number} の電話番号は無効です',
file: 'ファイル',
digitally_signed_by: '電子署名者:',
reason: '理由',
select: '選択',
checkbox: 'チェックボックス',
multiple: '複数選択',
radio: 'ラジオボタン',
cells: 'セル',
stamp: 'スタンプ',
minimize: '最小化',
payment: '支払い',
phone: '電話',
start_now: '今すぐ開始',
continue: '続行',
sign_now: '今すぐ署名',
type_here_: 'ここに入力...',
optional: '任意',
option: 'オプション',
appears_on: '表示ページ',
page: 'ページ',
select_your_option: 'オプションを選択',
complete_hightlighted_checkboxes_and_click: 'ハイライトされたチェックボックスを完了し、クリックしてください',
submit: '送信',
next: '次へ',
click_to_upload: 'クリックしてアップロード',
or_drag_and_drop_files: 'またはファイルをドラッグ&ドロップ',
send_copy_via_email: 'メールでコピーを送信',
download: 'ダウンロード',
clear: 'クリア',
redraw: '描き直す',
draw_initials: 'イニシャルを描く',
type_signature_here: 'ここに署名を入力',
type_initial_here: 'ここにイニシャルを入力',
form_has_been_completed: 'フォームが完了しました!',
document_has_been_signed: 'ドキュメントが署名されました!',
documents_have_been_signed: 'ドキュメントがすべて署名されました!',
create_a_free_account: '無料アカウントを作成',
powered_by: '提供元:',
please_check_the_box_to_continue: '続行するにはボックスにチェックを入れてください。',
open_source_documents_software: 'オープンソースのドキュメントソフトウェア',
verified_phone_number: '電話番号を認証',
use_international_format: '国際形式を使用してください:+1xxx',
six_digits_code: '6桁のコード',
change_phone_number: '電話番号を変更',
sending: '送信中...',
resend_code: 'コードを再送信',
verification_code_has_been_resent: '認証コードがSMSで再送信されました',
please_fill_all_required_fields: 'すべての必須項目を入力してください',
set_today: '今日の日付を設定',
toggle_multiline_text: '複数行テキスト切り替え',
draw_signature: '署名を描く',
type_initial: 'イニシャルを入力',
draw: '描く',
type: '入力',
type_text: 'テキストを入力',
email_has_been_sent: 'メールが送信されました',
processing: '処理中',
pay_with_strip: 'Stripeで支払う',
reupload: '再アップロード',
upload: 'アップロード',
files: 'ファイル',
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
wait_countdown_seconds: '{countdown} 秒お待ちください'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
export default i18n
@@ -339,8 +339,8 @@ export default {
return Promise.resolve({})
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'initials.png', { type: 'image/png' })
if (this.dryRun) {
@@ -373,6 +373,14 @@ export default {
return resolve(attachment)
})
}
}).catch((error) => {
if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return reject(error)
} else {
return resolve({})
}
})
})
}
@@ -683,13 +683,17 @@ export default {
}
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
if (this.field.required === true || this.pad.toData().length > 0) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return Promise.reject(new Error('Image too small or simple'))
return Promise.reject(new Error('Image too small or simple'))
} else {
Promise.resolve({})
}
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.dryRun) {
@@ -725,6 +729,14 @@ export default {
return resolve(attachment)
})
}
}).catch((error) => {
if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return reject(error)
} else {
return resolve({})
}
})
})
}
+7 -4
View File
@@ -160,15 +160,15 @@
</div>
<div
ref="touchValueTarget"
class="flex items-center h-full w-full field-area"
class="flex h-full w-full field-area"
dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center']"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
@click="focusValueInput"
>
<span
v-if="field"
class="flex justify-center items-center space-x-1"
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput }"
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
>
<div
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
@@ -403,10 +403,13 @@ export default {
},
fontClasses () {
if (!this.field.preferences) {
return {}
return { 'items-center': true }
}
return {
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
'items-start': this.field.preferences.valign === 'top',
'items-end': this.field.preferences.valign === 'bottom',
'justify-center': this.field.preferences.align === 'center',
'justify-start': this.field.preferences.align === 'left',
'justify-end': this.field.preferences.align === 'right',
+57 -1
View File
@@ -5,7 +5,20 @@
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"
hover-class="bg-base-200/50"
@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 +446,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 +476,7 @@ export default {
MobileFields,
Logo,
Dropzone,
HoverDropzone,
DocumentPreview,
DocumentControls,
IconInnerShadowTop,
@@ -665,6 +680,11 @@ export default {
required: false,
default: true
},
withReplaceAndCloneUpload: {
type: Boolean,
required: false,
default: false
},
withPhone: {
type: Boolean,
required: false,
@@ -724,7 +744,8 @@ export default {
copiedArea: null,
drawFieldType: null,
drawOption: null,
dragField: null
dragField: null,
isDragFile: false
}
},
computed: {
@@ -836,6 +857,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 +876,7 @@ export default {
window.removeEventListener('keydown', this.onKeyDown)
window.removeEventListener('resize', this.onWindowResize)
window.removeEventListener('dragleave', this.onWindowDragLeave)
},
beforeUpdate () {
this.documentRefs = []
@@ -868,6 +891,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 +1559,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 +1682,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)
@@ -175,7 +175,7 @@ export default {
fields () {
if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => {
if (f !== this.item) {
if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
acc.push(f)
}
+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/30': !hoverClass }, isDragEntering && hoverClass ? hoverClass : '']"
>
<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
},
icon: {
type: String,
required: false,
default: 'IconCloudUpload'
},
hoverClass: {
type: String,
required: false,
default: null
},
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 }"
+43 -7
View File
@@ -57,7 +57,7 @@
<span class="relative">
<select
class="select input-bordered bg-white select-sm text-center pl-2"
style="font-size: 16px; width: 86px; text-align-last: center;"
style="font-size: 16px; line-height: 12px; width: 86px; text-align-last: center;"
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
>
<option
@@ -77,12 +77,12 @@
</select>
<span
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
style="right: 13px; top: 7px"
style="right: 13px; top: 6px"
>
pt
</span>
</span>
<span>
<span class="flex">
<div
class="join"
style="height: 32px"
@@ -98,7 +98,7 @@
</button>
</div>
</span>
<span>
<span class="flex">
<div
class="join"
style="height: 32px"
@@ -114,6 +114,32 @@
</button>
</div>
</span>
<span class="flex">
<div class="dropdown modal-field-font-dropdown">
<label
tabindex="0"
class="cursor-pointer flex bg-white border input-bordered rounded-md h-8 items-center justify-center px-1"
style="-webkit-appearance: none; -moz-appearance: none;"
>
<component :is="valigns.find((v) => v.value === (preferences.valign || 'center'))?.icon" />
</label>
<div
tabindex="0"
class="dropdown-content p-0 mt-1 block z-10 menu shadow bg-white border border-base-300 rounded-md"
>
<div
v-for="(valign, index) in valigns"
:key="index"
:value="valign.value"
:class="{ 'bg-base-300': preferences.valign == valign.value }"
class="hover:bg-base-300 px-2 py-1.5 cursor-pointer"
@click="[valign.value ? preferences.valign = valign.value : delete preferences.valign, closeDropdown()]"
>
<component :is="valign.icon" />
</div>
</div>
</div>
</span>
<span>
<select
class="input input-bordered bg-white input-sm text-lg rounded-md"
@@ -134,7 +160,7 @@
</div>
<div class="mt-4">
<div
class="flex items-center border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
:style="{
color: preferences.color || 'black',
fontSize: (preferences.font_size || 12) + 'pt',
@@ -163,7 +189,7 @@
</template>
<script>
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter } from '@tabler/icons-vue'
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter, IconAlignBoxCenterTop, IconAlignBoxCenterBottom, IconAlignBoxCenterMiddle } from '@tabler/icons-vue'
export default {
name: 'FontModal',
@@ -213,6 +239,13 @@ export default {
{ icon: IconAlignRight, value: 'right' }
]
},
valigns () {
return [
{ icon: IconAlignBoxCenterTop, value: 'top' },
{ icon: IconAlignBoxCenterMiddle, value: 'center' },
{ icon: IconAlignBoxCenterBottom, value: 'bottom' }
]
},
sizes () {
return [...Array(23).keys()].map(i => i + 6)
},
@@ -230,12 +263,15 @@ export default {
'justify-center': this.preferences.align === 'center',
'justify-start': this.preferences.align === 'left',
'justify-end': this.preferences.align === 'right',
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
'items-start': this.preferences.valign === 'top',
'items-end': this.preferences.valign === 'bottom',
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
}
},
keys () {
return ['font_type', 'font_size', 'color', 'align', 'font']
return ['font_type', 'font_size', 'color', 'align', 'valign', 'font']
}
},
created () {
@@ -154,7 +154,7 @@ export default {
computed: {
fields () {
return this.template.fields.reduce((acc, f) => {
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid && ['number'].includes(f.type) && !f.preferences?.formula) {
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
acc.push(f)
}
@@ -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"
hover-class="bg-base-200/50"
icon="IconFilePlus"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
:with-description="false"
:title="t('upload_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"
hover-class="bg-base-200/50"
icon="IconFileSymlink"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
: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"
hover-class="bg-base-200/50"
icon="IconFiles"
:template-id="templateId"
:accept-file-types="acceptFileTypes"
: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',
upload_a_new_document: 'Upload 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',
upload_a_new_document: 'Subir 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',
upload_a_new_document: 'Carica 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',
upload_a_new_document: 'Enviar um novo documento',
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',
upload_a_new_document: 'Téléverser un nouveau document',
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',
upload_a_new_document: 'Neues Dokument hochladen',
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
})
}
+4
View File
@@ -90,6 +90,10 @@ class Submission < ApplicationRecord
expire_at && expire_at <= Time.current
end
def fields_uuid_index
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end
def audit_trail_url
return if audit_trail.blank?
+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
+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">
+5
View File
@@ -0,0 +1,5 @@
<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" />
</svg>

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

+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

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 19h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" /><path d="M19 22v-6" /><path d="M22 19l-3 -3l-3 3" /><path d="M3 7l9 6l9 -6" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M15 10v-5c0 -1.38 .62 -2 2 -2s2 .62 2 2v5m0 -3h-4"></path>
<path d="M19 21h-4l4 -7h-4"></path> <path d="M4 15l3 3l3 -3"></path>
<path d="M7 6v12"></path>
</svg>

After

Width:  |  Height:  |  Size: 395 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M4 15l3 3l3 -3"></path>
<path d="M7 6v12"></path>
<path d="M17 14a2 2 0 0 1 2 2v3a2 2 0 1 1 -4 0v-3a2 2 0 0 1 2 -2z"></path>
<path d="M17 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M19 5v3a2 2 0 0 1 -2 2h-1.5"></path>
</svg>

After

Width:  |  Height:  |  Size: 474 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
<path d="M10 15l-3 3l-3 -3"></path>
<path d="M7 6v12"></path>
<path d="M14 18.333c0 .369 .298 .667 .667 .667h2.666a.667 .667 0 0 0 .667 -.667v-2.666a.667 .667 0 0 0 -.667 -.667h-2.666a.667 .667 0 0 0 -.667 .667v2.666z"></path>
<path d="M14 10.833c0 .645 .522 1.167 1.167 1.167h4.666c.645 0 1.167 -.522 1.167 -1.167v-4.666c0 -.645 -.522 -1.167 -1.167 -1.167h-4.666c-.645 0 -1.167 .522 -1.167 1.167v4.666z"></path>
</svg>

After

Width:  |  Height:  |  Size: 647 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" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" />
</svg>

After

Width:  |  Height:  |  Size: 497 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>
+1 -1
View File
@@ -12,7 +12,7 @@
</div>
<div class="form-control">
<%= f.label :last_name, t('last_name'), class: 'label' %>
<%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
<%= f.text_field :last_name, required: false, class: 'base-input', dir: 'auto' %>
</div>
</div>
<div class="form-control">
+1 -1
View File
@@ -1,5 +1,5 @@
<% if 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) %>
<% user_config = current_user.user_configs.find_by(key: UserConfig::SHOW_APP_TOUR) || UserConfig.new(key: UserConfig::SHOW_APP_TOUR, user: current_user) %>
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
<app-tour data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="<%= local_assigns[:type] %>" data-next-page-path="<%= local_assigns[:next_page_path] %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
<% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<% uuid = SecureRandom.uuid %>
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
<label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label>
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
+8
View File
@@ -50,6 +50,14 @@
<% end %>
</li>
<% end %>
<% if Docuseal.multitenant? || current_user.role == 'superadmin' %>
<li>
<%= link_to Docuseal::CHATGPT_URL, target: 'blank', class: 'flex items-center' do %>
<%= svg_icon('sparkles', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="mr-1 whitespace-nowrap"><%= t('ask_ai') %></span>
<% end %>
</li>
<% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
+17 -14
View File
@@ -5,20 +5,23 @@
<%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %>
<%= local_assigns[:left_additional_html] %>
</div>
<div class="join">
<% if @pagy.prev %>
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
<% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
<% end %>
<span class="join-item btn uppercase min-h-full h-10">
<%= t('page_number', number: @pagy.page) %>
</span>
<% if @pagy.next %>
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
<% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
<% end %>
<div class="flex items-center space-x-1.5">
<%= local_assigns[:right_additional_html] %>
<div class="join">
<% if @pagy.prev %>
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
<% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
<% end %>
<span class="join-item btn font-medium uppercase min-h-full h-10">
<%= t('page_number', number: @pagy.page) %>
</span>
<% if @pagy.next %>
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
<% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
<% end %>
</div>
</div>
</div>
<% end %>
@@ -0,0 +1,34 @@
<% dashboard_templates_order = cookies.permanent[:dashboard_templates_order] || 'created_at' %>
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
<label tabindex="0" class="btn btn-sm h-10">
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
</label>
<ul tabindex="0" class="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box mb-1 min-w-48">
<toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
<li>
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'created_at' %>">
<%= svg_icon('sort_descending_numbers', class: 'w-4 h-4') %>
<span class="whitespace-nowrap"><%= t('newest_first') %></span>
</button>
</li>
</toggle-cookies>
<% if local_assigns[:with_recently_used] != false %>
<toggle-cookies data-value="used_at" data-key="dashboard_templates_order">
<li>
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'used_at' %>">
<%= svg_icon('sort_descending_small_big', class: 'w-4 h-4') %>
<span class="whitespace-nowrap"><%= t('recently_used') %></span>
</button>
</li>
</toggle-cookies>
<% end %>
<toggle-cookies data-value="name" data-key="dashboard_templates_order">
<li>
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'name' %>">
<%= svg_icon('sort_ascending_letters', class: 'w-4 h-4') %>
<span class="whitespace-nowrap"><%= t('name_a_z') %></span>
</button>
</li>
</toggle-cookies>
</ul>
</form>
@@ -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 %>
@@ -31,7 +31,7 @@
</submitters-autocomplete>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('<%= t('use_international_format_1xxx_') %>') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('phone')} (#{t('optional')})" %>" id="detailed_phone_<%= item['uuid'] %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
</linked-input>
</submitters-autocomplete>
</div>
+7
View File
@@ -0,0 +1,7 @@
<div id="submitters_error">
<% if local_assigns[:error] %>
<div class="text-center text-red-500">
<%= local_assigns[:error] %>
</div>
<% end %>
</div>
+9 -6
View File
@@ -1,8 +1,9 @@
<% align = field.dig('preferences', 'align') %>
<% valign = field.dig('preferences', 'valign') %>
<% color = field.dig('preferences', 'color') %>
<% font = field.dig('preferences', 'font') %>
<% 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' ? 'justify-end' : (align == 'center' ? 'justify-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') %>">
<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%">
@@ -61,17 +62,19 @@
</div>
<% elsif field['type'] == 'date' %>
<autosize-field></autosize-field>
<div class="flex items-center px-0.5">
<div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
<div class="w-full"><%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %></div>
</div>
<% elsif field['type'] == 'number' %>
<autosize-field></autosize-field>
<div class="flex items-center px-0.5 whitespace-nowrap">
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
<div class="flex w-full px-0.5 whitespace-nowrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
<div class="w-full"><%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %></div>
</div>
<% else %>
<autosize-field></autosize-field>
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
<div class="flex w-full px-0.5 whitespace-pre-wrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
<div class="w-full"><%= Array.wrap(value).join(', ') %></div>
</div>
<% end %>
</field-value>
+14 -10
View File
@@ -1,10 +1,11 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [[t('via_email'), 'email'], [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
<div class="flex justify-center">
<% options.each_with_index do |(label, value), index| %>
<div>
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
<%= label %>
</label>
@@ -13,18 +14,21 @@
</div>
</toggle-visible>
<div class="px-5 mb-5 mt-4">
<div id="email">
<%= render 'email_form', template: @template %>
</div>
<div id="phone" class="hidden">
<%= render 'phone_form', template: @template %>
</div>
<div id="detailed" class="hidden">
<%= render 'detailed_form', template: @template %>
<% unless require_phone_2fa %>
<div id="email">
<%= render 'email_form', template: @template %>
</div>
<div id="phone" class="hidden">
<%= render 'phone_form', template: @template %>
</div>
<% end %>
<div id="detailed" class="<%= 'hidden' unless require_phone_2fa %>">
<%= render 'detailed_form', template: @template, require_phone_2fa: %>
</div>
<div id="list" class="hidden">
<%= render 'list_form', template: @template %>
</div>
<%= render 'submissions/error' %>
</div>
<%= content_for(:modal_extra) %>
<% end %>
+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 %>
@@ -57,6 +57,7 @@
<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: %>
</div>
</div>
<% end %>
Whitespace-only changes.
+24 -3
View File
@@ -8,17 +8,18 @@
<div style="max-height: -webkit-fill-available;">
<div id="scrollbox">
<div class="mx-auto block pb-72" style="max-width: 1000px">
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %>
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 transition-transform duration-300 %>
<%= local_assigns[:banner_html] || capture do %>
<%= render('submit_form/banner') %>
<div class="sticky top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
<div id="signing_form_header" class="sticky min-[1230px]:static top-0 z-50 bg-base-100 py-2 px-2 flex items-center md:-mx-[8px]" style="margin-bottom: -16px">
<div class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<%= @submitter.submission.template.name %>
</div>
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %>
<% decline_modal_checkbox_uuid = SecureRandom.uuid %>
<div>
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button' do %>
<%= render 'shared/html_modal', title: t(:decline), btn_text: t(:decline), btn_class: 'btn btn-sm !px-5', button_id: 'decline_button', uuid: decline_modal_checkbox_uuid do %>
<%= render 'submit_form/decline_form', submitter: @submitter %>
<% end %>
</div>
@@ -35,6 +36,26 @@
</download-button>
</div>
</div>
<scroll-buttons class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
<% if @form_configs[:with_decline] %>
<label id="decline_button" for="<%= decline_modal_checkbox_uuid %>" class="btn btn-sm px-0">
<span class="min-[1366px]:inline hidden px-3">
<%= t(:decline) %>
</span>
<span class="inline min-[1366px]:hidden px-2">
<%= svg_icon('x', class: 'w-5 h-5') %>
</span>
</label>
<% end %>
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2">
<span data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5') %>
</span>
<span class="hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
</span>
</download-button>
</scroll-buttons>
<% end %>
<% schema.each do |item| %>
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
+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 -35
View File
@@ -4,42 +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>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %>
<% 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>
+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
View File
@@ -61,6 +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: %>
<% unless @template.archived_at? %>
<%= render 'submissions_filters/actions', filter_params: filter_params.merge(template_id: @template.id) %>
<% end %>
</div>
</div>
<% end %>
+77 -62
View File
@@ -1,77 +1,92 @@
<% 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 %>
<% 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' %>
<% 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 %>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html, right_additional_html: templates_order_select_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
Whitespace-only changes.
+28 -3
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">
@@ -51,7 +62,7 @@
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body'))) do |ff| %>
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true) do |ff| %>
<div class="form-control mb-2">
<%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %>
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
@@ -64,6 +75,7 @@
</autoresize-textarea>
</div>
<% end %>
<%= render 'templates_preferences/form_fields', ff: %>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
@@ -272,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">
@@ -334,6 +347,18 @@
</div>
<% end %>
<% end %>
<% if can?(:manage, :personalization_advanced) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center mt-4 justify-between w-full">
<span>
<%= t('ensure_unique_recipients') %>
</span>
<%= f.fields_for :preferences, Struct.new(:validate_unique_submitters).new(@template.preferences['validate_unique_submitters']) do |ff| %>
<%= ff.check_box :validate_unique_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
<% end %>
</div>
<% end %>
<% end %>
<div class="form-control mt-5 pb-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
</div>
+2 -1
View File
@@ -25,7 +25,8 @@ module DocuSeal
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT es it de fr pl uk cs pt he nl ar ko]
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT
es it de fr pl uk cs pt he nl ar ko ja]
config.i18n.fallbacks = [:en]
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
+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
+374 -40
View File
@@ -18,14 +18,23 @@ en: &en
language_nl: Nederlands
language_ar: العربية
language_ko: 한국어
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
ensure_unique_recipients: Ensure unique recipients
edit_per_party: Edit per party
reply_to: Reply to
pending_by_me: Pending by me
partially_completed: Partially completed
require_phone_2fa_to_open: Require phone 2FA to open
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: The sender has requested a two factor authentication via one time password sent to your <b>%{phone}</b> phone number.
send_verification_code: Send verification code
code_has_been_resent: Code has been re-sent
invalid_code: Invalid code
unarchive: Unarchive
signed: Signed
first_party: 'First Party'
@@ -47,6 +56,8 @@ en: &en
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode'
sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.'
sent_with_docuseal_pro_html: 'Sent with <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Show "Sent with <span class="link">DocuSeal Pro</span>" attribution in emails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".'
@@ -587,6 +598,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.
@@ -704,6 +716,28 @@ en: &en
welcome_to_docuseal: Welcome to DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create an send your first document
start_tour: Start Tour
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
submission_sources:
api: API
bulk: Bulk Send
@@ -712,6 +746,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}'
@@ -747,11 +783,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'
@@ -759,7 +795,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'
@@ -773,10 +809,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'
@@ -788,6 +824,14 @@ 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
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: El remitente ha solicitado una autenticación de dos factores mediante una contraseña de un solo uso enviada a su número de teléfono <b>%{phone}</b>.
send_verification_code: Enviar código de verificación
code_has_been_resent: El código ha sido reenviado
invalid_code: Código inválido
always_enforce_signing_order: Siempre imponer el orden de firma
bcc_recipients: Destinatarios CCO
edit_per_party: Editar por parte
@@ -817,6 +861,8 @@ es: &es
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> en Modo de Prueba'
sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Enviado con <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar el mensaje "Enviado con <span class="link">DocuSeal Pro</span>" en los correos electrónicos
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza.
hi_there: Hola
thanks: Gracias
@@ -1357,6 +1403,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.
@@ -1474,6 +1521,28 @@ es: &es
welcome_to_docuseal: Bienvenido a DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Inicia una guía rápida para aprender a crear y enviar tu primer documento.
start_tour: Iniciar guía
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
submission_sources:
api: API
bulk: Envío masivo
@@ -1482,6 +1551,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}'
@@ -1517,11 +1588,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'
@@ -1529,7 +1600,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'
@@ -1543,10 +1614,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'
@@ -1558,6 +1629,14 @@ 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
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Il mittente ha richiesto un'autenticazione a due fattori tramite una password monouso inviata al tuo numero di telefono <b>%{phone}</b>.
send_verification_code: Invia codice di verifica
code_has_been_resent: Il codice è stato inviato di nuovo
invalid_code: Codice non valido
always_enforce_signing_order: Applicare sempre l'ordine di firma
bcc_recipients: Destinatari BCC
edit_per_party: Modifica per partito
@@ -1586,6 +1665,8 @@ it: &it
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:'
sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando <a href="%{product_url}">%{product_name}</a> in Modalità di Test'
sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Inviato con <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostra la dicitura "Inviato con <span class="link">DocuSeal Pro</span>" nelle email
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia."
hi_there: Ciao
thanks: Grazie
@@ -2126,6 +2207,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.
@@ -2243,6 +2325,28 @@ it: &it
welcome_to_docuseal: Benvenuto in DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.
start_tour: Inizia il tour
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
submission_sources:
api: API
bulk: Invio massivo
@@ -2250,7 +2354,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}'
@@ -2286,11 +2392,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'
@@ -2298,7 +2404,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'
@@ -2312,10 +2418,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'
@@ -2327,6 +2433,14 @@ 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
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: L'expéditeur a demandé une authentification à deux facteurs via un mot de passe à usage unique envoyé à votre numéro de téléphone <b>%{phone}</b>.
send_verification_code: Envoyer le code de vérification
code_has_been_resent: Le code a été renvoyé
invalid_code: Code invalide
always_enforce_signing_order: Toujours appliquer l'ordre de signature
bcc_recipients: Destinataires en CCI
edit_per_party: Éditer par partie
@@ -2356,6 +2470,8 @@ fr: &fr
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:'
sent_using_product_name_in_testing_mode_html: 'Envoyé en utilisant <a href="%{product_url}">%{product_name}</a> en Mode Test'
sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Envoyé avec <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Afficher "Envoyé avec <span class="link">DocuSeal Pro</span>" dans les e-mails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance.
hi_there: Bonjour
thanks: Merci
@@ -2897,6 +3013,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.
@@ -3010,10 +3127,32 @@ 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
submission_sources:
api: API
bulk: Envoi en masse
@@ -3022,6 +3161,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}'
@@ -3057,11 +3198,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'
@@ -3069,7 +3210,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'
@@ -3083,10 +3224,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'
@@ -3098,6 +3239,14 @@ 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
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: O remetente solicitou uma autenticação de dois fatores via senha de uso único enviada para seu número <b>%{phone}</b>.
send_verification_code: Enviar código de verificação
code_has_been_resent: Código foi reenviado
invalid_code: Código inválido
always_enforce_signing_order: Sempre impor a ordem de assinatura
bcc_recipients: Destinatários BCC
edit_per_party: Edita por festa
@@ -3127,6 +3276,8 @@ pt: &pt
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> no Modo de Teste'
sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Enviado com <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar "Enviado com <span class="link">DocuSeal Pro</span>" nos e-mails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável.
hi_there: Olá
thanks: Obrigado
@@ -3395,7 +3546,7 @@ pt: &pt
verify_pdf: Verificar PDF
sign_out: Sair
page_number: 'Página %{number}'
powered_by: Oferecido por
powered_by: Desenvolvido por
count_documents_signed_with_html: '<b>%{count}</b> documentos assinados com'
storage: Armazenamento
notifications: Notificações
@@ -3667,6 +3818,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.
@@ -3784,6 +3936,28 @@ pt: &pt
welcome_to_docuseal: Bem-vindo ao DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Comece um tour rápido para aprender a criar e enviar seu primeiro documento.
start_tour: Iniciar tour
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
submission_sources:
api: API
bulk: Envio em massa
@@ -3791,8 +3965,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}'
@@ -3827,11 +4003,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'
@@ -3839,7 +4015,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'
@@ -3856,7 +4032,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'
@@ -3869,6 +4045,14 @@ 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
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Der Absender hat eine Zwei-Faktor-Authentifizierung per Einmalpasswort angefordert, das an Ihre <b>%{phone}</b>-Telefonnummer gesendet wurde.
send_verification_code: Bestätigungscode senden
code_has_been_resent: Code wurde erneut gesendet
invalid_code: Ungültiger Code
always_enforce_signing_order: Immer die Reihenfolge der Unterschriften erzwingen
bcc_recipients: BCC-Empfänger
edit_per_party: Bearbeiten pro Partei
@@ -3898,6 +4082,8 @@ de: &de
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:'
sent_using_product_name_in_testing_mode_html: 'Gesendet über <a href="%{product_url}">%{product_name}</a> im Testmodus'
sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: Gesendet mit <a href="%{product_url}">DocuSeal Pro</a>
show_send_with_docuseal_pro_attribution_in_emails_html: '"Gesendet mit <span class="link">DocuSeal Pro</span>" in E-Mails anzeigen'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
hi_there: Hallo
thanks: Danke
@@ -4438,6 +4624,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.
@@ -4555,6 +4742,28 @@ de: &de
welcome_to_docuseal: Willkommen bei DocuSeal
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Starte eine kurze Tour, um zu lernen, wie du dein erstes Dokument erstellst und versendest.
start_tour: Starten
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
submission_sources:
api: API
bulk: Massenversand
@@ -4563,6 +4772,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}'
@@ -4598,11 +4809,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'
@@ -4610,7 +4821,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'
@@ -4624,10 +4835,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'
@@ -4639,6 +4850,11 @@ de: &de
read: Lese deine Daten
pl:
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Nadawca zażądał uwierzytelnienia dwuetapowego poprzez jednorazowe hasło wysłane na Twój <b>%{phone}</b> numer telefonu.
send_verification_code: Wyślij kod weryfikacyjny
code_has_been_resent: Kod został wysłany ponownie
invalid_code: Niepoprawny kod
awaiting_completion_by_the_other_party: "Oczekuje na dokończenie przez drugą stronę"
view: Widok
hi_there: Cześć,
@@ -4696,8 +4912,16 @@ pl:
too_many_attempts: Zbyt wiele prób.
verification_code: Kod Weryfikacyjny
resend_code: Wyślij Kod Ponownie
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'
uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Відправник запросив двофакторну автентифікацію за допомогою одноразового пароля, відправленого на ваш номер телефону <b>%{phone}</b>.
send_verification_code: Надіслати код підтвердження
code_has_been_resent: Код повторно надіслано
invalid_code: Невірний код
awaiting_completion_by_the_other_party: "Очікує завершення з боку іншої сторони"
view: Переглянути
hi_there: Привіт,
@@ -4755,8 +4979,16 @@ uk:
too_many_attempts: Забагато спроб.
verification_code: Код перевірки
resend_code: Відправити код знову
powered_by: 'Працює на базі'
count_documents_signed_with_html: '<b>%{count}</b> документів підписано за допомогою'
open_source_documents_software: 'відкрите програмне забезпечення для документів'
cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: Odesílatel požádal o dvoufaktorové ověření pomocí jednorázového hesla zaslaného na vaše telefonní číslo <b>%{phone}</b>.
send_verification_code: Odeslat ověřovací kód
code_has_been_resent: Kód byl znovu odeslán
invalid_code: Neplatný kód
awaiting_completion_by_the_other_party: "Čeká se na dokončení druhou stranou"
view: Zobrazit
hi_there: Ahoj,
@@ -4814,8 +5046,16 @@ cs:
too_many_attempts: Příliš mnoho pokusů.
verification_code: Ověřovací Kód
resend_code: Znovu Odeslat Kód
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'
he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: השולח ביקש אימות דו-שלבי באמצעות סיסמה חד פעמית שנשלחה למספר הטלפון שלך <b>%{phone}</b>.
send_verification_code: שלח קוד אימות
code_has_been_resent: הקוד נשלח מחדש
invalid_code: קוד שגוי
awaiting_completion_by_the_other_party: "המתנה להשלמה מצד הצד השני"
view: תצוגה
hi_there: שלום,
@@ -4873,8 +5113,16 @@ he:
too_many_attempts: יותר מדי ניסיונות.
verification_code: קוד אימות
resend_code: שלח קוד מחדש
powered_by: 'מופעל על ידי'
count_documents_signed_with_html: '<b>%{count}</b> מסמכים נחתמו באמצעות'
open_source_documents_software: 'תוכנה בקוד פתוח למסמכים'
nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: De afzender heeft gevraagd om tweefactorauthenticatie via een eenmalig wachtwoord, verzonden naar uw <b>%{phone}</b> telefoonnummer.
send_verification_code: Verificatiecode verzenden
code_has_been_resent: Code is opnieuw verzonden
invalid_code: Invalid code
awaiting_completion_by_the_other_party: "In afwachting van voltooiing door de andere partij"
view: Bekijken
hi_there: Hallo,
@@ -4932,8 +5180,16 @@ nl:
too_many_attempts: Te veel pogingen.
verification_code: Verificatiecode
resend_code: Code Opnieuw Verzenden
powered_by: 'Aangedreven door'
count_documents_signed_with_html: '<b>%{count}</b> documenten ondertekend met'
open_source_documents_software: 'open source documenten software'
ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: "المرسل طلب تحقق ذو عاملين عبر كلمة مرور لمرة واحدة مرسل إلى رقم هاتفك <b>%{phone}</b>."
send_verification_code: "إرسال رمز التحقق"
code_has_been_resent: "تم إعادة إرسال الرمز"
invalid_code: "رمز غير صالح"
awaiting_completion_by_the_other_party: "في انتظار إكتمال الطرف الآخر"
view: عرض
hi_there: مرحبا,
@@ -4991,8 +5247,16 @@ ar:
too_many_attempts: عدد المحاولات كبير جدًا.
verification_code: رمز التحقق
resend_code: إعادة إرسال الرمز
powered_by: 'مشغل بواسطة'
count_documents_signed_with_html: '<b>%{count}</b> مستندات تم توقيعها باستخدام'
open_source_documents_software: 'برنامج مستندات مفتوح المصدر'
ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 발신자가 <b>%{phone}</b> 전화번호로 보내진 일회용 비밀번호를 통해 이중 인증을 요청했습니다.
send_verification_code: 인증 코드 보내기
code_has_been_resent: 코드가 재전송되었습니다.
invalid_code: 잘못된 코드
awaiting_completion_by_the_other_party: "다른 당사자의 완료를 기다리고 있습니다"
view: 보기
hi_there: 안녕하세요,
@@ -5050,6 +5314,76 @@ ko:
too_many_attempts: 시도 횟수가 너무 많습니다.
verification_code: 인증 코드
resend_code: 코드 재전송
powered_by: '제공:'
count_documents_signed_with_html: '<b>%{count}</b>개의 문서가 다음을 통해 서명됨'
open_source_documents_software: '오픈소스 문서 소프트웨어'
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_html: 送信者は、<b>%{phone}</b> に送信されたワンタイムパスワードによる2段階認証を要求しました。
send_verification_code: 認証コードを送信
code_has_been_resent: コードが再送信されました
invalid_code: 無効なコードです
awaiting_completion_by_the_other_party: 他の当事者による完了を待機中
view: 表示
hi_there: こんにちは
download: ダウンロード
decline: 辞退
declined: 辞退済み
decline_reason: 辞退の理由
provide_a_reason: 理由を入力してください
notify_the_sender_with_the_reason_you_declined: 辞退理由を送信者に通知してください
form_has_been_declined_on_html: '<span class="font-semibold">%{time}</span> にフォームが辞退されました'
name_declined_by_submitter: '"%{name}" は %{submitter} により辞退されました'
name_declined_by_submitter_with_the_following_reason: '"%{name}" は %{submitter} により次の理由で辞退されました:'
email: メール
digitally_signed_by: 電子署名者
role: 役割
provide_your_email_to_start: 開始するにはメールアドレスを入力してください
start: 開始
reason: 理由
starting: 開始中
form_expired_at_html: '<span class="font-semibold">%{time}</span> にフォームの有効期限が切れました'
invited_by_html: '<span class="font-semibold">%{name}</span> により招待されました'
you_have_been_invited_to_submit_a_form: フォームの提出に招待されました
verification_code_code: '認証コード: %{code}'
signed_on_time: '%{time} に署名済み'
completed_on_time: '%{time} に完了'
document_has_been_signed_already: ドキュメントはすでに署名されています
form_has_been_submitted_already: フォームはすでに送信されています
send_copy_to_email: メールにコピーを送信
sending: 送信中
resubmit: 再送信
form_has_been_deleted_by_html: '<span class="font-semibold">%{name}</span> によりフォームが削除されました。'
or: または
download_documents: ドキュメントをダウンロード
downloading: ダウンロード中
completed_successfully: 正常に完了しました
password: パスワード
sign_in: サインイン
signing_in: サインイン中
sign_in_with_microsoft: Microsoftでサインイン
sign_in_with_google: Googleでサインイン
forgot_your_password_: パスワードをお忘れですか?
create_free_account: 無料アカウントを作成
already_have_an_account: すでにアカウントをお持ちですか?
first_name: 名
last_name: 姓
sign_up: 登録
signing_up: 登録中
profile_details: プロフィールの詳細
sign_up_with_google: Googleで登録
sign_up_with_microsoft: Microsoftで登録
by_creating_an_account_you_agree_to_our_html: '<a target="_blank" href="https://www.docuseal.com/privacy">プライバシーポリシー</a>および<a target="_blank" href="https://www.docuseal.com/terms">利用規約</a>に同意の上、アカウントを作成します。'
enter_email_to_continue: 続行するにはメールを入力してください
the_code_has_been_sent_to_your_email: コードがあなたのメールに送信されました
enter_the_verification_code_from_your_email: メールに記載された認証コードを入力してください
too_many_attempts: 試行回数が多すぎます
verification_code: 認証コード
resend_code: コードを再送信
powered_by: '提供元:'
count_documents_signed_with_html: '<b>%{count}</b> 件のドキュメントが以下で署名されました'
open_source_documents_software: 'オープンソースのドキュメントソフトウェア'
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]
+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
+3 -1
View File
@@ -8,7 +8,9 @@ class ApiPathConsiderJsonMiddleware
def call(env)
if env['PATH_INFO'].starts_with?('/api') &&
(!env['PATH_INFO'].ends_with?('/documents') || env['REQUEST_METHOD'] != 'POST') &&
!env['PATH_INFO'].ends_with?('/attachments')
!env['PATH_INFO'].ends_with?('/attachments') &&
!env['PATH_INFO'].ends_with?('/submitter_sms_clicks') &&
!env['PATH_INFO'].ends_with?('/submitter_email_clicks')
env['CONTENT_TYPE'] = 'application/json'
end
+5 -1
View File
@@ -12,7 +12,7 @@ module Docuseal
DISCORD_URL = 'https://discord.gg/qygYCDGck9'
TWITTER_URL = 'https://twitter.com/docusealco'
TWITTER_HANDLE = '@docusealco'
CHATGPT_URL = 'https://chatgpt.com/g/g-9hg8AAw0r-docuseal'
CHATGPT_URL = "#{PRODUCT_URL}/chat".freeze
SUPPORT_EMAIL = 'support@docuseal.com'
HOST = ENV.fetch('HOST', 'localhost')
AATL_CERT_NAME = 'docuseal_aatl'
@@ -55,6 +55,10 @@ module Docuseal
ENV['MULTITENANT'] == 'true'
end
def advanced_formats?
multitenant?
end
def demo?
ENV['DEMO'] == 'true'
end
+38 -2
View File
@@ -15,10 +15,12 @@ module ReplaceEmailVariables
SUBMITTER_SLUG = /\{+submitter\.slug\}+/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
SUBMITTERS_N_FIRST_NAME = /\{+submitters\[(?<index>\d+)\]\.first_name\}+/i
SUBMITTERS_N_FIELD_VALUE = /\{+submitters\[(?<index>\d+)\]\.(?<field_name>[^}]+)\}+/i
DOCUMENTS_LINKS = /\{+documents\.links\}+/i
DOCUMENTS_LINK = /\{+documents\.link\}+/i
@@ -47,6 +49,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
@@ -59,6 +68,10 @@ module ReplaceEmailVariables
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :first_name)
end
text = replace(text, SUBMITTERS_N_FIELD_VALUE, html_escape:) do |match|
build_submitters_n_field(submitter.submission, match[:index].to_i - 1, :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
@@ -69,10 +82,33 @@ module ReplaceEmailVariables
)
end
def build_submitters_n_field(submission, index, field_name)
def build_submitters_n_field(submission, index, field_name, value_name = nil)
uuid = (submission.template_submitters || submission.template.submitters).dig(index, 'uuid')
submission.submitters.find { |s| s.uuid == uuid }.try(field_name)
submitter = submission.submitters.find { |s| s.uuid == uuid }
return unless submitter
value = submitter.try(field_name)
if value_name
field = (submission.template_fields || submission.template.fields).find { |e| e['name'] == value_name }
return unless field
value =
if field['type'].in?(%w[image signature initials stamp payment file])
attachment_uuid = Array.wrap(value[field['uuid']]).first
attachment = submitter.attachments.find { |e| e.uuid == attachment_uuid }
ActiveStorage::Blob.proxy_url(attachment.blob) if attachment
else
value[field&.dig('uuid')]
end
end
value
end
def replace(text, var, html_escape: false)
+9 -11
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),
@@ -134,16 +137,11 @@ module Submissions
end
def filtered_conditions_schema(submission, values: nil, include_submitter_uuid: nil)
fields_uuid_index = nil
(submission.template_schema || submission.template.schema).filter_map do |item|
if item['conditions'].present?
fields_uuid_index ||=
(submission.template_fields || submission.template.fields).index_by { |f| f['uuid'] }
values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
next unless check_item_conditions(item, values, fields_uuid_index, include_submitter_uuid:)
next unless check_item_conditions(item, values, submission.fields_uuid_index, include_submitter_uuid:)
end
item
@@ -151,21 +149,21 @@ module Submissions
end
def filtered_conditions_fields(submitter, only_submitter_fields: true)
fields = submitter.submission.template_fields || submitter.submission.template.fields
submission = submitter.submission
fields = submission.template_fields || submission.template.fields
fields_uuid_index = nil
values = nil
fields.filter_map do |field|
next if field['submitter_uuid'] != submitter.uuid && only_submitter_fields
if field['conditions'].present?
fields_uuid_index ||= fields.index_by { |f| f['uuid'] }
values ||= submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
values ||= submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
submitter_conditions = []
next unless check_item_conditions(field, values, fields_uuid_index,
next unless check_item_conditions(field, values, submission.fields_uuid_index,
include_submitter_uuid: submitter.uuid,
submitter_conditions_acc: submitter_conditions)
+8 -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
@@ -65,6 +66,12 @@ module Submissions
raise BaseError, 'Defined more signing parties than in template'
end
if template.preferences['validate_unique_submitters'] == true
submission_emails = submission.submitters.filter_map(&:email)
raise BaseError, 'Recipient emails should differ' if submission_emails.uniq.size != submission_emails.size
end
next if submission.submitters.blank?
maybe_add_invite_submitters(submission, template)
+5 -3
View File
@@ -50,12 +50,12 @@ module Submissions
}
document.sign(io, **sign_params)
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
else
document.write(io)
end
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - #{submission.template.name}.pdf"
@@ -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')
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more