mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a3a185310 | |||
| 7d381b7aee | |||
| bba5626dea | |||
| bdf5e58929 | |||
| 035168230e | |||
| 28168f082d | |||
| a29e09ed2f | |||
| e88874e757 | |||
| b6fb7c6977 | |||
| 8a60f17d13 | |||
| e1d860a42c | |||
| 56da326709 | |||
| 672008f8af | |||
| d86b16de6b | |||
| d41710f507 | |||
| 2b53166763 | |||
| 4e56a16a58 | |||
| ac7cc77017 | |||
| 9194879c43 | |||
| 1902cdaa55 | |||
| 6314987e69 | |||
| 65bcbd7737 | |||
| 9b25b8538e | |||
| bbf8bb2a94 | |||
| 2085227cd9 | |||
| 93e0730210 | |||
| 25c5c11ab3 | |||
| 6ed5b5d35d |
@@ -9,7 +9,7 @@ module Api
|
||||
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||
|
||||
documents =
|
||||
if @submission.submitters.all?(&:completed_at?)
|
||||
if @submission.completed_at?
|
||||
build_completed_documents(@submission, merge: is_merge)
|
||||
else
|
||||
build_preview_documents(@submission, merge: is_merge)
|
||||
@@ -30,7 +30,7 @@ module Api
|
||||
private
|
||||
|
||||
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 submission.merged_document_attachment.blank?
|
||||
|
||||
@@ -5,7 +5,7 @@ module Api
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
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[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||
@@ -36,20 +36,5 @@ module Api
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
module Api
|
||||
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
|
||||
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 :submission, only: %i[show index destroy]
|
||||
load_and_authorize_resource :submission, only: %i[show index update destroy]
|
||||
|
||||
before_action only: :create do
|
||||
authorize!(:create, Submission)
|
||||
end
|
||||
|
||||
before_action :maybe_return_template_error, only: :create
|
||||
|
||||
def index
|
||||
submissions = Submissions.search(current_user, @submissions, params[:q])
|
||||
submissions = filter_submissions(submissions, params)
|
||||
@@ -58,7 +60,7 @@ module Api
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -68,20 +70,6 @@ module Api
|
||||
def create
|
||||
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_sms] = false unless params.key?(:send_sms)
|
||||
|
||||
@@ -92,10 +80,17 @@ module Api
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
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|
|
||||
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
|
||||
|
||||
@@ -109,6 +104,25 @@ module Api
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
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
|
||||
if params[:permanently].in?(['true', true])
|
||||
@submission.destroy!
|
||||
@@ -123,6 +137,41 @@ module Api
|
||||
|
||||
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)
|
||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
@@ -4,6 +4,8 @@ module Api
|
||||
class SubmittersController < ApiBaseController
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
before_action :maybe_return_submitter_error, only: :update
|
||||
|
||||
def index
|
||||
submitters = Submitters.search(current_user, @submitters, params[:q])
|
||||
|
||||
@@ -36,14 +38,6 @@ module Api
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
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
|
||||
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||
|
||||
@@ -73,7 +67,9 @@ module Api
|
||||
end
|
||||
|
||||
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]
|
||||
Submitters.send_signature_requests([@submitter])
|
||||
end
|
||||
@@ -104,6 +100,16 @@ module Api
|
||||
|
||||
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)
|
||||
if params[:completed_after].present?
|
||||
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||
|
||||
@@ -5,6 +5,8 @@ module Api
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def index
|
||||
@templates = Templates.shared(current_user) if params[:shared].in?(['true', true])
|
||||
|
||||
templates = filter_templates(@templates, params)
|
||||
|
||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||
@@ -54,7 +56,7 @@ module Api
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
SearchEntries.enqueue_reindex(@template) if @template.saved_change_to_name?
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
@@ -115,7 +117,13 @@ module Api
|
||||
end
|
||||
|
||||
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 = 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?
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class EmailSmtpSettingsController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
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
|
||||
|
||||
@@ -23,6 +23,12 @@ class EmailSmtpSettingsController < ApplicationController
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@encrypted_config.destroy!
|
||||
|
||||
redirect_to settings_email_index_path, notice: I18n.t('smtp_settings_have_been_reset')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_encrypted_config
|
||||
|
||||
@@ -16,18 +16,16 @@ class SendSubmissionEmailController < ApplicationController
|
||||
@submitter =
|
||||
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
||||
elsif params[:submission_slug]
|
||||
submission = Submission.find_by(slug: params[:submission_slug])
|
||||
submission = Submission.find_by!(slug: params[:submission_slug])
|
||||
|
||||
if submission
|
||||
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
||||
end
|
||||
@submitter = submission.submitters.order(:completed_at).find_by(email: params[:email].to_s.downcase)
|
||||
|
||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||
else
|
||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
if @submitter
|
||||
if @submitter && completed_submitter?(@submitter)
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
@@ -41,6 +39,10 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def completed_submitter?(submitter)
|
||||
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
|
||||
end
|
||||
|
||||
def can_send?(submitter)
|
||||
return false if submitter.account.archived_at?
|
||||
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||
|
||||
@@ -93,9 +93,12 @@ class StartFormController < ApplicationController
|
||||
|
||||
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
|
||||
|
||||
def load_resubmit_submitter
|
||||
@@ -138,8 +141,7 @@ class StartFormController < ApplicationController
|
||||
|
||||
submitter ||=
|
||||
Submitter
|
||||
.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||
.where(submission: template.submissions.non_expired.active)
|
||||
.order(id: :desc)
|
||||
.where(declined_at: 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 }
|
||||
.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?
|
||||
|
||||
unless @resubmit_submitter
|
||||
|
||||
@@ -12,11 +12,12 @@ class SubmissionsArchivedController < ApplicationController
|
||||
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
|
||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
@submissions =
|
||||
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(completed_at: :desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class SubmissionsController < ApplicationController
|
||||
def show
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
unless @submission.submitters.all?(&:completed_at?)
|
||||
unless @submission.completed_at?
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submission],
|
||||
associations: [{ submitters: :start_form_submission_events }]
|
||||
@@ -87,6 +87,8 @@ class SubmissionsController < ApplicationController
|
||||
private
|
||||
|
||||
def create_submissions(template, submissions_params, params)
|
||||
normalize_message_submitter_uuids!(params)
|
||||
|
||||
submissions_attrs = submissions_params[:submission].to_h.values
|
||||
|
||||
submissions_attrs, _, new_fields =
|
||||
@@ -111,4 +113,23 @@ class SubmissionsController < ApplicationController
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
||||
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
|
||||
|
||||
@@ -13,11 +13,12 @@ class SubmissionsDashboardController < ApplicationController
|
||||
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
|
||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
@submissions =
|
||||
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(completed_at: :desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@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?
|
||||
|
||||
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
|
||||
if !@submission.completed_at? && !signature_valid &&
|
||||
(!current_user || !current_ability.can?(:read, @submission))
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class SubmissionsUnarchiveController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def create
|
||||
authorize!(:update, @submission)
|
||||
authorize!(:destroy, @submission)
|
||||
|
||||
@submission.update!(archived_at: nil)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
|
||||
|
||||
@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
|
||||
|
||||
@@ -28,11 +28,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
if last_submitter.completed_at < TTL.ago
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
@@ -18,7 +18,10 @@ class SubmitFormController < ApplicationController
|
||||
submission = @submitter.submission
|
||||
|
||||
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)
|
||||
|
||||
@@ -71,6 +74,12 @@ class SubmitFormController < ApplicationController
|
||||
status: :unprocessable_content
|
||||
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)
|
||||
|
||||
head :ok
|
||||
|
||||
@@ -13,6 +13,7 @@ class SubmitFormDeclineController < ApplicationController
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.viewer? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter,
|
||||
current_user,
|
||||
request)
|
||||
|
||||
@@ -12,6 +12,7 @@ class SubmitFormDelegateController < ApplicationController
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.viewer? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter,
|
||||
current_user,
|
||||
request)
|
||||
|
||||
@@ -14,6 +14,7 @@ class SubmitFormDownloadController < ApplicationController
|
||||
return head :unprocessable_content if @submitter.declined_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.completed_at? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
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_path(@submitter.slug) if @submitter.viewer?
|
||||
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return redirect_to submit_form_path(@submitter.slug)
|
||||
|
||||
@@ -48,6 +48,7 @@ class SubmitFormInviteController < ApplicationController
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at? &&
|
||||
!submitter.viewer? &&
|
||||
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
end
|
||||
|
||||
|
||||
@@ -7,13 +7,7 @@ class SubmitFormMetadataController < ApplicationController
|
||||
def index
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :not_found if @submitter.declined_at? ||
|
||||
@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)
|
||||
return head :not_found unless authorized_submitter?(@submitter)
|
||||
|
||||
submission = @submitter.submission
|
||||
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||
@@ -34,4 +28,17 @@ class SubmitFormMetadataController < ApplicationController
|
||||
|
||||
render json: { text_runs: }
|
||||
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
|
||||
|
||||
@@ -50,7 +50,7 @@ class SubmittersController < ApplicationController
|
||||
|
||||
def submitter_editable?(submission)
|
||||
!@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?
|
||||
end
|
||||
|
||||
@@ -58,7 +58,7 @@ class SubmittersController < ApplicationController
|
||||
if params[:send_email] == '1' && submitter.email.present?
|
||||
is_sent_recently = Docuseal.multitenant? &&
|
||||
EmailEvent.exists?(email: submitter.email,
|
||||
tag: 'submitter_invitation',
|
||||
tag: %w[submitter_invitation submitter_view_invitation],
|
||||
emailable: submitter,
|
||||
event_type: 'send',
|
||||
created_at: 4.hours.ago..Time.current)
|
||||
|
||||
@@ -10,7 +10,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
|
||||
@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
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class TemplatesController < ApplicationController
|
||||
submissions = Submissions::Filter.filter_by_status(submissions, params)
|
||||
|
||||
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
|
||||
submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@ class TemplatesPreferencesController < ApplicationController
|
||||
|
||||
RESETTABLE_PREFERENCE_KEYS = {
|
||||
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
|
||||
invitation_reminder_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
|
||||
params.require(:template).permit(
|
||||
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
|
||||
documents_copy_email_subject documents_copy_email_body
|
||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||
completed_notification_email_attach_documents
|
||||
completed_redirect_url validate_unique_submitters
|
||||
completed_notification_email_attach_documents completed_redirect_url validate_unique_submitters
|
||||
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
||||
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
|
||||
@@ -42,6 +42,7 @@ import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||
import PageContainer from './elements/page_container'
|
||||
import EmailEditor from './elements/email_editor'
|
||||
import MarkdownEditor from './elements/markdown_editor'
|
||||
import HtmlEditor from './elements/html_editor'
|
||||
import MountOnClick from './elements/mount_on_click'
|
||||
import RemoveOnEvent from './elements/remove_on_event'
|
||||
import ScrollTo from './elements/scroll_to'
|
||||
@@ -135,6 +136,7 @@ safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('email-editor', EmailEditor)
|
||||
safeRegisterElement('markdown-editor', MarkdownEditor)
|
||||
safeRegisterElement('html-editor', HtmlEditor)
|
||||
safeRegisterElement('mount-on-click', MountOnClick)
|
||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||
safeRegisterElement('scroll-to', ScrollTo)
|
||||
|
||||
@@ -9,8 +9,9 @@ function loadCodeMirror () {
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/lint'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
|
||||
]).then(([view, commands, language, html, htmlflow]) => {
|
||||
]).then(([view, commands, language, html, lint, htmlflow]) => {
|
||||
return {
|
||||
minimalSetup: [
|
||||
commands.history(),
|
||||
@@ -19,6 +20,8 @@ function loadCodeMirror () {
|
||||
],
|
||||
EditorView: view.EditorView,
|
||||
html: html.html,
|
||||
htmlLanguage: html.htmlLanguage,
|
||||
linter: lint.linter,
|
||||
htmlflow: htmlflow.default || htmlflow
|
||||
}
|
||||
})
|
||||
@@ -46,6 +49,70 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
this.previewViewTab.addEventListener('click', this.showPreviewView)
|
||||
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 = () => {
|
||||
@@ -76,7 +143,9 @@ export default targetable(class extends HTMLElement {
|
||||
this.input = this.querySelector('input[type="hidden"]')
|
||||
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({
|
||||
doc: this.input.value,
|
||||
@@ -85,8 +154,11 @@ export default targetable(class extends HTMLElement {
|
||||
html(),
|
||||
minimalSetup,
|
||||
EditorView.lineWrapping,
|
||||
linter((view) => this.buildDiagnostics(view.state.doc.toString()), { delay: 600 }),
|
||||
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({
|
||||
'&': {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -35,7 +35,7 @@ function loadTiptap () {
|
||||
}))
|
||||
}
|
||||
|
||||
class LinkTooltip {
|
||||
export class LinkTooltip {
|
||||
constructor (container, editor, templateEl) {
|
||||
this.container = container
|
||||
this.editor = editor
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
@submit.prevent="submitStep"
|
||||
/>
|
||||
<button
|
||||
v-if="!isFormVisible"
|
||||
v-if="!isFormVisible && currentField"
|
||||
id="expand_form_button"
|
||||
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%"
|
||||
@@ -174,6 +174,7 @@
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
v-if="currentField"
|
||||
v-show="isFormVisible"
|
||||
id="form_container"
|
||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
|
||||
@@ -1172,7 +1173,11 @@ export default {
|
||||
}
|
||||
},
|
||||
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 () {
|
||||
if (this.recalculateButtonDisabledKey) {
|
||||
|
||||
@@ -3206,15 +3206,6 @@ export default {
|
||||
e.preventDefault()
|
||||
|
||||
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 () {
|
||||
@@ -3231,32 +3222,25 @@ export default {
|
||||
if (!this.template.fields.length) {
|
||||
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))
|
||||
this.isSaving = true
|
||||
|
||||
if (submitterWithoutFields) {
|
||||
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)
|
||||
|
||||
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(() => {
|
||||
if (this.withRevisions) {
|
||||
this.captureRevision()
|
||||
}
|
||||
|
||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
})
|
||||
}
|
||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
})
|
||||
}
|
||||
},
|
||||
scrollToArea (area) {
|
||||
|
||||
@@ -600,7 +600,8 @@ export default {
|
||||
}
|
||||
},
|
||||
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 () {
|
||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
||||
|
||||
@@ -452,7 +452,7 @@
|
||||
class="pb-0.5 mt-0.5"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<label
|
||||
@@ -830,6 +830,7 @@ export default {
|
||||
} else if (format === 'percent') {
|
||||
return `${number}%`
|
||||
} else if (format === 'percent_space') {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
return `${String(number).replace('.', ',')} %`
|
||||
} else {
|
||||
return number
|
||||
|
||||
@@ -11,7 +11,8 @@ class ProcessSubmissionExpiredJob
|
||||
return if submission.archived_at?
|
||||
return if submission.template&.archived_at?
|
||||
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')
|
||||
end
|
||||
|
||||
@@ -5,30 +5,45 @@ class ProcessSubmitterCompletionJob
|
||||
|
||||
def perform(params = {})
|
||||
submitter = Submitter.find(params['submitter_id'])
|
||||
submission = submitter.submission
|
||||
|
||||
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)
|
||||
|
||||
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
||||
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
|
||||
if is_last
|
||||
if submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
|
||||
Submissions::EnsureCombinedGenerated.call(submitter)
|
||||
end
|
||||
|
||||
Submissions::EnsureAuditGenerated.call(submitter.submission)
|
||||
Submissions::EnsureAuditGenerated.call(submission)
|
||||
|
||||
enqueue_completed_emails(submitter)
|
||||
end
|
||||
|
||||
create_completed_documents!(submitter)
|
||||
|
||||
if !is_all_completed && submitter.submission.submitters_order_preserved? && params['send_invitation_email'] != false
|
||||
enqueue_next_submitter_request_notification(submitter)
|
||||
if !submission.completed_at && submission.submitters_order_preserved? && params['send_invitation_email'] != false &&
|
||||
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
|
||||
|
||||
enqueue_completed_webhooks(submitter, is_all_completed:)
|
||||
enqueue_completed_webhooks(submitter, is_last:)
|
||||
end
|
||||
|
||||
def create_completed_submitter!(submitter)
|
||||
@@ -77,7 +92,7 @@ class ProcessSubmitterCompletionJob
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||
def enqueue_completed_webhooks(submitter, is_last: false)
|
||||
event_uuids = {}
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -137,7 +152,7 @@ class ProcessSubmitterCompletionJob
|
||||
return if configs.value['enabled'] == 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?
|
||||
|
||||
@@ -157,9 +172,9 @@ class ProcessSubmitterCompletionJob
|
||||
bcc.to_s.scan(User::EMAIL_REGEXP)
|
||||
end
|
||||
|
||||
def enqueue_next_submitter_request_notification(submitter)
|
||||
def enqueue_next_submitter_request_notification(submitter) # rubocop:disable Metrics/PerceivedComplexity
|
||||
submission = submitter.submission
|
||||
submitters_index = submission.submitters.index_by(&:uuid)
|
||||
submitters_index = submission.submitters.reject(&:viewer?).index_by(&:uuid)
|
||||
|
||||
next_submitter_items =
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -19,7 +19,12 @@ class SendSubmitterInvitationEmailJob
|
||||
return
|
||||
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)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
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 ||
|
||||
@submitter.template&.preferences&.dig('request_email_body').presence
|
||||
|
||||
@@ -45,6 +45,45 @@ class SubmitterMailer < ApplicationMailer
|
||||
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)
|
||||
@current_account = submitter.submission.account
|
||||
@submitter = submitter
|
||||
@@ -53,8 +92,6 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
template_preferences = @submission.template&.preferences || {}
|
||||
|
||||
Submissions::EnsureResultGenerated.call(submitter)
|
||||
|
||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||
|
||||
add_completed_email_attachments!(
|
||||
@@ -109,8 +146,6 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
template_preferences = @submitter.template&.preferences || {}
|
||||
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||
|
||||
add_completed_email_attachments!(
|
||||
@@ -172,7 +207,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
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,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
@@ -216,6 +251,8 @@ class SubmitterMailer < ApplicationMailer
|
||||
def build_invite_subject(subject, email_config, submitter)
|
||||
if email_config || subject
|
||||
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?
|
||||
I18n.t(:you_are_invited_to_sign_a_document)
|
||||
else
|
||||
@@ -227,6 +264,14 @@ class SubmitterMailer < ApplicationMailer
|
||||
submitter.template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] }
|
||||
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)
|
||||
total_size = current_size
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class Account < ApplicationRecord
|
||||
has_many :encrypted_configs, dependent: :destroy
|
||||
has_many :account_configs, dependent: :destroy
|
||||
has_many :email_messages, dependent: :destroy
|
||||
has_many :email_message_assets, dependent: :destroy
|
||||
has_many :templates, dependent: :destroy
|
||||
has_many :template_folders, dependent: :destroy
|
||||
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#
|
||||
class AccountConfig < ApplicationRecord
|
||||
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_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||
@@ -63,6 +64,7 @@ class AccountConfig < ApplicationRecord
|
||||
|
||||
EMAIL_VARIABLES = {
|
||||
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_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
|
||||
@@ -75,6 +77,12 @@ class AccountConfig < ApplicationRecord
|
||||
'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 {
|
||||
{
|
||||
'subject' => I18n.t(:you_are_invited_to_sign_a_document),
|
||||
|
||||
@@ -33,6 +33,15 @@ class EmailMessage < ApplicationRecord
|
||||
|
||||
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
|
||||
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
|
||||
end
|
||||
|
||||
@@ -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
@@ -6,6 +6,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# archived_at :datetime
|
||||
# completed_at :datetime
|
||||
# expire_at :datetime
|
||||
# name :text
|
||||
# preferences :text not null
|
||||
@@ -25,9 +26,12 @@
|
||||
#
|
||||
# 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_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_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_slug (slug) UNIQUE
|
||||
# index_submissions_on_template_id (template_id)
|
||||
@@ -88,27 +92,17 @@ class Submission < ApplicationRecord
|
||||
|
||||
scope :active, -> { where(archived_at: nil) }
|
||||
scope :archived, -> { where.not(archived_at: nil) }
|
||||
scope :pending, lambda {
|
||||
where(expire_at: nil).or(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 :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 :non_expired, -> { where(expire_at: nil).or(where(expire_at: Time.current..)) }
|
||||
scope :pending, -> { non_expired.where(completed_at: nil) }
|
||||
scope :completed, -> { where.not(completed_at: nil) }
|
||||
scope :declined, lambda {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||
.where.not(declined_at: nil).limit(1).arel.exists)
|
||||
}
|
||||
scope :expired, -> { where(expire_at: ..Time.current).where(completed_at: nil) }
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,12 @@ class Submitter < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def viewer?
|
||||
return false if submission.template_submitters.blank?
|
||||
|
||||
submission.template_submitters.any? { |s| s['uuid'] == uuid && s['is_viewer'] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def anonymize_email_events
|
||||
|
||||
@@ -57,6 +57,11 @@
|
||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
||||
</div>
|
||||
<% 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 class="w-0 md:w-52"></div>
|
||||
</div>
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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') %>
|
||||
<autoresize-textarea>
|
||||
<%= text_area_tag name, value, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto', style: 'max-height: 400px' %>
|
||||
</autoresize-textarea>
|
||||
<% else %>
|
||||
<markdown-editor>
|
||||
<template data-target="markdown-editor.linkTooltipTemplate">
|
||||
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
|
||||
<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">
|
||||
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
|
||||
<%= 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>
|
||||
<markdown-editor>
|
||||
<template data-target="markdown-editor.linkTooltipTemplate">
|
||||
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
|
||||
<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">
|
||||
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
|
||||
<%= 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>
|
||||
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
|
||||
</markdown-editor>
|
||||
<% end %>
|
||||
</template>
|
||||
<div class="border border-base-content/20 rounded-2xl bg-white">
|
||||
<%= 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>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<p>
|
||||
<% 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) %>
|
||||
<% 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) %>
|
||||
<% else %>
|
||||
<%= t('sent_using_product_name_open_source_software_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/open", product_name: Docuseal.product_name) %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
@@ -71,10 +71,11 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
|
||||
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 mr-2 inline') %>
|
||||
<span class="mr-1 whitespace-nowrap"><%= t('sign_out') %></span>
|
||||
<% end %>
|
||||
<button form="destroy_user_session_form">
|
||||
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 inline') %>
|
||||
<span class="whitespace-nowrap"><%= t('sign_out') %></span>
|
||||
</button>
|
||||
<%= button_to '', destroy_user_session_path, method: :delete, data: { turbo: false }, form: { id: 'destroy_user_session_form' }, form_class: 'hidden' %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<li>
|
||||
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
|
||||
</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? %>
|
||||
<% 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? %>
|
||||
<li>
|
||||
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<%= local_assigns[:variables_form] %>
|
||||
<div>
|
||||
<%= 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 %>
|
||||
<%= render 'send_sms', f: %>
|
||||
<% end %>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<%= local_assigns[:variables_form] %>
|
||||
<div>
|
||||
<%= 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: %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
|
||||
@@ -38,9 +38,21 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% 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 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="form-control">
|
||||
<div class="flex justify-between">
|
||||
@@ -51,13 +63,13 @@
|
||||
</label>
|
||||
<% end %>
|
||||
</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 class="form-control">
|
||||
<%= f.label :message, t('body'), class: 'label' %>
|
||||
<% 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 %>
|
||||
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
||||
<%= 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 %>
|
||||
<% 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">
|
||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||
<span class="label"><%= t('save_as_default_template_message') %></span>
|
||||
@@ -81,17 +93,18 @@
|
||||
</ul>
|
||||
</toggle-visible>
|
||||
<% 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| %>
|
||||
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||
<div class="form-control">
|
||||
<div class="flex justify-between">
|
||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||
</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 class="form-control">
|
||||
<%= 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>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
||||
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
||||
<% 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' %>
|
||||
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
||||
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
||||
@@ -26,18 +27,18 @@
|
||||
<div class="px-5 mb-5 mt-4">
|
||||
<% unless only_detailed %>
|
||||
<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 id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
|
||||
<%= render 'phone_form', template: @template, variables_form: %>
|
||||
</div>
|
||||
<% end %>
|
||||
<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>
|
||||
<% if with_list %>
|
||||
<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>
|
||||
<% end %>
|
||||
<%= render 'submissions/error' %>
|
||||
|
||||
@@ -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>
|
||||
</a>
|
||||
<div class="space-x-3 flex items-center">
|
||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
||||
<% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %>
|
||||
<% if signed_in? && can?(:destroy, @submission) && @submission.archived_at? && !@submission.completed_at? %>
|
||||
<%= 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 %>
|
||||
<% if @submission.audit_trail.present? %>
|
||||
@@ -30,16 +29,19 @@
|
||||
<span class="hidden md:block"><%= t('event_log') %></span>
|
||||
<% 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? } %>
|
||||
<% 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) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% 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">
|
||||
<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">
|
||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:inline"><%= t('download') %></span>
|
||||
@@ -49,26 +51,36 @@
|
||||
<span class="hidden md:inline"><%= t('downloading') %></span>
|
||||
</span>
|
||||
</download-button>
|
||||
<% if is_all_completed && !is_combined_enabled %>
|
||||
<div class="dropdown dropdown-end">
|
||||
<% if show_download_dropdown %>
|
||||
<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">
|
||||
<span class="text-sm align-text-top">
|
||||
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
||||
</span>
|
||||
</label>
|
||||
<ul class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
||||
<li>
|
||||
<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">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
||||
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span><%= t('downloading') %></span>
|
||||
</span>
|
||||
</download-button>
|
||||
</li>
|
||||
<% if show_combined_download %>
|
||||
<li>
|
||||
<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">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
||||
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<span><%= t('downloading') %></span>
|
||||
</span>
|
||||
</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>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -166,7 +178,7 @@
|
||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||
</span>
|
||||
</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') %>">
|
||||
<%= 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') %>
|
||||
@@ -199,7 +211,11 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<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') %>
|
||||
<% else %>
|
||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||
@@ -207,9 +223,19 @@
|
||||
<span>
|
||||
<% if submitter&.declined_at? %>
|
||||
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
|
||||
<% elsif submitter&.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 %>
|
||||
<% if submitter.completed_at? %>
|
||||
<%= 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? %>
|
||||
<% if @submission.expired? %>
|
||||
<%= t(:expired) %>
|
||||
@@ -232,15 +258,15 @@
|
||||
</span>
|
||||
</div>
|
||||
<% 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">
|
||||
<%= 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>
|
||||
<% 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: %>
|
||||
<% 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">
|
||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||
<%= t('sign_in_person') %>
|
||||
|
||||
@@ -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">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
@@ -12,7 +13,7 @@
|
||||
<div>
|
||||
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template&.name %></p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,7 +30,7 @@
|
||||
<div class="py-2"></div>
|
||||
<% 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">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<%= @submitter.submission.name || @submitter.submission.template&.name %>
|
||||
</h1>
|
||||
<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">
|
||||
<button type="button" class="btn btn-sm md:!px-5 px-2" aria-label="<%= t(:decline) %>">
|
||||
<span class="hidden md:inline"><%= t(:decline) %></span>
|
||||
@@ -34,11 +34,11 @@
|
||||
</modal-button>
|
||||
<% end %>
|
||||
<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">
|
||||
<button id="delegate_button" type="button" class="btn btn-sm !px-5"><%= t(:delegate) %></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">
|
||||
<button id="decline_button" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
@@ -56,7 +56,7 @@
|
||||
</download-button>
|
||||
<% end %>
|
||||
<% 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">
|
||||
<button id="decline_button" type="button" class="btn btn-sm !px-5"><%= t(:decline) %></button>
|
||||
</modal-button>
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</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">
|
||||
<% 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">
|
||||
<button type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
@@ -85,7 +85,7 @@
|
||||
</modal-button>
|
||||
<% end %>
|
||||
<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">
|
||||
<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">
|
||||
@@ -96,7 +96,7 @@
|
||||
</span>
|
||||
</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">
|
||||
<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') %>
|
||||
@@ -104,7 +104,7 @@
|
||||
</modal-button>
|
||||
<% end %>
|
||||
<% 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">
|
||||
<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">
|
||||
@@ -177,12 +177,12 @@
|
||||
</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 'submit_form/decline_form', submitter: @submitter %>
|
||||
<% 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 'submit_form/delegate_form', submitter: @submitter %>
|
||||
<% 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 %>
|
||||
@@ -17,7 +17,8 @@
|
||||
</submitter-item>
|
||||
</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? %>
|
||||
</div>
|
||||
<div class="form-control mt-4">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<% 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">
|
||||
<% 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 %>
|
||||
<div>
|
||||
<% submitter = submitters.first %>
|
||||
@@ -51,7 +51,7 @@
|
||||
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||
<%= submitter.name || submitter.email || submitter.phone %>
|
||||
</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') %>">
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
@@ -144,7 +144,7 @@
|
||||
<a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
|
||||
<%= submitter.name || submitter.email || submitter.phone %>
|
||||
</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') %>">
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
<% unless can?(:manage, :countless) %>
|
||||
<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>
|
||||
<% end %>
|
||||
</a>
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
<% unless can?(:manage, :countless) %>
|
||||
<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>
|
||||
<% end %>
|
||||
</a>
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
<% unless can?(:manage, :countless) %>
|
||||
<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>
|
||||
<% end %>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
|
||||
<% 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| %>
|
||||
<% unless close_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| %>
|
||||
<% 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 %>
|
||||
<% is_viewer_row = field_submitter_uuids.exclude?(submitter['uuid']) %>
|
||||
<div class="form-control">
|
||||
<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 %>
|
||||
@@ -21,6 +23,8 @@
|
||||
<mount-on-click data-template-id="order_fields" class="link whitespace-nowrap text-sm mt-1 mr-1 block">
|
||||
<%= t('edit_order') %>
|
||||
</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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -141,11 +145,13 @@
|
||||
</div>
|
||||
<% if template.submitters.size > 2 && !is_order_set %>
|
||||
<template id="order_fields">
|
||||
<% last_viewer_order = -1 %>
|
||||
<% 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'] %>">
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
</turbo-stream>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= 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>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= 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>
|
||||
<% if can?(:manage, :reply_to) %>
|
||||
<div class="form-control">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<div id="<%= AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY %>_form">
|
||||
<% 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') %>
|
||||
<% 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? %>
|
||||
<% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 5 %>
|
||||
<% if is_custom_template_email || @template.preferences['submitters'].to_a.any? %>
|
||||
@@ -28,7 +31,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= 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>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -49,7 +52,7 @@
|
||||
<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_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'] %>
|
||||
<div class="form-control">
|
||||
<div class="flex justify-between">
|
||||
@@ -64,7 +67,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<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>
|
||||
<% 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' %>
|
||||
</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' %>
|
||||
<div class="collapse collapse-arrow join-item border border-base-300">
|
||||
<input type="checkbox" name="accordion">
|
||||
|
||||
@@ -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:'
|
||||
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_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>'
|
||||
show_send_with_docuseal_pro_attribution_in_emails_html: Show "Sent with <span class="link">DocuSeal Pro</span>" attribution in emails
|
||||
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
|
||||
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
|
||||
you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".'
|
||||
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:"
|
||||
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"
|
||||
review_and_sign: Review and Sign
|
||||
view_document: View Document
|
||||
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."
|
||||
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}'
|
||||
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_view_a_document: 'You are invited to view a document'
|
||||
you_are_invited_to_sign_documents: 'You are invited to sign documents'
|
||||
your_document_copy: 'Your document copy'
|
||||
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.
|
||||
|
||||
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,
|
||||
{account.name}
|
||||
submitter_completed_email_body: |
|
||||
@@ -306,6 +322,8 @@ en: &en
|
||||
invalid_timeserver: Invalid Timeserver
|
||||
email_templates: Email Templates
|
||||
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_sms: Signature Request SMS
|
||||
verification_code_sms: Verification Code SMS
|
||||
@@ -568,6 +586,7 @@ en: &en
|
||||
form_has_been_archived: Form has been archived.
|
||||
form_has_been_expired: Form has been expired.
|
||||
form_has_been_declined: Form has been declined.
|
||||
form_is_view_only: Form is view only.
|
||||
file_is_missing: File is missing
|
||||
folder_name_has_been_updated: Folder name has been updated.
|
||||
unable_to_rename_folder: Unable to rename folder.
|
||||
@@ -652,6 +671,10 @@ en: &en
|
||||
signers: Signers
|
||||
not_invited_yet: Not invited 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}'
|
||||
expire_on_time: 'Expire on %{time}'
|
||||
sign_in_person: Sign In-person
|
||||
@@ -876,6 +899,7 @@ en: &en
|
||||
mobile: Mobile
|
||||
tablet: Tablet
|
||||
reset_default: Reset default
|
||||
smtp_settings_have_been_reset: SMTP settings have been reset.
|
||||
send_signature_request_email: Send signature request email
|
||||
last_3_months: Last 3 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:'
|
||||
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_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>'
|
||||
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.
|
||||
@@ -1149,15 +1174,19 @@ es: &es
|
||||
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_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:"
|
||||
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
|
||||
view_document: Ver documento
|
||||
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."
|
||||
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}'
|
||||
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_view_a_document: 'Estás invitado/a a ver un documento'
|
||||
you_are_invited_to_sign_documents: 'Estás invitado/a a firmar documentos'
|
||||
your_document_copy: 'Tu copia del documento'
|
||||
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.
|
||||
|
||||
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,
|
||||
{account.name}
|
||||
submitter_completed_email_body: |
|
||||
@@ -1362,6 +1402,8 @@ es: &es
|
||||
invalid_timeserver: Servidor de tiempo inválido
|
||||
email_templates: Plantillas de correo electrónico
|
||||
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_sms: SMS de solicitud de firma
|
||||
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_expired: El formulario ha expirado.
|
||||
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
|
||||
folder_name_has_been_updated: El nombre de la carpeta ha sido actualizado.
|
||||
unable_to_rename_folder: No se pudo renombrar la carpeta.
|
||||
@@ -1708,6 +1751,10 @@ es: &es
|
||||
signers: Firmantes
|
||||
not_invited_yet: Aún no invitado
|
||||
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}'
|
||||
expire_on_time: 'Expira el %{time}'
|
||||
sign_in_person: Firma en persona
|
||||
@@ -1929,6 +1976,7 @@ es: &es
|
||||
mobile: Móvil
|
||||
tablet: Tableta
|
||||
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
|
||||
last_3_months: Últimos 3 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:'
|
||||
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_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>'
|
||||
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."
|
||||
@@ -2202,15 +2251,19 @@ it: &it
|
||||
thanks: Grazie
|
||||
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_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:"
|
||||
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
|
||||
view_document: Visualizza documento
|
||||
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."
|
||||
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}'
|
||||
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_view_a_document: 'Sei stato invitato a visualizzare un documento'
|
||||
you_are_invited_to_sign_documents: 'Sei stato invitato a firmare dei documenti'
|
||||
your_document_copy: 'La tua copia del documento'
|
||||
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.
|
||||
|
||||
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,
|
||||
{account.name}
|
||||
submitter_completed_email_body: |
|
||||
@@ -2415,6 +2479,8 @@ it: &it
|
||||
invalid_timeserver: Server di timestamp non valido
|
||||
email_templates: Modelli email
|
||||
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_sms: SMS di richiesta di firma
|
||||
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_expired: Il modulo è scaduto.
|
||||
form_has_been_declined: Il modulo è stato rifiutato.
|
||||
form_is_view_only: Il modulo è di sola lettura.
|
||||
file_is_missing: File mancante
|
||||
folder_name_has_been_updated: Il nome della cartella è stato aggiornato.
|
||||
unable_to_rename_folder: Impossibile rinominare la cartella.
|
||||
@@ -2761,6 +2828,10 @@ it: &it
|
||||
signers: Firmatari
|
||||
not_invited_yet: Non ancora invitato
|
||||
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}'
|
||||
expire_on_time: 'Scade il %{time}'
|
||||
sign_in_person: Firma di persona
|
||||
@@ -2982,6 +3053,7 @@ it: &it
|
||||
mobile: Mobile
|
||||
tablet: Tablet
|
||||
reset_default: Reimposta predefinito
|
||||
smtp_settings_have_been_reset: Le impostazioni SMTP sono state ripristinate.
|
||||
send_signature_request_email: Invia email di richiesta firma
|
||||
last_3_months: Ultimi 3 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 :'
|
||||
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_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>
|
||||
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.
|
||||
@@ -3255,15 +3328,19 @@ fr: &fr
|
||||
thanks: Merci
|
||||
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_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 :'
|
||||
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
|
||||
view_document: Consulter le document
|
||||
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.
|
||||
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}'
|
||||
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_view_a_document: Vous êtes invité à consulter un document
|
||||
you_are_invited_to_sign_documents: Vous êtes invité à signer des documents
|
||||
your_document_copy: Votre copie de document
|
||||
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.
|
||||
|
||||
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,
|
||||
{account.name}
|
||||
submitter_completed_email_body: |
|
||||
@@ -3468,6 +3556,8 @@ fr: &fr
|
||||
invalid_timeserver: Serveur d’horodatage invalide
|
||||
email_templates: Modèles d’e‑mail
|
||||
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_sms: SMS de demande de signature
|
||||
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_expired: Le formulaire a expiré.
|
||||
form_has_been_declined: Le formulaire a été refusé.
|
||||
form_is_view_only: Le formulaire est en lecture seule.
|
||||
file_is_missing: Fichier manquant
|
||||
folder_name_has_been_updated: Le nom du dossier a été mis à jour.
|
||||
unable_to_rename_folder: Impossible de renommer le dossier.
|
||||
@@ -3814,6 +3905,10 @@ fr: &fr
|
||||
signers: Signataires
|
||||
not_invited_yet: Pas encore invité
|
||||
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}
|
||||
expire_on_time: Expire le %{time}
|
||||
sign_in_person: Signer en personne
|
||||
@@ -4031,6 +4126,7 @@ fr: &fr
|
||||
mobile: Mobile
|
||||
tablet: Tablette
|
||||
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
|
||||
last_month: Mois dernier
|
||||
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:'
|
||||
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_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>'
|
||||
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.
|
||||
@@ -4305,15 +4402,19 @@ pt: &pt
|
||||
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_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:'
|
||||
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
|
||||
view_document: Visualizar documento
|
||||
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.'
|
||||
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}'
|
||||
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_view_a_document: Você foi convidado a visualizar um documento
|
||||
you_are_invited_to_sign_documents: Você foi convidado a assinar documentos
|
||||
your_document_copy: Sua cópia do documento
|
||||
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.
|
||||
|
||||
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,
|
||||
{account.name}
|
||||
submitter_completed_email_body: |
|
||||
@@ -4518,6 +4630,8 @@ pt: &pt
|
||||
invalid_timeserver: Servidor de carimbo de tempo inválido
|
||||
email_templates: Modelos de e-mail
|
||||
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_sms: SMS de solicitação de assinatura
|
||||
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_expired: O formulário expirou.
|
||||
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
|
||||
folder_name_has_been_updated: O nome da pasta foi atualizado.
|
||||
unable_to_rename_folder: Não foi possível renomear a pasta.
|
||||
@@ -4864,6 +4979,10 @@ pt: &pt
|
||||
signers: Signatários
|
||||
not_invited_yet: Ainda não convidado
|
||||
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}'
|
||||
expire_on_time: 'Expira em %{time}'
|
||||
sign_in_person: Assinar pessoalmente
|
||||
@@ -5085,6 +5204,7 @@ pt: &pt
|
||||
mobile: Celular
|
||||
tablet: Tablet
|
||||
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
|
||||
last_3_months: Últimos 3 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:'
|
||||
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_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>
|
||||
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.
|
||||
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_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:'
|
||||
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"
|
||||
review_and_sign: Prüfen und unterschreiben
|
||||
view_document: Dokument ansehen
|
||||
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.'
|
||||
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}'
|
||||
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_view_a_document: Sie sind eingeladen, ein Dokument anzusehen
|
||||
you_are_invited_to_sign_documents: Sie sind eingeladen, Dokumente zu unterschreiben
|
||||
your_document_copy: Ihre Dokumentkopie
|
||||
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.
|
||||
|
||||
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,
|
||||
{account.name}
|
||||
submitter_completed_email_body: |
|
||||
@@ -5571,6 +5707,8 @@ de: &de
|
||||
invalid_timeserver: Ungültiger Zeitstempelserver
|
||||
email_templates: E-Mail-Vorlagen
|
||||
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_sms: SMS für Signaturanfrage
|
||||
verification_code_sms: SMS mit Verifizierungscode
|
||||
@@ -5833,6 +5971,7 @@ de: &de
|
||||
form_has_been_archived: Das Formular wurde archiviert.
|
||||
form_has_been_expired: Das Formular ist abgelaufen.
|
||||
form_has_been_declined: Das Formular wurde abgelehnt.
|
||||
form_is_view_only: Das Formular ist schreibgeschützt.
|
||||
file_is_missing: Datei fehlt
|
||||
folder_name_has_been_updated: Der Ordnername wurde aktualisiert.
|
||||
unable_to_rename_folder: Der Ordner konnte nicht umbenannt werden.
|
||||
@@ -5917,6 +6056,10 @@ de: &de
|
||||
signers: Unterzeichner
|
||||
not_invited_yet: Noch nicht eingeladen
|
||||
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}'
|
||||
expire_on_time: 'Läuft ab am %{time}'
|
||||
sign_in_person: Vor Ort unterschreiben
|
||||
@@ -6138,6 +6281,7 @@ de: &de
|
||||
mobile: Mobil
|
||||
tablet: Tablet
|
||||
reset_default: Standard zurücksetzen
|
||||
smtp_settings_have_been_reset: Die SMTP-Einstellungen wurden zurückgesetzt.
|
||||
send_signature_request_email: Signaturanfrage-E-Mail senden
|
||||
last_3_months: Letzte 3 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:'
|
||||
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_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>
|
||||
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
|
||||
: 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_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:'
|
||||
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
|
||||
review_and_sign: Bekijken en ondertekenen
|
||||
view_document: Document bekijken
|
||||
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.
|
||||
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}'
|
||||
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_view_a_document: U bent uitgenodigd om een document te bekijken
|
||||
you_are_invited_to_sign_documents: U bent uitgenodigd om documenten te ondertekenen
|
||||
your_document_copy: Uw documentkopie
|
||||
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.
|
||||
|
||||
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,
|
||||
{account.name}
|
||||
submitter_completed_email_body: |
|
||||
@@ -7029,6 +7189,8 @@ nl: &nl
|
||||
invalid_timeserver: Ongeldige tijdserver
|
||||
email_templates: E-mailsjablonen
|
||||
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_sms: SMS voor handtekeningverzoek
|
||||
verification_code_sms: Verificatiecode-SMS
|
||||
@@ -7291,6 +7453,7 @@ nl: &nl
|
||||
form_has_been_archived: Formulier is gearchiveerd.
|
||||
form_has_been_expired: Formulier is verlopen.
|
||||
form_has_been_declined: Formulier is geweigerd.
|
||||
form_is_view_only: Formulier is alleen-lezen.
|
||||
file_is_missing: Bestand ontbreekt
|
||||
folder_name_has_been_updated: Mapnaam is bijgewerkt.
|
||||
unable_to_rename_folder: Kan map niet hernoemen.
|
||||
@@ -7375,6 +7538,10 @@ nl: &nl
|
||||
signers: Ondertekenaars
|
||||
not_invited_yet: Nog niet uitgenodigd
|
||||
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}
|
||||
expire_on_time: Verloopt op %{time}
|
||||
sign_in_person: In persoon ondertekenen
|
||||
@@ -7592,6 +7759,7 @@ nl: &nl
|
||||
mobile: Mobiel
|
||||
tablet: Tablet
|
||||
reset_default: Standaard herstellen
|
||||
smtp_settings_have_been_reset: De SMTP-instellingen zijn hersteld.
|
||||
send_signature_request_email: E-mail met handtekeningaanvraag verzenden
|
||||
last_3_months: Afgelopen 3 maanden
|
||||
last_6_months: Afgelopen 6 maanden
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ Rails.application.routes.draw do
|
||||
resources :submitter_email_clicks, only: %i[create]
|
||||
resources :submitter_form_views, only: %i[create]
|
||||
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'
|
||||
collection do
|
||||
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'
|
||||
resource :reveal_access_token, only: %i[show create], controller: 'reveal_access_token'
|
||||
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 :notifications, only: %i[index create], controller: 'notifications_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
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# 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
|
||||
enable_extension "btree_gin"
|
||||
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"
|
||||
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|
|
||||
t.bigint "account_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|
|
||||
t.bigint "account_id", null: false
|
||||
t.datetime "archived_at"
|
||||
t.datetime "completed_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "created_by_user_id"
|
||||
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.text "variables"
|
||||
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_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_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 ["slug"], name: "index_submissions_on_slug", unique: true
|
||||
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_documents", "templates"
|
||||
add_foreign_key "email_events", "accounts"
|
||||
add_foreign_key "email_message_assets", "accounts"
|
||||
add_foreign_key "email_messages", "accounts"
|
||||
add_foreign_key "email_messages", "users", column: "author_id"
|
||||
add_foreign_key "encrypted_configs", "accounts"
|
||||
|
||||
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
+390
-148
File diff suppressed because it is too large
Load Diff
@@ -152,9 +152,16 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
@@ -196,6 +203,7 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -324,6 +332,7 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -439,13 +448,19 @@ const token = jwt.sign({
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -453,6 +468,12 @@ const token = jwt.sign({
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -464,6 +485,12 @@ const token = jwt.sign({
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -251,6 +252,7 @@
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -339,9 +341,16 @@
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
@@ -403,13 +412,19 @@
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -417,6 +432,12 @@
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -428,6 +449,12 @@
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
|
||||
@@ -143,9 +143,16 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
@@ -187,6 +194,7 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -315,6 +323,7 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -430,13 +439,19 @@ const token = jwt.sign({
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -444,6 +459,12 @@ const token = jwt.sign({
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -455,6 +476,12 @@ const token = jwt.sign({
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
|
||||
@@ -164,9 +164,16 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
@@ -208,6 +215,7 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -336,6 +344,7 @@ const token = jwt.sign({
|
||||
"payment",
|
||||
"phone",
|
||||
"verification",
|
||||
"kba",
|
||||
"strikethrough"
|
||||
]
|
||||
},
|
||||
@@ -445,13 +454,19 @@ const token = jwt.sign({
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -459,6 +474,12 @@ const token = jwt.sign({
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -470,6 +491,12 @@ const token = jwt.sign({
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
|
||||
@@ -51,7 +51,7 @@ export class AppComponent {}
|
||||
"token": {
|
||||
"type": "string",
|
||||
"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,
|
||||
"properties": {
|
||||
"slug": {
|
||||
@@ -109,7 +109,7 @@ export class AppComponent {}
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
"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": {
|
||||
"type": "object",
|
||||
@@ -127,7 +127,7 @@ export class AppComponent {}
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -232,6 +232,12 @@ export class AppComponent {}
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -241,7 +247,7 @@ export class AppComponent {}
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -311,7 +317,7 @@ export class AppComponent {}
|
||||
"onComplete": {
|
||||
"type": "event emitter",
|
||||
"required": false,
|
||||
"description": "Event emitted the form completion.",
|
||||
"description": "Event emitted on form completion.",
|
||||
"example": "handleComplete($event)"
|
||||
},
|
||||
"onDecline": {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"data-token": {
|
||||
"type": "string",
|
||||
"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,
|
||||
"properties": {
|
||||
"slug": {
|
||||
@@ -105,7 +105,7 @@
|
||||
"data-language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
"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": {
|
||||
"type": "string",
|
||||
@@ -153,7 +153,7 @@
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -179,6 +179,12 @@
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -194,7 +200,7 @@
|
||||
"data-signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -48,7 +48,7 @@ export function App() {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"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,
|
||||
"properties": {
|
||||
"slug": {
|
||||
@@ -106,7 +106,7 @@ export function App() {
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
"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": {
|
||||
"type": "object",
|
||||
@@ -124,7 +124,7 @@ export function App() {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -229,6 +229,12 @@ export function App() {
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -244,7 +250,7 @@ export function App() {
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"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,
|
||||
"properties": {
|
||||
"slug": {
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
"language": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "UI language: en, es, it, de, fr, nl, pl, uk, cs, pt, he, ar, kr, ja languages are available. Be default the form is displayed in the user browser language automatically."
|
||||
"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": {
|
||||
"type": "object",
|
||||
@@ -145,7 +145,7 @@ export default {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true,
|
||||
"description": "Set `false` to hide field name. Hidding field names can be useful for when they are not in the human readable format. Field names are displayed by default."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
@@ -195,6 +195,12 @@ export default {
|
||||
"default": false,
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -204,7 +210,7 @@ export default {
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Allows pre-filling signature fields. The value can be a base64 encoded image string, a public URL to an image, or plain text that will be rendered as a typed signature using a standard font."
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
|
||||
+2212
-519
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# 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.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": [
|
||||
"form.viewed",
|
||||
"form.started",
|
||||
"form.completed"
|
||||
"form.completed",
|
||||
"form.declined"
|
||||
]
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "The event timestamp.",
|
||||
"example": "2023-09-24T11:20:42Z",
|
||||
"examples": [
|
||||
"2023-09-24T11:20:42Z"
|
||||
],
|
||||
"format": "date-time"
|
||||
},
|
||||
"data": {
|
||||
@@ -36,20 +39,20 @@ During the form filling and signing process, 3 types of events may occur and are
|
||||
"type": "number",
|
||||
"description": "The submitter's unique identifier."
|
||||
},
|
||||
"submission_id": {
|
||||
"type": "number",
|
||||
"description": "The unique submission identifier."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The submitter's email address",
|
||||
"format": "email",
|
||||
"example": "john.doe@example.com"
|
||||
"examples": [
|
||||
"john.doe@example.com"
|
||||
]
|
||||
},
|
||||
"ua": {
|
||||
"type": "string",
|
||||
"description": "The user agent string that provides information about the submitter's web browser.",
|
||||
"example": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||
"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": {
|
||||
"type": "string",
|
||||
@@ -62,27 +65,28 @@ During the form filling and signing process, 3 types of events may occur and are
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"description": "The submitter's phone number, formatted according to the E.164 standard.",
|
||||
"example": "+1234567890"
|
||||
"examples": [
|
||||
"+1234567890"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "The submitter's role name or title.",
|
||||
"example": "First Party"
|
||||
"examples": [
|
||||
"First Party"
|
||||
]
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify submitter within your app."
|
||||
},
|
||||
"application_key": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify submitter within your app. Backward compatibility with the previous version of the API. Use external_id instead."
|
||||
},
|
||||
"decline_reason": {
|
||||
"type": "string",
|
||||
"description": "Submitter provided decline message."
|
||||
},
|
||||
"sent_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the signing request was sent to the submitter.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"status": {
|
||||
@@ -98,22 +102,27 @@ During the form filling and signing process, 3 types of events may occur and are
|
||||
},
|
||||
"opened_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter opened the signing form.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter completed the signing form.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"declined_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter declined the signing form.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter was created.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"description": "The date and time when the submitter was last updated.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"submission": {
|
||||
@@ -189,6 +198,7 @@ During the form filling and signing process, 3 types of events may occur and are
|
||||
},
|
||||
"preferences": {
|
||||
"type": "object",
|
||||
"description": "Submitter preferences for notifications.",
|
||||
"properties": {
|
||||
"send_email": {
|
||||
"type": "boolean",
|
||||
@@ -210,7 +220,7 @@ During the form filling and signing process, 3 types of events may occur and are
|
||||
"type": "string",
|
||||
"description": "The field name."
|
||||
},
|
||||
"values": {
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The field value."
|
||||
}
|
||||
@@ -231,6 +241,7 @@ During the form filling and signing process, 3 types of events may occur and are
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"description": "List of completed documents signed by the submitter.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -16,13 +16,17 @@ Get submission creation, completion, expiration, and archiving notifications usi
|
||||
"description": "The event type.",
|
||||
"enum": [
|
||||
"submission.created",
|
||||
"submission.completed",
|
||||
"submission.expired",
|
||||
"submission.archived"
|
||||
]
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "The event timestamp.",
|
||||
"example": "2023-09-24T11:20:42Z",
|
||||
"examples": [
|
||||
"2023-09-24T11:20:42Z"
|
||||
],
|
||||
"format": "date-time"
|
||||
},
|
||||
"data": {
|
||||
@@ -33,8 +37,26 @@ Get submission creation, completion, expiration, and archiving notifications usi
|
||||
"type": "number",
|
||||
"description": "The submission's unique identifier."
|
||||
},
|
||||
"archived_at": {
|
||||
"name": {
|
||||
"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."
|
||||
},
|
||||
"created_at": {
|
||||
@@ -65,9 +87,19 @@ Get submission creation, completion, expiration, and archiving notifications usi
|
||||
]
|
||||
},
|
||||
"audit_log_url": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Audit log file URL."
|
||||
},
|
||||
"combined_document_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Combined PDF file URL with documents and Audit Log."
|
||||
},
|
||||
"submitters": {
|
||||
"type": "array",
|
||||
"description": "The list of submitters for the submission.",
|
||||
@@ -90,26 +122,40 @@ Get submission creation, completion, expiration, and archiving notifications usi
|
||||
"type": "string",
|
||||
"description": "The email address of the submitter.",
|
||||
"format": "email",
|
||||
"example": "john.doe@example.com"
|
||||
"examples": [
|
||||
"john.doe@example.com"
|
||||
]
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "The unique slug of the document template."
|
||||
},
|
||||
"sent_at": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The date and time when the signing request was sent to the submitter."
|
||||
},
|
||||
"opened_at": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The date and time when the submitter opened the signing form."
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The date and time when the submitter completed the signing form."
|
||||
},
|
||||
"declined_at": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The date and time when the submitter declined the signing form."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The name of the submitter."
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The phone number of the submitter, formatted according to the E.164 standard.",
|
||||
"example": "+1234567890"
|
||||
"examples": [
|
||||
"+1234567890"
|
||||
]
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "The role name or title of the submitter.",
|
||||
"example": "First Party"
|
||||
"examples": [
|
||||
"First Party"
|
||||
]
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Your application-specific unique string key to identify this submitter within your app."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Metadata object with additional submitter information.",
|
||||
"example": "{ 'customField': 'value' }"
|
||||
"examples": [
|
||||
"{ 'customField': 'value' }"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
@@ -154,10 +215,6 @@ Get submission creation, completion, expiration, and archiving notifications usi
|
||||
"awaiting"
|
||||
]
|
||||
},
|
||||
"application_key": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify this submitter within your app."
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"description": "An object with pre-filled values for the submission. Use field names for keys of the object. For more configurations see `fields` param."
|
||||
@@ -222,6 +279,7 @@ Get submission creation, completion, expiration, and archiving notifications usi
|
||||
},
|
||||
"created_by_user": {
|
||||
"type": "object",
|
||||
"description": "User who created the submission.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
@@ -243,6 +301,7 @@ Get submission creation, completion, expiration, and archiving notifications usi
|
||||
},
|
||||
"submission_events": {
|
||||
"type": "array",
|
||||
"description": "List of submission events.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
Get template creation and update notifications using these events:
|
||||
|
||||
- **'template.created'** is triggered when the template is created.
|
||||
- **'tempate.updated'** is triggered when the template is updated.
|
||||
- **'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.",
|
||||
"enum": [
|
||||
"template.created",
|
||||
"template.updated"
|
||||
"template.updated",
|
||||
"template.archived"
|
||||
]
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "The event timestamp.",
|
||||
"example": "2023-09-24T11:20:42Z",
|
||||
"examples": [
|
||||
"2023-09-24T11:20:42Z"
|
||||
],
|
||||
"format": "date-time"
|
||||
},
|
||||
"data": {
|
||||
@@ -74,6 +78,31 @@ Get template creation and update notifications using these events:
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "boolean",
|
||||
"description": "The flag indicating whether the field is required."
|
||||
@@ -120,6 +149,7 @@ Get template creation and update notifications using these events:
|
||||
},
|
||||
"submitters": {
|
||||
"type": "array",
|
||||
"description": "List of submitter roles defined in the template.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -138,12 +168,11 @@ Get template creation and update notifications using these events:
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the author of the template."
|
||||
},
|
||||
"account_id": {
|
||||
"type": "integer",
|
||||
"description": "Unique identifier of the account of the template."
|
||||
},
|
||||
"archived_at": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Date and time when the template was archived."
|
||||
},
|
||||
"created_at": {
|
||||
@@ -164,7 +193,10 @@ Get template creation and update notifications using these events:
|
||||
]
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Identifier of the template in the external system."
|
||||
},
|
||||
"folder_id": {
|
||||
@@ -175,12 +207,17 @@ Get template creation and update notifications using these events:
|
||||
"type": "string",
|
||||
"description": "Folder name where the template is placed."
|
||||
},
|
||||
"application_key": {
|
||||
"type": "string",
|
||||
"description": "Your application-specific unique string key to identify tempate_id within your app."
|
||||
"preferences": {
|
||||
"type": "object",
|
||||
"description": "Template preferences object."
|
||||
},
|
||||
"shared_link": {
|
||||
"type": "boolean",
|
||||
"description": "Flag indicating whether the shared link is enabled for the template."
|
||||
},
|
||||
"author": {
|
||||
"type": "object",
|
||||
"description": "Author of the template.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user