Compare commits

..
28 Commits
Author SHA1 Message Date
Alex Turchyn 6a3a185310 Merge from docusealco/wip 2026-07-13 13:23:35 +03:00
Pete Matsyburka 7d381b7aee add reset smtp action 2026-07-13 10:58:08 +03:00
Alex Turchyn bba5626dea add ability to edit html email 2026-07-10 21:57:23 +03:00
Pete Matsyburka bdf5e58929 fix autocomplete index use 2026-07-10 10:32:04 +03:00
Pete Matsyburka 035168230e match generated font frontend 2026-07-09 15:44:34 +03:00
Pete Matsyburka 28168f082d adjust view only 2026-07-09 12:46:38 +03:00
Pete Matsyburka a29e09ed2f add submission update api 2026-07-08 11:16:56 +03:00
Pete Matsyburka e88874e757 view only party 2026-07-07 17:34:23 +03:00
Pete Matsyburka b6fb7c6977 fix find blob by checksum 2026-07-07 09:50:11 +03:00
Pete Matsyburka 8a60f17d13 optimize created_at filter 2026-07-07 09:41:37 +03:00
Pete Matsyburka e1d860a42c add submission created_at index 2026-07-07 09:14:15 +03:00
Pete Matsyburka 56da326709 adjust shared query 2026-07-06 18:45:37 +03:00
Pete Matsyburka 672008f8af add shared templates index param 2026-07-06 17:56:35 +03:00
Alex Turchyn d86b16de6b Merge from docusealco/wip 2026-07-06 13:25:54 +03:00
Pete Matsyburka d41710f507 fix spec 2026-07-04 10:12:08 +03:00
Pete Matsyburka 2b53166763 add completed check 2026-07-04 09:59:35 +03:00
Pete Matsyburka 4e56a16a58 adjust filters 2026-07-04 09:59:35 +03:00
Pete Matsyburka ac7cc77017 use submission completed_at 2026-07-04 09:59:35 +03:00
Pete Matsyburka 9194879c43 populate completed_at 2026-07-04 09:58:56 +03:00
Pete Matsyburka 1902cdaa55 submission completed_at 2026-07-04 09:58:56 +03:00
Alex Turchyn 6314987e69 make possible to unarchive completed submission 2026-07-04 09:57:29 +03:00
Pete Matsyburka 65bcbd7737 adjust authorize 2026-07-04 09:30:43 +03:00
Pete Matsyburka 9b25b8538e radio group font settings 2026-07-02 10:14:43 +03:00
Pete Matsyburka bbf8bb2a94 fix autocomplete index use 2026-07-01 13:06:00 +03:00
Pete Matsyburka 2085227cd9 fix dynamic document areas 2026-06-30 17:55:28 +03:00
Alex Turchyn 93e0730210 update docs 2026-06-30 13:31:31 +03:00
Pete Matsyburka 25c5c11ab3 change footer url 2026-06-30 08:39:26 +03:00
Pete Matsyburka 6ed5b5d35d extract email assets 2026-06-29 13:21:03 +03:00
125 changed files with 8577 additions and 2464 deletions

No files matched your search

