Compare commits

...
87 Commits
Author SHA1 Message Date
Alex Turchyn 4f94858901 Merge from docusealco/wip 2025-06-30 12:56:05 +03:00
Pete Matsyburka d0ac4ad222 fix sqlite remove template 2025-06-30 11:44:55 +03:00
Pete Matsyburka 10e4476f4a fix signature prefill 2025-06-30 11:41:07 +03:00
Pete Matsyburka 74411d883a adjust link default parties 2025-06-29 12:31:41 +03:00
Alex Turchyn 18ee2d5316 add shared link form fields preferences 2025-06-28 23:06:25 +03:00
Pete Matsyburka b4f80422eb update doc 2025-06-28 13:30:05 +03:00
Pete Matsyburka 7fe9ab7601 update params 2025-06-28 10:37:10 +03:00
Pete Matsyburka 13ad614e6b fix rubocop 2025-06-28 10:17:50 +03:00
Alex Turchyn 7b1b4bcf2c show shared link enable button on disabled link page 2025-06-28 09:58:22 +03:00
Alex Turchyn a6715d4034 add meta description translations 2025-06-27 23:25:52 +03:00
Pete Matsyburka 254995c6c6 fix missing font variant 2025-06-27 14:45:29 +03:00
Pete Matsyburka 0e33e23d1f fix prefill condition 2025-06-27 12:49:25 +03:00
Pete Matsyburka 57503eefd2 adjust search index 2025-06-27 12:08:57 +03:00
Pete Matsyburka cd477c880c render image as file 2025-06-26 21:50:12 +03:00
Pete Matsyburka bf2f5b24dc update docs 2025-06-26 17:19:53 +03:00
Pete Matsyburka 7eba4865bc fix signature remember 2025-06-26 11:28:38 +03:00
Pete Matsyburka 64cd24a24d validate blank images 2025-06-26 11:04:25 +03:00
Pete Matsyburka de5acd0949 fix one-off 2025-06-26 10:05:16 +03:00
Pete Matsyburka 6ebbc3edac pdfium read text 2025-06-26 09:51:09 +03:00
Pete Matsyburka 2020f6c136 make shared templates searchable 2025-06-26 07:47:29 +03:00
Pete Matsyburka ee4acd5a82 validate signature image 2025-06-25 22:31:19 +03:00
Pete Matsyburka 2e487ac936 fix timezone selector 2025-06-25 21:36:03 +03:00
Pete Matsyburka dee5fc7c07 fix preview 2025-06-25 16:34:25 +03:00
Pete Matsyburka e130584fec fix timezone selector 2025-06-25 09:38:52 +03:00
Pete Matsyburka ade5c4832f fix bulk table mappings 2025-06-24 12:49:52 +03:00
Pete Matsyburka 33b14d5278 fix sqlite query 2025-06-23 20:34:30 +03:00
Pete Matsyburka 48e73c2c81 fix png quality 2025-06-23 14:53:35 +03:00
Pete Matsyburka 894d98e92b update widget version 2025-06-23 13:33:44 +03:00
Alex Turchyn 14c84eff93 Merge from docusealco/wip 2025-06-23 13:19:06 +03:00
Pete Matsyburka 01d982b093 embed preview toggle 2025-06-22 14:54:12 +03:00
Pete Matsyburka 35852ef6ab add onprem search reindex 2025-06-21 23:04:11 +03:00
Pete Matsyburka 47b235dc5f fix search 2025-06-21 22:17:54 +03:00
Pete Matsyburka 0e11d2a581 fix tour link 2025-06-21 17:11:26 +03:00
Pete Matsyburka e57e11f206 optimize query 2025-06-21 14:58:20 +03:00
Alex Turchyn 51f639065c add translations 2025-06-20 21:14:22 +03:00
Pete Matsyburka 6c1d4b466f adjust server selector 2025-06-20 13:41:42 +03:00
Pete Matsyburka b940c597dc fix pdfium error 2025-06-20 02:16:31 +03:00
Pete Matsyburka 1ce67bcb39 adjust folder autocomplete 2025-06-19 22:49:47 +03:00
Pete Matsyburka 5050a67d08 add datenow type 2025-06-19 19:58:22 +03:00
Pete Matsyburka 76321cef3c optimize autocomplete 2025-06-19 16:06:35 +03:00
Pete Matsyburka 8f3a203994 markdown link 2025-06-19 12:45:44 +03:00
Pete Matsyburka ea24562861 fix time format 2025-06-19 12:25:35 +03:00
Pete Matsyburka 4a888d048e fix empty file generate 2025-06-19 12:06:08 +03:00
Alex Turchyn 36331bb993 show found submissions when searching on templates 2025-06-19 12:05:43 +03:00
Pete Matsyburka 6f0e7c0ca8 skip autocomplete 2025-06-18 23:53:36 +03:00
Pete Matsyburka 2491b7b7e3 adjust indexes 2025-06-18 11:56:42 +03:00
Pete Matsyburka 7965c17f46 fix serializer 2025-06-17 18:03:28 +03:00
Pete Matsyburka 5519459519 refactor 2025-06-17 12:42:57 +03:00
Pete Matsyburka d8bf2cc101 add index 2025-06-17 11:03:40 +03:00
Pete Matsyburka 2aece98d67 add pro routes 2025-06-16 22:37:50 +03:00
Pete Matsyburka f602497255 require all submitters 2025-06-16 22:17:14 +03:00
Pete Matsyburka 11db68cdc7 fix merge 2025-06-16 18:55:23 +03:00
Pete Matsyburka bed98f4344 optimize query 2025-06-16 17:02:05 +03:00
Pete Matsyburka 055f7e2bc1 Merge from docusealco/wip 2025-06-15 23:53:35 +03:00
Pete Matsyburka dae3b4b6b5 add completed at index 2025-06-15 12:26:11 +03:00
Pete Matsyburka f2888a08dc fix query 2025-06-14 08:53:45 +03:00
Pete Matsyburka 95627c5963 add audit timezone 2025-06-13 22:37:04 +03:00
Pete Matsyburka 4bb3721db0 add submitter timezone 2025-06-13 19:15:05 +03:00
Pete Matsyburka b12111383b fix update oneoff 2025-06-11 23:39:19 +03:00
Pete Matsyburka 263c0ff939 fix migration 2025-06-11 18:50:01 +03:00
Pete Matsyburka efc8ba6b0a fix merge 2025-06-11 17:55:10 +03:00
Pete Matsyburka 4c530b3f88 fix query 2025-06-11 12:10:24 +03:00
Pete Matsyburka f6358c9d47 fix brakeman 2025-06-11 09:32:43 +03:00
Pete Matsyburka b3baea2f6f optimize query 2025-06-11 09:28:10 +03:00
Pete Matsyburka c0594a8b74 fix brakeman 2025-06-11 09:16:57 +03:00
Pete Matsyburka 1403401b03 permit field required 2025-06-10 11:26:12 +03:00
Pete Matsyburka 700184f776 add ngram 2025-06-10 09:19:13 +03:00
Pete Matsyburka b6a1a3ee4d fix archived 2025-06-09 21:46:26 +03:00
Pete Matsyburka 8cbf5802b5 fix signature upload 2025-06-09 17:06:20 +03:00
Pete Matsyburka ab9706afef fix template fields 2025-06-09 14:20:17 +03:00
Pete Matsyburka f07f9d8cf9 fix oneoff edit 2025-06-09 14:20:17 +03:00
Pete Matsyburka 591caecb92 fix redirect 2025-06-09 14:20:17 +03:00
Pete Matsyburka ef4c2b7213 update gem 2025-06-08 16:54:59 +03:00
Pete Matsyburka 18999255a9 remove constant 2025-06-08 13:34:41 +03:00
Pete Matsyburka 0a2a1de6b9 brakeman ignore 2025-06-07 21:28:01 +03:00
Pete Matsyburka 1b857b6eb3 fix preview 2025-06-07 21:12:37 +03:00
Pete Matsyburka e5e6b11c95 fix image name 2025-06-07 16:20:57 +03:00
Pete Matsyburka 3ec3f58d55 fix mysql migration 2025-06-07 09:55:45 +03:00
Pete Matsyburka 3bfce862ec fix reindex 2025-06-07 09:52:26 +03:00
Pete Matsyburka c01b67f7ec index template 2025-06-06 23:14:17 +03:00
Pete Matsyburka ea0efe9314 fix form cells 2025-06-06 22:10:00 +03:00
Pete Matsyburka 28b583691f partial download toggle 2025-06-06 16:28:53 +03:00
Pete Matsyburka 33811945be fulltext search 2025-06-06 16:03:16 +03:00
Pete Matsyburka 9d1860f038 fix specs 2025-06-06 08:47:56 +03:00
Pete Matsyburka f03166b4ea one-off submission 2025-06-06 08:47:56 +03:00
Pete Matsyburka c59d948d41 add stamp to all pages 2025-06-04 08:44:55 +03:00
Pete Matsyburka 57d63dd2f2 fix empty option condition 2025-06-03 22:29:14 +03:00
162 changed files with 16737 additions and 544 deletions

No files matched your search