@@ -9,7 +9,7 @@ module Api
(@submission.schema_documents || @submission.template.schema_documents).size > 1 (@submission.schema_documents || @submission.template.schema_documents).size > 1
documents = documents =
if @submission.submitters.all?(&:completed_at?) if @submission.completed_at?
build_completed_documents(@submission, merge: is_merge) build_completed_documents(@submission, merge: is_merge)
else else
build_preview_documents(@submission, merge: is_merge) build_preview_documents(@submission, merge: is_merge)
@@ -30,7 +30,7 @@ module Api
private private
def build_completed_documents(submission, merge: false) def build_completed_documents(submission, merge: false)
last_submitter = submission.submitters.max_by(&:completed_at) last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
if merge if merge
if submission.merged_document_attachment.blank? if submission.merged_document_attachment.blank?
@@ -5,7 +5,7 @@ module Api
load_and_authorize_resource :submission, parent: false load_and_authorize_resource :submission, parent: false
def index def index
submissions = build_completed_query(@submissions) submissions = @submissions.active.where.not(completed_at: nil)
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present? params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
@@ -36,20 +36,5 @@ module Api
} }
} }
end end
private
def build_completed_query(submissions)
submissions = submissions.where(
Submitter.where(completed_at: nil).where(
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
).select(1).arel.exists.not
)
submissions.joins(:submitters)
.group(:id)
.select(Submission.arel_table[Arel.star],
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
end
end end
end end
+67 -18
View File
@@ -2,17 +2,19 @@
module Api module Api
class SubmissionsController < ApiBaseController class SubmissionsController < ApiBaseController
SUBMISSION_COLUMNS = %i[id name slug source submitters_order expire_at created_at updated_at SUBMISSION_COLUMNS = %i[id name slug source submitters_order expire_at completed_at created_at updated_at
archived_at variables template_id template_submitters created_by_user_id].freeze archived_at variables template_id template_submitters created_by_user_id].freeze
TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze
load_and_authorize_resource :template, only: :create load_and_authorize_resource :template, only: :create
load_and_authorize_resource :submission, only: %i[show index destroy] load_and_authorize_resource :submission, only: %i[show index update destroy]
before_action only: :create do before_action only: :create do
authorize!(:create, Submission) authorize!(:create, Submission)
end end
before_action :maybe_return_template_error, only: :create
def index def index
submissions = Submissions.search(current_user, @submissions, params[:q]) submissions = Submissions.search(current_user, @submissions, params[:q])
submissions = filter_submissions(submissions, params) submissions = filter_submissions(submissions, params)
@@ -58,7 +60,7 @@ module Api
end end
end end
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?) if @submission.audit_trail_attachment.blank? && @submission.completed_at?
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission) @submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
end end
@@ -68,20 +70,6 @@ module Api
def create def create
Params::SubmissionCreateValidator.call(params) Params::SubmissionCreateValidator.call(params)
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
if @template.archived_at?
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
end
if @template.fields.blank?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
end
params[:send_email] = true unless params.key?(:send_email) params[:send_email] = true unless params.key?(:send_email)
params[:send_sms] = false unless params.key?(:send_sms) params[:send_sms] = false unless params.key?(:send_sms)
@@ -92,10 +80,17 @@ module Api
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
if submission.submitters.all? { |s| s.viewer? || s.completed_at? } &&
Submissions.maybe_update_completed_at(submission)
last_submitter = submission.submitters.reject(&:viewer?).max_by(&:completed_at)
end
submission.submitters.each do |submitter| submission.submitters.each do |submitter|
next unless submitter.completed_at? next unless submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false) ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id,
'is_last' => submitter == last_submitter,
'send_invitation_email' => false)
end end
end end
@@ -109,6 +104,25 @@ module Api
render json: { error: e.message }, status: :unprocessable_content render json: { error: e.message }, status: :unprocessable_content
end end
def update
@submission = assign_submission_attrs(@submission, submission_params)
@submission.save!
if @submission.saved_change_to_archived_at? && @submission.archived_at?
WebhookUrls.enqueue_events(@submission, 'submission.archived')
end
if @submission.saved_change_to_expire_at? && @submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submission.expire_at, 'submission_id' => @submission.id,
'expire_at' => @submission.expire_at.to_i)
end
SearchEntries.enqueue_reindex(@submission) if @submission.saved_change_to_name?
render json: Submissions::SerializeForApi.call(@submission, nil, params, with_events: false)
end
def destroy def destroy
if params[:permanently].in?(['true', true]) if params[:permanently].in?(['true', true])
@submission.destroy! @submission.destroy!
@@ -123,6 +137,41 @@ module Api
private private
def assign_submission_attrs(submission, attrs)
archived = attrs.key?(:archived) ? attrs[:archived] : attrs[:archived_at]
if archived.in?([true, false, 'true', 'false']) && current_ability.can?(:destroy, submission)
submission.archived_at = archived.in?(Submitters::TRUE_VALUES) ? Time.current : nil
end
submission.name = attrs[:name] if attrs.key?(:name)
submission.expire_at = attrs[:expire_at].presence if attrs.key?(:expire_at)
submission
end
def submission_params
submission_params = params.key?(:submission) ? params.require(:submission) : params
submission_params.permit(:name, :expire_at, :archived, :archived_at)
end
def maybe_return_template_error
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
if @template.archived_at?
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
end
return if @template.fields.present?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
end
def filter_submissions(submissions, params) def filter_submissions(submissions, params)
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present? submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
submissions = submissions.where(slug: params[:slug]) if params[:slug].present? submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
+15 -9
View File
@@ -4,6 +4,8 @@ module Api
class SubmittersController < ApiBaseController class SubmittersController < ApiBaseController
load_and_authorize_resource :submitter load_and_authorize_resource :submitter
before_action :maybe_return_submitter_error, only: :update
def index def index
submitters = Submitters.search(current_user, @submitters, params[:q]) submitters = Submitters.search(current_user, @submitters, params[:q])
@@ -36,14 +38,6 @@ module Api
# rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/MethodLength
def update def update
if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
end
if @submitter.declined_at?
return render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
end
submission = @submitter.submission submission = @submitter.submission
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name'] role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
@@ -73,7 +67,9 @@ module Api
end end
if @submitter.completed_at? if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id) is_last = Submissions.maybe_update_completed_at(@submitter.submission)
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id, 'is_last' => is_last)
elsif normalized_params[:send_email] || normalized_params[:send_sms] elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
@@ -104,6 +100,16 @@ module Api
private private
def maybe_return_submitter_error
if @submitter.completed_at? || @submitter.submission.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
end
return unless @submitter.declined_at?
render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
end
def maybe_filter_by_completed_at(submitters, params) def maybe_filter_by_completed_at(submitters, params)
if params[:completed_after].present? if params[:completed_after].present?
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..) submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
+10 -2
View File
@@ -5,6 +5,8 @@ module Api
load_and_authorize_resource :template load_and_authorize_resource :template
def index def index
@templates = Templates.shared(current_user) if params[:shared].in?(['true', true])
templates = filter_templates(@templates, params) templates = filter_templates(@templates, params)
templates = paginate(templates.preload(:author, folder: :parent_folder)) templates = paginate(templates.preload(:author, folder: :parent_folder))
@@ -54,7 +56,7 @@ module Api
@template.update!(template_params) @template.update!(template_params)
SearchEntries.enqueue_reindex(@template) SearchEntries.enqueue_reindex(@template) if @template.saved_change_to_name?
WebhookUrls.enqueue_events(@template, 'template.updated') WebhookUrls.enqueue_events(@template, 'template.updated')
@@ -115,7 +117,13 @@ module Api
end end
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(current_user, templates, params[:q]) templates =
if params[:shared].in?(['true', true])
Templates.search_shared(current_user, templates, params[:q])
else
Templates.search(current_user, templates, params[:q])
end
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?
@@ -3,7 +3,7 @@
class EmailSmtpSettingsController < ApplicationController class EmailSmtpSettingsController < ApplicationController
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, only: :create authorize_resource :encrypted_config, parent: false, only: %i[create destroy]
def index; end def index; end
@@ -23,6 +23,12 @@ class EmailSmtpSettingsController < ApplicationController
render :index, status: :unprocessable_content render :index, status: :unprocessable_content
end end
def destroy
@encrypted_config.destroy!
redirect_to settings_email_index_path, notice: I18n.t('smtp_settings_have_been_reset')
end
private private
def load_encrypted_config def load_encrypted_config
@@ -16,18 +16,16 @@ class SendSubmissionEmailController < ApplicationController
@submitter = @submitter =
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase) Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
elsif params[:submission_slug] elsif params[:submission_slug]
submission = Submission.find_by(slug: params[:submission_slug]) submission = Submission.find_by!(slug: params[:submission_slug])
if submission @submitter = submission.submitters.order(:completed_at).find_by(email: params[:email].to_s.downcase)
@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
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug]) @submitter = Submitter.find_by!(slug: params[:submitter_slug])
end end
if @submitter if @submitter && completed_submitter?(@submitter)
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter) SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
@@ -41,6 +39,10 @@ class SendSubmissionEmailController < ApplicationController
private private
def completed_submitter?(submitter)
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
end
def can_send?(submitter) def can_send?(submitter)
return false if submitter.account.archived_at? return false if submitter.account.archived_at?
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter, return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
+8 -4
View File
@@ -93,9 +93,12 @@ class StartFormController < ApplicationController
SearchEntries.enqueue_reindex(submitter) SearchEntries.enqueue_reindex(submitter)
return unless submitter.submission.expire_at? expire_at = submitter.submission.expire_at
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id) return unless expire_at
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id,
'expire_at' => expire_at.to_i)
end end
def load_resubmit_submitter def load_resubmit_submitter
@@ -138,8 +141,7 @@ class StartFormController < ApplicationController
submitter ||= submitter ||=
Submitter Submitter
.where(submission: template.submissions.where(expire_at: Time.current..) .where(submission: template.submissions.non_expired.active)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc) .order(id: :desc)
.where(declined_at: nil) .where(declined_at: nil)
.where(external_id: nil) .where(external_id: nil)
@@ -147,6 +149,8 @@ class StartFormController < ApplicationController
.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(find_params) .find_or_initialize_by(find_params)
submitter = Submitter.new(find_params) if submitter.submission&.completed_at? && submitter.viewer?
submitter.name = required_params['name'] if submitter.new_record? submitter.name = required_params['name'] if submitter.new_record?
unless @resubmit_submitter unless @resubmit_submitter
@@ -12,11 +12,12 @@ class SubmissionsArchivedController < ApplicationController
@submissions = Submissions.search(current_user, @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 =
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc) if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
else @submissions.order(completed_at: :desc)
@submissions.order(id: :desc) else
end @submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events)) @pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
+22 -1
View File
@@ -21,7 +21,7 @@ class SubmissionsController < ApplicationController
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
unless @submission.submitters.all?(&:completed_at?) unless @submission.completed_at?
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [@submission], records: [@submission],
associations: [{ submitters: :start_form_submission_events }] associations: [{ submitters: :start_form_submission_events }]
@@ -87,6 +87,8 @@ class SubmissionsController < ApplicationController
private private
def create_submissions(template, submissions_params, params) def create_submissions(template, submissions_params, params)
normalize_message_submitter_uuids!(params)
submissions_attrs = submissions_params[:submission].to_h.values submissions_attrs = submissions_params[:submission].to_h.values
submissions_attrs, _, new_fields = submissions_attrs, _, new_fields =
@@ -111,4 +113,23 @@ class SubmissionsController < ApplicationController
def submissions_params def submissions_params
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] }) params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
end end
def normalize_message_submitter_uuids!(params)
return if params[:request_email_per_submitter] == '1'
uuids = params[:email_message_submitter_uuids]
return if uuids.blank?
return if params[:subject].blank? && params[:body].blank?
params[:submitter_preferences] =
Array.wrap(uuids).index_with { { 'subject' => params[:subject], 'body' => params[:body] } }
params[:request_email_per_submitter] = '1'
params.delete(:subject)
params.delete(:body)
params
end
end end
@@ -13,11 +13,12 @@ class SubmissionsDashboardController < ApplicationController
@submissions = Submissions.search(current_user, @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 =
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc) if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
else @submissions.order(completed_at: :desc)
@submissions.order(id: :desc) else
end @submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events)) @pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
@@ -25,7 +25,7 @@ class SubmissionsPreviewController < ApplicationController
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at? raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
if !@submission.submitters.all?(&:completed_at?) && !signature_valid && if !@submission.completed_at? && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission)) (!current_user || !current_ability.can?(:read, @submission))
raise ActionController::RoutingError, I18n.t('not_found') raise ActionController::RoutingError, I18n.t('not_found')
end end
@@ -4,7 +4,7 @@ class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission load_and_authorize_resource :submission
def create def create
authorize!(:update, @submission) authorize!(:destroy, @submission)
@submission.update!(archived_at: nil) @submission.update!(archived_at: nil)
@@ -19,7 +19,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
@submitter ||= Submitter.find_by!(slug: submitter_slug) @submitter ||= Submitter.find_by!(slug: submitter_slug)
Submissions::EnsureResultGenerated.call(@submitter) Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
@@ -28,11 +28,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
Submissions::EnsureResultGenerated.call(last_submitter) Submissions::EnsureResultGenerated.call(last_submitter)
if !signature_valid && !current_user_submitter?(last_submitter) if !signature_valid && !current_user_submitter?(last_submitter)
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
if last_submitter.completed_at < TTL.ago if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
+10 -1
View File
@@ -18,7 +18,10 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission submission = @submitter.submission
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request) return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
if @submitter.completed_at? || submission.completed_at?
return redirect_to submit_form_completed_path(@submitter.slug)
end
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -71,6 +74,12 @@ class SubmitFormController < ApplicationController
status: :unprocessable_content status: :unprocessable_content
end end
if @submitter.viewer?
Rollbar.warning("Submit viewer: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: I18n.t('form_is_view_only') }, status: :unprocessable_content
end
Submitters::SubmitValues.call(@submitter, params, request) Submitters::SubmitValues.call(@submitter, params, request)
head :ok head :ok
@@ -13,6 +13,7 @@ class SubmitFormDeclineController < ApplicationController
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||
@submitter.submission.expired? || @submitter.submission.expired? ||
@submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
@submitter.viewer? ||
!Submitters::AuthorizedForForm.call(@submitter, !Submitters::AuthorizedForForm.call(@submitter,
current_user, current_user,
request) request)
@@ -12,6 +12,7 @@ class SubmitFormDelegateController < ApplicationController
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||
@submitter.submission.expired? || @submitter.submission.expired? ||
@submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
@submitter.viewer? ||
!Submitters::AuthorizedForForm.call(@submitter, !Submitters::AuthorizedForForm.call(@submitter,
current_user, current_user,
request) request)
@@ -14,6 +14,7 @@ class SubmitFormDownloadController < ApplicationController
return head :unprocessable_content if @submitter.declined_at? || return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? || @submitter.submission.archived_at? ||
@submitter.submission.expired? || @submitter.submission.expired? ||
@submitter.submission.completed_at? ||
@submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id, AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
@@ -12,6 +12,8 @@ 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?
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? || if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request) !Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return redirect_to submit_form_path(@submitter.slug) return redirect_to submit_form_path(@submitter.slug)
@@ -48,6 +48,7 @@ class SubmitFormInviteController < ApplicationController
!submitter.submission.archived_at? && !submitter.submission.archived_at? &&
!submitter.submission.expired? && !submitter.submission.expired? &&
!submitter.submission.template&.archived_at? && !submitter.submission.template&.archived_at? &&
!submitter.viewer? &&
Submitters::AuthorizedForForm.call(submitter, current_user, request) Submitters::AuthorizedForForm.call(submitter, current_user, request)
end end
@@ -7,13 +7,7 @@ class SubmitFormMetadataController < ApplicationController
def index def index
@submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :not_found if @submitter.declined_at? || return head :not_found unless authorized_submitter?(@submitter)
@submitter.completed_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
@submitter.account.archived_at? ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
submission = @submitter.submission submission = @submitter.submission
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
@@ -34,4 +28,17 @@ class SubmitFormMetadataController < ApplicationController
render json: { text_runs: } render json: { text_runs: }
end end
private
def authorized_submitter?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.completed_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at? &&
!submitter.account.archived_at? &&
Submitters::AuthorizedForForm.call(submitter, current_user, request)
end
end end
+2 -2
View File
@@ -50,7 +50,7 @@ class SubmittersController < ApplicationController
def submitter_editable?(submission) def submitter_editable?(submission)
!@submitter.submission_events.exists?(event_type: 'start_form') && !@submitter.submission_events.exists?(event_type: 'start_form') &&
!@submitter.completed_at? && !@submitter.declined_at? && !@submitter.completed_at? && !@submitter.declined_at? && !submission.completed_at? &&
!submission.archived_at? && !submission.expired? && !submission.template&.archived_at? !submission.archived_at? && !submission.expired? && !submission.template&.archived_at?
end end
@@ -58,7 +58,7 @@ class SubmittersController < ApplicationController
if params[:send_email] == '1' && submitter.email.present? if params[:send_email] == '1' && submitter.email.present?
is_sent_recently = Docuseal.multitenant? && is_sent_recently = Docuseal.multitenant? &&
EmailEvent.exists?(email: submitter.email, EmailEvent.exists?(email: submitter.email,
tag: 'submitter_invitation', tag: %w[submitter_invitation submitter_view_invitation],
emailable: submitter, emailable: submitter,
event_type: 'send', event_type: 'send',
created_at: 4.hours.ago..Time.current) created_at: 4.hours.ago..Time.current)
@@ -10,7 +10,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
@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?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc) @submissions.order(completed_at: :desc)
else else
@submissions.order(id: :desc) @submissions.order(id: :desc)
end end
+1 -1
View File
@@ -14,7 +14,7 @@ class TemplatesController < ApplicationController
submissions = Submissions::Filter.filter_by_status(submissions, params) submissions = Submissions::Filter.filter_by_status(submissions, 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?
submissions.order(Submitter.arel_table[:completed_at].maximum.desc) submissions.order(completed_at: :desc)
else else
submissions.order(id: :desc) submissions.order(id: :desc)
end end
@@ -5,6 +5,8 @@ class TemplatesPreferencesController < ApplicationController
RESETTABLE_PREFERENCE_KEYS = { RESETTABLE_PREFERENCE_KEYS = {
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters], AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[invitation_view_email_subject
invitation_view_email_body],
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
invitation_reminder_email_body], invitation_reminder_email_body],
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body], AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
@@ -48,12 +50,12 @@ class TemplatesPreferencesController < ApplicationController
def template_params def template_params
params.require(:template).permit( params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body preferences: %i[bcc_completed request_email_subject request_email_body
invitation_view_email_subject invitation_view_email_body
invitation_reminder_email_subject invitation_reminder_email_body invitation_reminder_email_subject invitation_reminder_email_body
documents_copy_email_subject documents_copy_email_body documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_enabled documents_copy_email_attach_audit
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
require_all_submitters submitters_order require_phone_2fa require_email_2fa require_all_submitters submitters_order require_phone_2fa require_email_2fa
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
+2
View File
@@ -42,6 +42,7 @@ import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container' import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor' import EmailEditor from './elements/email_editor'
import MarkdownEditor from './elements/markdown_editor' import MarkdownEditor from './elements/markdown_editor'
import HtmlEditor from './elements/html_editor'
import MountOnClick from './elements/mount_on_click' import MountOnClick from './elements/mount_on_click'
import RemoveOnEvent from './elements/remove_on_event' import RemoveOnEvent from './elements/remove_on_event'
import ScrollTo from './elements/scroll_to' import ScrollTo from './elements/scroll_to'
@@ -135,6 +136,7 @@ safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer) safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor) safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('markdown-editor', MarkdownEditor) safeRegisterElement('markdown-editor', MarkdownEditor)
safeRegisterElement('html-editor', HtmlEditor)
safeRegisterElement('mount-on-click', MountOnClick) safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('remove-on-event', RemoveOnEvent) safeRegisterElement('remove-on-event', RemoveOnEvent)
safeRegisterElement('scroll-to', ScrollTo) safeRegisterElement('scroll-to', ScrollTo)
+75 -3
View File
@@ -9,8 +9,9 @@ function loadCodeMirror () {
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'), import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'), import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'), import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/lint'),
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow') import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
]).then(([view, commands, language, html, htmlflow]) => { ]).then(([view, commands, language, html, lint, htmlflow]) => {
return { return {
minimalSetup: [ minimalSetup: [
commands.history(), commands.history(),
@@ -19,6 +20,8 @@ function loadCodeMirror () {
], ],
EditorView: view.EditorView, EditorView: view.EditorView,
html: html.html, html: html.html,
htmlLanguage: html.htmlLanguage,
linter: lint.linter,
htmlflow: htmlflow.default || htmlflow htmlflow: htmlflow.default || htmlflow
} }
}) })
@@ -46,6 +49,70 @@ export default targetable(class extends HTMLElement {
this.previewViewTab.addEventListener('click', this.showPreviewView) this.previewViewTab.addEventListener('click', this.showPreviewView)
this.codeViewTab.addEventListener('click', this.showCodeView) this.codeViewTab.addEventListener('click', this.showCodeView)
this.form = this.closest('form')
this.form?.addEventListener('submit', this.validateOnSubmit)
}
disconnectedCallback () {
this.form?.removeEventListener('submit', this.validateOnSubmit)
}
validateOnSubmit = (e) => {
if (!this.htmlLanguage) return
const bodyType = this.form.querySelector('input[name$="[body_type]"]:checked')?.value
if (bodyType && bodyType !== 'html') return
const diagnostics = this.buildDiagnostics(this.input.value)
if (diagnostics.length === 0) return
e.preventDefault()
this.showCodeView()
const pos = Math.min(diagnostics[0].from, this.editorView.state.doc.length)
this.editorView.dispatch({ selection: { anchor: pos }, scrollIntoView: true })
this.editorView.focus()
alert(diagnostics[0].message)
}
buildDiagnostics (value) {
const diagnostics = []
if (!value.trim()) return diagnostics
if (!/^\s*(<!doctype[^>]*>\s*)?<html/i.test(value)) {
diagnostics.push({
from: 0,
to: Math.min(5, value.length),
severity: 'error',
message: 'The email template must start with the <html> tag'
})
}
const seen = new Set()
this.htmlLanguage.parser.parse(value).iterate({
enter: (node) => {
if (!node.type.isError || seen.has(node.from) || seen.size >= 20) return
seen.add(node.from)
diagnostics.push({
from: node.from,
to: Math.min(node.to + 1, value.length),
severity: 'error',
message: 'The email template contains invalid HTML'
})
}
})
return diagnostics
} }
showCodeView = () => { showCodeView = () => {
@@ -76,7 +143,9 @@ export default targetable(class extends HTMLElement {
this.input = this.querySelector('input[type="hidden"]') this.input = this.querySelector('input[type="hidden"]')
this.input.style.display = 'none' this.input.style.display = 'none'
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror() const { EditorView, minimalSetup, html, htmlLanguage, linter, htmlflow } = await loadCodeMirror()
this.htmlLanguage = htmlLanguage
this.editorView = new EditorView({ this.editorView = new EditorView({
doc: this.input.value, doc: this.input.value,
@@ -85,8 +154,11 @@ export default targetable(class extends HTMLElement {
html(), html(),
minimalSetup, minimalSetup,
EditorView.lineWrapping, EditorView.lineWrapping,
linter((view) => this.buildDiagnostics(view.state.doc.toString()), { delay: 600 }),
EditorView.updateListener.of(update => { EditorView.updateListener.of(update => {
if (update.docChanged) this.input.value = update.state.doc.toString() if (update.docChanged) {
this.input.value = update.state.doc.toString()
}
}), }),
EditorView.theme({ EditorView.theme({
'&': { '&': {
+649
View File
@@ -0,0 +1,649 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
import { actionable } from '@github/catalyst/lib/actionable'
import { LinkTooltip } from './markdown_editor'
async function loadTiptap () {
const [core, document, text, hardBreak, gapcursor, dropcursor, extensions, pmState, pmView] = await Promise.all([
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-gapcursor'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-dropcursor'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
])
return {
Editor: core.Editor,
Extension: core.Extension,
Node: core.Node,
Mark: core.Mark,
Document: document.default || document,
Text: text.default || text,
HardBreak: hardBreak.default || hardBreak,
Gapcursor: gapcursor.default || gapcursor,
Dropcursor: dropcursor.default || dropcursor,
UndoRedo: extensions.UndoRedo,
Plugin: pmState.Plugin,
Decoration: pmView.Decoration,
DecorationSet: pmView.DecorationSet
}
}
const editorStylesheet = new CSSStyleSheet()
editorStylesheet.replaceSync(`
:host {
display: block;
max-height: 360px;
overflow: auto;
border-radius: 0 0 1rem 1rem;
}
.ProseMirror {
word-wrap: break-word;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
font-feature-settings: "liga" 0;
outline: none;
min-height: 220px;
padding: 12px;
}
img.ProseMirror-separator {
display: inline !important;
border: none !important;
margin: 0 !important;
width: 0 !important;
height: 0 !important;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
margin: 0;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-hideselection *::selection {
background: transparent;
}
.ProseMirror-hideselection *::-moz-selection {
background: transparent;
}
.ProseMirror-hideselection * {
caret-color: transparent;
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
.variable-highlight {
background-color: #fef3c7;
padding: 1px 2px;
border-radius: 4px;
}
`)
function collectDomAttrs (dom) {
const attrs = {}
for (let i = 0; i < dom.attributes.length; i++) {
attrs[dom.attributes[i].name] = dom.attributes[i].value
}
return { htmlAttrs: attrs }
}
function collectSpanDomAttrs (dom) {
const result = collectDomAttrs(dom)
if (result.htmlAttrs.style) {
const temp = document.createElement('span')
temp.style.cssText = result.htmlAttrs.style
if (['bold', '700'].includes(temp.style.fontWeight)) {
temp.style.removeProperty('font-weight')
}
if (temp.style.fontStyle === 'italic') {
temp.style.removeProperty('font-style')
}
if (temp.style.textDecoration === 'underline') {
temp.style.removeProperty('text-decoration')
}
if (temp.style.cssText) {
result.htmlAttrs.style = temp.style.cssText
} else {
delete result.htmlAttrs.style
}
}
return result
}
function buildExtensions ({ Node, Mark, Extension, Plugin, Decoration, DecorationSet }) {
const blockNode = (name, tag, content, extra = {}) => Node.create({
name,
group: 'block',
content: content || 'block+',
...extra,
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{ tag, getAttrs: collectDomAttrs }]
},
renderHTML ({ node }) {
return [tag, node.attrs.htmlAttrs, 0]
}
})
const attrsMark = (name, tag) => Mark.create({
name,
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{ tag, getAttrs: collectDomAttrs }]
},
renderHTML ({ mark }) {
return [tag, mark.attrs.htmlAttrs, 0]
}
})
const SpanMark = Mark.create({
name: 'span',
excludes: '',
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{ tag: 'span', getAttrs: collectSpanDomAttrs }]
},
renderHTML ({ mark }) {
return ['span', mark.attrs.htmlAttrs, 0]
}
})
const toggleMark = (name, renderTag, parseRules, shortcuts) => Mark.create({
name,
parseHTML () {
return parseRules
},
renderHTML () {
return [renderTag, 0]
},
addCommands () {
const commandName = `toggle${name[0].toUpperCase()}${name.slice(1)}`
return {
[commandName]: () => ({ commands }) => commands.toggleMark(name)
}
},
addKeyboardShortcuts () {
return {
[shortcuts]: () => this.editor.commands.toggleMark(name)
}
}
})
const Heading = Node.create({
name: 'heading',
group: 'block',
content: 'inline*',
addAttributes () {
return {
htmlAttrs: { default: {} },
level: { default: 1 }
}
},
parseHTML () {
return [1, 2, 3, 4, 5, 6].map((level) => ({
tag: `h${level}`,
getAttrs: (dom) => ({ ...collectDomAttrs(dom), level })
}))
},
renderHTML ({ node }) {
return [`h${node.attrs.level}`, node.attrs.htmlAttrs, 0]
}
})
const ImageNode = Node.create({
name: 'image',
inline: true,
group: 'inline',
draggable: true,
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{ tag: 'img', getAttrs: collectDomAttrs }]
},
renderHTML ({ node }) {
return ['img', node.attrs.htmlAttrs]
}
})
const HrNode = Node.create({
name: 'horizontalRule',
group: 'block',
atom: true,
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{ tag: 'hr', getAttrs: collectDomAttrs }]
},
renderHTML ({ node }) {
return ['hr', node.attrs.htmlAttrs]
}
})
const StyleNode = Node.create({
name: 'style',
group: 'block',
atom: true,
selectable: false,
addAttributes () {
return {
htmlAttrs: { default: {} },
css: { default: '' }
}
},
parseHTML () {
return [{ tag: 'style', getAttrs: (dom) => ({ ...collectDomAttrs(dom), css: dom.textContent }) }]
},
renderHTML ({ node }) {
return ['style', node.attrs.htmlAttrs, node.attrs.css]
}
})
const EmptySpanNode = Node.create({
name: 'emptySpan',
inline: true,
group: 'inline',
atom: true,
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{
tag: 'span',
priority: 60,
getAttrs (dom) {
if (dom.childNodes.length === 0 && dom.attributes.length > 0) {
return collectDomAttrs(dom)
}
return false
}
}]
},
renderHTML ({ node }) {
return ['span', node.attrs.htmlAttrs]
}
})
const LinkMark = Mark.create({
name: 'link',
inclusive: true,
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{ tag: 'a', getAttrs: collectDomAttrs }]
},
renderHTML ({ mark }) {
return ['a', mark.attrs.htmlAttrs, 0]
},
addCommands () {
return {
setLink: ({ href }) => ({ editor, commands }) => {
const htmlAttrs = { ...(editor.getAttributes('link').htmlAttrs || {}), href }
return commands.setMark('link', { htmlAttrs })
},
unsetLink: () => ({ commands }) => commands.unsetMark('link', { extendEmptyMarkRange: true })
}
}
})
const buildDecorations = (doc) => {
const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
doc.descendants((node, pos) => {
if (!node.isText) return
let match
while ((match = regex.exec(node.text)) !== null) {
decorations.push(
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
class: 'variable-highlight'
})
)
}
})
return DecorationSet.create(doc, decorations)
}
const VariableHighlight = Extension.create({
name: 'variableHighlight',
addProseMirrorPlugins () {
return [new Plugin({
state: {
init (_, { doc }) {
return buildDecorations(doc)
},
apply (tr, oldSet) {
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
}
},
props: {
decorations (state) {
return this.getState(state)
}
}
})]
}
})
return [
blockNode('paragraph', 'p', 'inline*'),
Heading,
blockNode('section', 'section'),
blockNode('article', 'article', null, { isolating: true }),
blockNode('header', 'header', null, { isolating: true }),
blockNode('footer', 'footer', null, { isolating: true }),
blockNode('div', 'div'),
blockNode('center', 'center'),
blockNode('blockquote', 'blockquote'),
blockNode('pre', 'pre'),
blockNode('orderedList', 'ol', '(listItem | block)+'),
blockNode('bulletList', 'ul', '(listItem | block)+'),
blockNode('listItem', 'li', 'block+', { group: null }),
blockNode('table', 'table', '(colgroup | tableHead | tableBody | tableFoot | tableRow)+'),
blockNode('tableHead', 'thead', 'tableRow+', { group: null }),
blockNode('tableBody', 'tbody', 'tableRow+', { group: null }),
blockNode('tableFoot', 'tfoot', 'tableRow+', { group: null }),
blockNode('tableRow', 'tr', '(tableCell | tableHeader)+', { group: null }),
blockNode('tableCell', 'td', 'block*', { group: null }),
blockNode('tableHeader', 'th', 'block*', { group: null }),
blockNode('colgroup', 'colgroup', 'col*', { group: null }),
Node.create({
name: 'col',
atom: true,
addAttributes () {
return { htmlAttrs: { default: {} } }
},
parseHTML () {
return [{ tag: 'col', getAttrs: collectDomAttrs }]
},
renderHTML ({ node }) {
return ['col', node.attrs.htmlAttrs]
}
}),
ImageNode,
HrNode,
StyleNode,
EmptySpanNode,
SpanMark,
LinkMark,
toggleMark('bold', 'strong', [{ tag: 'strong' }, { tag: 'b' }, { style: 'font-weight=bold' }, { style: 'font-weight=700' }], 'Mod-b'),
toggleMark('italic', 'em', [{ tag: 'em' }, { tag: 'i' }, { style: 'font-style=italic' }], 'Mod-i'),
toggleMark('underline', 'u', [{ tag: 'u' }, { style: 'text-decoration=underline' }], 'Mod-u'),
toggleMark('strike', 's', [{ tag: 's' }, { tag: 'del' }, { tag: 'strike' }, { style: 'text-decoration=line-through' }], 'Mod-Shift-s'),
attrsMark('subscript', 'sub'),
attrsMark('superscript', 'sup'),
VariableHighlight
]
}
export default actionable(targetable(class extends HTMLElement {
static [target.static] = [
'textarea',
'editorElement',
'boldButton',
'italicButton',
'underlineButton',
'linkButton',
'linkTooltipTemplate'
]
async connectedCallback () {
if (!this.textarea || !this.editorElement) return
this.textarea.style.display = 'none'
this.adjustShortcutsForPlatform()
const tiptap = await loadTiptap()
const { Editor, Extension, Document, Text, HardBreak, UndoRedo, Gapcursor, Dropcursor } = tiptap
this.emailDocument = new DOMParser().parseFromString(this.textarea.value, 'text/html')
const shadow = this.editorElement.attachShadow({ mode: 'open' })
shadow.adoptedStyleSheets = [editorStylesheet]
this.emailDocument.head.querySelectorAll('style').forEach((style) => {
shadow.appendChild(style.cloneNode(true))
})
const container = document.createElement('div')
const bodyStyle = this.emailDocument.body.getAttribute('style')
if (bodyStyle) container.setAttribute('style', bodyStyle)
shadow.appendChild(container)
const LinkShortcut = Extension.create({
name: 'linkShortcut',
addKeyboardShortcuts: () => ({
'Mod-k': () => {
this.toggleLink()
return true
}
})
})
this.editor = new Editor({
element: container,
extensions: [
Document,
Text,
HardBreak,
UndoRedo,
Gapcursor,
Dropcursor,
...buildExtensions(tiptap),
LinkShortcut
],
content: this.emailDocument.body.innerHTML,
injectCSS: false,
editorProps: {
attributes: {
dir: 'auto'
},
handleDOMEvents: {
click: (_, event) => {
if (event.target.closest('a')) event.preventDefault()
return false
}
}
},
onUpdate: ({ editor }) => {
this.emailDocument.body.innerHTML = editor.getHTML()
this.textarea.value = this.emailDocument.documentElement.outerHTML
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
},
onSelectionUpdate: ({ editor }) => {
this.updateToolbarState()
this.handleLinkTooltip(editor)
},
onBlur: () => {
setTimeout(() => {
if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
this.linkTooltip.hide()
}
}, 0)
}
})
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
}
adjustShortcutsForPlatform () {
if ((navigator.userAgentData?.platform)?.toLowerCase()?.includes('mac')) {
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
const tip = tooltip.getAttribute('data-tip')
if (tip && tip.includes('Ctrl')) {
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
}
})
}
}
bold (e) {
e.preventDefault()
this.editor.chain().focus().toggleBold().run()
this.updateToolbarState()
}
italic (e) {
e.preventDefault()
this.editor.chain().focus().toggleItalic().run()
this.updateToolbarState()
}
underline (e) {
e.preventDefault()
this.editor.chain().focus().toggleUnderline().run()
this.updateToolbarState()
}
linkSelection (e) {
e.preventDefault()
this.toggleLink()
this.updateToolbarState()
}
undo (e) {
e.preventDefault()
this.editor.chain().focus().undo().run()
this.updateToolbarState()
}
redo (e) {
e.preventDefault()
this.editor.chain().focus().redo().run()
this.updateToolbarState()
}
updateToolbarState () {
this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
}
handleLinkTooltip (editor) {
const { from } = editor.state.selection
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
if (!mark) {
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
return
}
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
let linkStart = from
const start = editor.state.doc.resolve(from).start()
for (let i = from - 1; i >= start; i--) {
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
linkStart = i
} else {
break
}
}
this.linkTooltip.hide()
this.linkTooltip.show(mark.attrs.htmlAttrs?.href, linkStart > start ? linkStart - 1 : linkStart)
this.linkTooltip.currentMark = mark
}
toggleLink () {
if (this.editor.isActive('link')) {
this.linkTooltip.hide()
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
this.updateToolbarState()
} else {
const { from } = this.editor.state.selection
this.linkTooltip.hide()
this.linkTooltip.show(this.editor.getAttributes('link').htmlAttrs?.href, from, { focus: true })
}
}
insertVariable (e) {
const variable = e.target.closest('[data-variable]')?.dataset.variable
if (variable) {
const { from, to } = this.editor.state.selection
if (variable.includes('link') && from !== to) {
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
} else {
this.editor.chain().focus().insertContent(`{${variable}}`).run()
}
}
}
disconnectedCallback () {
this.linkTooltip?.hide()
if (this.editor) {
this.editor.destroy()
}
}
}))
+1 -1
View File
@@ -35,7 +35,7 @@ function loadTiptap () {
})) }))
} }
class LinkTooltip { export class LinkTooltip {
constructor (container, editor, templateEl) { constructor (container, editor, templateEl) {
this.container = container this.container = container
this.editor = editor this.editor = editor
+7 -2
View File
@@ -147,7 +147,7 @@
@submit.prevent="submitStep" @submit.prevent="submitStep"
/> />
<button <button
v-if="!isFormVisible" v-if="!isFormVisible && currentField"
id="expand_form_button" id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base" class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
style="width: 96%; margin-left: 2%" style="width: 96%; margin-left: 2%"
@@ -174,6 +174,7 @@
/> />
</button> </button>
<div <div
v-if="currentField"
v-show="isFormVisible" v-show="isFormVisible"
id="form_container" id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden" class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
@@ -1172,7 +1173,11 @@ export default {
} }
}, },
isAnonymousChecboxes () { isAnonymousChecboxes () {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4 if (this.currentField) {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
} else {
return false
}
}, },
isButtonDisabled () { isButtonDisabled () {
if (this.recalculateButtonDisabledKey) { if (this.recalculateButtonDisabledKey) {
+13 -29
View File
@@ -3206,15 +3206,6 @@ export default {
e.preventDefault() e.preventDefault()
alert(this.t('please_draw_fields_to_prepare_the_document')) alert(this.t('please_draw_fields_to_prepare_the_document'))
} else {
const submitterWithoutFields =
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
if (submitterWithoutFields) {
e.preventDefault()
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
}
} }
}, },
onSaveClick () { onSaveClick () {
@@ -3231,32 +3222,25 @@ export default {
if (!this.template.fields.length) { if (!this.template.fields.length) {
alert(this.t('please_draw_fields_to_prepare_the_document')) alert(this.t('please_draw_fields_to_prepare_the_document'))
} else { } else {
const submitterWithoutFields = this.isSaving = true
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
if (submitterWithoutFields) { const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
} else {
this.isSaving = true
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic) dynamicDocumentRefs.map((ref) => ref.update())
dynamicDocumentRefs.map((ref) => ref.update()) this.rebuildVariablesSchema({ disable: false })
this.rebuildVariablesSchema({ disable: false }) const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody()) Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => {
if (this.withRevisions) {
this.captureRevision()
}
Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => { window.Turbo.visit(`/templates/${this.template.id}`)
if (this.withRevisions) { }).finally(() => {
this.captureRevision() this.isSaving = false
} })
window.Turbo.visit(`/templates/${this.template.id}`)
}).finally(() => {
this.isSaving = false
})
}
} }
}, },
scrollToArea (area) { scrollToArea (area) {
@@ -600,7 +600,8 @@ export default {
} }
}, },
showFont () { showFont () {
return ['text', 'number', 'date', 'select', 'heading', 'cells'].includes(this.field.type) return ['text', 'number', 'date', 'select', 'heading', 'cells'].includes(this.field.type) ||
(['radio', 'multiple'].includes(this.field.type) && this.field.areas?.every((a) => !a.option_uuid))
}, },
showDescription () { showDescription () {
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type) return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
@@ -452,7 +452,7 @@
class="pb-0.5 mt-0.5" class="pb-0.5 mt-0.5"
> >
<li <li
v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)" v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type) || (['radio', 'multiple'].includes(field.type) && field.areas?.every((a) => !a.option_uuid))"
class="field-settings-font" class="field-settings-font"
> >
<label <label
@@ -830,6 +830,7 @@ export default {
} else if (format === 'percent') { } else if (format === 'percent') {
return `${number}%` return `${number}%`
} else if (format === 'percent_space') { } else if (format === 'percent_space') {
// eslint-disable-next-line no-irregular-whitespace
return `${String(number).replace('.', ',')} %` return `${String(number).replace('.', ',')} %`
} else { } else {
return number return number
+2 -1
View File
@@ -11,7 +11,8 @@ class ProcessSubmissionExpiredJob
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 if submission.completed_at?
return if params['expire_at'] && submission.expire_at&.to_i != params['expire_at']
WebhookUrls.enqueue_events(submission, 'submission.expired') WebhookUrls.enqueue_events(submission, 'submission.expired')
end end
+65 -12
View File
@@ -5,30 +5,45 @@ class ProcessSubmitterCompletionJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
submission = submitter.submission
create_completed_submitter!(submitter) create_completed_submitter!(submitter)
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil) is_last =
if params.key?('is_last')
params['is_last']
else
viewer_uuids = submission.template_submitters.to_a.filter_map { |s| s['uuid'] if s['is_viewer'] }
incomplete = submission.submitters.where(completed_at: nil)
incomplete = incomplete.where.not(uuid: viewer_uuids) if viewer_uuids.present?
!incomplete.exists? &&
submitter.completed_at == submission.submitters.maximum(:completed_at)
end
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at) if is_last
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true) if submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
Submissions::EnsureCombinedGenerated.call(submitter) Submissions::EnsureCombinedGenerated.call(submitter)
end end
Submissions::EnsureAuditGenerated.call(submitter.submission) Submissions::EnsureAuditGenerated.call(submission)
enqueue_completed_emails(submitter) enqueue_completed_emails(submitter)
end end
create_completed_documents!(submitter) create_completed_documents!(submitter)
if !is_all_completed && submitter.submission.submitters_order_preserved? && params['send_invitation_email'] != false if !submission.completed_at && submission.submitters_order_preserved? && params['send_invitation_email'] != false &&
enqueue_next_submitter_request_notification(submitter) Submission.exists?(id: submission.id, completed_at: nil)
next_submitters = enqueue_next_submitter_request_notification(submitter)
enqueue_next_submitter_viewer_notification(submission, next_submitters) unless is_last
end end
enqueue_completed_webhooks(submitter, is_all_completed:) enqueue_completed_webhooks(submitter, is_last:)
end end
def create_completed_submitter!(submitter) def create_completed_submitter!(submitter)
@@ -77,7 +92,7 @@ class ProcessSubmitterCompletionJob
end end
end end
def enqueue_completed_webhooks(submitter, is_all_completed: false) def enqueue_completed_webhooks(submitter, is_last: false)
event_uuids = {} event_uuids = {}
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook| WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
@@ -89,7 +104,7 @@ class ProcessSubmitterCompletionJob
'webhook_url_id' => webhook.id) 'webhook_url_id' => webhook.id)
end end
next unless webhook.events.include?('submission.completed') && is_all_completed next unless webhook.events.include?('submission.completed') && is_last
event_uuids['submission.completed'] ||= SecureRandom.uuid event_uuids['submission.completed'] ||= SecureRandom.uuid
@@ -137,7 +152,7 @@ class ProcessSubmitterCompletionJob
return if configs.value['enabled'] == false return if configs.value['enabled'] == false
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false } to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name) .sort_by { |e| e.completed_at || Time.current }.select(&:email?).map(&:friendly_name)
return if to.blank? return if to.blank?
@@ -157,9 +172,9 @@ class ProcessSubmitterCompletionJob
bcc.to_s.scan(User::EMAIL_REGEXP) bcc.to_s.scan(User::EMAIL_REGEXP)
end end
def enqueue_next_submitter_request_notification(submitter) def enqueue_next_submitter_request_notification(submitter) # rubocop:disable Metrics/PerceivedComplexity
submission = submitter.submission submission = submitter.submission
submitters_index = submission.submitters.index_by(&:uuid) submitters_index = submission.submitters.reject(&:viewer?).index_by(&:uuid)
next_submitter_items = next_submitter_items =
if submission.template_submitters.any? { |s| s['order'] } if submission.template_submitters.any? { |s| s['order'] }
@@ -188,5 +203,43 @@ class ProcessSubmitterCompletionJob
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
Submitters.send_signature_requests(next_submitters) Submitters.send_signature_requests(next_submitters)
next_submitters
end
def enqueue_next_submitter_viewer_notification(submission, next_submitters)
viewers = submission.submitters.select(&:viewer?)
return [] if viewers.blank?
next_submitter_uuids = next_submitters.to_set(&:uuid)
viewers_index = viewers.index_by(&:uuid)
next_viewers =
if submission.template_submitters.any? { |s| s['order'] }
next_orders = submission.template_submitters
.select { |s| next_submitter_uuids.include?(s['uuid']) }
.pluck('order')
submission.template_submitters.filter_map do |s|
viewers_index[s['uuid']] if next_orders.include?(s['order'])
end
else
preceding_submitter_uuid = nil
submission.template_submitters.filter_map do |template_submitter|
viewer = viewers_index[template_submitter['uuid']]
if viewer
viewer if next_submitter_uuids.include?(preceding_submitter_uuid)
else
preceding_submitter_uuid = template_submitter['uuid']
nil
end
end
end
Submitters.send_signature_requests(next_viewers)
end end
end end
@@ -19,7 +19,12 @@ class SendSubmitterInvitationEmailJob
return return
end end
mail = SubmitterMailer.invitation_email(submitter) mail =
if submitter.viewer?
SubmitterMailer.invitation_view_email(submitter)
else
SubmitterMailer.invitation_email(submitter)
end
Submitters::ValidateSending.call(submitter, mail) Submitters::ValidateSending.call(submitter, mail)
+51 -6
View File
@@ -16,7 +16,7 @@ class SubmitterMailer < ApplicationMailer
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {} template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
@body = @email_message&.body.presence || @body = @email_message&.normalized_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&.dig('request_email_body').presence @submitter.template&.preferences&.dig('request_email_body').presence
@@ -45,6 +45,45 @@ class SubmitterMailer < ApplicationMailer
end end
end end
def invitation_view_email(submitter)
@current_account = submitter.submission.account
@submitter = submitter
if submitter.preferences['email_message_uuid']
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
@body = @email_message&.normalized_body.presence ||
@submitter.template&.preferences&.dig('invitation_view_email_body').presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence
@subject = @email_message&.subject.presence ||
@submitter.template&.preferences&.dig('invitation_view_email_subject').presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY)
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_view_invitation', @submitter)
reply_to = build_submitter_reply_to(@submitter, email_config: @email_config)
maybe_set_custom_domain(@submitter)
I18n.with_locale(@current_account.locale) do
subject = build_invite_subject(@subject, @email_config, submitter)
mail(
to: @submitter.friendly_name,
from: from_address_for_submitter(submitter),
subject:,
reply_to:
)
end
end
def completed_email(submitter, user, to: nil) def completed_email(submitter, user, to: nil)
@current_account = submitter.submission.account @current_account = submitter.submission.account
@submitter = submitter @submitter = submitter
@@ -53,8 +92,6 @@ class SubmitterMailer < ApplicationMailer
template_preferences = @submission.template&.preferences || {} template_preferences = @submission.template&.preferences || {}
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)
add_completed_email_attachments!( add_completed_email_attachments!(
@@ -109,8 +146,6 @@ class SubmitterMailer < ApplicationMailer
template_preferences = @submitter.template&.preferences || {} template_preferences = @submitter.template&.preferences || {}
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!(
@@ -172,7 +207,7 @@ class SubmitterMailer < ApplicationMailer
end end
def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true) def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true)
documents = with_documents ? Submitters.select_attachments_for_download(submitter) : [] documents = with_documents ? select_completed_documents(submitter) : []
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
@@ -216,6 +251,8 @@ class SubmitterMailer < ApplicationMailer
def build_invite_subject(subject, email_config, submitter) def build_invite_subject(subject, email_config, submitter)
if email_config || subject if email_config || subject
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:) ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
elsif submitter.viewer?
I18n.t(:you_are_invited_to_view_a_document)
elsif submitter.with_signature_fields? elsif submitter.with_signature_fields?
I18n.t(:you_are_invited_to_sign_a_document) I18n.t(:you_are_invited_to_sign_a_document)
else else
@@ -227,6 +264,14 @@ class SubmitterMailer < ApplicationMailer
submitter.template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } submitter.template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] }
end end
def select_completed_documents(submitter)
last_submitter = Submitter.where(submission_id: submitter.submission_id).completed.order(:completed_at).last
Submissions::EnsureResultGenerated.call(last_submitter)
Submitters.select_attachments_for_download(last_submitter)
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)
total_size = current_size total_size = current_size
+1
View File
@@ -24,6 +24,7 @@ class Account < ApplicationRecord
has_many :encrypted_configs, dependent: :destroy has_many :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy has_many :account_configs, dependent: :destroy
has_many :email_messages, dependent: :destroy has_many :email_messages, dependent: :destroy
has_many :email_message_assets, dependent: :destroy
has_many :templates, dependent: :destroy has_many :templates, dependent: :destroy
has_many :template_folders, dependent: :destroy has_many :template_folders, dependent: :destroy
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) }, has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
+8
View File
@@ -22,6 +22,7 @@
# #
class AccountConfig < ApplicationRecord class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email' SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
SUBMITTER_VIEW_INVITATION_EMAIL_KEY = 'submitter_view_invitation_email'
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email' SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email' SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email' SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
@@ -63,6 +64,7 @@ class AccountConfig < ApplicationRecord
EMAIL_VARIABLES = { EMAIL_VARIABLES = {
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze, SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze, SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze,
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze, SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze
@@ -75,6 +77,12 @@ class AccountConfig < ApplicationRecord
'body' => I18n.t(:submitter_invitation_email_sign_body) 'body' => I18n.t(:submitter_invitation_email_sign_body)
} }
}, },
SUBMITTER_VIEW_INVITATION_EMAIL_KEY => lambda {
{
'subject' => I18n.t(:you_are_invited_to_view_a_document),
'body' => I18n.t(:submitter_invitation_email_view_body)
}
},
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda { SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
{ {
'subject' => I18n.t(:you_are_invited_to_sign_a_document), 'subject' => I18n.t(:you_are_invited_to_sign_a_document),
+9
View File
@@ -33,6 +33,15 @@ class EmailMessage < ApplicationRecord
before_validation :set_sha1, on: :create before_validation :set_sha1, on: :create
def normalized_body
@normalized_body ||=
if body&.include?(EmailMessages::ASSET_PREFIX)
EmailMessages.rebuild_body_with_assets(account_id, body)
else
body
end
end
def set_sha1 def set_sha1
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json) self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
end end
+30
View File
@@ -0,0 +1,30 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: email_message_assets
#
# id :bigint not null, primary key
# data :text not null
# sha1 :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_email_message_assets_on_account_id_and_sha1 (account_id,sha1) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class EmailMessageAsset < ApplicationRecord
belongs_to :account
before_validation :set_sha1, on: :create
def set_sha1
self.sha1 = Digest::SHA1.hexdigest(data.to_s)
end
end
+11 -17
View File
@@ -6,6 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# archived_at :datetime # archived_at :datetime
# completed_at :datetime
# expire_at :datetime # expire_at :datetime
# name :text # name :text
# preferences :text not null # preferences :text not null
@@ -25,9 +26,12 @@
# #
# Indexes # Indexes
# #
# index_submissions_on_account_id_and_completed_at (account_id,completed_at) WHERE ((completed_at IS NOT NULL) AND (archived_at IS NULL))
# index_submissions_on_account_id_and_id (account_id,id) # index_submissions_on_account_id_and_id (account_id,id)
# index_submissions_on_account_id_and_id_pending (account_id,id) WHERE ((completed_at IS NULL) AND (archived_at IS NULL))
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL) # index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
# 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_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
# index_submissions_on_created_at (created_at)
# index_submissions_on_created_by_user_id (created_by_user_id) # index_submissions_on_created_by_user_id (created_by_user_id)
# index_submissions_on_slug (slug) UNIQUE # index_submissions_on_slug (slug) UNIQUE
# index_submissions_on_template_id (template_id) # index_submissions_on_template_id (template_id)
@@ -88,27 +92,17 @@ class Submission < ApplicationRecord
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
scope :pending, lambda { scope :non_expired, -> { where(expire_at: nil).or(where(expire_at: Time.current..)) }
where(expire_at: nil).or(where(expire_at: Time.current..)) scope :pending, -> { non_expired.where(completed_at: nil) }
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) scope :completed, -> { where.not(completed_at: nil) }
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :declined, lambda { scope :declined, lambda {
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists) .where.not(declined_at: nil).limit(1).arel.exists)
}
scope :expired, lambda {
where(expire_at: ..Time.current)
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
} }
scope :expired, -> { where(expire_at: ..Time.current).where(completed_at: nil) }
scope :select_for_list, lambda { scope :select_for_list, lambda {
select(:id, :name, :created_by_user_id, :account_id, select(:id, :name, :created_by_user_id, :account_id, :completed_at,
:created_at, :archived_at, :expire_at, :template_id, :template_submitters) :created_at, :archived_at, :expire_at, :template_id, :template_submitters)
} }
+6
View File
@@ -116,6 +116,12 @@ class Submitter < ApplicationRecord
end end
end end
def viewer?
return false if submission.template_submitters.blank?
submission.template_submitters.any? { |s| s['uuid'] == uuid && s['is_viewer'] }
end
private private
def anonymize_email_events def anonymize_email_events
@@ -57,6 +57,11 @@
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
<% if Docuseal.multitenant? && @encrypted_config.persisted? %>
<div class="flex justify-center pt-2">
<%= button_to t('reset_default'), settings_email_path(@encrypted_config), method: :delete, class: 'link', data: { turbo_confirm: t('are_you_sure_') } %>
</div>
<% end %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
</div> </div>
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" />
<path d="M9 15l2 2l4 -4" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

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

After

Width:  |  Height:  |  Size: 482 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M16 7h4" />
<path d="M16 16v1l2 2l.5 -.5m1.5 -2.5v-11c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v7" />
<path d="M18 19h-13a2 2 0 1 1 0 -4h4a2 2 0 1 0 0 -4h-3" />
<path d="M3 3l18 18" />
</svg>

After

Width:  |  Height:  |  Size: 480 B

@@ -0,0 +1,53 @@
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-action="click:<%= editor_tag %>#bold" data-target="<%= editor_tag %>.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-action="click:<%= editor_tag %>#italic" data-target="<%= editor_tag %>.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:<%= editor_tag %>#underline" data-target="<%= editor_tag %>.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:<%= editor_tag %>#linkSelection" data-target="<%= editor_tag %>.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
</div>
</div>
<div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-action="click:<%= editor_tag %>#undo" data-target="<%= editor_tag %>.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-action="click:<%= editor_tag %>#redo" data-target="<%= editor_tag %>.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button>
</div>
</div>
<% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
<% local_assigns[:variables]&.each do |variable| %>
<button type="button" data-variable="<%= variable %>" data-action="click:<%= editor_tag %>#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
</div>
</div>
<% end %>
</div>
@@ -0,0 +1 @@
<%= render 'personalization_settings/markdown_editor', name:, value:, variables: local_assigns[:variables] %>
@@ -1,76 +1,18 @@
<% if value.to_s.start_with?('<html') %> <markdown-editor>
<autoresize-textarea> <template data-target="markdown-editor.linkTooltipTemplate">
<%= text_area_tag name, value, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto', style: 'max-height: 400px' %> <div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
</autoresize-textarea> <input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
<% else %> <button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
<markdown-editor> <%= svg_icon('check', class: 'w-4 h-4 text-success') %>
<template data-target="markdown-editor.linkTooltipTemplate"> </button>
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false"> <button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off"> <%= svg_icon('x', class: 'w-4 h-4 text-error') %>
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer"> </button>
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
</button>
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
</button>
</div>
</template>
<div class="border border-base-content/20 rounded-2xl bg-white">
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
</div>
</div>
<div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button>
</div>
</div>
<% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
<% local_assigns[:variables]&.each do |variable| %>
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
</div>
</div>
<% end %>
</div>
<div data-target="markdown-editor.editorElement"></div>
</div> </div>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %> </template>
</markdown-editor> <div class="border border-base-content/20 rounded-2xl bg-white">
<% end %> <%= render 'personalization_settings/editor_toolbar', editor_tag: 'markdown-editor', variables: local_assigns[:variables] %>
<div data-target="markdown-editor.editorElement"></div>
</div>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
</markdown-editor>
+3 -1
View File
@@ -4,7 +4,9 @@
<p> <p>
<% if @current_account&.testing? %> <% if @current_account&.testing? %>
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %> <%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %> <% elsif Docuseal.multitenant? %>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %> <%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %>
<%= t('sent_using_product_name_open_source_software_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/open", product_name: Docuseal.product_name) %>
<% end %> <% end %>
</p> </p>
+5 -4
View File
@@ -71,10 +71,11 @@
<% end %> <% end %>
<% end %> <% end %>
<li> <li>
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %> <button form="destroy_user_session_form">
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 mr-2 inline') %> <%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 inline') %>
<span class="mr-1 whitespace-nowrap"><%= t('sign_out') %></span> <span class="whitespace-nowrap"><%= t('sign_out') %></span>
<% end %> </button>
<%= button_to '', destroy_user_session_path, method: :delete, data: { turbo: false }, form: { id: 'destroy_user_session_form' }, form_class: 'hidden' %>
</li> </li>
</ul> </ul>
</div> </div>
+5 -5
View File
@@ -12,12 +12,12 @@
<li> <li>
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% if (!Docuseal.multitenant? || EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% unless Docuseal.multitenant? %> <% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
<li> <li>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
@@ -92,7 +92,7 @@
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div>
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails: %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<% if has_phone_field %> <% if has_phone_field %>
<%= render 'send_sms', f: %> <%= render 'send_sms', f: %>
<% end %> <% end %>
+1 -1
View File
@@ -70,7 +70,7 @@
<%= local_assigns[:variables_form] %> <%= local_assigns[:variables_form] %>
<div> <div>
<%= render('submitters_order', f:, template:) if can_send_emails %> <%= render('submitters_order', f:, template:) if can_send_emails %>
<%= render 'send_email', f:, template:, can_send_emails: %> <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
<%= render 'extra_fields', f: %> <%= render 'extra_fields', f: %>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -38,9 +38,21 @@
<% end %> <% end %>
</div> </div>
<% 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) %>
<% view_config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY) %>
<% config_body = (config.value['body_type'] == 'html' && config.value['html_body'].presence) || config.value['body'] %>
<% view_template_subject = template&.preferences&.dig('invitation_view_email_subject').presence %>
<% view_template_body = template&.preferences&.dig('invitation_view_email_body').presence %>
<% default_subject = template&.preferences&.dig('request_email_subject').presence || config.value['subject'] %>
<% default_body = template&.preferences&.dig('request_email_body').presence || config_body %>
<% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden"> <div id="<%= message_field_id %>" 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&.dig('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_submitters.size < 5 && template&.preferences&.dig('submitters').to_a.size > 1 %>
<% if local_assigns[:viewer_submitter_uuids].present? && local_assigns[:submitter].blank? %>
<% (template_submitters.pluck('uuid') - local_assigns[:viewer_submitter_uuids].to_a).each do |signer_uuid| %>
<%= hidden_field_tag 'email_message_submitter_uuids[]', signer_uuid %>
<% end %>
<% end %>
<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">
@@ -51,13 +63,13 @@
</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&.dig('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 || (is_edit_viewer ? view_template_subject : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || (is_edit_viewer ? view_config.value['subject'] : default_subject), required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.label :message, t('body'), class: 'label' %> <%= f.label :message, t('body'), class: 'label' %>
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %> <% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
<%= render 'personalization_settings/markdown_editor', name: f.field_name(: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'], variables: body_variables %> <%= render 'personalization_settings/email_body_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_edit_viewer ? view_template_body : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || (is_edit_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %> <% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer"> <label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %> <%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
<span class="label"><%= t('save_as_default_template_message') %></span> <span class="label"><%= t('save_as_default_template_message') %></span>
@@ -81,17 +93,18 @@
</ul> </ul>
</toggle-visible> </toggle-visible>
<% template_submitters.each_with_index do |submitter, index| %> <% template_submitters.each_with_index do |submitter, index| %>
<% is_viewer = local_assigns[:viewer_submitter_uuids].include?(submitter['uuid']) %>
<%= 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&.dig('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 || (is_viewer ? view_template_subject : nil) || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || (is_viewer ? view_config.value['subject'] : default_subject), required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :message, t('body'), class: 'label' %> <%= ff.label :message, t('body'), class: 'label' %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(: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'], variables: body_variables %> <%= render 'personalization_settings/email_body_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_viewer ? view_template_body : nil) || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || (is_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
</div> </div>
</div> </div>
<% end %> <% end %>
+4 -3
View File
@@ -1,6 +1,7 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %> <% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %> <% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %> <% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% viewer_submitter_uuids = Set.new(@template.submitters.pluck('uuid')) - Set.new(@template.fields.to_a.pluck('submitter_uuid')) %>
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %> <% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %> <% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
<% can_send_emails = Accounts.can_send_emails?(current_account) %> <% can_send_emails = Accounts.can_send_emails?(current_account) %>
@@ -26,18 +27,18 @@
<div class="px-5 mb-5 mt-4"> <div class="px-5 mb-5 mt-4">
<% unless only_detailed %> <% unless only_detailed %>
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>"> <div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
<%= render 'email_form', template: @template, variables_form:, can_send_emails: %> <%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
</div> </div>
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>"> <div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
<%= render 'phone_form', template: @template, variables_form: %> <%= render 'phone_form', template: @template, variables_form: %>
</div> </div>
<% end %> <% end %>
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>"> <div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form:, can_send_emails: %> <%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
</div> </div>
<% if with_list %> <% if with_list %>
<div id="list" class="hidden"> <div id="list" class="hidden">
<%= render 'list_form', template: @template, can_send_emails: %> <%= render 'list_form', template: @template, can_send_emails:, viewer_submitter_uuids: %>
</div> </div>
<% end %> <% end %>
<%= render 'submissions/error' %> <%= render 'submissions/error' %>
+50 -24
View File
@@ -15,8 +15,7 @@
<h1 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).to_s.split(/(_)/).each do |item| %><%= item %><wbr><% end %></h1> <h1 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).to_s.split(/(_)/).each do |item| %><%= item %><wbr><% end %></h1>
</a> </a>
<div class="space-x-3 flex items-center"> <div class="space-x-3 flex items-center">
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %> <% if signed_in? && can?(:destroy, @submission) && @submission.archived_at? && !@submission.completed_at? %>
<% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %>
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %> <%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
<% end %> <% end %>
<% if @submission.audit_trail.present? %> <% if @submission.audit_trail.present? %>
@@ -30,16 +29,19 @@
<span class="hidden md:block"><%= t('event_log') %></span> <span class="hidden md:block"><%= t('event_log') %></span>
<% end %> <% end %>
<% end %> <% end %>
<% if signed_in? && !is_all_completed && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %> <% if signed_in? && !@submission.completed_at? && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %>
<% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %> <% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %>
<% if pending_submitters_count.positive? %> <% if pending_submitters_count.positive? %>
<%= button_to button_title(title: t('re_send_emails'), title_class: 'hidden md:inline', disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), submission_resend_email_index_path(@submission), class: 'white-button', data: { turbo_confirm: t('are_you_sure_you_want_to_re_send_email_to_n_recipients', count: pending_submitters_count) } %> <%= button_to button_title(title: t('re_send_emails'), title_class: 'hidden md:inline', disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), submission_resend_email_index_path(@submission), class: 'white-button', data: { turbo_confirm: t('are_you_sure_you_want_to_re_send_email_to_n_recipients', count: pending_submitters_count) } %>
<% end %> <% end %>
<% end %> <% end %>
<% if @submission.submitters.to_a.any?(&:completed_at?) %> <% if @submission.submitters.to_a.any?(&:completed_at?) %>
<% if is_all_completed || !is_combined_enabled %> <% show_combined_download = @submission.completed_at? && !is_combined_enabled %>
<% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %>
<% show_download_dropdown = show_combined_download || show_unarchive %>
<% if @submission.completed_at? || !is_combined_enabled %>
<div class="join relative"> <div class="join relative">
<download-button role="button" tabindex="0" aria-label="<%= t('download') %>" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact_blank) : submissions_preview_download_index_path(@submission.slug, combined: is_combined_enabled.presence)) : submission_download_index_path(@submission, combined: is_combined_enabled.presence) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>"> <download-button role="button" tabindex="0" aria-label="<%= t('download') %>" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact_blank) : submissions_preview_download_index_path(@submission.slug, combined: is_combined_enabled.presence)) : submission_download_index_path(@submission, combined: is_combined_enabled.presence) %>" class="base-button <%= '!rounded-r-none !pr-2' if show_download_dropdown %>">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %> <%= svg_icon('download', class: 'w-6 h-6') %>
<span class="hidden md:inline"><%= t('download') %></span> <span class="hidden md:inline"><%= t('download') %></span>
@@ -49,26 +51,36 @@
<span class="hidden md:inline"><%= t('downloading') %></span> <span class="hidden md:inline"><%= t('downloading') %></span>
</span> </span>
</download-button> </download-button>
<% if is_all_completed && !is_combined_enabled %> <% if show_download_dropdown %>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end has-[button:disabled]:dropdown-open">
<label tabindex="0" aria-label="<%= t('download') %>" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"> <label tabindex="0" aria-label="<%= t('download') %>" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
<span class="text-sm align-text-top"> <span class="text-sm align-text-top">
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %> <%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
</span> </span>
</label> </label>
<ul class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right"> <ul class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
<li> <% if show_combined_download %>
<download-button role="button" tabindex="0" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: true }.compact) : submissions_preview_download_index_path(@submission.slug, combined: true)) : submission_download_index_path(@submission, combined: true) %>" class="flex items-center"> <li>
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <download-button role="button" tabindex="0" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: true }.compact) : submissions_preview_download_index_path(@submission.slug, combined: true)) : submission_download_index_path(@submission, combined: true) %>" class="flex items-center">
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %> <span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span> <%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
</span> <span class="whitespace-nowrap"><%= t('download_combined_pdf') %></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><%= t('downloading') %></span> <%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
</span> <span><%= t('downloading') %></span>
</download-button> </span>
</li> </download-button>
</li>
<% end %>
<% if show_unarchive %>
<li>
<button form="submission_unarchive_form">
<%= button_title(title: t('unarchive'), disabled_with: t('unarchive'), icon: svg_icon('rotate', class: 'w-6 h-6 flex-shrink-0')) %>
</button>
<%= button_to '', submission_unarchive_index_path(@submission), form: { id: 'submission_unarchive_form' }, form_class: 'hidden' %>
</li>
<% end %>
</ul> </ul>
</div> </div>
<% end %> <% end %>
@@ -166,7 +178,7 @@
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span> </span>
</div> </div>
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %> <% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !@submission.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>"> <span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %> <%= svg_icon('pencil', class: 'w-5 h-5') %>
@@ -199,7 +211,11 @@
</div> </div>
<% end %> <% end %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
<% if @submission.expire_at? && submitter && !submitter.completed_at? %> <% if submitter&.viewer? %>
<%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5') %>
<% elsif submitter && !submitter.completed_at? && @submission.completed_at? %>
<%= svg_icon('writing_off', class: 'w-5 h-5') %>
<% elsif @submission.expire_at? && submitter && !submitter.completed_at? %>
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %> <%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
<% else %> <% else %>
<%= svg_icon('writing', class: 'w-5 h-5') %> <%= svg_icon('writing', class: 'w-5 h-5') %>
@@ -207,9 +223,19 @@
<span> <span>
<% if submitter&.declined_at? %> <% if submitter&.declined_at? %>
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %> <%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif submitter&.viewer? %>
<% if submitter.opened_at? %>
<%= t('viewed_on_time', time: l(submitter.opened_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
<% elsif @submission.completed_at? %>
<%= t('not_viewed') %>
<% else %>
<%= t('not_viewed_yet') %>
<% end %>
<% elsif submitter %> <% elsif submitter %>
<% if submitter.completed_at? %> <% if submitter.completed_at? %>
<%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %> <%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %>
<% elsif @submission.completed_at? %>
<%= t('not_completed') %>
<% elsif @submission.expire_at? %> <% elsif @submission.expire_at? %>
<% if @submission.expired? %> <% if @submission.expired? %>
<%= t(:expired) %> <%= t(:expired) %>
@@ -232,15 +258,15 @@
</span> </span>
</div> </div>
<% end %> <% end %>
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && (Docuseal.multitenant? || Accounts.can_send_emails?(current_account)) && !@submission.expired? && !submitter.declined_at? %> <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && (Docuseal.multitenant? || Accounts.can_send_emails?(current_account)) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter), class: 'btn btn-sm btn-primary w-full' %> <%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter), class: 'btn btn-sm btn-primary w-full' %>
</div> </div>
<% end %> <% end %>
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %> <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
<%= render 'submissions/send_sms_button', submitter: %> <%= render 'submissions/send_sms_button', submitter: %>
<% end %> <% end %>
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %> <% if signed_in? && submitter && !submitter.viewer? && !submitter.completed_at? && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
<div class="mt-2 mb-1"> <div class="mt-2 mb-1">
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>"> <a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
<%= t('sign_in_person') %> <%= t('sign_in_person') %>
+3 -2
View File
@@ -1,3 +1,4 @@
<% completed_at = @submitter.completed_at || @submitter.submission.completed_at %>
<main class="max-w-md mx-auto px-2 mt-12 mb-4"> <main 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">
@@ -12,7 +13,7 @@
<div> <div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @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(completed_at.to_date, format: :long)) %>
</p> </p>
</div> </div>
</div> </div>
@@ -29,7 +30,7 @@
<div class="py-2"></div> <div class="py-2"></div>
<% end %> <% end %>
<% end %> <% end %>
<% if @submitter.completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %> <% if completed_at > 30.minutes.ago || (current_user && current_user.account.submitters.exists?(id: @submitter.id)) %>
<download-button role="button" tabindex="0" aria-label="<%= t('download_documents') %>" data-src="<%= submit_form_documents_path(@submitter.slug) %>" class="base-button w-full"> <download-button role="button" tabindex="0" aria-label="<%= t('download_documents') %>" data-src="<%= submit_form_documents_path(@submitter.slug) %>" class="base-button w-full">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %> <%= svg_icon('download', class: 'w-6 h-6') %>
+10 -10
View File
@@ -23,7 +23,7 @@
<%= @submitter.submission.name || @submitter.submission.template&.name %> <%= @submitter.submission.name || @submitter.submission.template&.name %>
</h1> </h1>
<div class="flex items-center gap-2 group" style="margin-left: 20px; flex-shrink: 0"> <div class="flex items-center gap-2 group" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>" class="hidden group-has-[.complete-button]:flex"> <modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>" class="hidden group-has-[.complete-button]:flex">
<button type="button" class="btn btn-sm md:!px-5 px-2" aria-label="<%= t(:decline) %>"> <button type="button" class="btn btn-sm md:!px-5 px-2" aria-label="<%= t(:decline) %>">
<span class="hidden md:inline"><%= t(:decline) %></span> <span class="hidden md:inline"><%= t(:decline) %></span>
@@ -34,11 +34,11 @@
</modal-button> </modal-button>
<% end %> <% end %>
<span id="complete_button_container" class="peer contents"></span> <span id="complete_button_container" class="peer contents"></span>
<% if @form_configs[:with_delegate] %> <% if @form_configs[:with_delegate] && !@submitter.viewer? %>
<modal-button data-target="<%= delegate_modal_id = SecureRandom.uuid %>" class="hidden peer-empty:flex"> <modal-button data-target="<%= delegate_modal_id = SecureRandom.uuid %>" class="hidden peer-empty:flex">
<button id="delegate_button" type="button" class="btn btn-sm !px-5"><%= t(:delegate) %></button> <button id="delegate_button" type="button" class="btn btn-sm !px-5"><%= t(:delegate) %></button>
</modal-button> </modal-button>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>"> <button id="decline_button" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %> <%= svg_icon('x', class: 'w-5 h-5') %>
@@ -56,7 +56,7 @@
</download-button> </download-button>
<% end %> <% end %>
<% else %> <% else %>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button" type="button" class="btn btn-sm !px-5"><%= t(:decline) %></button> <button id="decline_button" type="button" class="btn btn-sm !px-5"><%= t(:decline) %></button>
</modal-button> </modal-button>
@@ -77,7 +77,7 @@
</div> </div>
</header> </header>
<scroll-buttons inert class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10 group"> <scroll-buttons inert class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10 group">
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden group-has-[.complete-button]:flex"> <modal-button data-target="<%= decline_modal_id %>" class="hidden group-has-[.complete-button]:flex">
<button type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>"> <button type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %> <%= svg_icon('x', class: 'w-5 h-5') %>
@@ -85,7 +85,7 @@
</modal-button> </modal-button>
<% end %> <% end %>
<span id="complete_button_container_scroll" class="peer contents"></span> <span id="complete_button_container_scroll" class="peer contents"></span>
<% if @form_configs[:with_delegate] %> <% if @form_configs[:with_delegate] && !@submitter.viewer? %>
<modal-button data-target="<%= delegate_modal_id %>" class="hidden peer-empty:flex"> <modal-button data-target="<%= delegate_modal_id %>" class="hidden peer-empty:flex">
<button id="delegate_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:delegate) %>"> <button id="delegate_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:delegate) %>">
<span class="min-[1366px]:inline hidden px-3"> <span class="min-[1366px]:inline hidden px-3">
@@ -96,7 +96,7 @@
</span> </span>
</button> </button>
</modal-button> </modal-button>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button_mobile" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>"> <button id="decline_button_mobile" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %> <%= svg_icon('x', class: 'w-5 h-5') %>
@@ -104,7 +104,7 @@
</modal-button> </modal-button>
<% end %> <% end %>
<% else %> <% else %>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] && !@submitter.viewer? %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:decline) %>"> <button id="decline_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:decline) %>">
<span class="min-[1366px]:inline hidden px-3"> <span class="min-[1366px]:inline hidden px-3">
@@ -177,12 +177,12 @@
</div> </div>
</div> </div>
</div> </div>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] && !@submitter.viewer? %>
<%= render 'shared/html_modal', title: t(:decline), uuid: decline_modal_id do %> <%= render 'shared/html_modal', title: t(:decline), uuid: decline_modal_id do %>
<%= render 'submit_form/decline_form', submitter: @submitter %> <%= render 'submit_form/decline_form', submitter: @submitter %>
<% end %> <% end %>
<% end %> <% end %>
<% if @form_configs[:with_delegate] %> <% if @form_configs[:with_delegate] && !@submitter.viewer? %>
<%= render 'shared/html_modal', title: t(:delegate), uuid: delegate_modal_id do %> <%= render 'shared/html_modal', title: t(:delegate), uuid: delegate_modal_id do %>
<%= render 'submit_form/delegate_form', submitter: @submitter %> <%= render 'submit_form/delegate_form', submitter: @submitter %>
<% end %> <% end %>
@@ -0,0 +1,14 @@
<% if @body.present? %>
<%= render 'custom_content', content: @body, submitter: @submitter %>
<% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<% end %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(:you_have_been_invited_to_view_the_name, name: @submitter.submission.name || @submitter.submission.template&.name) %></p>
<p><%= link_to I18n.t(:view_document), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>
<% end %>
+2 -1
View File
@@ -17,7 +17,8 @@
</submitter-item> </submitter-item>
</div> </div>
<div> <div>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false, can_send_emails: Accounts.can_send_emails?(current_account) %> <% viewer_submitter_uuids = Set.new((@submitter.submission.template_submitters || @submitter.template.submitters).pluck('uuid')) - Set.new((@submitter.submission.template_fields || @submitter.template.fields).to_a.pluck('submitter_uuid')) %>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false, can_send_emails: Accounts.can_send_emails?(current_account), viewer_submitter_uuids: %>
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %> <%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
</div> </div>
<div class="form-control mt-4"> <div class="form-control mt-4">
+3 -3
View File
@@ -28,7 +28,7 @@
<% end %> <% end %>
<div class="w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between px-5 md:px-6 pb-5 md:items-center pt-5 relative cursor-pointer"> <div class="w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between px-5 md:px-6 pb-5 md:items-center pt-5 relative cursor-pointer">
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %> <% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %> <% is_submission_completed = submission.completed_at? %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
<div> <div>
<% submitter = submitters.first %> <% submitter = submitters.first %>
@@ -51,7 +51,7 @@
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer"> <a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %> <%= submitter.name || submitter.email || submitter.phone %>
</a> </a>
<% if !submitter.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if !submitter.completed_at? && !submission.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>"> <span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %> <%= svg_icon('pencil', class: 'w-5 h-5') %>
@@ -144,7 +144,7 @@
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer"> <a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %> <%= submitter.name || submitter.email || submitter.phone %>
</a> </a>
<% if !submitter.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if !submitter.completed_at? && !submission.completed_at? && can?(:update, submission) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? && !submitter.declined_at? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>"> <span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %> <%= svg_icon('pencil', class: 'w-5 h-5') %>
+3 -3
View File
@@ -37,7 +37,7 @@
</div> </div>
<% unless can?(:manage, :countless) %> <% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium"> <div class="badge badge-neutral badge-outline font-medium">
<%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %> <%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.count %>
</div> </div>
<% end %> <% end %>
</a> </a>
@@ -48,7 +48,7 @@
</div> </div>
<% unless can?(:manage, :countless) %> <% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium"> <div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %> <%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.count %>
</div> </div>
<% end %> <% end %>
</a> </a>
@@ -59,7 +59,7 @@
</div> </div>
<% unless can?(:manage, :countless) %> <% unless can?(:manage, :countless) %>
<div class="badge badge-neutral badge-outline font-medium"> <div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %> <%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.count %>
</div> </div>
<% end %> <% end %>
</a> </a>
@@ -1,5 +1,6 @@
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %> <% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
<% is_order_set = template.submitters.any? { |s| s['order'] } %> <% is_order_set = template.submitters.any? { |s| s['order'] } %>
<% field_submitter_uuids = Set.new(template.fields.pluck('submitter_uuid')) %>
<%= 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| %> <%= 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 %> <% unless close_on_submit %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit> <toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
@@ -10,6 +11,7 @@
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :invite_via_field_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'invite_via_field_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %> <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :invite_via_field_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'invite_via_field_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), 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}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %> <% 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}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %>
<%= ff.hidden_field :uuid %> <%= ff.hidden_field :uuid %>
<% is_viewer_row = field_submitter_uuids.exclude?(submitter['uuid']) %>
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
<%= 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 %> <%= 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 %>
@@ -21,6 +23,8 @@
<mount-on-click data-template-id="order_fields" class="link whitespace-nowrap text-sm mt-1 mr-1 block"> <mount-on-click data-template-id="order_fields" class="link whitespace-nowrap text-sm mt-1 mr-1 block">
<%= t('edit_order') %> <%= t('edit_order') %>
</mount-on-click> </mount-on-click>
<% elsif is_viewer_row %>
<span class="inline-flex items-center justify-center h-6 px-2 rounded-full bg-base-200 text-base-content text-xs font-medium normal-case whitespace-nowrap"><%= t('view_only') %></span>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -141,11 +145,13 @@
</div> </div>
<% if template.submitters.size > 2 && !is_order_set %> <% if template.submitters.size > 2 && !is_order_set %>
<template id="order_fields"> <template id="order_fields">
<% last_viewer_order = -1 %>
<% template.submitters.each_with_index do |submitter, index| %> <% template.submitters.each_with_index do |submitter, index| %>
<% default_order = field_submitter_uuids.include?(submitter['uuid']) ? (last_viewer_order += 1) : [last_viewer_order, 0].max %>
<turbo-stream action="replace" target="order_<%= submitter['uuid'] %>"> <turbo-stream action="replace" target="order_<%= submitter['uuid'] %>">
<template> <template>
<div id="order_<%= submitter['uuid'] %>"> <div id="order_<%= submitter['uuid'] %>">
<%= select_tag "template[submitters][#{index}][order]", options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, submitter['order'].presence || index), class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %> <%= select_tag "template[submitters][#{index}][order]", options_for_select(template.submitters.map.with_index { |_, i| [(i + 1).ordinalize, i] }, default_order), class: 'select select-xs text-sm input-bordered bg-white pl-3.5' %>
</div> </div>
</template> </template>
</turbo-stream> </turbo-stream>
@@ -21,7 +21,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %> <%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:completed_notification_email_body), value: ff.object.completed_notification_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY] %> <%= render 'personalization_settings/email_body_editor', name: ff.field_name(:completed_notification_email_body), value: ff.object.completed_notification_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY], html_textarea: true %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
@@ -21,7 +21,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %> <%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:documents_copy_email_body), value: ff.object.documents_copy_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY] %> <%= render 'personalization_settings/email_body_editor', name: ff.field_name(:documents_copy_email_body), value: ff.object.documents_copy_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY], html_textarea: true %>
</div> </div>
<% if can?(:manage, :reply_to) %> <% if can?(:manage, :reply_to) %>
<div class="form-control"> <div class="form-control">
@@ -1,6 +1,9 @@
<div id="<%= AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY %>_form"> <div id="<%= AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY %>_form">
<% template_email_preferences_values = @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank %> <% template_email_preferences_values = @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank %>
<% default_template_email_preferences_values = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body') %> <% default_template_email_preferences_values = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body') %>
<% viewer_default_email_preferences_values = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY).value.values_at('subject', 'body') %>
<% viewer_template_email_preferences_values = @template.preferences.values_at('invitation_view_email_subject', 'invitation_view_email_body').compact_blank.presence %>
<% field_submitter_uuids = Set.new(@template.fields.pluck('submitter_uuid')) %>
<% is_custom_template_email = template_email_preferences_values.present? %> <% is_custom_template_email = template_email_preferences_values.present? %>
<% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 5 %> <% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 5 %>
<% if is_custom_template_email || @template.preferences['submitters'].to_a.any? %> <% if is_custom_template_email || @template.preferences['submitters'].to_a.any? %>
@@ -28,7 +31,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :request_email_body, t('email_body'), class: 'label' %> <%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:request_email_body), value: ff.object.request_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %> <%= render 'personalization_settings/email_body_editor', name: ff.field_name(:request_email_body), value: ff.object.request_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY], html_textarea: true %>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -49,7 +52,7 @@
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>"> <div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %> <% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %> <% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %>
<% submitter_email_values = submitter_email_preferences_values || template_email_preferences_values.presence || default_template_email_preferences_values %> <% submitter_email_values = field_submitter_uuids.include?(submitter['uuid']) ? (submitter_email_preferences_values || template_email_preferences_values.presence || default_template_email_preferences_values) : (viewer_template_email_preferences_values || submitter_email_preferences_values || viewer_default_email_preferences_values) %>
<%= hidden_field_tag 'template[preferences][submitters][][uuid]', submitter['uuid'] %> <%= hidden_field_tag 'template[preferences][submitters][][uuid]', submitter['uuid'] %>
<div class="form-control"> <div class="form-control">
<div class="flex justify-between"> <div class="flex justify-between">
@@ -64,7 +67,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"><%= t('email_body') %></label> <label class="label"><%= t('email_body') %></label>
<%= render 'personalization_settings/markdown_editor', name: 'template[preferences][submitters][][request_email_body]', value: submitter_email_values.last, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %> <%= render 'personalization_settings/email_body_editor', name: 'template[preferences][submitters][][request_email_body]', value: submitter_email_values.last, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY], html_textarea: true %>
</div> </div>
</div> </div>
<% end %> <% end %>
@@ -0,0 +1,34 @@
<div id="<%= AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY %>_form">
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY).value %>
<% template_email_preferences_values = @template.preferences.values_at('invitation_view_email_subject', 'invitation_view_email_body').compact_blank.presence %>
<% is_custom_template_email = template_email_preferences_values.present? %>
<% if is_custom_template_email %>
<%= button_to nil, template_preferences_path(@template), id: 'submitter_view_invitation_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_view_invitation_email_template_form' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert_view"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:invitation_view_email_subject, :invitation_view_email_body).new(@template.preferences['invitation_view_email_subject'].presence || configs['subject'], @template.preferences['invitation_view_email_body'].presence || configs['body']) do |ff| %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :invitation_view_email_subject, t('email_subject'), class: 'label' %>
<% if is_custom_template_email %>
<label for="submitter_view_invitation_email_reset_link" class="label underline">
<%= t('reset_default') %>
</label>
<% end %>
</div>
<%= ff.text_field :invitation_view_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<%= ff.label :invitation_view_email_body, t('email_body'), class: 'label' %>
<%= render 'personalization_settings/email_body_editor', name: ff.field_name(:invitation_view_email_body), value: ff.object.invitation_view_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY], html_textarea: true %>
</div>
<% end %>
<% end %>
<div class="form-control pt-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_view_invitation_email_template_form', class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert_view" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
</div>
@@ -109,6 +109,18 @@
<%= render 'templates_preferences/submitter_invitation_email_form' %> <%= render 'templates_preferences/submitter_invitation_email_form' %>
</div> </div>
</div> </div>
<% field_submitter_uuids = Set.new(@template.fields.pluck('submitter_uuid')) %>
<% unless @template.submitters.all? { |s| field_submitter_uuids.include?(s['uuid']) } %>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<%= t('view_documents_email') %>
</div>
<div class="collapse-content">
<%= render 'templates_preferences/submitter_view_invitation_email_form' %>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %> <%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %>
<div class="collapse collapse-arrow join-item border border-base-300"> <div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion"> <input type="checkbox" name="accordion">
+168
View File
@@ -86,21 +86,26 @@ en: &en
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode' sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode'
sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.' sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.'
sent_using_product_name_open_source_software_html: 'Sent using <a href="%{product_url}">%{product_name}</a> open-source software.'
sent_with_docuseal_pro_html: 'Sent with <a href="%{product_url}">DocuSeal Pro</a>' sent_with_docuseal_pro_html: 'Sent with <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Show "Sent with <span class="link">DocuSeal Pro</span>" attribution in emails show_send_with_docuseal_pro_attribution_in_emails_html: Show "Sent with <span class="link">DocuSeal Pro</span>" attribution in emails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.' you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".' you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".'
you_have_been_invited_to_view_the_name: 'You have been invited to view the "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: "Alternatively, you can review and download your copy using the link below:" alternatively_you_can_review_and_download_your_copy_using_the_link_below: "Alternatively, you can review and download your copy using the link below:"
please_check_the_copy_of_your_name_in_the_email_attachments: 'Please check the copy of your "%{name}" in the email attachments.' please_check_the_copy_of_your_name_in_the_email_attachments: 'Please check the copy of your "%{name}" in the email attachments.'
awaiting_completion_by_the_other_party: "Awaiting completion by the other party" awaiting_completion_by_the_other_party: "Awaiting completion by the other party"
review_and_sign: Review and Sign review_and_sign: Review and Sign
view_document: View Document
review_and_submit: Review and Submit review_and_submit: Review and Submit
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Please contact us by replying to this email if you have any questions." please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Please contact us by replying to this email if you have any questions."
submitter_invitation_sms_body_sign: '{account.name} has invited you to sign a document: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} has invited you to sign a document: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} has invited you to view a document: {submitter.link}'
verification_code_sms_body: 'Verification code: {code}' verification_code_sms_body: 'Verification code: {code}'
you_are_invited_to_submit_a_form: 'You are invited to submit a form' you_are_invited_to_submit_a_form: 'You are invited to submit a form'
you_are_invited_to_sign_a_document: 'You are invited to sign a document' you_are_invited_to_sign_a_document: 'You are invited to sign a document'
you_are_invited_to_view_a_document: 'You are invited to view a document'
you_are_invited_to_sign_documents: 'You are invited to sign documents' you_are_invited_to_sign_documents: 'You are invited to sign documents'
your_document_copy: 'Your document copy' your_document_copy: 'Your document copy'
name_has_been_completed_by_submitters: '"%{name}" has been completed by %{submitters}.' name_has_been_completed_by_submitters: '"%{name}" has been completed by %{submitters}.'
@@ -114,6 +119,17 @@ en: &en
Please contact us by replying to this email if you have any questions. Please contact us by replying to this email if you have any questions.
Thanks,
{account.name}
submitter_invitation_email_view_body: |
Hi there,
You have been invited to view the "{template.name}".
[View Document]({submitter.link})
Please contact us by replying to this email if you have any questions.
Thanks, Thanks,
{account.name} {account.name}
submitter_completed_email_body: | submitter_completed_email_body: |
@@ -306,6 +322,8 @@ en: &en
invalid_timeserver: Invalid Timeserver invalid_timeserver: Invalid Timeserver
email_templates: Email Templates email_templates: Email Templates
signature_request_email: Signature request email signature_request_email: Signature request email
view_documents_email: View documents email
view_only: View only
signature_request_reminder_email: Signature request reminder email signature_request_reminder_email: Signature request reminder email
signature_request_sms: Signature Request SMS signature_request_sms: Signature Request SMS
verification_code_sms: Verification Code SMS verification_code_sms: Verification Code SMS
@@ -568,6 +586,7 @@ en: &en
form_has_been_archived: Form has been archived. form_has_been_archived: Form has been archived.
form_has_been_expired: Form has been expired. form_has_been_expired: Form has been expired.
form_has_been_declined: Form has been declined. form_has_been_declined: Form has been declined.
form_is_view_only: Form is view only.
file_is_missing: File is missing file_is_missing: File is missing
folder_name_has_been_updated: Folder name has been updated. folder_name_has_been_updated: Folder name has been updated.
unable_to_rename_folder: Unable to rename folder. unable_to_rename_folder: Unable to rename folder.
@@ -652,6 +671,10 @@ en: &en
signers: Signers signers: Signers
not_invited_yet: Not invited yet not_invited_yet: Not invited yet
not_completed_yet: Not completed yet not_completed_yet: Not completed yet
not_completed: Not completed
not_viewed_yet: Not viewed yet
not_viewed: Not viewed
viewed_on_time: 'Viewed on %{time}'
declined_on_time: 'Declined on %{time}' declined_on_time: 'Declined on %{time}'
expire_on_time: 'Expire on %{time}' expire_on_time: 'Expire on %{time}'
sign_in_person: Sign In-person sign_in_person: Sign In-person
@@ -876,6 +899,7 @@ en: &en
mobile: Mobile mobile: Mobile
tablet: Tablet tablet: Tablet
reset_default: Reset default reset_default: Reset default
smtp_settings_have_been_reset: SMTP settings have been reset.
send_signature_request_email: Send signature request email send_signature_request_email: Send signature request email
last_3_months: Last 3 months last_3_months: Last 3 months
last_6_months: Last 6 months last_6_months: Last 6 months
@@ -1142,6 +1166,7 @@ es: &es
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> en Modo de Prueba' sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> en Modo de Prueba'
sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.'
sent_using_product_name_open_source_software_html: 'Enviado usando el software de código abierto de <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Enviado con <a href="%{product_url}">DocuSeal Pro</a>' sent_with_docuseal_pro_html: 'Enviado con <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar el mensaje "Enviado con <span class="link">DocuSeal Pro</span>" en los correos electrónicos show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar el mensaje "Enviado con <span class="link">DocuSeal Pro</span>" en los correos electrónicos
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza.
@@ -1149,15 +1174,19 @@ es: &es
thanks: Gracias thanks: Gracias
you_have_been_invited_to_submit_the_name_form: 'Has sido invitado/a a enviar el formulario "%{name}".' you_have_been_invited_to_submit_the_name_form: 'Has sido invitado/a a enviar el formulario "%{name}".'
you_have_been_invited_to_sign_the_name: 'Has sido invitado/a a firmar el "%{name}".' you_have_been_invited_to_sign_the_name: 'Has sido invitado/a a firmar el "%{name}".'
you_have_been_invited_to_view_the_name: 'Has sido invitado/a a ver el "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: "Alternativamente, puedes revisar y descargar tu copia usando el enlace a continuación:" alternatively_you_can_review_and_download_your_copy_using_the_link_below: "Alternativamente, puedes revisar y descargar tu copia usando el enlace a continuación:"
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, revisa la copia de tu "%{name}" en los archivos adjuntos del correo electrónico.' please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, revisa la copia de tu "%{name}" en los archivos adjuntos del correo electrónico.'
review_and_sign: Revisar y Firmar review_and_sign: Revisar y Firmar
view_document: Ver documento
review_and_submit: Revisar y Enviar review_and_submit: Revisar y Enviar
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta." please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta."
submitter_invitation_sms_body_sign: '{account.name} te ha invitado a firmar un documento: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} te ha invitado a firmar un documento: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} te ha invitado a ver un documento: {submitter.link}'
verification_code_sms_body: 'Código de verificación: {code}' verification_code_sms_body: 'Código de verificación: {code}'
you_are_invited_to_submit_a_form: 'Estás invitado/a a enviar un formulario' you_are_invited_to_submit_a_form: 'Estás invitado/a a enviar un formulario'
you_are_invited_to_sign_a_document: 'Estás invitado/a a firmar un documento' you_are_invited_to_sign_a_document: 'Estás invitado/a a firmar un documento'
you_are_invited_to_view_a_document: 'Estás invitado/a a ver un documento'
you_are_invited_to_sign_documents: 'Estás invitado/a a firmar documentos' you_are_invited_to_sign_documents: 'Estás invitado/a a firmar documentos'
your_document_copy: 'Tu copia del documento' your_document_copy: 'Tu copia del documento'
name_has_been_completed_by_submitters: '"%{name}" ha sido completado por %{submitters}.' name_has_been_completed_by_submitters: '"%{name}" ha sido completado por %{submitters}.'
@@ -1171,6 +1200,17 @@ es: &es
Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta. Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta.
Gracias,
{account.name}
submitter_invitation_email_view_body: |
Hola,
Has sido invitado/a a ver el "{template.name}".
[Ver documento]({submitter.link})
Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta.
Gracias, Gracias,
{account.name} {account.name}
submitter_completed_email_body: | submitter_completed_email_body: |
@@ -1362,6 +1402,8 @@ es: &es
invalid_timeserver: Servidor de tiempo inválido invalid_timeserver: Servidor de tiempo inválido
email_templates: Plantillas de correo electrónico email_templates: Plantillas de correo electrónico
signature_request_email: Correo de solicitud de firma signature_request_email: Correo de solicitud de firma
view_documents_email: Correo de visualización de documentos
view_only: Solo lectura
signature_request_reminder_email: Correo de recordatorio de solicitud de firma signature_request_reminder_email: Correo de recordatorio de solicitud de firma
signature_request_sms: SMS de solicitud de firma signature_request_sms: SMS de solicitud de firma
verification_code_sms: SMS de código de verificación verification_code_sms: SMS de código de verificación
@@ -1624,6 +1666,7 @@ es: &es
form_has_been_archived: El formulario ha sido archivado. form_has_been_archived: El formulario ha sido archivado.
form_has_been_expired: El formulario ha expirado. form_has_been_expired: El formulario ha expirado.
form_has_been_declined: El formulario ha sido rechazado. form_has_been_declined: El formulario ha sido rechazado.
form_is_view_only: El formulario es de solo lectura.
file_is_missing: Falta el archivo file_is_missing: Falta el archivo
folder_name_has_been_updated: El nombre de la carpeta ha sido actualizado. folder_name_has_been_updated: El nombre de la carpeta ha sido actualizado.
unable_to_rename_folder: No se pudo renombrar la carpeta. unable_to_rename_folder: No se pudo renombrar la carpeta.
@@ -1708,6 +1751,10 @@ es: &es
signers: Firmantes signers: Firmantes
not_invited_yet: Aún no invitado not_invited_yet: Aún no invitado
not_completed_yet: Aún no completado not_completed_yet: Aún no completado
not_completed: No completado
not_viewed_yet: Aún no visto
not_viewed: No visto
viewed_on_time: 'Visto el %{time}'
declined_on_time: 'Rechazado el %{time}' declined_on_time: 'Rechazado el %{time}'
expire_on_time: 'Expira el %{time}' expire_on_time: 'Expira el %{time}'
sign_in_person: Firma en persona sign_in_person: Firma en persona
@@ -1929,6 +1976,7 @@ es: &es
mobile: Móvil mobile: Móvil
tablet: Tableta tablet: Tableta
reset_default: Restablecer por defecto reset_default: Restablecer por defecto
smtp_settings_have_been_reset: La configuración SMTP ha sido restablecida.
send_signature_request_email: Enviar correo de solicitud de firma send_signature_request_email: Enviar correo de solicitud de firma
last_3_months: Últimos 3 meses last_3_months: Últimos 3 meses
last_6_months: Últimos 6 meses last_6_months: Últimos 6 meses
@@ -2195,6 +2243,7 @@ it: &it
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:'
sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando <a href="%{product_url}">%{product_name}</a> in Modalità di Test' sent_using_product_name_in_testing_mode_html: 'Inviato utilizzando <a href="%{product_url}">%{product_name}</a> in Modalità di Test'
sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.'
sent_using_product_name_open_source_software_html: 'Inviato utilizzando il software open source di <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Inviato con <a href="%{product_url}">DocuSeal Pro</a>' sent_with_docuseal_pro_html: 'Inviato con <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostra la dicitura "Inviato con <span class="link">DocuSeal Pro</span>" nelle email show_send_with_docuseal_pro_attribution_in_emails_html: Mostra la dicitura "Inviato con <span class="link">DocuSeal Pro</span>" nelle email
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia." sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia."
@@ -2202,15 +2251,19 @@ it: &it
thanks: Grazie thanks: Grazie
you_have_been_invited_to_submit_the_name_form: 'Sei stato invitato a inviare il modulo "%{name}".' you_have_been_invited_to_submit_the_name_form: 'Sei stato invitato a inviare il modulo "%{name}".'
you_have_been_invited_to_sign_the_name: 'Sei stato invitato a firmare il "%{name}".' you_have_been_invited_to_sign_the_name: 'Sei stato invitato a firmare il "%{name}".'
you_have_been_invited_to_view_the_name: 'Sei stato invitato a visualizzare il "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: "In alternativa, puoi rivedere e scaricare la tua copia utilizzando il link qui sotto:" alternatively_you_can_review_and_download_your_copy_using_the_link_below: "In alternativa, puoi rivedere e scaricare la tua copia utilizzando il link qui sotto:"
please_check_the_copy_of_your_name_in_the_email_attachments: "Per favore, controlla la copia del tuo \"%{name}\" negli allegati dell'email." please_check_the_copy_of_your_name_in_the_email_attachments: "Per favore, controlla la copia del tuo \"%{name}\" negli allegati dell'email."
review_and_sign: Rivedi e Firma review_and_sign: Rivedi e Firma
view_document: Visualizza documento
review_and_submit: Rivedi e Invia review_and_submit: Rivedi e Invia
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Per favore, contattaci rispondendo a questa email se hai domande." please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Per favore, contattaci rispondendo a questa email se hai domande."
submitter_invitation_sms_body_sign: '{account.name} ti ha invitato a firmare un documento: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} ti ha invitato a firmare un documento: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} ti ha invitato a visualizzare un documento: {submitter.link}'
verification_code_sms_body: 'Codice di verifica: {code}' verification_code_sms_body: 'Codice di verifica: {code}'
you_are_invited_to_submit_a_form: 'Sei stato invitato a inviare un modulo' you_are_invited_to_submit_a_form: 'Sei stato invitato a inviare un modulo'
you_are_invited_to_sign_a_document: 'Sei stato invitato a firmare un documento' you_are_invited_to_sign_a_document: 'Sei stato invitato a firmare un documento'
you_are_invited_to_view_a_document: 'Sei stato invitato a visualizzare un documento'
you_are_invited_to_sign_documents: 'Sei stato invitato a firmare dei documenti' you_are_invited_to_sign_documents: 'Sei stato invitato a firmare dei documenti'
your_document_copy: 'La tua copia del documento' your_document_copy: 'La tua copia del documento'
name_has_been_completed_by_submitters: '"%{name}" è stato completato da %{submitters}.' name_has_been_completed_by_submitters: '"%{name}" è stato completato da %{submitters}.'
@@ -2224,6 +2277,17 @@ it: &it
Per favore, contattaci rispondendo a questa email se hai domande. Per favore, contattaci rispondendo a questa email se hai domande.
Grazie,
{account.name}
submitter_invitation_email_view_body: |
Ciao,
Sei stato invitato a visualizzare il "{template.name}".
[Visualizza documento]({submitter.link})
Per favore, contattaci rispondendo a questa email se hai domande.
Grazie, Grazie,
{account.name} {account.name}
submitter_completed_email_body: | submitter_completed_email_body: |
@@ -2415,6 +2479,8 @@ it: &it
invalid_timeserver: Server di timestamp non valido invalid_timeserver: Server di timestamp non valido
email_templates: Modelli email email_templates: Modelli email
signature_request_email: Email di richiesta di firma signature_request_email: Email di richiesta di firma
view_documents_email: Email di visualizzazione documenti
view_only: Sola lettura
signature_request_reminder_email: Email di promemoria di richiesta di firma signature_request_reminder_email: Email di promemoria di richiesta di firma
signature_request_sms: SMS di richiesta di firma signature_request_sms: SMS di richiesta di firma
verification_code_sms: SMS con codice di verifica verification_code_sms: SMS con codice di verifica
@@ -2677,6 +2743,7 @@ it: &it
form_has_been_archived: Il modulo è stato archiviato. form_has_been_archived: Il modulo è stato archiviato.
form_has_been_expired: Il modulo è scaduto. form_has_been_expired: Il modulo è scaduto.
form_has_been_declined: Il modulo è stato rifiutato. form_has_been_declined: Il modulo è stato rifiutato.
form_is_view_only: Il modulo è di sola lettura.
file_is_missing: File mancante file_is_missing: File mancante
folder_name_has_been_updated: Il nome della cartella è stato aggiornato. folder_name_has_been_updated: Il nome della cartella è stato aggiornato.
unable_to_rename_folder: Impossibile rinominare la cartella. unable_to_rename_folder: Impossibile rinominare la cartella.
@@ -2761,6 +2828,10 @@ it: &it
signers: Firmatari signers: Firmatari
not_invited_yet: Non ancora invitato not_invited_yet: Non ancora invitato
not_completed_yet: Non ancora completato not_completed_yet: Non ancora completato
not_completed: Non completato
not_viewed_yet: Non ancora visualizzato
not_viewed: Non visualizzato
viewed_on_time: 'Visualizzato il %{time}'
declined_on_time: 'Rifiutato il %{time}' declined_on_time: 'Rifiutato il %{time}'
expire_on_time: 'Scade il %{time}' expire_on_time: 'Scade il %{time}'
sign_in_person: Firma di persona sign_in_person: Firma di persona
@@ -2982,6 +3053,7 @@ it: &it
mobile: Mobile mobile: Mobile
tablet: Tablet tablet: Tablet
reset_default: Reimposta predefinito reset_default: Reimposta predefinito
smtp_settings_have_been_reset: Le impostazioni SMTP sono state ripristinate.
send_signature_request_email: Invia email di richiesta firma send_signature_request_email: Invia email di richiesta firma
last_3_months: Ultimi 3 mesi last_3_months: Ultimi 3 mesi
last_6_months: Ultimi 6 mesi last_6_months: Ultimi 6 mesi
@@ -3248,6 +3320,7 @@ fr: &fr
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous :' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous :'
sent_using_product_name_in_testing_mode_html: Envoyé avec <a href="%{product_url}">%{product_name}</a> en mode test sent_using_product_name_in_testing_mode_html: Envoyé avec <a href="%{product_url}">%{product_name}</a> en mode test
sent_using_product_name_free_document_signing_html: Envoyé avec <a href="%{product_url}">%{product_name}</a> signature de documents gratuite. sent_using_product_name_free_document_signing_html: Envoyé avec <a href="%{product_url}">%{product_name}</a> signature de documents gratuite.
sent_using_product_name_open_source_software_html: Envoyé avec le logiciel open source <a href="%{product_url}">%{product_name}</a>.
sent_with_docuseal_pro_html: Envoyé avec <a href="%{product_url}">DocuSeal Pro</a> sent_with_docuseal_pro_html: Envoyé avec <a href="%{product_url}">DocuSeal Pro</a>
show_send_with_docuseal_pro_attribution_in_emails_html: Afficher l’attribution "Envoyé avec <span class="link">DocuSeal Pro</span>" dans les e‑mails show_send_with_docuseal_pro_attribution_in_emails_html: Afficher l’attribution "Envoyé avec <span class="link">DocuSeal Pro</span>" dans les e‑mails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Une empreinte (checksum) PDF est fournie pour générer une signature de confiance. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Une empreinte (checksum) PDF est fournie pour générer une signature de confiance.
@@ -3255,15 +3328,19 @@ fr: &fr
thanks: Merci thanks: Merci
you_have_been_invited_to_submit_the_name_form: Vous avez été invité à soumettre le formulaire "%{name}". you_have_been_invited_to_submit_the_name_form: Vous avez été invité à soumettre le formulaire "%{name}".
you_have_been_invited_to_sign_the_name: Vous avez été invité à signer "%{name}". you_have_been_invited_to_sign_the_name: Vous avez été invité à signer "%{name}".
you_have_been_invited_to_view_the_name: Vous avez été invité à consulter "%{name}".
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Vous pouvez également consulter et télécharger votre exemplaire à l’aide du lien ci‑dessous :' alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Vous pouvez également consulter et télécharger votre exemplaire à l’aide du lien ci‑dessous :'
please_check_the_copy_of_your_name_in_the_email_attachments: Veuillez vérifier la copie de votre "%{name}" dans les pièces jointes de l’e‑mail. please_check_the_copy_of_your_name_in_the_email_attachments: Veuillez vérifier la copie de votre "%{name}" dans les pièces jointes de l’e‑mail.
review_and_sign: Examiner et signer review_and_sign: Examiner et signer
view_document: Consulter le document
review_and_submit: Examiner et soumettre review_and_submit: Examiner et soumettre
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: Veuillez nous contacter en répondant à cet e‑mail si vous avez des questions. please_contact_us_by_replying_to_this_email_if_you_have_any_questions: Veuillez nous contacter en répondant à cet e‑mail si vous avez des questions.
submitter_invitation_sms_body_sign: "{account.name} vous a invité à signer un document : {submitter.link}" submitter_invitation_sms_body_sign: "{account.name} vous a invité à signer un document : {submitter.link}"
submitter_invitation_sms_body_view: "{account.name} vous a invité à consulter un document : {submitter.link}"
verification_code_sms_body: 'Code de vérification : {code}' verification_code_sms_body: 'Code de vérification : {code}'
you_are_invited_to_submit_a_form: Vous êtes invité à soumettre un formulaire you_are_invited_to_submit_a_form: Vous êtes invité à soumettre un formulaire
you_are_invited_to_sign_a_document: Vous êtes invité à signer un document you_are_invited_to_sign_a_document: Vous êtes invité à signer un document
you_are_invited_to_view_a_document: Vous êtes invité à consulter un document
you_are_invited_to_sign_documents: Vous êtes invité à signer des documents you_are_invited_to_sign_documents: Vous êtes invité à signer des documents
your_document_copy: Votre copie de document your_document_copy: Votre copie de document
name_has_been_completed_by_submitters: '"%{name}" a été complété par %{submitters}.' name_has_been_completed_by_submitters: '"%{name}" a été complété par %{submitters}.'
@@ -3277,6 +3354,17 @@ fr: &fr
Veuillez nous contacter en répondant à cet e‑mail si vous avez des questions. Veuillez nous contacter en répondant à cet e‑mail si vous avez des questions.
Merci,
{account.name}
submitter_invitation_email_view_body: |
Bonjour,
Vous avez été invité à consulter "{template.name}".
[Consulter le document]({submitter.link})
Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.
Merci, Merci,
{account.name} {account.name}
submitter_completed_email_body: | submitter_completed_email_body: |
@@ -3468,6 +3556,8 @@ fr: &fr
invalid_timeserver: Serveur d’horodatage invalide invalid_timeserver: Serveur d’horodatage invalide
email_templates: Modèles d’e‑mail email_templates: Modèles d’e‑mail
signature_request_email: E‑mail de demande de signature signature_request_email: E‑mail de demande de signature
view_documents_email: E-mail de consultation des documents
view_only: Lecture seule
signature_request_reminder_email: E‑mail de rappel de demande de signature signature_request_reminder_email: E‑mail de rappel de demande de signature
signature_request_sms: SMS de demande de signature signature_request_sms: SMS de demande de signature
verification_code_sms: SMS de code de vérification verification_code_sms: SMS de code de vérification
@@ -3730,6 +3820,7 @@ fr: &fr
form_has_been_archived: Le formulaire a été archivé. form_has_been_archived: Le formulaire a été archivé.
form_has_been_expired: Le formulaire a expiré. form_has_been_expired: Le formulaire a expiré.
form_has_been_declined: Le formulaire a été refusé. form_has_been_declined: Le formulaire a été refusé.
form_is_view_only: Le formulaire est en lecture seule.
file_is_missing: Fichier manquant file_is_missing: Fichier manquant
folder_name_has_been_updated: Le nom du dossier a été mis à jour. folder_name_has_been_updated: Le nom du dossier a été mis à jour.
unable_to_rename_folder: Impossible de renommer le dossier. unable_to_rename_folder: Impossible de renommer le dossier.
@@ -3814,6 +3905,10 @@ fr: &fr
signers: Signataires signers: Signataires
not_invited_yet: Pas encore invité not_invited_yet: Pas encore invité
not_completed_yet: Pas encore terminé not_completed_yet: Pas encore terminé
not_completed: Non terminé
not_viewed_yet: Pas encore consulté
not_viewed: Non consulté
viewed_on_time: Consulté le %{time}
declined_on_time: Refusé le %{time} declined_on_time: Refusé le %{time}
expire_on_time: Expire le %{time} expire_on_time: Expire le %{time}
sign_in_person: Signer en personne sign_in_person: Signer en personne
@@ -4031,6 +4126,7 @@ fr: &fr
mobile: Mobile mobile: Mobile
tablet: Tablette tablet: Tablette
reset_default: Réinitialiser par défaut reset_default: Réinitialiser par défaut
smtp_settings_have_been_reset: Les paramètres SMTP ont été réinitialisés.
send_signature_request_email: Envoyer un e-mail de demande de signature send_signature_request_email: Envoyer un e-mail de demande de signature
last_month: Mois dernier last_month: Mois dernier
last_3_months: 3 derniers mois last_3_months: 3 derniers mois
@@ -4298,6 +4394,7 @@ pt: &pt
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:'
sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> no Modo de Teste' sent_using_product_name_in_testing_mode_html: 'Enviado usando <a href="%{product_url}">%{product_name}</a> no Modo de Teste'
sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.'
sent_using_product_name_open_source_software_html: 'Enviado usando o software de código aberto <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: 'Enviado com <a href="%{product_url}">DocuSeal Pro</a>' sent_with_docuseal_pro_html: 'Enviado com <a href="%{product_url}">DocuSeal Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar "Enviado com <span class="link">DocuSeal Pro</span>" nos e-mails show_send_with_docuseal_pro_attribution_in_emails_html: Mostrar "Enviado com <span class="link">DocuSeal Pro</span>" nos e-mails
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável.
@@ -4305,15 +4402,19 @@ pt: &pt
thanks: Obrigado thanks: Obrigado
you_have_been_invited_to_submit_the_name_form: 'Você foi convidado a submeter o formulário "%{name}".' you_have_been_invited_to_submit_the_name_form: 'Você foi convidado a submeter o formulário "%{name}".'
you_have_been_invited_to_sign_the_name: 'Você foi convidado a assinar "%{name}".' you_have_been_invited_to_sign_the_name: 'Você foi convidado a assinar "%{name}".'
you_have_been_invited_to_view_the_name: 'Você foi convidado a visualizar "%{name}".'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Você pode revisar e baixar sua cópia usando o link abaixo:' alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Você pode revisar e baixar sua cópia usando o link abaixo:'
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, verifique a cópia de "%{name}" nos anexos do e-mail.' please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, verifique a cópia de "%{name}" nos anexos do e-mail.'
review_and_sign: Revisar e assinar review_and_sign: Revisar e assinar
view_document: Visualizar documento
review_and_submit: Revisar e submeter review_and_submit: Revisar e submeter
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.' please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.'
submitter_invitation_sms_body_sign: '{account.name} convidou você para assinar um documento: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} convidou você para assinar um documento: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} convidou você para visualizar um documento: {submitter.link}'
verification_code_sms_body: 'Código de verificação: {code}' verification_code_sms_body: 'Código de verificação: {code}'
you_are_invited_to_submit_a_form: Você foi convidado a submeter um formulário you_are_invited_to_submit_a_form: Você foi convidado a submeter um formulário
you_are_invited_to_sign_a_document: Você foi convidado a assinar um documento you_are_invited_to_sign_a_document: Você foi convidado a assinar um documento
you_are_invited_to_view_a_document: Você foi convidado a visualizar um documento
you_are_invited_to_sign_documents: Você foi convidado a assinar documentos you_are_invited_to_sign_documents: Você foi convidado a assinar documentos
your_document_copy: Sua cópia do documento your_document_copy: Sua cópia do documento
name_has_been_completed_by_submitters: '"%{name}" foi concluído por %{submitters}.' name_has_been_completed_by_submitters: '"%{name}" foi concluído por %{submitters}.'
@@ -4327,6 +4428,17 @@ pt: &pt
Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida. Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.
Obrigado,
{account.name}
submitter_invitation_email_view_body: |
Olá,
Você foi convidado a visualizar "{template.name}".
[Visualizar documento]({submitter.link})
Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.
Obrigado, Obrigado,
{account.name} {account.name}
submitter_completed_email_body: | submitter_completed_email_body: |
@@ -4518,6 +4630,8 @@ pt: &pt
invalid_timeserver: Servidor de carimbo de tempo inválido invalid_timeserver: Servidor de carimbo de tempo inválido
email_templates: Modelos de e-mail email_templates: Modelos de e-mail
signature_request_email: E-mail de solicitação de assinatura signature_request_email: E-mail de solicitação de assinatura
view_documents_email: E-mail de visualização de documentos
view_only: Somente leitura
signature_request_reminder_email: E-mail de lembrete de solicitação de assinatura signature_request_reminder_email: E-mail de lembrete de solicitação de assinatura
signature_request_sms: SMS de solicitação de assinatura signature_request_sms: SMS de solicitação de assinatura
verification_code_sms: SMS com código de verificação verification_code_sms: SMS com código de verificação
@@ -4780,6 +4894,7 @@ pt: &pt
form_has_been_archived: O formulário foi arquivado. form_has_been_archived: O formulário foi arquivado.
form_has_been_expired: O formulário expirou. form_has_been_expired: O formulário expirou.
form_has_been_declined: O formulário foi recusado. form_has_been_declined: O formulário foi recusado.
form_is_view_only: O formulário é somente leitura.
file_is_missing: O arquivo está ausente file_is_missing: O arquivo está ausente
folder_name_has_been_updated: O nome da pasta foi atualizado. folder_name_has_been_updated: O nome da pasta foi atualizado.
unable_to_rename_folder: Não foi possível renomear a pasta. unable_to_rename_folder: Não foi possível renomear a pasta.
@@ -4864,6 +4979,10 @@ pt: &pt
signers: Signatários signers: Signatários
not_invited_yet: Ainda não convidado not_invited_yet: Ainda não convidado
not_completed_yet: Ainda não concluído not_completed_yet: Ainda não concluído
not_completed: Não concluído
not_viewed_yet: Ainda não visualizado
not_viewed: Não visualizado
viewed_on_time: 'Visualizado em %{time}'
declined_on_time: 'Recusado em %{time}' declined_on_time: 'Recusado em %{time}'
expire_on_time: 'Expira em %{time}' expire_on_time: 'Expira em %{time}'
sign_in_person: Assinar pessoalmente sign_in_person: Assinar pessoalmente
@@ -5085,6 +5204,7 @@ pt: &pt
mobile: Celular mobile: Celular
tablet: Tablet tablet: Tablet
reset_default: Redefinir para padrão reset_default: Redefinir para padrão
smtp_settings_have_been_reset: As configurações SMTP foram redefinidas.
send_signature_request_email: Enviar e-mail de solicitação de assinatura send_signature_request_email: Enviar e-mail de solicitação de assinatura
last_3_months: Últimos 3 meses last_3_months: Últimos 3 meses
last_6_months: Últimos 6 meses last_6_months: Últimos 6 meses
@@ -5351,21 +5471,26 @@ de: &de
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:'
sent_using_product_name_in_testing_mode_html: 'Gesendet über <a href="%{product_url}">%{product_name}</a> im Testmodus' sent_using_product_name_in_testing_mode_html: 'Gesendet über <a href="%{product_url}">%{product_name}</a> im Testmodus'
sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.' sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.'
sent_using_product_name_open_source_software_html: 'Gesendet mit der Open-Source-Software von <a href="%{product_url}">%{product_name}</a>.'
sent_with_docuseal_pro_html: Gesendet mit <a href="%{product_url}">DocuSeal Pro</a> sent_with_docuseal_pro_html: Gesendet mit <a href="%{product_url}">DocuSeal Pro</a>
show_send_with_docuseal_pro_attribution_in_emails_html: '"Gesendet mit <span class="link">DocuSeal Pro</span>" in E-Mails anzeigen' show_send_with_docuseal_pro_attribution_in_emails_html: '"Gesendet mit <span class="link">DocuSeal Pro</span>" in E-Mails anzeigen'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfsumme wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfsumme wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
you_have_been_invited_to_submit_the_name_form: 'Sie wurden eingeladen, das Formular "%{name}" einzureichen.' you_have_been_invited_to_submit_the_name_form: 'Sie wurden eingeladen, das Formular "%{name}" einzureichen.'
you_have_been_invited_to_sign_the_name: 'Sie wurden eingeladen, "%{name}" zu unterschreiben.' you_have_been_invited_to_sign_the_name: 'Sie wurden eingeladen, "%{name}" zu unterschreiben.'
you_have_been_invited_to_view_the_name: 'Sie wurden eingeladen, "%{name}" anzusehen.'
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Alternativ können Sie Ihre Kopie über den untenstehenden Link ansehen und herunterladen:' alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'Alternativ können Sie Ihre Kopie über den untenstehenden Link ansehen und herunterladen:'
please_check_the_copy_of_your_name_in_the_email_attachments: 'Bitte prüfen Sie die Kopie von "%{name}" im E-Mail-Anhang.' please_check_the_copy_of_your_name_in_the_email_attachments: 'Bitte prüfen Sie die Kopie von "%{name}" im E-Mail-Anhang.'
awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei" awaiting_completion_by_the_other_party: "Warten auf die Fertigstellung durch die andere Partei"
review_and_sign: Prüfen und unterschreiben review_and_sign: Prüfen und unterschreiben
view_document: Dokument ansehen
review_and_submit: Prüfen und einreichen review_and_submit: Prüfen und einreichen
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.' please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.'
submitter_invitation_sms_body_sign: '{account.name} hat Sie eingeladen, ein Dokument zu unterschreiben: {submitter.link}' submitter_invitation_sms_body_sign: '{account.name} hat Sie eingeladen, ein Dokument zu unterschreiben: {submitter.link}'
submitter_invitation_sms_body_view: '{account.name} hat Sie eingeladen, ein Dokument anzusehen: {submitter.link}'
verification_code_sms_body: 'Verifizierungscode: {code}' verification_code_sms_body: 'Verifizierungscode: {code}'
you_are_invited_to_submit_a_form: Sie sind eingeladen, ein Formular einzureichen you_are_invited_to_submit_a_form: Sie sind eingeladen, ein Formular einzureichen
you_are_invited_to_sign_a_document: Sie sind eingeladen, ein Dokument zu unterschreiben you_are_invited_to_sign_a_document: Sie sind eingeladen, ein Dokument zu unterschreiben
you_are_invited_to_view_a_document: Sie sind eingeladen, ein Dokument anzusehen
you_are_invited_to_sign_documents: Sie sind eingeladen, Dokumente zu unterschreiben you_are_invited_to_sign_documents: Sie sind eingeladen, Dokumente zu unterschreiben
your_document_copy: Ihre Dokumentkopie your_document_copy: Ihre Dokumentkopie
name_has_been_completed_by_submitters: '"%{name}" wurde von %{submitters} abgeschlossen.' name_has_been_completed_by_submitters: '"%{name}" wurde von %{submitters} abgeschlossen.'
@@ -5379,6 +5504,17 @@ de: &de
Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben. Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.
Danke,
{account.name}
submitter_invitation_email_view_body: |
Hallo,
Sie wurden eingeladen, "{template.name}" anzusehen.
[Dokument ansehen]({submitter.link})
Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.
Danke, Danke,
{account.name} {account.name}
submitter_completed_email_body: | submitter_completed_email_body: |
@@ -5571,6 +5707,8 @@ de: &de
invalid_timeserver: Ungültiger Zeitstempelserver invalid_timeserver: Ungültiger Zeitstempelserver
email_templates: E-Mail-Vorlagen email_templates: E-Mail-Vorlagen
signature_request_email: E-Mail für Signaturanfrage signature_request_email: E-Mail für Signaturanfrage
view_documents_email: E-Mail zur Dokumentenansicht
view_only: Nur Ansicht
signature_request_reminder_email: E-Mail-Erinnerung für Signaturanfrage signature_request_reminder_email: E-Mail-Erinnerung für Signaturanfrage
signature_request_sms: SMS für Signaturanfrage signature_request_sms: SMS für Signaturanfrage
verification_code_sms: SMS mit Verifizierungscode verification_code_sms: SMS mit Verifizierungscode
@@ -5833,6 +5971,7 @@ de: &de
form_has_been_archived: Das Formular wurde archiviert. form_has_been_archived: Das Formular wurde archiviert.
form_has_been_expired: Das Formular ist abgelaufen. form_has_been_expired: Das Formular ist abgelaufen.
form_has_been_declined: Das Formular wurde abgelehnt. form_has_been_declined: Das Formular wurde abgelehnt.
form_is_view_only: Das Formular ist schreibgeschützt.
file_is_missing: Datei fehlt file_is_missing: Datei fehlt
folder_name_has_been_updated: Der Ordnername wurde aktualisiert. folder_name_has_been_updated: Der Ordnername wurde aktualisiert.
unable_to_rename_folder: Der Ordner konnte nicht umbenannt werden. unable_to_rename_folder: Der Ordner konnte nicht umbenannt werden.
@@ -5917,6 +6056,10 @@ de: &de
signers: Unterzeichner signers: Unterzeichner
not_invited_yet: Noch nicht eingeladen not_invited_yet: Noch nicht eingeladen
not_completed_yet: Noch nicht abgeschlossen not_completed_yet: Noch nicht abgeschlossen
not_completed: Nicht abgeschlossen
not_viewed_yet: Noch nicht angesehen
not_viewed: Nicht angesehen
viewed_on_time: 'Angesehen am %{time}'
declined_on_time: 'Abgelehnt am %{time}' declined_on_time: 'Abgelehnt am %{time}'
expire_on_time: 'Läuft ab am %{time}' expire_on_time: 'Läuft ab am %{time}'
sign_in_person: Vor Ort unterschreiben sign_in_person: Vor Ort unterschreiben
@@ -6138,6 +6281,7 @@ de: &de
mobile: Mobil mobile: Mobil
tablet: Tablet tablet: Tablet
reset_default: Standard zurücksetzen reset_default: Standard zurücksetzen
smtp_settings_have_been_reset: Die SMTP-Einstellungen wurden zurückgesetzt.
send_signature_request_email: Signaturanfrage-E-Mail senden send_signature_request_email: Signaturanfrage-E-Mail senden
last_3_months: Letzte 3 Monate last_3_months: Letzte 3 Monate
last_6_months: Letzte 6 Monate last_6_months: Letzte 6 Monate
@@ -6808,22 +6952,27 @@ nl: &nl
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'U bent uitgenodigd voor %{account_name} %{product_name}. Meld u aan via de onderstaande link:' you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'U bent uitgenodigd voor %{account_name} %{product_name}. Meld u aan via de onderstaande link:'
sent_using_product_name_in_testing_mode_html: Verzonden met <a href="%{product_url}">%{product_name}</a> in testmodus sent_using_product_name_in_testing_mode_html: Verzonden met <a href="%{product_url}">%{product_name}</a> in testmodus
sent_using_product_name_free_document_signing_html: Verzonden met <a href="%{product_url}">%{product_name}</a> gratis documentondertekening. sent_using_product_name_free_document_signing_html: Verzonden met <a href="%{product_url}">%{product_name}</a> gratis documentondertekening.
sent_using_product_name_open_source_software_html: Verzonden met de open-source software <a href="%{product_url}">%{product_name}</a>.
sent_with_docuseal_pro_html: Verzonden met <a href="%{product_url}">DocuSeal Pro</a> sent_with_docuseal_pro_html: Verzonden met <a href="%{product_url}">DocuSeal Pro</a>
show_send_with_docuseal_pro_attribution_in_emails_html: Toon de vermelding 'Verzonden met <span class="link">DocuSeal Pro</span>' in e-mails show_send_with_docuseal_pro_attribution_in_emails_html: Toon de vermelding 'Verzonden met <span class="link">DocuSeal Pro</span>' in e-mails
? sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature ? sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature
: Onderteken documenten met een vertrouwd certificaat geleverd door DocuSeal. Uw documenten en gegevens worden nooit gedeeld met DocuSeal. PDF-checksum wordt verstrekt om een vertrouwde handtekening te genereren. : Onderteken documenten met een vertrouwd certificaat geleverd door DocuSeal. Uw documenten en gegevens worden nooit gedeeld met DocuSeal. PDF-checksum wordt verstrekt om een vertrouwde handtekening te genereren.
you_have_been_invited_to_submit_the_name_form: U bent uitgenodigd om het formulier "%{name}" in te dienen. you_have_been_invited_to_submit_the_name_form: U bent uitgenodigd om het formulier "%{name}" in te dienen.
you_have_been_invited_to_sign_the_name: U bent uitgenodigd om "%{name}" te ondertekenen. you_have_been_invited_to_sign_the_name: U bent uitgenodigd om "%{name}" te ondertekenen.
you_have_been_invited_to_view_the_name: U bent uitgenodigd om "%{name}" te bekijken.
alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'U kunt uw exemplaar ook bekijken en downloaden via de onderstaande link:' alternatively_you_can_review_and_download_your_copy_using_the_link_below: 'U kunt uw exemplaar ook bekijken en downloaden via de onderstaande link:'
please_check_the_copy_of_your_name_in_the_email_attachments: Controleer de kopie van uw "%{name}" in de e-mailbijlagen. please_check_the_copy_of_your_name_in_the_email_attachments: Controleer de kopie van uw "%{name}" in de e-mailbijlagen.
awaiting_completion_by_the_other_party: In afwachting van voltooiing door de andere partij awaiting_completion_by_the_other_party: In afwachting van voltooiing door de andere partij
review_and_sign: Bekijken en ondertekenen review_and_sign: Bekijken en ondertekenen
view_document: Document bekijken
review_and_submit: Bekijken en indienen review_and_submit: Bekijken en indienen
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft. please_contact_us_by_replying_to_this_email_if_you_have_any_questions: Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft.
submitter_invitation_sms_body_sign: "{account.name} heeft u uitgenodigd om een document te ondertekenen: {submitter.link}" submitter_invitation_sms_body_sign: "{account.name} heeft u uitgenodigd om een document te ondertekenen: {submitter.link}"
submitter_invitation_sms_body_view: "{account.name} heeft u uitgenodigd om een document te bekijken: {submitter.link}"
verification_code_sms_body: 'Verificatiecode: {code}' verification_code_sms_body: 'Verificatiecode: {code}'
you_are_invited_to_submit_a_form: U bent uitgenodigd om een formulier in te dienen you_are_invited_to_submit_a_form: U bent uitgenodigd om een formulier in te dienen
you_are_invited_to_sign_a_document: U bent uitgenodigd om een document te ondertekenen you_are_invited_to_sign_a_document: U bent uitgenodigd om een document te ondertekenen
you_are_invited_to_view_a_document: U bent uitgenodigd om een document te bekijken
you_are_invited_to_sign_documents: U bent uitgenodigd om documenten te ondertekenen you_are_invited_to_sign_documents: U bent uitgenodigd om documenten te ondertekenen
your_document_copy: Uw documentkopie your_document_copy: Uw documentkopie
name_has_been_completed_by_submitters: '"%{name}" is voltooid door %{submitters}.' name_has_been_completed_by_submitters: '"%{name}" is voltooid door %{submitters}.'
@@ -6837,6 +6986,17 @@ nl: &nl
Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft. Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft.
Bedankt,
{account.name}
submitter_invitation_email_view_body: |
Hallo,
U bent uitgenodigd om "{template.name}" te bekijken.
[Document bekijken]({submitter.link})
Neem contact met ons op door op deze e-mail te antwoorden als u vragen heeft.
Bedankt, Bedankt,
{account.name} {account.name}
submitter_completed_email_body: | submitter_completed_email_body: |
@@ -7029,6 +7189,8 @@ nl: &nl
invalid_timeserver: Ongeldige tijdserver invalid_timeserver: Ongeldige tijdserver
email_templates: E-mailsjablonen email_templates: E-mailsjablonen
signature_request_email: E-mail voor handtekeningverzoek signature_request_email: E-mail voor handtekeningverzoek
view_documents_email: E-mail voor documentweergave
view_only: Alleen bekijken
signature_request_reminder_email: E-mailherinnering voor handtekeningverzoek signature_request_reminder_email: E-mailherinnering voor handtekeningverzoek
signature_request_sms: SMS voor handtekeningverzoek signature_request_sms: SMS voor handtekeningverzoek
verification_code_sms: Verificatiecode-SMS verification_code_sms: Verificatiecode-SMS
@@ -7291,6 +7453,7 @@ nl: &nl
form_has_been_archived: Formulier is gearchiveerd. form_has_been_archived: Formulier is gearchiveerd.
form_has_been_expired: Formulier is verlopen. form_has_been_expired: Formulier is verlopen.
form_has_been_declined: Formulier is geweigerd. form_has_been_declined: Formulier is geweigerd.
form_is_view_only: Formulier is alleen-lezen.
file_is_missing: Bestand ontbreekt file_is_missing: Bestand ontbreekt
folder_name_has_been_updated: Mapnaam is bijgewerkt. folder_name_has_been_updated: Mapnaam is bijgewerkt.
unable_to_rename_folder: Kan map niet hernoemen. unable_to_rename_folder: Kan map niet hernoemen.
@@ -7375,6 +7538,10 @@ nl: &nl
signers: Ondertekenaars signers: Ondertekenaars
not_invited_yet: Nog niet uitgenodigd not_invited_yet: Nog niet uitgenodigd
not_completed_yet: Nog niet voltooid not_completed_yet: Nog niet voltooid
not_completed: Niet voltooid
not_viewed_yet: Nog niet bekeken
not_viewed: Niet bekeken
viewed_on_time: Bekeken op %{time}
declined_on_time: Geweigerd op %{time} declined_on_time: Geweigerd op %{time}
expire_on_time: Verloopt op %{time} expire_on_time: Verloopt op %{time}
sign_in_person: In persoon ondertekenen sign_in_person: In persoon ondertekenen
@@ -7592,6 +7759,7 @@ nl: &nl
mobile: Mobiel mobile: Mobiel
tablet: Tablet tablet: Tablet
reset_default: Standaard herstellen reset_default: Standaard herstellen
smtp_settings_have_been_reset: De SMTP-instellingen zijn hersteld.
send_signature_request_email: E-mail met handtekeningaanvraag verzenden send_signature_request_email: E-mail met handtekeningaanvraag verzenden
last_3_months: Afgelopen 3 maanden last_3_months: Afgelopen 3 maanden
last_6_months: Afgelopen 6 maanden last_6_months: Afgelopen 6 maanden
+2 -2
View File
@@ -29,7 +29,7 @@ Rails.application.routes.draw do
resources :submitter_email_clicks, only: %i[create] resources :submitter_email_clicks, only: %i[create]
resources :submitter_form_views, only: %i[create] resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[index show update] resources :submitters, only: %i[index show update]
resources :submissions, only: %i[index show create destroy] do resources :submissions, only: %i[index show create update destroy] do
resources :documents, only: %i[index], controller: 'submission_documents' resources :documents, only: %i[index], controller: 'submission_documents'
collection do collection do
resources :init, only: %i[create], controller: 'submissions' resources :init, only: %i[create], controller: 'submissions'
@@ -189,7 +189,7 @@ Rails.application.routes.draw do
resources :api, only: %i[index create], controller: 'api_settings' resources :api, only: %i[index create], controller: 'api_settings'
resource :reveal_access_token, only: %i[show create], controller: 'reveal_access_token' resource :reveal_access_token, only: %i[show create], controller: 'reveal_access_token'
end end
resources :email, only: %i[index create], controller: 'email_smtp_settings' resources :email, only: %i[index create destroy], controller: 'email_smtp_settings'
resources :sso, only: %i[index], controller: 'sso_settings' resources :sso, only: %i[index], controller: 'sso_settings'
resources :notifications, only: %i[index create], controller: 'notifications_settings' resources :notifications, only: %i[index create], controller: 'notifications_settings'
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings' resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateEmailMessageAssets < ActiveRecord::Migration[8.1]
def change
create_table :email_message_assets do |t|
t.references :account, null: false, foreign_key: true, index: false
t.text :data, null: false
t.string :sha1, null: false
t.timestamps
end
add_index :email_message_assets, %i[account_id sha1], unique: true
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddSubmissionCompletedAt < ActiveRecord::Migration[8.1]
def change
add_column :submissions, :completed_at, :datetime
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
class PopulateSubmissionCompletedAt < ActiveRecord::Migration[8.1]
disable_ddl_transaction!
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
def up
max_id = MigrationSubmission.maximum(:id)
return unless max_id
max_completed_at =
Arel::Nodes::Grouping.new(
Submitter.arel_table.project(Submitter.arel_table[:completed_at].maximum)
.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
.ast
)
(1..max_id).step(10_000) do |start_id|
range = start_id...(start_id + 10_000)
incomplete_submitter =
Submitter.where(completed_at: nil, submission_id: range)
.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
.select(1)
MigrationSubmission.where(completed_at: nil, id: range)
.where.not(incomplete_submitter.arel.exists)
.update_all(completed_at: max_completed_at)
end
end
def down
nil
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexOnSubmissionsCompletedAt < ActiveRecord::Migration[8.1]
def change
add_index :submissions, %i[account_id completed_at],
where: 'completed_at IS NOT NULL AND archived_at IS NULL',
name: 'index_submissions_on_account_id_and_completed_at',
if_not_exists: true
return unless connection.supports_partial_index?
add_index :submissions, %i[account_id id],
where: 'completed_at IS NULL AND archived_at IS NULL',
name: 'index_submissions_on_account_id_and_id_pending',
if_not_exists: true
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddSubmissionCreatedAtIndex < ActiveRecord::Migration[8.1]
def change
add_index :submissions, :created_at, if_not_exists: true
end
end
+15 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do ActiveRecord::Schema[8.1].define(version: 2026_07_07_055354) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "btree_gin" enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
@@ -214,6 +214,15 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
t.index ["message_id"], name: "index_email_events_on_message_id" t.index ["message_id"], name: "index_email_events_on_message_id"
end end
create_table "email_message_assets", force: :cascade do |t|
t.bigint "account_id", null: false
t.datetime "created_at", null: false
t.text "data", null: false
t.string "sha1", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "sha1"], name: "index_email_message_assets_on_account_id_and_sha1", unique: true
end
create_table "email_messages", force: :cascade do |t| create_table "email_messages", force: :cascade do |t|
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.bigint "author_id", null: false t.bigint "author_id", null: false
@@ -349,6 +358,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
create_table "submissions", force: :cascade do |t| create_table "submissions", force: :cascade do |t|
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.datetime "archived_at" t.datetime "archived_at"
t.datetime "completed_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.bigint "created_by_user_id" t.bigint "created_by_user_id"
t.datetime "expire_at" t.datetime "expire_at"
@@ -364,9 +374,12 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "variables" t.text "variables"
t.text "variables_schema" t.text "variables_schema"
t.index ["account_id", "completed_at"], name: "index_submissions_on_account_id_and_completed_at", where: "((completed_at IS NOT NULL) AND (archived_at IS NULL))"
t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id" t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id"
t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id_pending", where: "((completed_at IS NULL) AND (archived_at IS NULL))"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)" t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)"
t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)" t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)"
t.index ["created_at"], name: "index_submissions_on_created_at"
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id" t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["slug"], name: "index_submissions_on_slug", unique: true t.index ["slug"], name: "index_submissions_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id" t.index ["template_id"], name: "index_submissions_on_template_id"
@@ -579,6 +592,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_06_27_083558) do
add_foreign_key "dynamic_document_versions", "dynamic_documents" add_foreign_key "dynamic_document_versions", "dynamic_documents"
add_foreign_key "dynamic_documents", "templates" add_foreign_key "dynamic_documents", "templates"
add_foreign_key "email_events", "accounts" add_foreign_key "email_events", "accounts"
add_foreign_key "email_message_assets", "accounts"
add_foreign_key "email_messages", "accounts" add_foreign_key "email_messages", "accounts"
add_foreign_key "email_messages", "users", column: "author_id" add_foreign_key "email_messages", "users", column: "author_id"
add_foreign_key "encrypted_configs", "accounts" add_foreign_key "encrypted_configs", "accounts"
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+390 -148
View File
File diff suppressed because it is too large. Load diff
+29 -2
View File
@@ -152,9 +152,16 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
"dateFormats": {
"type": "array",
"required": false,
"description": "A list of formats to be used for the date field. Formats may include date ('YYYY', 'MM', 'DD'), time ('HH', 'hh', 'mm', 'ss', 'A') and timezone ('z') parts. The first format in the list is used as the default.",
"example": "[\"MM/DD/YYYY\", \"YYYY-MM-DD HH:mm:ss z\"]"
},
"drawFieldType": { "drawFieldType": {
"type": "string", "type": "string",
"required": false, "required": false,
@@ -196,6 +203,7 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -324,6 +332,7 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -439,13 +448,19 @@ const token = jwt.sign({
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default." "description": "Set `false` to not show the documents list on the left. Documents list is displayed by default."
},
"withDynamicDocuments": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to allow converting DOCX files to editable dynamic documents."
}, },
"withFieldsList": { "withFieldsList": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default." "description": "Set `false` to not show the fields list on the right. Fields list is displayed by default."
}, },
"withFieldsDetection": { "withFieldsDetection": {
"type": "boolean", "type": "boolean",
@@ -453,6 +468,12 @@ const token = jwt.sign({
"default": false, "default": false,
"description": "Display a button to automatically detect and add fields to the document with AI." "description": "Display a button to automatically detect and add fields to the document with AI."
}, },
"withCustomFieldsTab": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display a separate \"Custom\" fields tab in the fields list. Custom fields can be configured using the `fields` or `requiredFields` prop."
},
"withFieldPlaceholder": { "withFieldPlaceholder": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -464,6 +485,12 @@ const token = jwt.sign({
"required": false, "required": false,
"description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default." "description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default."
}, },
"withRevisions": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to save revisions and display a dropdown next to the Save button that provides access to the template revisions history."
},
"onlyDefinedFields": { "onlyDefinedFields": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
+29 -2
View File
@@ -119,6 +119,7 @@
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -251,6 +252,7 @@
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -339,9 +341,16 @@
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
"data-date-formats": {
"type": "string",
"required": false,
"description": "Comma separated list of formats to be used for the date field. Formats may include date ('YYYY', 'MM', 'DD'), time ('HH', 'hh', 'mm', 'ss', 'A') and timezone ('z') parts. The first format in the list is used as the default.",
"example": "MM/DD/YYYY,YYYY-MM-DD HH:mm:ss z"
},
"data-draw-field-type": { "data-draw-field-type": {
"type": "string", "type": "string",
"required": false, "required": false,
@@ -403,13 +412,19 @@
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default." "description": "Set `false` to not show the documents list on the left. Documents list is displayed by default."
},
"data-with-dynamic-documents": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to allow converting DOCX files to editable dynamic documents."
}, },
"data-with-fields-list": { "data-with-fields-list": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default." "description": "Set `false` to not show the fields list on the right. Fields list is displayed by default."
}, },
"data-with-fields-detection": { "data-with-fields-detection": {
"type": "boolean", "type": "boolean",
@@ -417,6 +432,12 @@
"default": false, "default": false,
"description": "Display a button to automatically detect and add fields to the document with AI." "description": "Display a button to automatically detect and add fields to the document with AI."
}, },
"data-with-custom-fields-tab": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display a separate \"Custom\" fields tab in the fields list. Custom fields can be configured using the `data-fields` or `data-required-fields` attribute."
},
"data-with-field-placeholder": { "data-with-field-placeholder": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -428,6 +449,12 @@
"required": false, "required": false,
"description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default." "description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default."
}, },
"data-with-revisions": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to save revisions and display a dropdown next to the Save button that provides access to the template revisions history."
},
"data-preview": { "data-preview": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
+29 -2
View File
@@ -143,9 +143,16 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
"dateFormats": {
"type": "array",
"required": false,
"description": "A list of formats to be used for the date field. Formats may include date ('YYYY', 'MM', 'DD'), time ('HH', 'hh', 'mm', 'ss', 'A') and timezone ('z') parts. The first format in the list is used as the default.",
"example": "[\"MM/DD/YYYY\", \"YYYY-MM-DD HH:mm:ss z\"]"
},
"drawFieldType": { "drawFieldType": {
"type": "string", "type": "string",
"required": false, "required": false,
@@ -187,6 +194,7 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -315,6 +323,7 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -430,13 +439,19 @@ const token = jwt.sign({
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default." "description": "Set `false` to not show the documents list on the left. Documents list is displayed by default."
},
"withDynamicDocuments": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to allow converting DOCX files to editable dynamic documents."
}, },
"withFieldsList": { "withFieldsList": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default." "description": "Set `false` to not show the fields list on the right. Fields list is displayed by default."
}, },
"withFieldsDetection": { "withFieldsDetection": {
"type": "boolean", "type": "boolean",
@@ -444,6 +459,12 @@ const token = jwt.sign({
"default": false, "default": false,
"description": "Display a button to automatically detect and add fields to the document with AI." "description": "Display a button to automatically detect and add fields to the document with AI."
}, },
"withCustomFieldsTab": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display a separate \"Custom\" fields tab in the fields list. Custom fields can be configured using the `fields` or `requiredFields` prop."
},
"withFieldPlaceholder": { "withFieldPlaceholder": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -455,6 +476,12 @@ const token = jwt.sign({
"required": false, "required": false,
"description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default." "description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default."
}, },
"withRevisions": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to save revisions and display a dropdown next to the Save button that provides access to the template revisions history."
},
"onlyDefinedFields": { "onlyDefinedFields": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
+29 -2
View File
@@ -164,9 +164,16 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
"date-formats": {
"type": "array",
"required": false,
"description": "A list of formats to be used for the date field. Formats may include date ('YYYY', 'MM', 'DD'), time ('HH', 'hh', 'mm', 'ss', 'A') and timezone ('z') parts. The first format in the list is used as the default.",
"example": "[\"MM/DD/YYYY\", \"YYYY-MM-DD HH:mm:ss z\"]"
},
"draw-field-type": { "draw-field-type": {
"type": "string", "type": "string",
"required": false, "required": false,
@@ -208,6 +215,7 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -336,6 +344,7 @@ const token = jwt.sign({
"payment", "payment",
"phone", "phone",
"verification", "verification",
"kba",
"strikethrough" "strikethrough"
] ]
}, },
@@ -445,13 +454,19 @@ const token = jwt.sign({
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the documents list on the left. Documents list is displayed by default." "description": "Set `false` to not show the documents list on the left. Documents list is displayed by default."
},
"with-dynamic-documents": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to allow converting DOCX files to editable dynamic documents."
}, },
"with-fields-list": { "with-fields-list": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to now show the fields list on the right. Fields list is displayed by default." "description": "Set `false` to not show the fields list on the right. Fields list is displayed by default."
}, },
"with-fields-detection": { "with-fields-detection": {
"type": "boolean", "type": "boolean",
@@ -459,6 +474,12 @@ const token = jwt.sign({
"default": false, "default": false,
"description": "Display a button to automatically detect and add fields to the document with AI." "description": "Display a button to automatically detect and add fields to the document with AI."
}, },
"with-custom-fields-tab": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to display a separate \"Custom\" fields tab in the fields list. Custom fields can be configured using the `:fields` or `:required-fields` prop."
},
"with-field-placeholder": { "with-field-placeholder": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -470,6 +491,12 @@ const token = jwt.sign({
"required": false, "required": false,
"description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default." "description": "Set to `true` to enable Signature ID by default for newly added fields. If set to `false`, the Signature ID toggle will be displayed under field settings, with the Signature ID turned off by default."
}, },
"with-revisions": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set `true` to save revisions and display a dropdown next to the Save button that provides access to the template revisions history."
},
"autosave": { "autosave": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
+11 -5
View File
@@ -51,7 +51,7 @@ export class AppComponent {}
"token": { "token": {
"type": "string", "type": "string",
"doc_type": "object", "doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.", "description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend</b>.",
"required": false, "required": false,
"properties": { "properties": {
"slug": { "slug": {
@@ -109,7 +109,7 @@ export class AppComponent {}
"language": { "language": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically." "description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. By default the form is displayed in the user browser language automatically."
}, },
"i18n": { "i18n": {
"type": "object", "type": "object",
@@ -127,7 +127,7 @@ export class AppComponent {}
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default." "description": "Set `false` to hide field name. Hiding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
}, },
"withFieldPlaceholder": { "withFieldPlaceholder": {
"type": "boolean", "type": "boolean",
@@ -232,6 +232,12 @@ export class AppComponent {}
"default": false, "default": false,
"description": "Set `true` to display the complete button in the form header." "description": "Set `true` to display the complete button in the form header."
}, },
"onlyRequiredFields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set to `true` to display only required fields in the step-by-step form, hiding all optional fields."
},
"allowToResubmit": { "allowToResubmit": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -241,7 +247,7 @@ export class AppComponent {}
"signature": { "signature": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font." "description": "Allows pre-filling signature fields. The value can be a base64 encoded data:image/ string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
}, },
"rememberSignature": { "rememberSignature": {
"type": "boolean", "type": "boolean",
@@ -311,7 +317,7 @@ export class AppComponent {}
"onComplete": { "onComplete": {
"type": "event emitter", "type": "event emitter",
"required": false, "required": false,
"description": "Event emitted the form completion.", "description": "Event emitted on form completion.",
"example": "handleComplete($event)" "example": "handleComplete($event)"
}, },
"onDecline": { "onDecline": {
+10 -4
View File
@@ -47,7 +47,7 @@
"data-token": { "data-token": {
"type": "string", "type": "string",
"doc_type": "object", "doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.", "description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend</b>.",
"required": false, "required": false,
"properties": { "properties": {
"slug": { "slug": {
@@ -105,7 +105,7 @@
"data-language": { "data-language": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically." "description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. By default the form is displayed in the user browser language automatically."
}, },
"data-i18n": { "data-i18n": {
"type": "string", "type": "string",
@@ -153,7 +153,7 @@
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default." "description": "Set `false` to hide field name. Hiding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
}, },
"data-with-field-placeholder": { "data-with-field-placeholder": {
"type": "boolean", "type": "boolean",
@@ -179,6 +179,12 @@
"default": false, "default": false,
"description": "Set `true` to display the complete button in the form header." "description": "Set `true` to display the complete button in the form header."
}, },
"data-only-required-fields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set to `true` to display only required fields in the step-by-step form, hiding all optional fields."
},
"data-allow-to-resubmit": { "data-allow-to-resubmit": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -194,7 +200,7 @@
"data-signature": { "data-signature": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font." "description": "Allows pre-filling signature fields. The value can be a base64 encoded data:image/ string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
}, },
"data-remember-signature": { "data-remember-signature": {
"type": "boolean", "type": "boolean",
+10 -4
View File
@@ -48,7 +48,7 @@ export function App() {
"token": { "token": {
"type": "string", "type": "string",
"doc_type": "object", "doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.", "description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend</b>.",
"required": false, "required": false,
"properties": { "properties": {
"slug": { "slug": {
@@ -106,7 +106,7 @@ export function App() {
"language": { "language": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically." "description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. By default the form is displayed in the user browser language automatically."
}, },
"i18n": { "i18n": {
"type": "object", "type": "object",
@@ -124,7 +124,7 @@ export function App() {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default." "description": "Set `false` to hide field name. Hiding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
}, },
"withFieldPlaceholder": { "withFieldPlaceholder": {
"type": "boolean", "type": "boolean",
@@ -229,6 +229,12 @@ export function App() {
"default": false, "default": false,
"description": "Set `true` to display the complete button in the form header." "description": "Set `true` to display the complete button in the form header."
}, },
"onlyRequiredFields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set to `true` to display only required fields in the step-by-step form, hiding all optional fields."
},
"allowToResubmit": { "allowToResubmit": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -244,7 +250,7 @@ export function App() {
"signature": { "signature": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font." "description": "Allows pre-filling signature fields. The value can be a base64 encoded data:image/ string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
}, },
"rememberSignature": { "rememberSignature": {
"type": "boolean", "type": "boolean",
+10 -4
View File
@@ -57,7 +57,7 @@ export default {
"token": { "token": {
"type": "string", "type": "string",
"doc_type": "object", "doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.", "description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend</b>.",
"required": false, "required": false,
"properties": { "properties": {
"slug": { "slug": {
@@ -115,7 +115,7 @@ export default {
"language": { "language": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically." "description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. By default the form is displayed in the user browser language automatically."
}, },
"i18n": { "i18n": {
"type": "object", "type": "object",
@@ -145,7 +145,7 @@ export default {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
"default": true, "default": true,
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default." "description": "Set `false` to hide field name. Hiding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
}, },
"with-field-placeholder": { "with-field-placeholder": {
"type": "boolean", "type": "boolean",
@@ -195,6 +195,12 @@ export default {
"default": false, "default": false,
"description": "Set `true` to display the complete button in the form header." "description": "Set `true` to display the complete button in the form header."
}, },
"only-required-fields": {
"type": "boolean",
"required": false,
"default": false,
"description": "Set to `true` to display only required fields in the step-by-step form, hiding all optional fields."
},
"allow-to-resubmit": { "allow-to-resubmit": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@@ -204,7 +210,7 @@ export default {
"signature": { "signature": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font." "description": "Allows pre-filling signature fields. The value can be a base64 encoded data:image/ string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
}, },
"remember-signature": { "remember-signature": {
"type": "boolean", "type": "boolean",
+2212 -519
View File
File diff suppressed because it is too large. Load diff
+27 -16
View File
@@ -1,6 +1,6 @@
# Form Webhook # Form Webhook
During the form filling and signing process, 3 types of events may occur and are dispatched at different stages: During the form filling and signing process, 4 types of events may occur and are dispatched at different stages:
- **'form.viewed'** event is triggered when the submitter first opens the form. - **'form.viewed'** event is triggered when the submitter first opens the form.
- **'form.started'** event is triggered when the submitter initiates filling out the form. - **'form.started'** event is triggered when the submitter initiates filling out the form.
@@ -19,13 +19,16 @@ During the form filling and signing process, 3 types of events may occur and are
"enum": [ "enum": [
"form.viewed", "form.viewed",
"form.started", "form.started",
"form.completed" "form.completed",
"form.declined"
] ]
}, },
"timestamp": { "timestamp": {
"type": "string", "type": "string",
"description": "The event timestamp.", "description": "The event timestamp.",
"example": "2023-09-24T11:20:42Z", "examples": [
"2023-09-24T11:20:42Z"
],
"format": "date-time" "format": "date-time"
}, },
"data": { "data": {
@@ -36,20 +39,20 @@ During the form filling and signing process, 3 types of events may occur and are
"type": "number", "type": "number",
"description": "The submitter's unique identifier." "description": "The submitter's unique identifier."
}, },
"submission_id": {
"type": "number",
"description": "The unique submission identifier."
},
"email": { "email": {
"type": "string", "type": "string",
"description": "The submitter's email address", "description": "The submitter's email address",
"format": "email", "format": "email",
"example": "john.doe@example.com" "examples": [
"john.doe@example.com"
]
}, },
"ua": { "ua": {
"type": "string", "type": "string",
"description": "The user agent string that provides information about the submitter's web browser.", "description": "The user agent string that provides information about the submitter's web browser.",
"example": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" "examples": [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
]
}, },
"ip": { "ip": {
"type": "string", "type": "string",
@@ -62,27 +65,28 @@ During the form filling and signing process, 3 types of events may occur and are
"phone": { "phone": {
"type": "string", "type": "string",
"description": "The submitter's phone number, formatted according to the E.164 standard.", "description": "The submitter's phone number, formatted according to the E.164 standard.",
"example": "+1234567890" "examples": [
"+1234567890"
]
}, },
"role": { "role": {
"type": "string", "type": "string",
"description": "The submitter's role name or title.", "description": "The submitter's role name or title.",
"example": "First Party" "examples": [
"First Party"
]
}, },
"external_id": { "external_id": {
"type": "string", "type": "string",
"description": "Your application-specific unique string key to identify submitter within your app." "description": "Your application-specific unique string key to identify submitter within your app."
}, },
"application_key": {
"type": "string",
"description": "Your application-specific unique string key to identify submitter within your app. Backward compatibility with the previous version of the API. Use external_id instead."
},
"decline_reason": { "decline_reason": {
"type": "string", "type": "string",
"description": "Submitter provided decline message." "description": "Submitter provided decline message."
}, },
"sent_at": { "sent_at": {
"type": "string", "type": "string",
"description": "The date and time when the signing request was sent to the submitter.",
"format": "date-time" "format": "date-time"
}, },
"status": { "status": {
@@ -98,22 +102,27 @@ During the form filling and signing process, 3 types of events may occur and are
}, },
"opened_at": { "opened_at": {
"type": "string", "type": "string",
"description": "The date and time when the submitter opened the signing form.",
"format": "date-time" "format": "date-time"
}, },
"completed_at": { "completed_at": {
"type": "string", "type": "string",
"description": "The date and time when the submitter completed the signing form.",
"format": "date-time" "format": "date-time"
}, },
"declined_at": { "declined_at": {
"type": "string", "type": "string",
"description": "The date and time when the submitter declined the signing form.",
"format": "date-time" "format": "date-time"
}, },
"created_at": { "created_at": {
"type": "string", "type": "string",
"description": "The date and time when the submitter was created.",
"format": "date-time" "format": "date-time"
}, },
"updated_at": { "updated_at": {
"type": "string", "type": "string",
"description": "The date and time when the submitter was last updated.",
"format": "date-time" "format": "date-time"
}, },
"submission": { "submission": {
@@ -189,6 +198,7 @@ During the form filling and signing process, 3 types of events may occur and are
}, },
"preferences": { "preferences": {
"type": "object", "type": "object",
"description": "Submitter preferences for notifications.",
"properties": { "properties": {
"send_email": { "send_email": {
"type": "boolean", "type": "boolean",
@@ -210,7 +220,7 @@ During the form filling and signing process, 3 types of events may occur and are
"type": "string", "type": "string",
"description": "The field name." "description": "The field name."
}, },
"values": { "value": {
"type": "string", "type": "string",
"description": "The field value." "description": "The field value."
} }
@@ -231,6 +241,7 @@ During the form filling and signing process, 3 types of events may occur and are
}, },
"documents": { "documents": {
"type": "array", "type": "array",
"description": "List of completed documents signed by the submitter.",
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
+77 -18
View File
@@ -16,13 +16,17 @@ Get submission creation, completion, expiration, and archiving notifications usi
"description": "The event type.", "description": "The event type.",
"enum": [ "enum": [
"submission.created", "submission.created",
"submission.completed",
"submission.expired",
"submission.archived" "submission.archived"
] ]
}, },
"timestamp": { "timestamp": {
"type": "string", "type": "string",
"description": "The event timestamp.", "description": "The event timestamp.",
"example": "2023-09-24T11:20:42Z", "examples": [
"2023-09-24T11:20:42Z"
],
"format": "date-time" "format": "date-time"
}, },
"data": { "data": {
@@ -33,8 +37,26 @@ Get submission creation, completion, expiration, and archiving notifications usi
"type": "number", "type": "number",
"description": "The submission's unique identifier." "description": "The submission's unique identifier."
}, },
"archived_at": { "name": {
"type": "string", "type": "string",
"description": "Name of the document submission."
},
"slug": {
"type": "string",
"description": "Unique slug of the submission."
},
"expire_at": {
"type": [
"string",
"null"
],
"description": "The date and time when the submission will expire."
},
"archived_at": {
"type": [
"string",
"null"
],
"description": "The submission archive date." "description": "The submission archive date."
}, },
"created_at": { "created_at": {
@@ -65,9 +87,19 @@ Get submission creation, completion, expiration, and archiving notifications usi
] ]
}, },
"audit_log_url": { "audit_log_url": {
"type": "string", "type": [
"string",
"null"
],
"description": "Audit log file URL." "description": "Audit log file URL."
}, },
"combined_document_url": {
"type": [
"string",
"null"
],
"description": "Combined PDF file URL with documents and Audit Log."
},
"submitters": { "submitters": {
"type": "array", "type": "array",
"description": "The list of submitters for the submission.", "description": "The list of submitters for the submission.",
@@ -90,26 +122,40 @@ Get submission creation, completion, expiration, and archiving notifications usi
"type": "string", "type": "string",
"description": "The email address of the submitter.", "description": "The email address of the submitter.",
"format": "email", "format": "email",
"example": "john.doe@example.com" "examples": [
"john.doe@example.com"
]
}, },
"slug": { "slug": {
"type": "string", "type": "string",
"description": "The unique slug of the document template." "description": "The unique slug of the document template."
}, },
"sent_at": { "sent_at": {
"type": "string", "type": [
"string",
"null"
],
"description": "The date and time when the signing request was sent to the submitter." "description": "The date and time when the signing request was sent to the submitter."
}, },
"opened_at": { "opened_at": {
"type": "string", "type": [
"string",
"null"
],
"description": "The date and time when the submitter opened the signing form." "description": "The date and time when the submitter opened the signing form."
}, },
"completed_at": { "completed_at": {
"type": "string", "type": [
"string",
"null"
],
"description": "The date and time when the submitter completed the signing form." "description": "The date and time when the submitter completed the signing form."
}, },
"declined_at": { "declined_at": {
"type": "string", "type": [
"string",
"null"
],
"description": "The date and time when the submitter declined the signing form." "description": "The date and time when the submitter declined the signing form."
}, },
"created_at": { "created_at": {
@@ -121,27 +167,42 @@ Get submission creation, completion, expiration, and archiving notifications usi
"description": "The date and time when the submitter was last updated." "description": "The date and time when the submitter was last updated."
}, },
"name": { "name": {
"type": "string", "type": [
"string",
"null"
],
"description": "The name of the submitter." "description": "The name of the submitter."
}, },
"phone": { "phone": {
"type": "string", "type": [
"string",
"null"
],
"description": "The phone number of the submitter, formatted according to the E.164 standard.", "description": "The phone number of the submitter, formatted according to the E.164 standard.",
"example": "+1234567890" "examples": [
"+1234567890"
]
}, },
"role": { "role": {
"type": "string", "type": "string",
"description": "The role name or title of the submitter.", "description": "The role name or title of the submitter.",
"example": "First Party" "examples": [
"First Party"
]
}, },
"external_id": { "external_id": {
"type": "string", "type": [
"string",
"null"
],
"description": "Your application-specific unique string key to identify this submitter within your app." "description": "Your application-specific unique string key to identify this submitter within your app."
}, },
"metadata": { "metadata": {
"type": "object", "type": "object",
"description": "Metadata object with additional submitter information.", "description": "Metadata object with additional submitter information.",
"example": "{ 'customField': 'value' }" "examples": [
"{ 'customField': 'value' }"
]
}, },
"status": { "status": {
"type": "string", "type": "string",
@@ -154,10 +215,6 @@ Get submission creation, completion, expiration, and archiving notifications usi
"awaiting" "awaiting"
] ]
}, },
"application_key": {
"type": "string",
"description": "Your application-specific unique string key to identify this submitter within your app."
},
"values": { "values": {
"type": "object", "type": "object",
"description": "An object with pre-filled values for the submission. Use field names for keys of the object. For more configurations see `fields` param." "description": "An object with pre-filled values for the submission. Use field names for keys of the object. For more configurations see `fields` param."
@@ -222,6 +279,7 @@ Get submission creation, completion, expiration, and archiving notifications usi
}, },
"created_by_user": { "created_by_user": {
"type": "object", "type": "object",
"description": "User who created the submission.",
"properties": { "properties": {
"id": { "id": {
"type": "integer", "type": "integer",
@@ -243,6 +301,7 @@ Get submission creation, completion, expiration, and archiving notifications usi
}, },
"submission_events": { "submission_events": {
"type": "array", "type": "array",
"description": "List of submission events.",
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
+49 -12
View File
@@ -3,7 +3,8 @@
Get template creation and update notifications using these events: Get template creation and update notifications using these events:
- **'template.created'** is triggered when the template is created. - **'template.created'** is triggered when the template is created.
- **'tempate.updated'** is triggered when the template is updated. - **'template.updated'** is triggered when the template is updated.
- **'template.archived'** is triggered when the template is archived.
@@ -14,13 +15,16 @@ Get template creation and update notifications using these events:
"description": "The event type.", "description": "The event type.",
"enum": [ "enum": [
"template.created", "template.created",
"template.updated" "template.updated",
"template.archived"
] ]
}, },
"timestamp": { "timestamp": {
"type": "string", "type": "string",
"description": "The event timestamp.", "description": "The event timestamp.",
"example": "2023-09-24T11:20:42Z", "examples": [
"2023-09-24T11:20:42Z"
],
"format": "date-time" "format": "date-time"
}, },
"data": { "data": {
@@ -74,6 +78,31 @@ Get template creation and update notifications using these events:
"type": "string", "type": "string",
"description": "The field name." "description": "The field name."
}, },
"type": {
"type": "string",
"description": "The field type.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification",
"kba",
"strikethrough"
]
},
"required": { "required": {
"type": "boolean", "type": "boolean",
"description": "The flag indicating whether the field is required." "description": "The flag indicating whether the field is required."
@@ -120,6 +149,7 @@ Get template creation and update notifications using these events:
}, },
"submitters": { "submitters": {
"type": "array", "type": "array",
"description": "List of submitter roles defined in the template.",
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -138,12 +168,11 @@ Get template creation and update notifications using these events:
"type": "integer", "type": "integer",
"description": "Unique identifier of the author of the template." "description": "Unique identifier of the author of the template."
}, },
"account_id": {
"type": "integer",
"description": "Unique identifier of the account of the template."
},
"archived_at": { "archived_at": {
"type": "string", "type": [
"string",
"null"
],
"description": "Date and time when the template was archived." "description": "Date and time when the template was archived."
}, },
"created_at": { "created_at": {
@@ -164,7 +193,10 @@ Get template creation and update notifications using these events:
] ]
}, },
"external_id": { "external_id": {
"type": "string", "type": [
"string",
"null"
],
"description": "Identifier of the template in the external system." "description": "Identifier of the template in the external system."
}, },
"folder_id": { "folder_id": {
@@ -175,12 +207,17 @@ Get template creation and update notifications using these events:
"type": "string", "type": "string",
"description": "Folder name where the template is placed." "description": "Folder name where the template is placed."
}, },
"application_key": { "preferences": {
"type": "string", "type": "object",
"description": "Your application-specific unique string key to identify tempate_id within your app." "description": "Template preferences object."
},
"shared_link": {
"type": "boolean",
"description": "Flag indicating whether the shared link is enabled for the template."
}, },
"author": { "author": {
"type": "object", "type": "object",
"description": "Author of the template.",
"properties": { "properties": {
"id": { "id": {
"type": "integer", "type": "integer",
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff. Show more