+1 -1
View File
@@ -66,7 +66,7 @@ RSpec/ExampleLength:
Max: 50 Max: 50
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 9 Max: 15
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 5 Max: 5
+1 -1
View File
@@ -387,7 +387,7 @@ GEM
puma (6.5.0) puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.14) rack (3.1.16)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
@@ -10,6 +10,12 @@ module Api
def create def create
submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
end
attachment = Submitters.create_attachment!(submitter, params) attachment = Submitters.create_attachment!(submitter, params)
if params[:remember_signature] == 'true' && submitter.email.present? if params[:remember_signature] == 'true' && submitter.email.present?
@@ -10,7 +10,7 @@ module Api
end end
def index def index
submissions = Submissions.search(@submissions, params[:q]) submissions = Submissions.search(current_user, @submissions, params[:q])
submissions = filter_submissions(submissions, params) submissions = filter_submissions(submissions, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
@@ -80,6 +80,8 @@ module Api
end end
end end
SearchEntries.enqueue_reindex(submissions)
render json: build_create_json(submissions) render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError, rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
DownloadUtils::UnableToDownload => e DownloadUtils::UnableToDownload => e
@@ -110,7 +112,9 @@ module Api
submissions = submissions.where(slug: params[:slug]) if params[:slug].present? submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
if params[:template_folder].present? if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:template_folder])
submissions = folder ? submissions.joins(:template).where(template: { folder_id: folder.id }) : submissions.none
end end
if params.key?(:archived) if params.key?(:archived)
@@ -183,14 +187,15 @@ module Api
def submissions_params def submissions_params
permitted_attrs = [ permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last, :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:expire_at, :require_phone_2fa, :expire_at, :name,
{ {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
:require_phone_2fa,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message, :readonly, :required, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]] { default_value: [], value: [], preferences: {} }] }]]
} }
] ]
+19 -13
View File
@@ -5,7 +5,7 @@ module Api
load_and_authorize_resource :submitter load_and_authorize_resource :submitter
def index def index
submitters = Submitters.search(@submitters, params[:q]) submitters = Submitters.search(current_user, @submitters, params[:q])
submitters = filter_submitters(submitters, params) submitters = filter_submitters(submitters, params)
@@ -37,14 +37,16 @@ module Api
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
end end
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name'] submission = @submitter.submission
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
normalized_params, new_attachments = normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template, submitter_params.merge(role:),
for_submitter: @submitter) @submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
for_submitter: @submitter
)
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission, Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
[normalized_params],
default_submitter_uuid: @submitter.uuid) default_submitter_uuid: @submitter.uuid)
assign_submitter_attrs(@submitter, normalized_params) assign_submitter_attrs(@submitter, normalized_params)
@@ -65,10 +67,10 @@ module Api
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, SearchEntries.enqueue_reindex(@submitter)
with_urls: true,
with_events: false, render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
params:) with_events: false, params:)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
@@ -80,9 +82,9 @@ module Api
submitter_params.permit( submitter_params.permit(
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role, :send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :go_to_last, :completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [[:name, :uuid, :default_value, :value, fields: [[:name, :uuid, :default_value, :value, :required,
:readonly, :validation_pattern, :invalid_message, :readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }]] } { default_value: [], value: [], preferences: {} }]] }
) )
@@ -191,6 +193,10 @@ module Api
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to') submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('require_phone_2fa')
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
end
if submitter_preferences.key?('go_to_last') if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last'] submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end end
@@ -21,17 +21,20 @@ module Api
) )
cloned_template.source = :api cloned_template.source = :api
cloned_template.save!
schema_documents = Templates::CloneAttachments.call(template: cloned_template, schema_documents = Templates::CloneAttachments.call(template: cloned_template,
original_template: @template, original_template: @template,
documents: params[:documents]) documents: params[:documents])
cloned_template.save!
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url| WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id, SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
'webhook_url_id' => webhook_url.id) 'webhook_url_id' => webhook_url.id)
end end
SearchEntries.enqueue_reindex(cloned_template)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents) render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end end
end end
+9 -2
View File
@@ -65,6 +65,8 @@ module Api
@template.update!(template_params) @template.update!(template_params)
SearchEntries.enqueue_reindex(@template)
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url| WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id, SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
'webhook_url_id' => webhook_url.id) 'webhook_url_id' => webhook_url.id)
@@ -86,12 +88,17 @@ module Api
private private
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(templates, params[:q]) templates = Templates.search(current_user, templates, params[:q])
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active 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[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].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.where(slug: params[:slug]) if params[:slug].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
if params[:folder].present?
folder = TemplateFolder.accessible_by(current_ability).find_by(name: params[:folder])
templates = folder ? templates.where(folder:) : templates.none
end
templates templates
end end
+4
View File
@@ -5,8 +5,12 @@ class ErrorsController < ActionController::Base
'This feature is available in Pro Edition: https://www.docuseal.com/pricing' 'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/submissions/html',
'/api/submissions/html',
'/templates/html', '/templates/html',
'/api/templates/html', '/api/templates/html',
'/submissions/pdf',
'/api/submissions/pdf',
'/templates/pdf', '/templates/pdf',
'/api/templates/pdf', '/api/templates/pdf',
'/templates/doc', '/templates/doc',
@@ -0,0 +1,17 @@
# frozen_string_literal: true
class SearchEntriesReindexController < ApplicationController
def create
authorize!(:manage, EncryptedConfig)
ReindexAllSearchEntriesJob.perform_async
AccountConfig.find_or_initialize_by(account_id: Account.minimum(:id), key: :fulltext_search)
.update!(value: true)
Docuseal.instance_variable_set(:@fulltext_search, nil)
redirect_back(fallback_location: settings_account_path,
notice: "Started building search index. Visit #{root_url}jobs/busy to check progress.")
end
end
@@ -11,11 +11,16 @@ class SendSubmissionEmailController < ApplicationController
def create def create
if params[:template_slug] if params[:template_slug]
@submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase, template = Template.find_by!(slug: params[:template_slug])
template: { slug: params[:template_slug] })
@submitter =
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
elsif params[:submission_slug] elsif params[:submission_slug]
@submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase, submission = Submission.find_by(slug: params[:submission_slug])
submission: { slug: params[:submission_slug] })
if submission
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
end
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
else else
@@ -24,14 +29,18 @@ class SendSubmissionEmailController < ApplicationController
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter, SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
end
respond_to do |f| respond_to do |f|
f.html { render :success } f.html { render :success }
f.json { head :ok } f.json { head :ok }
end end
end end
private
def already_sent?(submitter)
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
end
end end
+1
View File
@@ -34,6 +34,7 @@ class SetupController < ApplicationController
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) } { key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
] ]
@account.encrypted_configs.create!(encrypted_configs) @account.encrypted_configs.create!(encrypted_configs)
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
Docuseal.refresh_default_url_options! Docuseal.refresh_default_url_options!
+42 -8
View File
@@ -33,12 +33,12 @@ class StartFormController < ApplicationController
@submitter = find_or_initialize_submitter(@template, submitter_params) @submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
else else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message @error_message = multiple_submitters_error_message
return render :show return render :show, status: :unprocessable_entity
end end
if (is_new_record = @submitter.new_record?) if (is_new_record = @submitter.new_record?)
@@ -49,10 +49,12 @@ class StartFormController < ApplicationController
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent) @submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end end
if @submitter.save if @submitter.errors.blank? && @submitter.save
if is_new_record if is_new_record
enqueue_submission_create_webhooks(@submitter) enqueue_submission_create_webhooks(@submitter)
SearchEntries.enqueue_reindex(@submitter)
if @submitter.submission.expire_at? if @submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at, ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
'submission_id' => @submitter.submission_id) 'submission_id' => @submitter.submission_id)
@@ -61,7 +63,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
render :show render :show, status: :unprocessable_entity
end end
end end
end end
@@ -69,9 +71,20 @@ class StartFormController < ApplicationController
def completed def completed
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at? return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
submitter_params = params.permit(:name, :email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
required_fields = @template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
required_params.except('name').compact_blank.blank?
@submitter = Submitter.where(submission: @template.submissions) @submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil) .where.not(completed_at: nil)
.find_by!(email: params[:email]) .find_by!(required_params)
end end
private private
@@ -102,7 +115,16 @@ class StartFormController < ApplicationController
end end
def find_or_initialize_submitter(template, submitter_params) def find_or_initialize_submitter(template, submitter_params)
Submitter required_fields = template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
find_params = required_params.except('name')
submitter = Submitter.new if find_params.compact_blank.blank?
submitter ||=
Submitter
.where(submission: template.submissions.where(expire_at: Time.current..) .where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc) .order(id: :desc)
@@ -110,7 +132,17 @@ class StartFormController < ApplicationController
.where(external_id: nil) .where(external_id: nil)
.where(ip: [nil, request.remote_ip]) .where(ip: [nil, request.remote_ip])
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank) .find_or_initialize_by(find_params)
submitter.name = required_params['name'] if submitter.new_record?
unless @resubmit_submitter
required_params.each do |key, value|
submitter.errors.add(key.to_sym, :blank) if value.blank?
end
end
submitter
end end
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
@@ -123,6 +155,8 @@ class StartFormController < ApplicationController
metadata: @resubmit_submitter&.metadata.presence || {} metadata: @resubmit_submitter&.metadata.presence || {}
) )
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
if submitter.values.present? if submitter.values.present?
@resubmit_submitter.attachments.each do |attachment| @resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid) submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
@@ -142,7 +176,7 @@ class StartFormController < ApplicationController
end end
def filter_undefined_submitters(template) def filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template) Templates.filter_undefined_submitters(template.submitters)
end end
def submitter_params def submitter_params
@@ -4,12 +4,12 @@ class SubmissionsArchivedController < ApplicationController
load_and_authorize_resource :submission, parent: false load_and_authorize_resource :submission, parent: false
def index def index
@submissions = @submissions.joins(:template) @submissions = @submissions.left_joins(:template)
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil })) .or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:template_accesses, :created_by_user, template: :author) .preload(:template_accesses, :created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params) @submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
+7 -3
View File
@@ -8,6 +8,10 @@ class SubmissionsController < ApplicationController
prepend_before_action :maybe_redirect_com, only: %i[show] prepend_before_action :maybe_redirect_com, only: %i[show]
before_action only: :create do
authorize!(:create, Submission)
end
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
@@ -26,8 +30,6 @@ class SubmissionsController < ApplicationController
end end
def create def create
authorize!(:create, Submission)
save_template_message(@template, params) if params[:save_message] == '1' save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1' if params[:is_custom_message] != '1'
@@ -56,6 +58,8 @@ class SubmissionsController < ApplicationController
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
SearchEntries.enqueue_reindex(submissions)
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added') redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error, render turbo_stream: turbo_stream.replace(:submitters_error,
@@ -81,7 +85,7 @@ class SubmissionsController < ApplicationController
I18n.t('submission_has_been_archived') I18n.t('submission_has_been_archived')
end end
redirect_back(fallback_location: template_path(@submission.template), notice:) redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path, notice:)
end end
private private
@@ -4,13 +4,13 @@ class SubmissionsDashboardController < ApplicationController
load_and_authorize_resource :submission, parent: false load_and_authorize_resource :submission, parent: false
def index def index
@submissions = @submissions.joins(:template) @submissions = @submissions.left_joins(:template)
@submissions = @submissions.where(archived_at: nil) @submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil }) .where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user, template: :author) .preload(:template_accesses, :created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params) @submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
+13 -8
View File
@@ -8,6 +8,7 @@ class SubmitFormController < ApplicationController
skip_authorization_check skip_authorization_check
before_action :load_submitter, only: %i[show update completed] before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_locked_page, only: :show
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
@@ -15,19 +16,14 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if submission.template.archived_at? ||
submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return render :awaiting if (@form_configs[:enforce_signing_order] || return render :awaiting if (@form_configs[:enforce_signing_order] ||
submission.template.preferences['submitters_order'] == 'preserved') && submission.template&.preferences&.dig('submitters_order') == 'preserved') &&
!Submitters.current_submitter_order?(@submitter) !Submitters.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter) Submissions.preload_with_pages(submission)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@@ -54,7 +50,7 @@ class SubmitFormController < ApplicationController
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end 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 return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end end
@@ -84,6 +80,15 @@ class SubmitFormController < ApplicationController
private private
def maybe_render_locked_page
return render :archived if @submitter.submission.template&.archived_at? ||
@submitter.submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if @submitter.submission.expired?
render :declined if @submitter.declined_at?
end
def load_submitter def load_submitter
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
end end
@@ -11,7 +11,7 @@ class SubmitFormDeclineController < ApplicationController
submitter.completed_at? || submitter.completed_at? ||
submitter.submission.archived_at? || submitter.submission.archived_at? ||
submitter.submission.expired? || submitter.submission.expired? ||
submitter.submission.template.archived_at? submitter.submission.template&.archived_at?
ApplicationRecord.transaction do ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current) submitter.update!(declined_at: Time.current)
@@ -14,7 +14,10 @@ class SubmitFormDownloadController < ApplicationController
return head :unprocessable_entity if @submitter.declined_at? || return head :unprocessable_entity if @submitter.declined_at? ||
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||
@submitter.submission.expired? || @submitter.submission.expired? ||
@submitter.submission.template.archived_at? @submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
last_completed_submitter = @submitter.submission.submitters last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id) .where.not(id: @submitter.id)
@@ -25,7 +28,7 @@ class SubmitFormDownloadController < ApplicationController
if last_completed_submitter if last_completed_submitter
Submitters.select_attachments_for_download(last_completed_submitter) Submitters.select_attachments_for_download(last_completed_submitter)
else else
@submitter.submission.template.schema_documents.preload(:blob) @submitter.submission.schema_documents.preload(:blob)
end end
urls = attachments.map do |attachment| urls = attachments.map do |attachment|
@@ -12,7 +12,7 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at? if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return redirect_to submit_form_path(@submitter.slug) return redirect_to submit_form_path(@submitter.slug)
end end
@@ -45,7 +45,7 @@ class SubmitFormInviteController < ApplicationController
!submitter.completed_at? && !submitter.completed_at? &&
!submitter.submission.archived_at? && !submitter.submission.archived_at? &&
!submitter.submission.expired? && !submitter.submission.expired? &&
!submitter.submission.template.archived_at? !submitter.submission.template&.archived_at?
end end
def filter_invite_submitters(submitter, key = 'invite_by_uuid') def filter_invite_submitters(submitter, key = 'invite_by_uuid')
@@ -8,7 +8,7 @@ class SubmitFormValuesController < ApplicationController
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at? return render json: {} if submitter.completed_at? || submitter.declined_at?
return render json: {} if submitter.submission.template.archived_at? || return render json: {} if submitter.submission.template&.archived_at? ||
submitter.submission.archived_at? || submitter.submission.archived_at? ||
submitter.submission.expired? submitter.submission.expired?
@@ -22,13 +22,17 @@ class SubmittersAutocompleteController < ApplicationController
def search_submitters(submitters) def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field]) if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym] if Docuseal.fulltext_search?
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
else
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%" term = "#{params[:q].downcase}%"
submitters.where(column.matches(term)) submitters.where(column.matches(term))
end
else else
Submitters.search(submitters, params[:q]) Submitters.search(current_user, submitters, params[:q])
end end
end end
end end
+2
View File
@@ -38,6 +38,8 @@ class SubmittersController < ApplicationController
if @submitter.save if @submitter.save
maybe_resend_email_sms(@submitter, params) maybe_resend_email_sms(@submitter, params)
SearchEntries.enqueue_reindex(@submitter)
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved') redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
else else
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save') redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
@@ -6,13 +6,15 @@ class SubmittersResubmitController < ApplicationController
def update def update
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
submission = @submitter.template.submissions.new(created_by_user: current_user, submission = @submitter.account.submissions.new(created_by_user: current_user,
submitters_order: :preserved, submitters_order: :preserved,
**@submitter.submission.slice(:template_fields, **@submitter.submission.slice(:template_fields,
:account_id, :account_id,
:template_schema, :name,
:template_submitters, :template_id,
:preferences)) :template_schema,
:template_submitters,
:preferences))
@submitter.submission.submitters.each do |submitter| @submitter.submission.submitters.each do |submitter|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name, new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
@@ -27,6 +29,10 @@ class SubmittersResubmitController < ApplicationController
submission.save! submission.save!
@submitter.submission.documents_attachments.each do |attachment|
submission.documents_attachments.create!(uuid: attachment.uuid, blob_id: attachment.blob_id)
end
redirect_to submit_form_path(slug: @new_submitter.slug) redirect_to submit_form_path(slug: @new_submitter.slug)
end end
@@ -6,7 +6,9 @@ class TemplateFoldersAutocompleteController < ApplicationController
LIMIT = 100 LIMIT = 100
def index def index
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT) template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at]) render json: template_folders.as_json(only: %i[name archived_at])
@@ -6,7 +6,7 @@ class TemplateFoldersController < ApplicationController
def show def show
@templates = @template_folder.templates.active.accessible_by(current_ability) @templates = @template_folder.templates.active.accessible_by(current_ability)
.preload(:author, :template_accesses) .preload(:author, :template_accesses)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order]) @templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
@pagy, @templates = pagy_auto(@templates, limit: 12) @pagy, @templates = pagy_auto(@templates, limit: 12)
@@ -5,7 +5,7 @@ class TemplatesArchivedController < ApplicationController
def index def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(current_user, @templates, params[:q])
@pagy, @templates = pagy_auto(@templates, limit: 12) @pagy, @templates = pagy_auto(@templates, limit: 12)
end end
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index def index
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true) @submissions = Submissions.search(current_user, @submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params) @submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@@ -22,6 +22,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
Templates::CloneAttachments.call(template: cloned_template, original_template: @template, Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
excluded_attachment_uuids: documents.map(&:uuid)) excluded_attachment_uuids: documents.map(&:uuid))
SearchEntries.enqueue_reindex(cloned_template)
respond_to do |f| respond_to do |f|
f.html { redirect_to edit_template_path(cloned_template) } f.html { redirect_to edit_template_path(cloned_template) }
f.json { render json: { id: cloned_template.id } } f.json { render json: { id: cloned_template.id } }
+10 -2
View File
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
def show def show
submissions = @template.submissions.accessible_by(current_ability) submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true) submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status)) submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
@base_submissions = submissions @base_submissions = submissions
@@ -72,6 +72,8 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SearchEntries.enqueue_reindex(@template)
enqueue_template_created_webhooks(@template) enqueue_template_created_webhooks(@template)
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
@@ -81,7 +83,13 @@ class TemplatesController < ApplicationController
end end
def update def update
@template.update!(template_params) @template.assign_attributes(template_params)
is_name_changed = @template.name_changed?
@template.save!
SearchEntries.enqueue_reindex(@template) if is_name_changed
enqueue_template_updated_webhooks(@template) enqueue_template_updated_webhooks(@template)
@@ -25,7 +25,7 @@ class TemplatesDashboardController < ApplicationController
@templates = @templates.none @templates = @templates.none
else else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates) @templates = filter_templates(@templates).preload(:author, :template_accesses)
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order]) @templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
limit = limit =
@@ -36,13 +36,15 @@ class TemplatesDashboardController < ApplicationController
end end
@pagy, @templates = pagy_auto(@templates, limit:) @pagy, @templates = pagy_auto(@templates, limit:)
load_related_submissions if params[:q].present? && @templates.blank?
end end
end end
private private
def filter_templates(templates) def filter_templates(templates)
rel = templates.active.preload(:author, :template_accesses) rel = templates.active
if params[:q].blank? if params[:q].blank?
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
@@ -51,13 +53,18 @@ class TemplatesDashboardController < ApplicationController
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id) 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)) rel = Template.where(
Template.arel_table[:id].in(
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
.union(shared_template_ids.arel)
)
)
else else
rel = rel.where(folder_id: current_account.default_template_folder.id) rel = rel.where(folder_id: current_account.default_template_folder.id)
end end
end end
Templates.search(rel, params[:q]) Templates.search(current_user, rel, params[:q])
end end
def sort_template_folders(template_folders, current_user, order) def sort_template_folders(template_folders, current_user, order)
@@ -93,4 +100,19 @@ class TemplatesDashboardController < ApplicationController
template_folders.order(id: :desc) template_folders.order(id: :desc)
end end
end end
def load_related_submissions
@related_submissions = Submission.accessible_by(current_ability)
.left_joins(:template)
.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end
end end
@@ -13,7 +13,7 @@ class TemplatesFormPreviewController < ApplicationController
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) } @submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
Submitters.preload_with_pages(@submitter) Submissions.preload_with_pages(@submitter.submission)
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments) @attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
@@ -25,13 +25,13 @@ class TemplatesPreferencesController < ApplicationController
documents_copy_email_attach_documents documents_copy_email_reply_to documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters completed_redirect_url validate_unique_submitters
submitters_order require_phone_2fa require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration default_expire_at_duration
default_expire_at default_expire_at
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body], [completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]] submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
).tap do |attrs| ).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1' attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
@@ -10,7 +10,11 @@ class TemplatesShareLinkController < ApplicationController
@template.update!(template_params) @template.update!(template_params)
head :ok if params[:redir].present?
redirect_to params[:redir]
else
head :ok
end
end end
private private
@@ -25,6 +25,8 @@ class TemplatesUploadsController < ApplicationController
enqueue_template_created_webhooks(@template) enqueue_template_created_webhooks(@template)
SearchEntries.enqueue_reindex(@template)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
+2
View File
@@ -35,6 +35,7 @@ import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox' import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour' import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone' import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -105,6 +106,7 @@ safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour) safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone) safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick) safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
+1 -1
View File
@@ -1,7 +1,7 @@
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.addEventListener('click', () => { this.addEventListener('click', () => {
if (!this.element.checked) { if (this.element && !this.element.disabled && !this.element.checked) {
this.element.checked = true this.element.checked = true
this.element.dispatchEvent(new Event('change', { bubbles: true })) this.element.dispatchEvent(new Event('change', { bubbles: true }))
} }
@@ -0,0 +1,17 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', this.handleChange)
})
}
handleChange = () => {
if (this.checkedCount !== 0) {
this.closest('form')?.requestSubmit()
}
}
get checkedCount () {
return this.querySelectorAll('input[type="checkbox"]:checked').length
}
}
@@ -36,7 +36,9 @@ export default class extends HTMLElement {
} }
fetch = (text, resolve) => { fetch = (text, resolve) => {
const q = text.split(/[;,\s]+/).pop().trim() const q = this.dataset.field === 'email'
? text.split(/[;,\s]+/).pop().trim()
: text
if (q) { if (q) {
const queryParams = new URLSearchParams({ q, field: this.dataset.field }) const queryParams = new URLSearchParams({ q, field: this.dataset.field })
+1
View File
@@ -1390,6 +1390,7 @@ export default {
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) { if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
formData.append('completed', 'true') formData.append('completed', 'true')
formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone)
} }
let saveStepRequest let saveStepRequest
@@ -203,7 +203,7 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
data () { data () {
return { return {
isInitialsStarted: !!this.previousValue, isInitialsStarted: false,
isUsePreviousValue: true, isUsePreviousValue: true,
isDrawInitials: false, isDrawInitials: false,
uploadImageInputKey: Math.random().toString() uploadImageInputKey: Math.random().toString()
@@ -218,6 +218,9 @@ export default {
} }
} }
}, },
created () {
this.isInitialsStarted = !!this.computedPreviousValue
},
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
@@ -362,11 +365,20 @@ export default {
formData.append('file', file) formData.append('file', file)
formData.append('submitter_slug', this.submitterSlug) formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments') formData.append('name', 'attachments')
formData.append('type', 'initials')
return fetch(this.baseUrl + '/api/attachments', { return fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then((resp) => resp.json()).then((attachment) => { }).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
return Promise.reject(new Error(data.error))
}
const attachment = await resp.json()
this.$emit('attached', attachment) this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid) this.$emit('update:model-value', attachment.uuid)
@@ -133,7 +133,7 @@
class="mx-auto bg-white border border-base-300 rounded max-h-44" class="mx-auto bg-white border border-base-300 rounded max-h-44"
> >
<FileDropzone <FileDropzone
v-if="format === 'upload' && !modelValue" v-if="format === 'upload' && !modelValue && !computedPreviousValue"
:message="`${t('upload')} ${field.name || t('signature')}`" :message="`${t('upload')} ${field.name || t('signature')}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:dry-run="dryRun" :dry-run="dryRun"
@@ -201,7 +201,7 @@
</div> </div>
</div> </div>
<input <input
v-if="isTextSignature" v-if="isTextSignature && !modelValue && !computedPreviousValue"
id="signature_text_input" id="signature_text_input"
ref="textInput" ref="textInput"
class="base-input !text-2xl w-full mt-6" class="base-input !text-2xl w-full mt-6"
@@ -391,7 +391,7 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
data () { data () {
return { return {
isSignatureStarted: !!this.previousValue, isSignatureStarted: false,
isShowQr: false, isShowQr: false,
isOtherReason: false, isOtherReason: false,
isUsePreviousValue: true, isUsePreviousValue: true,
@@ -422,6 +422,8 @@ export default {
} }
}, },
created () { created () {
this.isSignatureStarted = !!this.computedPreviousValue
if (this.requireSigningReason) { if (this.requireSigningReason) {
this.field.preferences ||= {} this.field.preferences ||= {}
this.field.preferences.reason_field_uuid ||= v4() this.field.preferences.reason_field_uuid ||= v4()
@@ -716,11 +718,20 @@ export default {
formData.append('submitter_slug', this.submitterSlug) formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments') formData.append('name', 'attachments')
formData.append('remember_signature', this.rememberSignature) formData.append('remember_signature', this.rememberSignature)
formData.append('type', 'signature')
return fetch(this.baseUrl + '/api/attachments', { return fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then((resp) => resp.json()).then((attachment) => { }).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
return Promise.reject(new Error(data.error))
}
const attachment = await resp.json()
this.$emit('attached', attachment) this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid) this.$emit('update:model-value', attachment.uuid)
@@ -1052,6 +1052,12 @@ export default {
field.readonly = true field.readonly = true
} }
if (type === 'datenow') {
field.type = 'date'
field.readonly = true
field.default_value = '{{date}}'
}
if (type === 'date') { if (type === 'date') {
field.preferences = { field.preferences = {
format: this.defaultDateFormat format: this.defaultDateFormat
@@ -1450,6 +1456,12 @@ export default {
} }
} }
if (field.type === 'datenow') {
field.type = 'date'
field.readonly = true
field.default_value = '{{date}}'
}
if (['stamp', 'heading'].includes(field.type)) { if (['stamp', 'heading'].includes(field.type)) {
field.readonly = true field.readonly = true
} }
@@ -424,7 +424,7 @@
</a> </a>
</li> </li>
</template> </template>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)"> <li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -51,7 +51,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck } from '@tabler/icons-vue'
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
@@ -101,6 +101,7 @@ export default {
signature: this.t('signature'), signature: this.t('signature'),
initials: this.t('initials'), initials: this.t('initials'),
date: this.t('date'), date: this.t('date'),
datenow: this.t('date_signed'),
number: this.t('number'), number: this.t('number'),
image: this.t('image'), image: this.t('image'),
file: this.t('file'), file: this.t('file'),
@@ -142,6 +143,7 @@ export default {
signature: IconWritingSign, signature: IconWritingSign,
initials: IconLetterCaseUpper, initials: IconLetterCaseUpper,
date: IconCalendarEvent, date: IconCalendarEvent,
datenow: IconCalendarCheck,
number: IconSquareNumber1, number: IconSquareNumber1,
image: IconPhoto, image: IconPhoto,
checkbox: IconCheckbox, checkbox: IconCheckbox,
@@ -164,7 +166,7 @@ export default {
return acc return acc
}, {}) }, {})
} else { } else {
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading')) return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
} }
} }
}, },
+1 -1
View File
@@ -347,7 +347,7 @@ export default {
return acc return acc
}, {}) }, {})
} else { } else {
return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading')) return Object.fromEntries(Object.entries(this.fieldIcons).filter(([key]) => key !== 'heading' && key !== 'datenow'))
} }
}, },
submitterFields () { submitterFields () {
+6
View File
@@ -1,6 +1,7 @@
const en = { const en = {
font: 'Font', font: 'Font',
party: 'Party', party: 'Party',
date_signed: 'Date Signed',
method: 'Method', method: 'Method',
reorder_fields: 'Reorder fields', reorder_fields: 'Reorder fields',
verify_id: 'Verify ID', verify_id: 'Verify ID',
@@ -167,6 +168,7 @@ const en = {
} }
const es = { const es = {
date_signed: 'Fecha actual',
fuente: 'Fuente', fuente: 'Fuente',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
@@ -335,6 +337,7 @@ const es = {
} }
const it = { const it = {
date_signed: 'Data attuale',
font: 'Carattere', font: 'Carattere',
party: 'Parte', party: 'Parte',
method: 'Metodo', method: 'Metodo',
@@ -503,6 +506,7 @@ const it = {
} }
const pt = { const pt = {
date_signed: 'Data atual',
fonte: 'Fonte', fonte: 'Fonte',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
@@ -671,6 +675,7 @@ const pt = {
} }
const fr = { const fr = {
date_signed: 'Date actuelle',
font: 'Police', font: 'Police',
party: 'Partie', party: 'Partie',
method: 'Méthode', method: 'Méthode',
@@ -839,6 +844,7 @@ const fr = {
} }
const de = { const de = {
date_now: 'Akt. Datum',
font: 'Schriftart', font: 'Schriftart',
party: 'Partei', party: 'Partei',
method: 'Verfahren', method: 'Verfahren',
@@ -406,7 +406,7 @@ export default {
this.mappings.every((m) => m.column_index !== index) this.mappings.every((m) => m.column_index !== index)
}) })
if (columnIndex !== -1) { if (columnIndex !== -1 && this.rows.some((row) => row[columnIndex])) {
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid }) this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
} }
}) })
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class GenerateAttachmentPreviewJob
include Sidekiq::Job
InvalidFormat = Class.new(StandardError)
sidekiq_options queue: :images
def perform(params = {})
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
if attachment.content_type == Templates::ProcessDocument::PDF_CONTENT_TYPE
Templates::ProcessDocument.generate_pdf_preview_images(attachment, attachment.download)
elsif attachment.image?
Templates::ProcessDocument.generate_preview_image(attachment, attachment.download)
else
raise InvalidFormat, attachment.id
end
end
end
+1 -1
View File
@@ -7,7 +7,7 @@ class ProcessSubmissionExpiredJob
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
return if submission.archived_at? return if submission.archived_at?
return if submission.template.archived_at? return if submission.template&.archived_at?
return if submission.submitters.where.not(declined_at: nil).exists? return if submission.submitters.where.not(declined_at: nil).exists?
return unless submission.submitters.exists?(completed_at: nil) return unless submission.submitters.exists?(completed_at: nil)
+3 -3
View File
@@ -82,7 +82,7 @@ class ProcessSubmitterCompletionJob
user = submission.created_by_user || submitter.template.author user = submission.created_by_user || submitter.template.author
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false && if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
submitter.template.preferences['completed_notification_email_enabled'] != false submitter.template&.preferences&.dig('completed_notification_email_enabled') != false
if submission.submitters.map(&:email).exclude?(user.email) && if submission.submitters.map(&:email).exclude?(user.email) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false && user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
user.role != 'integration' user.role != 'integration'
@@ -98,7 +98,7 @@ class ProcessSubmitterCompletionJob
end end
def maybe_enqueue_copy_emails(submitter) def maybe_enqueue_copy_emails(submitter)
return if submitter.template.preferences['documents_copy_email_enabled'] == false return if submitter.template&.preferences&.dig('documents_copy_email_enabled') == false
configs = AccountConfigs.find_or_initialize_for_key(submitter.account, configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
@@ -119,7 +119,7 @@ class ProcessSubmitterCompletionJob
def build_bcc_addresses(submission) def build_bcc_addresses(submission)
bcc = submission.preferences['bcc_completed'].presence || bcc = submission.preferences['bcc_completed'].presence ||
submission.template.preferences['bcc_completed'].presence || submission.template&.preferences&.dig('bcc_completed').presence ||
submission.account.account_configs submission.account.account_configs
.find_by(key: AccountConfig::BCC_EMAILS)&.value .find_by(key: AccountConfig::BCC_EMAILS)&.value
@@ -0,0 +1,9 @@
# frozen_string_literal: true
class ReindexAllSearchEntriesJob
include Sidekiq::Job
def perform
SearchEntries.reindex_all
end
end
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class ReindexSearchEntryJob
include Sidekiq::Job
InvalidFormat = Class.new(StandardError)
def perform(params = {})
entry = SearchEntry.find_or_initialize_by(params.slice('record_type', 'record_id'))
SearchEntries.reindex_record(entry.record)
end
end
+20 -20
View File
@@ -23,11 +23,11 @@ class SubmitterMailer < ApplicationMailer
@body = @email_message&.body.presence || @body = @email_message&.body.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence || template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
@submitter.template.preferences['request_email_body'].presence @submitter.template&.preferences&.dig('request_email_body').presence
@subject = @email_message&.subject.presence || @subject = @email_message&.subject.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence || template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence ||
@submitter.template.preferences['request_email_subject'].presence @submitter.template&.preferences&.dig('request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@@ -53,6 +53,8 @@ class SubmitterMailer < ApplicationMailer
@submission = submitter.submission @submission = submitter.submission
@user = user @user = user
template_preferences = @submission.template&.preferences || {}
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
@@ -60,15 +62,15 @@ class SubmitterMailer < ApplicationMailer
add_completed_email_attachments!( add_completed_email_attachments!(
submitter, submitter,
with_documents: @email_config&.value&.dig('attach_documents') != false && with_documents: @email_config&.value&.dig('attach_documents') != false &&
@submitter.template.preferences['completed_notification_email_attach_documents'] != false, template_preferences['completed_notification_email_attach_documents'] != false,
with_audit_log: @email_config&.value&.dig('attach_audit_log') != false && with_audit_log: @email_config&.value&.dig('attach_audit_log') != false &&
@submitter.template.preferences['completed_notification_email_attach_audit'] != false template_preferences['completed_notification_email_attach_audit'] != false
) )
@subject = @submitter.template.preferences['completed_notification_email_subject'].presence @subject = template_preferences['completed_notification_email_subject'].presence
@subject ||= @email_config.value['subject'] if @email_config @subject ||= @email_config.value['subject'] if @email_config
@body = @submitter.template.preferences['completed_notification_email_body'].presence @body = template_preferences['completed_notification_email_body'].presence
@body ||= @email_config.value['body'] if @email_config @body ||= @email_config.value['body'] if @email_config
assign_message_metadata('submitter_completed', @submitter) assign_message_metadata('submitter_completed', @submitter)
@@ -97,7 +99,7 @@ class SubmitterMailer < ApplicationMailer
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name, to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
reply_to: @submitter.friendly_name, reply_to: @submitter.friendly_name,
subject: I18n.t(:name_declined_by_submitter, subject: I18n.t(:name_declined_by_submitter,
name: @submission.template.name.truncate(20), name: (@submission.name || @submission.template.name).truncate(20),
submitter: @submitter.name || @submitter.email || @submitter.phone)) submitter: @submitter.name || @submitter.email || @submitter.phone))
end end
end end
@@ -107,22 +109,24 @@ class SubmitterMailer < ApplicationMailer
@submitter = submitter @submitter = submitter
@sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig @sig = submitter.signed_id(expires_in: SIGN_TTL, purpose: :download_completed) if sig
template_preferences = @submitter.template&.preferences || {}
Submissions::EnsureResultGenerated.call(@submitter) Submissions::EnsureResultGenerated.call(@submitter)
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
add_completed_email_attachments!( add_completed_email_attachments!(
submitter, submitter,
with_documents: @submitter.template.preferences['documents_copy_email_attach_documents'] != false && with_documents: template_preferences['documents_copy_email_attach_documents'] != false &&
(@email_config.nil? || @email_config.value['attach_documents'] != false), (@email_config.nil? || @email_config.value['attach_documents'] != false),
with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false && with_audit_log: template_preferences['documents_copy_email_attach_audit'] != false &&
(@email_config.nil? || @email_config.value['attach_audit_log'] != false) (@email_config.nil? || @email_config.value['attach_audit_log'] != false)
) )
@subject = @submitter.template.preferences['documents_copy_email_subject'].presence @subject = template_preferences['documents_copy_email_subject'].presence
@subject ||= @email_config.value['subject'] if @email_config @subject ||= @email_config.value['subject'] if @email_config
@body = @submitter.template.preferences['documents_copy_email_body'].presence @body = template_preferences['documents_copy_email_body'].presence
@body ||= @email_config.value['body'] if @email_config @body ||= @email_config.value['body'] if @email_config
assign_message_metadata('submitter_documents_copy', @submitter) assign_message_metadata('submitter_documents_copy', @submitter)
@@ -130,11 +134,7 @@ class SubmitterMailer < ApplicationMailer
I18n.with_locale(@current_account.locale) do I18n.with_locale(@current_account.locale) do
subject = subject =
if @subject.present? @subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy)
ReplaceEmailVariables.call(@subject, submitter:)
else
I18n.t(:your_document_copy)
end
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
to: to || @submitter.friendly_name, to: to || @submitter.friendly_name,
@@ -147,7 +147,7 @@ class SubmitterMailer < ApplicationMailer
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil) def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
reply_to = submitter.preferences['reply_to'].presence reply_to = submitter.preferences['reply_to'].presence
reply_to ||= submitter.template.preferences['documents_copy_email_reply_to'].presence if documents_copy_email reply_to ||= submitter.template&.preferences&.dig('documents_copy_email_reply_to').presence if documents_copy_email
reply_to ||= email_config.value['reply_to'].presence if email_config reply_to ||= email_config.value['reply_to'].presence if email_config
if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
@@ -212,7 +212,7 @@ class SubmitterMailer < ApplicationMailer
end end
def build_submitter_preferences_index(submitter) def build_submitter_preferences_index(submitter)
submitter.template.preferences['submitters'].to_a.index_by { |e| e['uuid'] } submitter.template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] }
end end
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil) def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
+2
View File
@@ -29,6 +29,7 @@ class AccountConfig < ApplicationRecord
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit' ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
ALLOW_TO_DECLINE_KEY = 'allow_to_decline' ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
ALLOW_TO_PARTIAL_DOWNLOAD_KEY = 'allow_to_partial_download'
SUBMITTER_REMINDERS = 'submitter_reminders' SUBMITTER_REMINDERS = 'submitter_reminders'
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order' ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
@@ -41,6 +42,7 @@ class AccountConfig < ApplicationRecord
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'
WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_AUDIT_SUBMITTER_TIMEZONE_KEY = 'with_audit_submitter_timezone'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature' REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
+2 -2
View File
@@ -13,7 +13,7 @@
# account_id :bigint not null # account_id :bigint not null
# submission_id :bigint not null # submission_id :bigint not null
# submitter_id :bigint not null # submitter_id :bigint not null
# template_id :bigint not null # template_id :bigint
# #
# Indexes # Indexes
# #
@@ -24,7 +24,7 @@ class CompletedSubmitter < ApplicationRecord
belongs_to :submitter belongs_to :submitter
belongs_to :submission belongs_to :submission
belongs_to :account belongs_to :account
belongs_to :template belongs_to :template, optional: true
has_many :completed_documents, dependent: :destroy, has_many :completed_documents, dependent: :destroy,
primary_key: :submitter_id, primary_key: :submitter_id,
+1
View File
@@ -20,6 +20,7 @@
# #
# index_email_events_on_account_id_and_event_datetime (account_id,event_datetime) # index_email_events_on_account_id_and_event_datetime (account_id,event_datetime)
# index_email_events_on_email (email) # index_email_events_on_email (email)
# index_email_events_on_email_event_types (email) WHERE ((event_type)::text = ANY ((ARRAY['bounce'::character varying, 'soft_bounce'::character varying, 'complaint'::character varying, 'soft_complaint'::character varying])::text[]))
# index_email_events_on_emailable (emailable_type,emailable_id) # index_email_events_on_emailable (emailable_type,emailable_id)
# index_email_events_on_message_id (message_id) # index_email_events_on_message_id (message_id)
# #
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: search_entries
#
# id :bigint not null, primary key
# ngram :tsvector
# record_type :string not null
# tsvector :tsvector not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# record_id :bigint not null
#
# Indexes
#
# index_search_entries_on_account_id_ngram_submission (account_id,ngram) WHERE ((record_type)::text = 'Submission'::text) USING gin
# index_search_entries_on_account_id_ngram_submitter (account_id,ngram) WHERE ((record_type)::text = 'Submitter'::text) USING gin
# index_search_entries_on_account_id_ngram_template (account_id,ngram) WHERE ((record_type)::text = 'Template'::text) USING gin
# index_search_entries_on_account_id_tsvector_submission (account_id,tsvector) WHERE ((record_type)::text = 'Submission'::text) USING gin
# index_search_entries_on_account_id_tsvector_submitter (account_id,tsvector) WHERE ((record_type)::text = 'Submitter'::text) USING gin
# index_search_entries_on_account_id_tsvector_template (account_id,tsvector) WHERE ((record_type)::text = 'Template'::text) USING gin
# index_search_entries_on_record_id_and_record_type (record_id,record_type) UNIQUE
#
class SearchEntry < ApplicationRecord
belongs_to :record, polymorphic: true
belongs_to :account
end
+20 -6
View File
@@ -7,6 +7,7 @@
# id :bigint not null, primary key # id :bigint not null, primary key
# archived_at :datetime # archived_at :datetime
# expire_at :datetime # expire_at :datetime
# name :text
# preferences :text not null # preferences :text not null
# slug :string not null # slug :string not null
# source :text not null # source :text not null
@@ -18,14 +19,16 @@
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null # account_id :bigint not null
# created_by_user_id :bigint # created_by_user_id :bigint
# template_id :bigint not null # template_id :bigint
# #
# Indexes # Indexes
# #
# index_submissions_on_account_id_and_id (account_id,id) # index_submissions_on_account_id_and_id (account_id,id)
# index_submissions_on_created_by_user_id (created_by_user_id) # index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
# index_submissions_on_slug (slug) UNIQUE # index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
# index_submissions_on_template_id (template_id) # index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id)
# #
# Foreign Keys # Foreign Keys
# #
@@ -33,10 +36,12 @@
# fk_rails_... (template_id => templates.id) # fk_rails_... (template_id => templates.id)
# #
class Submission < ApplicationRecord class Submission < ApplicationRecord
belongs_to :template belongs_to :template, optional: true
belongs_to :account belongs_to :account
belongs_to :created_by_user, class_name: 'User', optional: true belongs_to :created_by_user, class_name: 'User', optional: true
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
has_many :submitters, dependent: :destroy has_many :submitters, dependent: :destroy
has_many :submission_events, dependent: :destroy has_many :submission_events, dependent: :destroy
@@ -56,6 +61,7 @@ class Submission < ApplicationRecord
has_one_attached :combined_document has_one_attached :combined_document
has_many_attached :preview_documents has_many_attached :preview_documents
has_many_attached :documents
has_many :template_accesses, primary_key: :template_id, foreign_key: :template_id, dependent: nil, inverse_of: false has_many :template_accesses, primary_key: :template_id, foreign_key: :template_id, dependent: nil, inverse_of: false
@@ -96,6 +102,14 @@ class Submission < ApplicationRecord
expire_at && expire_at <= Time.current expire_at && expire_at <= Time.current
end end
def schema_documents
if template_id?
template_schema_documents
else
documents_attachments
end
end
def fields_uuid_index def fields_uuid_index
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] } @fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
end end
+8 -5
View File
@@ -16,6 +16,7 @@
# preferences :text not null # preferences :text not null
# sent_at :datetime # sent_at :datetime
# slug :string not null # slug :string not null
# timezone :string
# ua :string # ua :string
# uuid :string not null # uuid :string not null
# values :text not null # values :text not null
@@ -27,11 +28,12 @@
# #
# Indexes # Indexes
# #
# index_submitters_on_account_id_and_id (account_id,id) # index_submitters_on_account_id_and_id (account_id,id)
# index_submitters_on_email (email) # index_submitters_on_completed_at_and_account_id (completed_at,account_id)
# index_submitters_on_external_id (external_id) # index_submitters_on_email (email)
# index_submitters_on_slug (slug) UNIQUE # index_submitters_on_external_id (external_id)
# index_submitters_on_submission_id (submission_id) # index_submitters_on_slug (slug) UNIQUE
# index_submitters_on_submission_id (submission_id)
# #
# Foreign Keys # Foreign Keys
# #
@@ -41,6 +43,7 @@ class Submitter < ApplicationRecord
belongs_to :submission belongs_to :submission
belongs_to :account belongs_to :account
has_one :template, through: :submission has_one :template, through: :submission
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
attribute :values, :string, default: -> { {} } attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
+9 -5
View File
@@ -23,11 +23,13 @@
# #
# Indexes # Indexes
# #
# index_templates_on_account_id (account_id) # index_templates_on_account_id (account_id)
# index_templates_on_author_id (author_id) # index_templates_on_account_id_and_folder_id_and_id (account_id,folder_id,id) WHERE (archived_at IS NULL)
# index_templates_on_external_id (external_id) # index_templates_on_account_id_and_id_archived (account_id,id) WHERE (archived_at IS NOT NULL)
# index_templates_on_folder_id (folder_id) # index_templates_on_author_id (author_id)
# index_templates_on_slug (slug) UNIQUE # index_templates_on_external_id (external_id)
# index_templates_on_folder_id (folder_id)
# index_templates_on_slug (slug) UNIQUE
# #
# Foreign Keys # Foreign Keys
# #
@@ -42,6 +44,8 @@ class Template < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :folder, class_name: 'TemplateFolder' belongs_to :folder, class_name: 'TemplateFolder'
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
before_validation :maybe_set_default_folder, on: :create before_validation :maybe_set_default_folder, on: :create
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
Whitespace-only changes.
+13 -1
View File
@@ -14,7 +14,10 @@
<div class="form-control"> <div class="form-control">
<%= ff.label :timezone, t('time_zone'), class: 'label' %> <%= ff.label :timezone, t('time_zone'), class: 'label' %>
<%= ff.select :timezone, nil, {}, class: 'base-select' do %> <%= ff.select :timezone, nil, {}, class: 'base-select' do %>
<%= time_zone_options_for_select(current_account.timezone) %> <% tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[current_account.timezone] || current_account.timezone) %>
<% items = ActiveSupport::TimeZone.all.map { |z| [z.to_s, z.name] } %>
<% items.unshift([tzinfo.to_s, current_account.timezone]) unless ActiveSupport::TimeZone.all.find { |e| e.tzinfo == tzinfo } %>
<%= options_for_select(items, current_account.timezone) %>
<% end %> <% end %>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -165,6 +168,15 @@
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
<%= render 'extra_preferences' %>
<% if !Docuseal.multitenant? && SearchEntry.table_exists? && (!Docuseal.fulltext_search? || params[:reindex] == 'true') && can?(:manage, EncryptedConfig) %>
<div class="flex items-center justify-between py-2.5">
<span>
Efficient search with search index
</span>
<%= button_to params[:reindex] == 'true' ? 'Reindex' : 'Build Search Index', settings_search_entries_reindex_index_path, method: :post, class: 'btn btn-sm btn-neutral text-white px-4' %>
</div>
<% end %>
</div> </div>
<% end %> <% end %>
<%= render 'compliances' %> <%= render 'compliances' %>
@@ -20,7 +20,7 @@
</a> </a>
</div> </div>
</div> </div>
<div id="us_server_selector" class="flex justify-center hidden"> <div id="server_selector" class="flex justify-center <%= 'hidden' unless eu_server %>">
<div class="dropdown"> <div class="dropdown">
<label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px"> <label tabindex="0" class="relative btn btn-sm bg-transparent font-medium normal-case border-base-content/20 justify-start" style="width: 141px; padding: 0 20px">
<% if eu_server %> <% if eu_server %>
+6 -2
View File
@@ -2,14 +2,18 @@
if (!window.customElements.get('server-selector')) { if (!window.customElements.get('server-selector')) {
customElements.define('server-selector', class extends HTMLElement { customElements.define('server-selector', class extends HTMLElement {
connectedCallback() { connectedCallback() {
const usServerSelector = this.querySelector('#us_server_selector'); const serverSelector = this.querySelector('#server_selector');
const globalServerSelector = this.querySelector('#global_server_selector'); const globalServerSelector = this.querySelector('#global_server_selector');
const euServerAlert = this.querySelector('#eu_server_alert'); const euServerAlert = this.querySelector('#eu_server_alert');
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/; const usTimezones = /^(?:America\/(?:New_York|Detroit|Kentucky|Indiana|Chicago|Menominee|North_Dakota|Denver|Boise|Phoenix|Los_Angeles|Anchorage|Juneau|Sitka|Metlakatla|Yakutat|Nome|Adak)|Pacific\/Honolulu)/;
if (!serverSelector.classList.contains('hidden')) {
return
}
if (usTimezones.test(timezone)) { if (usTimezones.test(timezone)) {
usServerSelector.classList.remove('hidden'); serverSelector.classList.remove('hidden');
} else if (timezone.includes('Europe')) { } else if (timezone.includes('Europe')) {
globalServerSelector.classList.remove('hidden'); globalServerSelector.classList.remove('hidden');
euServerAlert?.classList?.remove('hidden'); euServerAlert?.classList?.remove('hidden');
+12 -10
View File
@@ -1,27 +1,29 @@
<% link = pagy_anchor(@pagy) %> <% link = pagy_anchor(pagy) %>
<% if @pagy.pages > 1 %> <% if pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between"> <div class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm"> <div class="hidden md:block text-sm">
<% if @pagy.count.nil? %> <% if pagy.count.nil? %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to)) %> <%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_without_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to)) %>
<% else %> <% else %>
<%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, @pagy.from), to: local_assigns.fetch(:to, @pagy.to), count: local_assigns.fetch(:count, @pagy.count)) %> <%= t("pagination.#{local_assigns.fetch(:items_name, 'items')}.range_with_total", from: local_assigns.fetch(:from, pagy.from), to: local_assigns.fetch(:to, pagy.to), count: local_assigns.fetch(:count, pagy.count)) %>
<% end %> <% end %>
<%= local_assigns[:left_additional_html] %> <%= local_assigns[:left_additional_html] %>
</div> </div>
<div class="flex items-center space-x-1.5"> <div class="flex items-center space-x-1.5">
<%= local_assigns[:right_additional_html] %> <%= local_assigns[:right_additional_html] %>
<div class="join"> <div class="join">
<% if @pagy.prev %> <% if pagy.prev %>
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %> <%== link.call(pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
<% else %> <% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span> <span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
<% end %> <% end %>
<span class="join-item btn font-medium uppercase min-h-full h-10"> <span class="join-item btn font-medium uppercase min-h-full h-10">
<%= t('page_number', number: @pagy.page) %> <%= t('page_number', number: pagy.page) %>
</span> </span>
<% if @pagy.next %> <% if local_assigns[:next_page_path].present? %>
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %> <%= link_to '»', local_assigns[:next_page_path], class: 'join-item btn min-h-full h-10' %>
<% elsif pagy.next %>
<%== link.call(pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
<% else %> <% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span> <span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
<% end %> <% end %>
+2 -2
View File
@@ -26,9 +26,9 @@
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %> <%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { template_slug: @template.slug, email: params[:email] }, class: 'base-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
<% if Templates.filter_undefined_submitters(@template).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %> <% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</div> </div>
+24 -21
View File
@@ -1,28 +1,31 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %> <% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %> <% I18n.with_locale(@template.account.locale) do %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4"> <% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<div class="space-y-6 mx-auto"> <% end %>
<div class="space-y-6"> <div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'banner' %>
<p class="text-xl font-semibold text-center"> <p class="text-xl font-semibold text-center">
<%= t('share_link_is_currently_disabled') %> <%= t('share_link_is_currently_disabled') %>
</p> </p>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4"> <div>
<div class="flex items-center"> <p class="text-lg font-bold mb-1"><%= @template.name %></p>
<div class="mr-3"> <% if @template.archived_at? %>
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <p dir="auto" class="text-sm"><%= t('form_has_been_deleted_by_html', name: @template.account.name) %></p>
</div> <% end %>
<div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
<% if @template.archived_at? %>
<p dir="auto" class="text-sm"><%= t('form_has_been_deleted_by_html', name: @template.account.name) %></p>
<% end %>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<% if can?(:update, @template) %>
<toggle-submit class="block">
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
</toggle-submit>
<% end %>
</div> </div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %> <%= render 'shared/attribution', link_path: '/start', account: @template.account %>
+29 -8
View File
@@ -1,5 +1,7 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %> <% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_description, "#{@template.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %> <% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4"> <div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto"> <div class="space-y-6 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
@@ -27,13 +29,32 @@
</div> </div>
<% if !@template.archived_at? && !@template.account.archived_at? %> <% if !@template.archived_at? && !@template.account.archived_at? %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %> <%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<div dir="auto" class="form-control !mt-0"> <% if @error_message %>
<%= f.label :email, t('email'), class: 'label' %> <div class="alert items-start bg-base-100 border border-red-400 text-red-400">
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t('provide_your_email_to_start') %> <%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<% if @error_message %> <div class="text-sm"><%= @error_message %></div>
<span class="label-text-alt text-red-400 mt-1"><%= @error_message %></span> </div>
<% end %> <% end %>
</div> <% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<% multiple_fields = link_form_fields.size > 1 %>
<% if link_form_fields.include?('name') %>
<div dir="auto" class="form-control !mt-0">
<%= f.label :name, t('name'), class: 'label' %>
<%= f.text_field :name, value: current_user&.full_name || params[:name] || @submitter.name, required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_name' : 'provide_your_name_to_start') %>
</div>
<% end %>
<% if link_form_fields.include?('email') %>
<div dir="auto" class="form-control !mt-0">
<%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_email' : 'provide_your_email_to_start') %>
</div>
<% end %>
<% if link_form_fields.include?('phone') %>
<div dir="auto" class="form-control !mt-0">
<%= f.label :phone, t('phone'), class: 'label' %>
<%= f.telephone_field :phone, value: params[:phone] || @submitter.phone, pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_phone_in_international_format' : 'provide_your_phone_in_international_format_to_start') %>
</div>
<% end %>
<toggle-submit dir="auto" class="form-control"> <toggle-submit dir="auto" class="form-control">
<%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %> <%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
</toggle-submit> </toggle-submit>
@@ -20,7 +20,7 @@
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero?, value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %> <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>"> <div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
+1 -1
View File
@@ -29,7 +29,7 @@
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('email'), required: index.zero?, value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %> <%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('email'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
</submitter-item> </submitter-item>
+1 -1
View File
@@ -21,7 +21,7 @@
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= 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 w-full', placeholder: t('phone'), required: index.zero?, id: "phone_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 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
<% if submitters.size > 1 %> <% if submitters.size > 1 %>
+11 -10
View File
@@ -1,4 +1,5 @@
<% submitter_preferences_index = template.preferences['submitters'].to_a.index_by { |e| e['uuid'] } %> <% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
<div class="form-control"> <div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %> <% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@@ -37,18 +38,18 @@
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %> <% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="message_field" class="card card-compact bg-base-300/40 hidden"> <div id="message_field" class="card card-compact bg-base-300/40 hidden">
<div class="card-body"> <div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template.preferences['submitters'].to_a.size > 1 %> <%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2"> <div class="peer-checked:hidden form-control space-y-2">
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %> <%= f.label :subject, t('subject'), class: 'label' %>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %> <% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline"> <label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %> <%= t('edit_per_party') %>
</label> </label>
<% end %> <% end %>
</div> </div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %> <%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
@@ -58,7 +59,7 @@
</span> </span>
</div> </div>
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %> <%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %> <% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer"> <label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
@@ -69,10 +70,10 @@
</div> </div>
<%= render 'submissions/message_fields' %> <%= render 'submissions/message_fields' %>
</div> </div>
<% if template.submitters.size > 1 && template.submitters.size < 5 && local_assigns[:message_per_submitter] != false %> <% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2"> <div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %> <% uuid = SecureRandom.uuid %>
<% options = template.submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %> <% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap"> <ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %> <% options.each_with_index do |(label, val), index| %>
@@ -83,14 +84,14 @@
<% end %> <% end %>
</ul> </ul>
</toggle-visible> </toggle-visible>
<% template.submitters.each_with_index do |submitter, index| %> <% template_submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %> <%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>"> <div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
</div> </div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %> <%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
@@ -100,7 +101,7 @@
</span> </span>
</div> </div>
<autoresize-textarea> <autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %> <%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
</div> </div>
</div> </div>
+2 -2
View File
@@ -28,9 +28,9 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% elsif field['type'].in?(['image', 'initials', 'stamp']) %> <% elsif field['type'].in?(['image', 'initials', 'stamp']) && attachments_index[value].image? %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment']) %> <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
<div class="px-0.5 flex flex-col justify-center"> <div class="px-0.5 flex flex-col justify-center">
<% Array.wrap(value).each do |val| %> <% Array.wrap(value).each do |val| %>
+15 -13
View File
@@ -4,9 +4,9 @@
<% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %> <% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %>
<div style="max-width: 1600px" class="mx-auto pl-4"> <div style="max-width: 1600px" class="mx-auto pl-4">
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100"> <div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
<a href="<%= signed_in? && @submission.account_id == current_account&.id ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1"> <a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
<span><%= render 'submissions/logo' %></span> <span><%= render 'submissions/logo' %></span>
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% @submission.template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %></span> <span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %></span>
</a> </a>
<div class="space-x-3 flex items-center"> <div class="space-x-3 flex items-center">
<% last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %> <% last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
@@ -73,13 +73,15 @@
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% schema = Submissions.filtered_conditions_schema(@submission, values:) %> <% schema = Submissions.filtered_conditions_schema(@submission, values:) %>
<% schema.each do |item| %> <% schema.each do |item| %>
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %> <% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> <% if document.preview_images.first %>
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy"> <a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<div class="pb-2 pt-1.5 text-center" dir="auto"> <img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<%= item['name'].presence || document.filename.base %> <div class="pb-2 pt-1.5 text-center" dir="auto">
</div> <%= item['name'].presence || document.filename.base %>
</a> </div>
</a>
<% end %>
<% end %> <% end %>
</div> </div>
<div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"> <div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
@@ -89,10 +91,10 @@
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %> <% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %> <% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<% schema.each do |item| %> <% schema.each do |item| %>
<% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %> <% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %> <% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.first.metadata %> <% lazyload_metadata = document.preview_images.first&.metadata || {} %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %> <% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %> <% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative"> <div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
@@ -242,9 +244,9 @@
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
</div> </div>
<% elsif field['type'].in?(['image', 'stamp']) %> <% elsif field['type'].in?(['image', 'stamp']) && attachments_index[value].image? %>
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'] == 'file' || field['type'] == 'payment' %> <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<% Array.wrap(value).each do |val| %> <% Array.wrap(value).each do |val| %>
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>"> <a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">
@@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<% view_archived_html = capture do %> <% view_archived_html = capture do %>
<% if current_account.submissions.where.not(archived_at: nil).exists? %> <% if can?(:manage, :countless) || current_account.submissions.where.not(archived_at: nil).exists? %>
<div> <div>
<a href="<%= submissions_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a> <a href="<%= submissions_archived_index_path %>" class="link text-sm"><%= t('view_archived') %></a>
</div> </div>
@@ -11,7 +11,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submission.template.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submission.name || @submission.template.name %></p>
<% last_submitter = @submission.submitters.completed.order(:completed_at).last %> <% last_submitter = @submission.submitters.completed.order(:completed_at).last %>
<% if last_submitter %> <% if last_submitter %>
<p dir="auto" class="text-sm"> <p dir="auto" class="text-sm">
@@ -2,4 +2,4 @@
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %> <% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %> <% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %> <% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template.preferences['completed_redirect_url'] %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template.preferences['completed_message'] || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
+1 -1
View File
@@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div dir="auto"> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.account.name) %></p> <p class="text-sm"><%= t('form_has_been_deleted_by_html', name: @submitter.account.name) %></p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div dir="auto"> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('awaiting_completion_by_the_other_party') %></p> <p class="text-sm"><%= t('awaiting_completion_by_the_other_party') %></p>
</div> </div>
</div> </div>
+4 -4
View File
@@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div> <div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p dir="auto" class="text-sm"> <p dir="auto" class="text-sm">
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %> <%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %>
</p> </p>
@@ -23,7 +23,7 @@
<toggle-submit> <toggle-submit>
<%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, class: 'white-button w-full' %> <%= button_to button_title(title: t('send_copy_to_email'), disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, class: 'white-button w-full' %>
</toggle-submit> </toggle-submit>
<% if Templates.filter_undefined_submitters(@submitter.submission.template).size != 1 %> <% if Templates.filter_undefined_submitters(@submitter.submission.template_submitters).size != 1 %>
<div class="divider uppercase"><%= t('or') %></div> <div class="divider uppercase"><%= t('or') %></div>
<% else %> <% else %>
<div class="py-2"></div> <div class="py-2"></div>
@@ -42,8 +42,8 @@
</download-button> </download-button>
<% end %> <% end %>
</div> </div>
<% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template) %> <% undefined_submitters = Templates.filter_undefined_submitters(@submitter.submission.template_submitters) %>
<% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false %> <% if undefined_submitters.size == 1 && undefined_submitters.first['uuid'] == @submitter.uuid && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @submitter.template && !@submitter.template.archived_at? %>
<div class="divider uppercase"><%= t('or') %></div> <div class="divider uppercase"><%= t('or') %></div>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), resubmit_form_path, params: { resubmit: @submitter.slug }, method: :put, class: 'white-button w-full' %>
+1 -1
View File
@@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div dir="auto"> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %></p> <p class="text-sm"><%= t('form_has_been_declined_on_html', time: l(@submitter.declined_at, format: :long)) %></p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -10,7 +10,7 @@
<%= svg_icon('writing_sign', class: 'w-10 h-10') %> <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div> </div>
<div dir="auto"> <div dir="auto">
<p class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p> <p class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %></p> <p class="text-sm"><%= t('form_expired_at_html', time: l(@submitter.submission.expire_at, format: :long)) %></p>
</div> </div>
</div> </div>
+19 -15
View File
@@ -1,5 +1,7 @@
<% content_for(:html_title, "#{@submitter.submission.template.name} | DocuSeal") %> <% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% content_for(:html_description, "#{@submitter.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %> <% I18n.with_locale(@submitter.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
<% end %>
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %> <% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %> <% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
@@ -13,7 +15,7 @@
<%= render('submit_form/banner') %> <%= render('submit_form/banner') %>
<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 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;"> <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 %> <%= @submitter.submission.name || @submitter.submission.template.name %>
</div> </div>
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0"> <div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] %>
@@ -24,16 +26,18 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4"> <% if @form_configs[:with_partial_download] %>
<span class="flex items-center justify-center" data-target="download-button.defaultButton"> <download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %> <span class="flex items-center justify-center" data-target="download-button.defaultButton">
<span class="hidden md:inline"><%= t('download') %></span> <%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
</span> <span class="hidden md:inline"><%= t('download') %></span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> </span>
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %> <span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
<span class="hidden md:inline"><%= t('downloading') %></span> <%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
</span> <span class="hidden md:inline"><%= t('downloading') %></span>
</download-button> </span>
</download-button>
<% end %>
</div> </div>
</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"> <scroll-buttons class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10">
@@ -58,11 +62,11 @@
</scroll-buttons> </scroll-buttons>
<% end %> <% end %>
<% schema.each do |item| %> <% schema.each do |item| %>
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %> <% document = @submitter.submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<div id="document-<%= document.uuid %>"> <div id="document-<%= document.uuid %>">
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %> <% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.last.metadata %> <% lazyload_metadata = document.preview_images.last&.metadata || {} %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %> <% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %> <% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
<div class="relative my-4 shadow-md"> <div class="relative my-4 shadow-md">
@@ -2,6 +2,6 @@
<%= render 'custom_content', content: @body, submitter: @submitter %> <%= render 'custom_content', content: @body, submitter: @submitter %>
<% else %> <% else %>
<p><%= t('hi_there') %>,</p> <p><%= t('hi_there') %>,</p>
<p><%= I18n.t(:name_has_been_completed_by_submitters, name: @submitter.submission.template.name, submitters: @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.uniq.join(', ')) %></p> <p><%= I18n.t(:name_has_been_completed_by_submitters, name: @submitter.submission.name || @submitter.submission.template.name, submitters: @submitter.submission.submitters.order(:completed_at).map { |e| e.name || e.email || e.phone }.uniq.join(', ')) %></p>
<p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p> <p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p>
<% end %> <% end %>
@@ -1,4 +1,4 @@
<p><%= t('hi_there') %>,</p> <p><%= t('hi_there') %>,</p>
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p> <p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.name || @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
<%= simple_format(h(@submitter.submission_events.find_by(event_type: :decline_form).data['reason'])) %> <%= simple_format(h(@submitter.submission_events.find_by(event_type: :decline_form).data['reason'])) %>
<p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p> <p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p>
@@ -2,10 +2,10 @@
<%= render 'custom_content', content: @body, submitter: @submitter, sig: @sig %> <%= render 'custom_content', content: @body, submitter: @submitter, sig: @sig %>
<% else %> <% else %>
<p><%= t('hi_there') %>,</p> <p><%= t('hi_there') %>,</p>
<p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.template.name) %> <p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %>
<p><%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %></p> <p><%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %></p>
<p> <p>
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %> <%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %>
</p> </p>
<p> <p>
<%= t('thanks') %>,<br><%= @current_account.name %> <%= t('thanks') %>,<br><%= @current_account.name %>
@@ -6,7 +6,7 @@
<% end %> <% end %>
<% else %> <% else %>
<p><%= t('hi_there') %>,</p> <p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.template.name) %></p> <p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p> <p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p> <p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p> <p>
+6 -6
View File
@@ -2,12 +2,12 @@
<div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row items-strech"> <div class="bg-base-200 rounded-2xl flex flex-col sm:flex-row items-strech">
<% if local_assigns[:with_template] %> <% if local_assigns[:with_template] %>
<% template = submission.template %> <% template = submission.template %>
<a href="<%= template_path(template) %>" class="px-5 sm:pr-3 py-3 group sm:rounded-l-2xl sm:rounded-tr-none rounded-t-2xl flex sm:flex-col justify-between sm:w-52 w-full flex-shrink-0 bg-base-300/60 space-x-2 sm:space-x-0"> <a href="<%= template ? template_path(template) : submission_path(submission) %>" class="px-5 sm:pr-3 py-3 group sm:rounded-l-2xl sm:rounded-tr-none rounded-t-2xl flex sm:flex-col justify-between sm:w-52 w-full flex-shrink-0 bg-base-300/60 space-x-2 sm:space-x-0">
<div> <div>
<div class="font-medium items-start w-full group-hover:link text-sm flex space-x-1"> <div class="font-medium items-start w-full group-hover:link text-sm flex space-x-1">
<%= svg_icon('file_text', class: 'w-4 h-4 mt-0.5 flex-shrink-0') %> <%= svg_icon('file_text', class: 'w-4 h-4 mt-0.5 flex-shrink-0') %>
<span style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"> <span style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
<% template.name.split(/(_)/).each do |item| %><%= item %><wbr><% end %> <% (submission.name || template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %>
<%= svg_icon('arrow_right', class: 'w-4 h-4 sm:inline group-hover:visible invisible hidden') %> <%= svg_icon('arrow_right', class: 'w-4 h-4 sm:inline group-hover:visible invisible hidden') %>
</span> </span>
</div> </div>
@@ -78,7 +78,7 @@
</download-button> </download-button>
</div> </div>
</div> </div>
<% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %> <% elsif !submission.archived_at? && !template&.archived_at? && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<div class="flex-1 md:flex-none md:w-36 flex"> <div class="flex-1 md:flex-none md:w-36 flex">
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]"> <a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]">
@@ -101,7 +101,7 @@
<%= t('view') %> <%= t('view') %>
</a> </a>
</div> </div>
<% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && !template&.archived_at? && can?(:destroy, submission) %>
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %> <%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
</span> </span>
@@ -164,7 +164,7 @@
<span class="inline"><%= t('download')[..-2] %>...</span> <span class="inline"><%= t('download')[..-2] %>...</span>
</span> </span>
</download-button> </download-button>
<% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %> <% elsif !template&.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<div class="relative flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]"> <a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]">
@@ -208,7 +208,7 @@
<%= t('view') %> <%= t('view') %>
</a> </a>
</div> </div>
<% if !submission.archived_at? && !template.archived_at? %> <% if !submission.archived_at? && !template&.archived_at? %>
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %> <%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
</span> </span>
+1 -1
View File
@@ -113,7 +113,7 @@
</div> </div>
<% end %> <% end %>
<% view_archived_html = capture do %> <% view_archived_html = capture do %>
<% if @template.submissions.where.not(archived_at: nil).exists? && !@template.archived_at? %> <% if (can?(:manage, :countless) || @template.submissions.where.not(archived_at: nil).exists?) && !@template.archived_at? %>
<div> <div>
<a href="<%= template_archived_index_path(@template) %>" class="link text-sm"><%= t('view_archived') %></a> <a href="<%= template_archived_index_path(@template) %>" class="link text-sm"><%= t('view_archived') %></a>
</div> </div>
+11 -2
View File
@@ -1,4 +1,4 @@
<% has_archived = current_account.templates.where.not(archived_at: nil).exists? %> <% has_archived = can?(:manage, :countless) || 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)) %> <% 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 %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<dashboard-dropzone> <dashboard-dropzone>
@@ -56,7 +56,7 @@
<% if @templates.present? %> <% if @templates.present? %>
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<%= render partial: 'templates/template', collection: @templates %> <%= render partial: 'templates/template', collection: @templates %>
<% if show_dropzone && (current_user.created_at > 2.weeks.ago || params[:tour] == 'true') && current_user == true_user %> <% if (show_dropzone && current_user.created_at > 2.weeks.ago && current_user == true_user) || 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_or_initialize_by(key: UserConfig::SHOW_APP_TOUR) %>
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %> <% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
<div class="hidden md:block"> <div class="hidden md:block">
@@ -98,5 +98,14 @@
<%= t('templates_not_found') %> <%= t('templates_not_found') %>
</div> </div>
</div> </div>
<% if @related_submissions.present? %>
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
<%= t('submissions') %>
</h1>
<div class="space-y-4 mt-4">
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
</div>
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q]) %>
<% end %>
<% end %> <% end %>
<%= render 'shared/review_form' %> <%= render 'shared/review_form' %>
@@ -0,0 +1,87 @@
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
<%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %>
<% unless close_on_submit %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
<% end %>
<div class="space-y-3 divide-y">
<% template.submitters.each_with_index do |submitter, index| %>
<div class="<%= 'pt-3' if index.positive? %>">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
<%= ff.hidden_field :uuid %>
<div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %>
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] != false ? [t('submission_requester'), 'is_requester'] : nil), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %>
</toggle-attribute>
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
<% end %>
</div>
<% if template.submitters.size == 2 %>
<checkbox-group class="mt-3 flex items-center space-x-4">
<% if local_assigns[:with_submission_requester] != false %>
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
</toggle-attribute>
<span class="select-none">
<%= t('submission_requester') %>
</span>
</label>
<% end %>
<% if index == 1 %>
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex">
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, template.submitters.first['uuid'], '' %>
</indeterminate-checkbox>
</toggle-attribute>
<span class="select-none">
<%= t('invite_by_name', name: template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span>
</span>
</label>
<% end %>
</checkbox-group>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% if local_assigns[:with_toggles] != false %>
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<%= form_for template, url: template_preferences_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
<span>
<%= t('enforce_recipients_order') %>
</span>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(template.preferences['submitters_order']) do |ff| %>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
<% end %>
</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 %>
<% end %>
<div class="form-control mt-5">
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
<% unless close_on_submit %>
<div class="flex justify-center">
<span id="form_saved_alert" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
<% end %>
</div>
+2 -75
View File
@@ -287,81 +287,8 @@
<%= render 'templates_code_modal/preferences', class: 'pt-2' %> <%= render 'templates_code_modal/preferences', class: 'pt-2' %>
</div> </div>
<% if show_recipients %> <% if show_recipients %>
<div id="recipients" class="hidden mt-2 mb-4 px-5"> <div id="recipients" class="hidden mt-2 mb-4 px-5 pt-1 pb-2">
<%= form_for @template, url: template_recipients_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form } do |f| %> <%= render 'recipients', template: @template %>
<div class="space-y-3 divide-y">
<% @template.submitters.each_with_index do |submitter, index| %>
<div class="pt-3">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
<%= ff.hidden_field :uuid %>
<div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if @template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %>
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
<%= ff.select :option, [[t('not_specified'), 'not_set'], [t('submission_requester'), 'is_requester'], [t('specified_email'), 'email'], *(@template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(@template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %>
</toggle-attribute>
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
<% end %>
</div>
<% if @template.submitters.size == 2 %>
<checkbox-group class="mt-3 flex items-center space-x-4">
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
</toggle-attribute>
<span class="select-none">
<%= t('submission_requester') %>
</span>
</label>
<% if index == 1 %>
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex">
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, @template.submitters.first['uuid'], '' %>
</indeterminate-checkbox>
</toggle-attribute>
<span class="select-none">
<%= t('invite_by_name', name: @template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span>
</span>
</label>
<% end %>
</checkbox-group>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
<span>
<%= t('enforce_recipients_order') %>
</span>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
<% end %>
</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>
</div> </div>
<% end %> <% end %>
<% if show_api %> <% if show_api %>
+31 -2
View File
@@ -1,10 +1,10 @@
<%= render 'shared/turbo_modal_large', title: t('share_link') do %> <%= render 'shared/turbo_modal_large', title: t('share_link') do %>
<div class="mt-2 mb-4 px-5"> <div class="mt-2 mb-4 px-5">
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %> <%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center justify-between gap-1 px-1"> <label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('enable_shared_link') %></span> <span><%= t('enable_shared_link') %></span>
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %> <%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div> </label>
<div class="flex gap-2 mt-3"> <div class="flex gap-2 mt-3">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly> <input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<check-on-click data-element-id="template_shared_link"> <check-on-click data-element-id="template_shared_link">
@@ -12,5 +12,34 @@
</check-on-click> </check-on-click>
</div> </div>
<% end %> <% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 mb-5' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-12 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size > 1 %>
<div class="alert items-start bg-base-100 border-base-300 mt-4">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<div><%= t('this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link') %></div>
</div>
<% end %>
<% if @template.submitters.to_a.length > 1 %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4 mb-5">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
</div>
</div>
<% end %>
</div> </div>
<% end %> <% end %>
+16
View File
@@ -3,6 +3,22 @@
{ {
"fingerprint": "25f4ce5fee1e1180fa1919dc4ee78db3ab3457a956e4679503aa745771a43836", "fingerprint": "25f4ce5fee1e1180fa1919dc4ee78db3ab3457a956e4679503aa745771a43836",
"note": "Permitted parameters are necessary for creating submitters via API" "note": "Permitted parameters are necessary for creating submitters via API"
},
{
"fingerprint": "645f2f352a813105b8b2eeadc68101d2e7ea04b8e839aa1336616be56f1108ca",
"note": "Safe SQL"
},
{
"fingerprint": "5bcf4e6426d17b0efd98e47fde7c012f09500b9595af9cff226cf09341c023be",
"note": "Safe SQL"
},
{
"fingerprint": "8bf010d01d5cfabdc2124db1378ca14a24a675431047291488abc186d10ba314",
"note": "Safe SQL"
},
{
"fingerprint": "5f52190d03ee922bba9792012d8fcbeb7d4736006bb899b3be9cc10d679e0af1",
"note": "Safe Param"
} }
] ]
} }
+166 -10
View File
@@ -22,7 +22,10 @@ en: &en
hi_there: Hi there hi_there: Hi there
thanks: Thanks thanks: Thanks
private: Private private: Private
default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
stripe_integration: Stripe Integration stripe_integration: Stripe Integration
require_all_recipients: Require all recipients
stripe_account_has_been_connected: Stripe account has been connected. stripe_account_has_been_connected: Stripe account has been connected.
re_connect_stripe: Re-connect Stripe re_connect_stripe: Re-connect Stripe
bcc_recipients: BCC recipients bcc_recipients: BCC recipients
@@ -113,6 +116,11 @@ en: &en
role: Role role: Role
reason: Reason reason: Reason
provide_your_email_to_start: Provide your email to start provide_your_email_to_start: Provide your email to start
provide_your_email: Provide your email
provide_your_name_to_start: Provide your name to start
provide_your_name: Provide your name
provide_your_phone_in_international_format_to_start: Provide your phone in international format to start
provide_your_phone_in_international_format: Provide your phone in international format
start: Start start: Start
enforce_recipients_order: Enforce recipients order enforce_recipients_order: Enforce recipients order
starting: Starting starting: Starting
@@ -749,6 +757,12 @@ en: &en
share_link: Share link share_link: Share link
enable_shared_link: Enable shared link enable_shared_link: Enable shared link
share_link_is_currently_disabled: Share link is currently disabled share_link_is_currently_disabled: Share link is currently disabled
select_data_residency: Select data residency
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Review or download completed documents. Fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.
link_form_fields: Link form fields
at_least_one_field_must_be_displayed_in_the_form: At least one field must be displayed in the form.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: This template has multiple parties, which prevents the use of a shared link as it's unclear which party is responsible for specific fields. To resolve this, define the default party details.
submission_sources: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@@ -851,6 +865,9 @@ en: &en
range_without_total: "%{from}-%{to} items" range_without_total: "%{from}-%{to} items"
es: &es es: &es
default_parties: Partes predeterminadas
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
require_all_recipients: Requerir a todos los destinatarios
stripe_integration: Integración con Stripe stripe_integration: Integración con Stripe
stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada. stripe_account_has_been_connected: La cuenta de Stripe ha sido conectada.
re_connect_stripe: Volver a conectar Stripe re_connect_stripe: Volver a conectar Stripe
@@ -946,6 +963,11 @@ es: &es
role: Rol role: Rol
reason: Razón reason: Razón
provide_your_email_to_start: Proporciona tu correo electrónico para comenzar provide_your_email_to_start: Proporciona tu correo electrónico para comenzar
provide_your_email: Proporciona tu correo electrónico
provide_your_name_to_start: Proporciona tu nombre para comenzar
provide_your_name: Proporciona tu nombre
provide_your_phone_in_international_format_to_start: Proporciona tu teléfono en formato internacional para comenzar
provide_your_phone_in_international_format: Proporciona tu teléfono en formato internacional
start: Comenzar start: Comenzar
starting: Comenzando starting: Comenzando
form_has_been_deleted_by_html: 'El formulario ha sido eliminado por <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'El formulario ha sido eliminado por <span class="font-semibold">%{name}</span>.'
@@ -1581,6 +1603,12 @@ es: &es
share_link: Enlace para compartir share_link: Enlace para compartir
enable_shared_link: Habilitar enlace compartido enable_shared_link: Habilitar enlace compartido
share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente
select_data_residency: Seleccionar ubicación de datos
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} te ha invitado a completar y firmar documentos en línea fácilmente con una solución de firma digital segura, rápida y fácil de usar.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Revisa o descarga los documentos completados. Completa y firma documentos en línea fácilmente con una solución de firma digital segura, rápida y fácil de usar.
link_form_fields: Vincular campos del formulario
at_least_one_field_must_be_displayed_in_the_form: Al menos un campo debe mostrarse en el formulario.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Esta plantilla tiene varias partes, lo que impide el uso de un enlace compartido porque no está claro qué parte es responsable de campos específicos. Para resolverlo, define los detalles predeterminados de la parte.
submission_sources: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@@ -1683,6 +1711,9 @@ es: &es
range_without_total: "%{from}-%{to} elementos" range_without_total: "%{from}-%{to} elementos"
it: &it it: &it
default_parties: Parti predefiniti
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
require_all_recipients: Richiedi tutti i destinatari
stripe_integration: Integrazione Stripe stripe_integration: Integrazione Stripe
stripe_account_has_been_connected: L'account Stripe è stato collegato. stripe_account_has_been_connected: L'account Stripe è stato collegato.
re_connect_stripe: Ricollega Stripe re_connect_stripe: Ricollega Stripe
@@ -1777,6 +1808,11 @@ it: &it
role: Ruolo role: Ruolo
reason: Ragione reason: Ragione
provide_your_email_to_start: Fornisci la tua email per iniziare provide_your_email_to_start: Fornisci la tua email per iniziare
provide_your_email: Fornisci la tua email
provide_your_name_to_start: Fornisci il tuo nome per iniziare
provide_your_name: Fornisci il tuo nome
provide_your_phone_in_international_format_to_start: Fornisci il tuo telefono in formato internazionale per iniziare
provide_your_phone_in_international_format: Fornisci il tuo telefono in formato internazionale
start: Inizia start: Inizia
starting: Iniziando starting: Iniziando
form_has_been_deleted_by_html: 'Il modulo è stato eliminato da <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'Il modulo è stato eliminato da <span class="font-semibold">%{name}</span>.'
@@ -2411,6 +2447,12 @@ it: &it
share_link: Link di condivisione share_link: Link di condivisione
enable_shared_link: Abilita link condiviso enable_shared_link: Abilita link condiviso
share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato
select_data_residency: Seleziona la residenza dei dati
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} ti ha invitato a compilare e firmare documenti online con facilità utilizzando una soluzione di firma digitale sicura, veloce e facile da usare.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Rivedi o scarica i documenti completati. Compila e firma documenti online facilmente con una soluzione di firma digitale sicura, veloce e facile da usare.
link_form_fields: Collega i campi del modulo
at_least_one_field_must_be_displayed_in_the_form: Almeno un campo deve essere visualizzato nel modulo.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Questo modello ha più parti, il che impedisce l’uso di un link di condivisione perché non è chiaro quale parte sia responsabile di campi specifici. Per risolvere, definisci i dettagli predefiniti della parte.
submission_sources: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@@ -2513,6 +2555,9 @@ it: &it
range_without_total: "%{from}-%{to} elementi" range_without_total: "%{from}-%{to} elementi"
fr: &fr fr: &fr
default_parties: Parties par défaut
authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton
require_all_recipients: Exiger tous les destinataires
stripe_integration: Intégration Stripe stripe_integration: Intégration Stripe
stripe_account_has_been_connected: Le compte Stripe a été connecté. stripe_account_has_been_connected: Le compte Stripe a été connecté.
re_connect_stripe: Reconnecter Stripe re_connect_stripe: Reconnecter Stripe
@@ -2607,7 +2652,12 @@ fr: &fr
digitally_signed_by: Signé numériquement par digitally_signed_by: Signé numériquement par
role: Rôle role: Rôle
reason: Raison reason: Raison
provide_your_email_to_start: Entrez votre adresse email pour commencer provide_your_email_to_start: Fournissez votre e-mail pour commencer
provide_your_email: Fournissez votre e-mail
provide_your_name_to_start: Fournissez votre nom pour commencer
provide_your_name: Fournissez votre nom
provide_your_phone_in_international_format_to_start: Fournissez votre téléphone au format international pour commencer
provide_your_phone_in_international_format: Fournissez votre téléphone au format international
start: Démarrer start: Démarrer
starting: Démarrage starting: Démarrage
form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.'
@@ -3244,6 +3294,12 @@ fr: &fr
share_link: Lien de partage share_link: Lien de partage
enable_shared_link: Activer le lien de partage enable_shared_link: Activer le lien de partage
share_link_is_currently_disabled: Le lien de partage est actuellement désactivé share_link_is_currently_disabled: Le lien de partage est actuellement désactivé
select_data_residency: Sélectionner la résidence des données
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} vous a invité à remplir et signer des documents en ligne facilement avec une solution de signature numérique sécurisée, rapide et conviviale.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Consultez ou téléchargez les documents complétés. Remplissez et signez des documents en ligne facilement avec une solution de signature numérique sécurisée, rapide et conviviale.
link_form_fields: Lier les champs du formulaire
at_least_one_field_must_be_displayed_in_the_form: Au moins un champ doit être affiché dans le formulaire.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Ce modèle contient plusieurs parties, ce qui empêche l’utilisation d’un lien de partage car il n’est pas clair quelle partie est responsable de certains champs. Pour résoudre cela, définissez les détails de la partie par défaut.
submission_sources: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@@ -3346,6 +3402,9 @@ fr: &fr
range_without_total: "%{from} à %{to} éléments" range_without_total: "%{from} à %{to} éléments"
pt: &pt pt: &pt
default_parties: Partes padrão
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
require_all_recipients: Exigir todos os destinatários
stripe_integration: Integração com Stripe stripe_integration: Integração com Stripe
stripe_account_has_been_connected: Conta Stripe foi conectada. stripe_account_has_been_connected: Conta Stripe foi conectada.
re_connect_stripe: Reconectar Stripe re_connect_stripe: Reconectar Stripe
@@ -3440,7 +3499,12 @@ pt: &pt
digitally_signed_by: Assinado digitalmente por digitally_signed_by: Assinado digitalmente por
role: Função role: Função
reason: Motivo reason: Motivo
provide_your_email_to_start: Forneça o seu email para começar provide_your_email_to_start: Informe seu e-mail para começar
provide_your_email: Informe seu e-mail
provide_your_name_to_start: Informe seu nome para começar
provide_your_name: Informe seu nome
provide_your_phone_in_international_format_to_start: Informe seu telefone no formato internacional para começar
provide_your_phone_in_international_format: Informe seu telefone no formato internacional
start: Iniciar start: Iniciar
starting: Iniciando starting: Iniciando
form_has_been_deleted_by_html: 'O formulário foi eliminado por <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'O formulário foi eliminado por <span class="font-semibold">%{name}</span>.'
@@ -4076,6 +4140,12 @@ pt: &pt
share_link: Link de compartilhamento share_link: Link de compartilhamento
enable_shared_link: Ativar link compartilhado enable_shared_link: Ativar link compartilhado
share_link_is_currently_disabled: O link compartilhado está desativado no momento share_link_is_currently_disabled: O link compartilhado está desativado no momento
select_data_residency: Selecionar local dos dados
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} convidou você para preencher e assinar documentos online com facilidade usando uma solução de assinatura digital segura, rápida e fácil de usar.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Reveja ou baixe os documentos concluídos. Preencha e assine documentos online com facilidade usando uma solução de assinatura digital segura, rápida e fácil de usar.
link_form_fields: Vincular campos do formulário
at_least_one_field_must_be_displayed_in_the_form: Pelo menos um campo deve ser exibido no formulário.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Este modelo tem várias partes, o que impede o uso de um link de compartilhamento, pois não está claro qual parte é responsável por campos específicos. Para resolver isso, defina os detalhes padrão da parte.
submission_sources: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@@ -4179,6 +4249,9 @@ pt: &pt
range_without_total: "%{from}-%{to} itens" range_without_total: "%{from}-%{to} itens"
de: &de de: &de
default_parties: Standardparteien
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
require_all_recipients: Alle Empfänger erforderlich
stripe_integration: Stripe-Integration stripe_integration: Stripe-Integration
stripe_account_has_been_connected: Stripe-Konto wurde verbunden. stripe_account_has_been_connected: Stripe-Konto wurde verbunden.
re_connect_stripe: Stripe erneut verbinden re_connect_stripe: Stripe erneut verbinden
@@ -4273,7 +4346,12 @@ de: &de
digitally_signed_by: Digital signiert von digitally_signed_by: Digital signiert von
role: Rolle role: Rolle
reason: Grund reason: Grund
provide_your_email_to_start: Gib deine E-Mail-Adresse ein, um zu starten provide_your_email_to_start: Geben Sie Ihre E-Mail-Adresse ein, um zu starten
provide_your_email: Geben Sie Ihre E-Mail-Adresse ein
provide_your_name_to_start: Geben Sie Ihren Namen ein, um zu starten
provide_your_name: Geben Sie Ihren Namen ein
provide_your_phone_in_international_format_to_start: Geben Sie Ihre Telefonnummer im internationalen Format ein, um zu starten
provide_your_phone_in_international_format: Geben Sie Ihre Telefonnummer im internationalen Format ein
start: Starten start: Starten
starting: Starten starting: Starten
form_has_been_deleted_by_html: 'Das Formular wurde von <span class="font-semibold">%{name}</span> gelöscht.' form_has_been_deleted_by_html: 'Das Formular wurde von <span class="font-semibold">%{name}</span> gelöscht.'
@@ -4909,6 +4987,12 @@ de: &de
share_link: Freigabelink share_link: Freigabelink
enable_shared_link: 'Freigabelink aktivieren' enable_shared_link: 'Freigabelink aktivieren'
share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert' share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert'
select_data_residency: Datenstandort auswählen
account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: '%{account_name} hat Sie eingeladen, Dokumente mühelos online mit einer sicheren, schnellen und benutzerfreundlichen digitalen Signaturlösung auszufüllen und zu unterschreiben.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Überprüfen oder laden Sie abgeschlossene Dokumente herunter. Füllen und unterschreiben Sie Dokumente mühelos online mit einer sicheren, schnellen und benutzerfreundlichen digitalen Signaturlösung.
link_form_fields: Formularfelder verknüpfen
at_least_one_field_must_be_displayed_in_the_form: Mindestens ein Feld muss im Formular angezeigt werden.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Diese Vorlage enthält mehrere Parteien, was die Verwendung eines Freigabelinks verhindert, da unklar ist, welche Partei für bestimmte Felder verantwortlich ist. Um dies zu beheben, definieren Sie die Standardparteidetails.
submission_sources: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
@@ -5033,7 +5117,13 @@ pl:
digitally_signed_by: Podpis cyfrowy przez digitally_signed_by: Podpis cyfrowy przez
form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>'
role: Rola role: Rola
provide_your_email_to_start: Podaj swój adres email, aby rozpocząć provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć
provide_your_email: Podaj swój e-mail
provide_your_name_to_start: Podaj swoje imię, aby rozpocząć
provide_your_name: Podaj swoje imię
provide_your_phone_in_international_format_to_start: Podaj numer telefonu w formacie międzynarodowym, aby rozpocząć
provide_your_phone_in_international_format: Podaj numer telefonu w formacie międzynarodowym
use_international_format_1xxx_: 'Użyj formatu międzynarodowego: +1xxx...'
start: Rozpocznij start: Rozpocznij
reason: Powód reason: Powód
starting: Rozpoczynanie starting: Rozpoczynanie
@@ -5080,6 +5170,9 @@ pl:
please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem. please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem.
privacy_policy: Polityka Prywatności privacy_policy: Polityka Prywatności
esignature_disclosure: Użycie e-podpisu esignature_disclosure: Użycie e-podpisu
select_data_residency: Wybierz lokalizację danych
company_name: Nazwa firmy
optional: opcjonalne
uk: uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@@ -5103,7 +5196,13 @@ uk:
digitally_signed_by: Цифровий підпис від digitally_signed_by: Цифровий підпис від
verification_code_code: 'Код підтвердження: %{code}' verification_code_code: 'Код підтвердження: %{code}'
role: Роль role: Роль
provide_your_email_to_start: Введіть свій email, щоб почати provide_your_email_to_start: Введіть свій емейл, щоб почати
provide_your_email: Введіть свій емейл
provide_your_name_to_start: Введіть своє ім’я, щоб почати
provide_your_name: Введіть своє ім’я
provide_your_phone_in_international_format_to_start: Введіть номер телефону у міжнародному форматі, щоб почати
provide_your_phone_in_international_format: Введіть номер телефону у міжнародному форматі
use_international_format_1xxx_: 'Використовуйте міжнародний формат: +1xxx...'
form_expired_at_html: 'Строк подачі завершився о <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Строк подачі завершився о <span class="font-semibold">%{time}</span>'
start: Почати start: Почати
reason: Причина reason: Причина
@@ -5151,6 +5250,9 @@ uk:
please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням." please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
privacy_policy: Політика конфіденційності privacy_policy: Політика конфіденційності
esignature_disclosure: Використання e-підпису esignature_disclosure: Використання e-підпису
select_data_residency: Виберіть місце зберігання даних
company_name: Назва компанії
optional: необов’язково
cs: cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@@ -5174,7 +5276,13 @@ cs:
digitally_signed_by: Digitálně podepsáno uživatelem digitally_signed_by: Digitálně podepsáno uživatelem
verification_code_code: 'Ověřovací kód: %{code}' verification_code_code: 'Ověřovací kód: %{code}'
role: Role role: Role
provide_your_email_to_start: Zadejte svůj email pro zahájení provide_your_email_to_start: Zadejte svůj e-mail pro zahájení
provide_your_email: Zadejte svůj e-mail
provide_your_name_to_start: Zadejte své jméno pro zahájení
provide_your_name: Zadejte své jméno
provide_your_phone_in_international_format_to_start: Zadejte své telefonní číslo v mezinárodním formátu pro zahájení
provide_your_phone_in_international_format: Zadejte své telefonní číslo v mezinárodním formátu
use_international_format_1xxx_: 'Použijte mezinárodní formát: +1xxx...'
reason: Důvod reason: Důvod
form_expired_at_html: 'Formulář vypršel <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Formulář vypršel <span class="font-semibold">%{time}</span>'
start: Zahájit start: Zahájit
@@ -5222,6 +5330,9 @@ cs:
please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním. please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním.
privacy_policy: Zásady Ochrany Osobních Údajů privacy_policy: Zásady Ochrany Osobních Údajů
esignature_disclosure: Použití e-podpisu esignature_disclosure: Použití e-podpisu
select_data_residency: Vyberte umístění dat
company_name: Název společnosti
optional: volitelné
he: he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@@ -5246,7 +5357,13 @@ he:
role: תפקיד role: תפקיד
reason: סיבה reason: סיבה
verification_code_code: 'קוד אימות: %{code}' verification_code_code: 'קוד אימות: %{code}'
provide_your_email_to_start: ספק את כתובת הדוא"ל שלך כדי להתחיל provide_your_email_to_start: הזן את האימייל שלך כדי להתחיל
provide_your_email: הזן את האימייל שלך
provide_your_name_to_start: הזן את שמך כדי להתחיל
provide_your_name: הזן את שמך
provide_your_phone_in_international_format_to_start: הזן את מספר הטלפון שלך בפורמט בינלאומי כדי להתחיל
provide_your_phone_in_international_format: הזן את מספר הטלפון שלך בפורמט בינלאומי
use_international_format_1xxx_: 'השתמש בפורמט בינלאומי: +1xxx...'
start: התחל start: התחל
starting: מתחיל starting: מתחיל
form_expired_at_html: 'הטופס פג תוקף ב- <span class="font-semibold">%{time}</span>' form_expired_at_html: 'הטופס פג תוקף ב- <span class="font-semibold">%{time}</span>'
@@ -5293,6 +5410,9 @@ he:
please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.' please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
privacy_policy: 'מדיניות פרטיות' privacy_policy: 'מדיניות פרטיות'
esignature_disclosure: 'גילוי חתימה אלקטרונית' esignature_disclosure: 'גילוי חתימה אלקטרונית'
select_data_residency: בחר מיקום נתונים
company_name: שם החברה
optional: אופציונלי
nl: nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
@@ -5316,7 +5436,13 @@ nl:
digitally_signed_by: Digitaal ondertekend door digitally_signed_by: Digitaal ondertekend door
role: Rol role: Rol
verification_code_code: 'Verificatiecode: %{code}' verification_code_code: 'Verificatiecode: %{code}'
provide_your_email_to_start: Geef uw e-mailadres om te beginnen provide_your_email_to_start: Voer je e-mailadres in om te beginnen
provide_your_email: Voer je e-mailadres in
provide_your_name_to_start: Voer je naam in om te beginnen
provide_your_name: Voer je naam in
provide_your_phone_in_international_format_to_start: Voer je telefoonnummer in internationaal formaat in om te beginnen
provide_your_phone_in_international_format: Voer je telefoonnummer in internationaal formaat in
use_international_format_1xxx_: 'Gebruik het internationale formaat: +1xxx...'
start: Start start: Start
reason: Reden reason: Reden
form_expired_at_html: 'Formulier is verlopen op <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Formulier is verlopen op <span class="font-semibold">%{time}</span>'
@@ -5364,6 +5490,9 @@ nl:
please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening. please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening.
privacy_policy: Privacybeleid privacy_policy: Privacybeleid
esignature_disclosure: Gebruik van e-handtekening esignature_disclosure: Gebruik van e-handtekening
select_data_residency: Selecteer gegevenslocatie
company_name: Bedrijfsnaam
optional: optioneel
ar: ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@@ -5386,7 +5515,13 @@ ar:
email: البريد الإلكتروني email: البريد الإلكتروني
digitally_signed_by: تم التوقيع الرقمي بواسطة digitally_signed_by: تم التوقيع الرقمي بواسطة
role: الدور role: الدور
provide_your_email_to_start: قدم بريدك الإلكتروني للبدء provide_your_email_to_start: أدخل بريدك الإلكتروني للبدء
provide_your_email: أدخل بريدك الإلكتروني
provide_your_name_to_start: أدخل اسمك للبدء
provide_your_name: أدخل اسمك
provide_your_phone_in_international_format_to_start: أدخل رقم هاتفك بصيغة دولية للبدء
provide_your_phone_in_international_format: أدخل رقم هاتفك بصيغة دولية
use_international_format_1xxx_: 'استخدم التنسيق الدولي: +1xxx...'
start: بدء start: بدء
starting: بداية starting: بداية
verification_code_code: 'رمز التحقق: %{code}' verification_code_code: 'رمز التحقق: %{code}'
@@ -5435,6 +5570,9 @@ ar:
please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.' please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
privacy_policy: 'سياسة الخصوصية' privacy_policy: 'سياسة الخصوصية'
esignature_disclosure: 'إفصاح التوقيع الإلكتروني' esignature_disclosure: 'إفصاح التوقيع الإلكتروني'
select_data_residency: اختر موقع تخزين البيانات
company_name: اسم الشركة
optional: اختياري
ko: ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함 require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@@ -5457,7 +5595,13 @@ ko:
email: 이메일 email: 이메일
digitally_signed_by: 디지털로 서명됨 digitally_signed_by: 디지털로 서명됨
role: 역할 role: 역할
provide_your_email_to_start: 시작하려면 이메일을 제공하세요 provide_your_email_to_start: 시작하려면 이메일을 입력하세요
provide_your_email: 이메일을 입력하세요
provide_your_name_to_start: 시작하려면 이름을 입력하세요
provide_your_name: 이름을 입력하세요
provide_your_phone_in_international_format_to_start: 시작하려면 국제 형식의 전화번호를 입력하세요
provide_your_phone_in_international_format: 국제 형식의 전화번호를 입력하세요
use_international_format_1xxx_: '국제 형식을 사용하세요: +1xxx...'
start: 시작 start: 시작
reason: 이유 reason: 이유
starting: 시작 중 starting: 시작 중
@@ -5506,6 +5650,9 @@ ko:
please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.' please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
privacy_policy: 개인정보 처리방침 privacy_policy: 개인정보 처리방침
esignature_disclosure: 전자서명 공개 esignature_disclosure: 전자서명 공개
select_data_residency: 데이터 저장 위치 선택
company_name: 회사 이름
optional: 선택 사항
ja: ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です require_phone_2fa_to_open: 電話による2段階認証が必要です
@@ -5529,6 +5676,12 @@ ja:
digitally_signed_by: 電子署名者 digitally_signed_by: 電子署名者
role: 役割 role: 役割
provide_your_email_to_start: 開始するにはメールアドレスを入力してください provide_your_email_to_start: 開始するにはメールアドレスを入力してください
provide_your_email: メールアドレスを入力してください
provide_your_name_to_start: 開始するには名前を入力してください
provide_your_name: 名前を入力してください
provide_your_phone_in_international_format_to_start: 開始するには国際形式の電話番号を入力してください
provide_your_phone_in_international_format: 国際形式の電話番号を入力してください
use_international_format_1xxx_: '国際形式を使用してください:+1xxx...'
start: 開始 start: 開始
reason: 理由 reason: 理由
starting: 開始中 starting: 開始中
@@ -5577,6 +5730,9 @@ ja:
please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。' please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
privacy_policy: プライバシーポリシー privacy_policy: プライバシーポリシー
esignature_disclosure: 電子署名に関する開示 esignature_disclosure: 電子署名に関する開示
select_data_residency: データ保存場所を選択
company_name: 会社名
optional: 任意
en-US: en-US:
<<: *en <<: *en
+1
View File
@@ -163,6 +163,7 @@ Rails.application.routes.draw do
scope '/settings', as: :settings do scope '/settings', as: :settings do
unless Docuseal.multitenant? unless Docuseal.multitenant?
resources :storage, only: %i[index create], controller: 'storage_settings' resources :storage, only: %i[index create], controller: 'storage_settings'
resources :search_entries_reindex, only: %i[create]
resources :sms, only: %i[index], controller: 'sms_settings' resources :sms, only: %i[index], controller: 'sms_settings'
end end
resources :email, only: %i[index create], controller: 'email_smtp_settings' resources :email, only: %i[index create], controller: 'email_smtp_settings'
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddNameToSubmissions < ActiveRecord::Migration[8.0]
def change
add_column :submissions, :name, :text
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class RemoveNotNullTemplateId < ActiveRecord::Migration[8.0]
def change
change_column_null :submissions, :template_id, true
end
end
@@ -2,7 +2,7 @@
class AddEmailEventsDateIndex < ActiveRecord::Migration[8.0] class AddEmailEventsDateIndex < ActiveRecord::Migration[8.0]
def change def change
remove_index :email_events, :account_id
add_index :email_events, %i[account_id event_datetime] add_index :email_events, %i[account_id event_datetime]
remove_index :email_events, :account_id
end end
end end
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff. Show more