mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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)
|
||||
|
||||
@@ -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,7 +2,7 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -13,6 +13,8 @@ module Api
|
||||
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,16 @@ module Api
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
submissions.each do |submission|
|
||||
if submission.submitters.all?(&:completed_at?) && Submissions.maybe_update_completed_at(submission)
|
||||
last_submitter = submission.submitters.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
|
||||
|
||||
@@ -123,6 +117,22 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
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?
|
||||
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])..)
|
||||
|
||||
@@ -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 }]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ 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?
|
||||
|
||||
WebhookUrls.enqueue_events(submission, 'submission.expired')
|
||||
end
|
||||
|
||||
@@ -5,30 +5,38 @@ 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
|
||||
!submission.submitters.exists?(completed_at: nil) &&
|
||||
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
|
||||
if !submission.completed_at && submission.submitters_order_preserved? && params['send_invitation_email'] != false &&
|
||||
Submission.exists?(id: submission.id, completed_at: nil)
|
||||
enqueue_next_submitter_request_notification(submitter)
|
||||
end
|
||||
|
||||
enqueue_completed_webhooks(submitter, is_all_completed:)
|
||||
enqueue_completed_webhooks(submitter, is_last:)
|
||||
end
|
||||
|
||||
def create_completed_submitter!(submitter)
|
||||
@@ -77,7 +85,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 +97,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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
|
||||
@@ -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,7 +26,9 @@
|
||||
#
|
||||
# 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_by_user_id (created_by_user_id)
|
||||
@@ -89,26 +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)
|
||||
where(expire_at: nil).or(where(expire_at: Time.current..)).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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<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 %>
|
||||
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_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) %>
|
||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||
@@ -91,7 +91,7 @@
|
||||
</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/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_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 %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -86,6 +86,7 @@ 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.
|
||||
@@ -1142,6 +1143,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.
|
||||
@@ -2195,6 +2197,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."
|
||||
@@ -3248,6 +3251,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.
|
||||
@@ -4298,6 +4302,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.
|
||||
@@ -5351,6 +5356,7 @@ 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.
|
||||
@@ -6808,6 +6814,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
+14
-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_01_165617) 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,7 +374,9 @@ 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_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
||||
@@ -579,6 +591,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",
|
||||
|
||||
+59
-2
@@ -1,13 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module EmailMessages
|
||||
MIN_BODY_SIZE = 2.kilobytes
|
||||
MIN_ASSET_SIZE = 256.bytes
|
||||
STYLE_REGEXP = %r{<style[^>]*>.*?</style>(?:\s*<style[^>]*>.*?</style>)*}mi
|
||||
BASE64_REGEXP = %r{(data:[^,]*;base64,)([A-Za-z0-9+/=]+)}
|
||||
ASSET_REGEXP = Regexp.union(STYLE_REGEXP, BASE64_REGEXP)
|
||||
ASSET_PREFIX = '[[asset:'
|
||||
PLACEHOLDER_REGEXP = /\[\[asset:(\h{40})\]\]/
|
||||
|
||||
module_function
|
||||
|
||||
def find_or_create_for_account_user(account, user, subject, body)
|
||||
subject = I18n.t(:you_are_invited_to_sign_a_document) if subject.blank?
|
||||
|
||||
message = account.email_messages.new(author: user, subject:, body:).tap(&:validate)
|
||||
body, assets = maybe_extract_assets(account, body)
|
||||
|
||||
account.email_messages.find_by(sha1: message.sha1) || message.tap { |m| m.save!(validate: false) }
|
||||
new_message = account.email_messages.new(author: user, subject:, body:).tap(&:validate)
|
||||
|
||||
message = account.email_messages.find_by(sha1: new_message.sha1)
|
||||
|
||||
message ||= new_message.tap do |m|
|
||||
m.save!(validate: false)
|
||||
|
||||
save_new_assets!(account, assets)
|
||||
end
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
def save_new_assets!(account, assets)
|
||||
return if assets.blank?
|
||||
|
||||
existing_assets_sha1 = account.email_message_assets.where(sha1: assets.map(&:sha1)).pluck(:sha1)
|
||||
|
||||
assets.each do |asset|
|
||||
asset.save!(validate: false) if existing_assets_sha1.exclude?(asset.sha1)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_extract_assets(account, body)
|
||||
return [body, []] if body.blank? || body.bytesize < MIN_BODY_SIZE
|
||||
|
||||
assets_index = {}
|
||||
|
||||
result = body.gsub(ASSET_REGEXP) do
|
||||
match = Regexp.last_match
|
||||
prefix, data = match[1] ? [match[1], match[2]] : ['', match[0]]
|
||||
|
||||
next match[0] if data.blank? || data.bytesize < MIN_ASSET_SIZE
|
||||
|
||||
asset = account.email_message_assets.new(data:).tap(&:validate)
|
||||
assets_index[asset.sha1] = asset
|
||||
|
||||
"#{prefix}#{ASSET_PREFIX}#{asset.sha1}]]"
|
||||
end
|
||||
|
||||
[result, assets_index.values]
|
||||
end
|
||||
|
||||
def rebuild_body_with_assets(account_id, body)
|
||||
shas = body.scan(PLACEHOLDER_REGEXP).flatten.uniq
|
||||
data = EmailMessageAsset.where(account_id:, sha1: shas).pluck(:sha1, :data).to_h
|
||||
|
||||
body.gsub(PLACEHOLDER_REGEXP) { data[Regexp.last_match(1)] || Regexp.last_match(0) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,15 +116,6 @@ module Mcp
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
submissions.each do |submission|
|
||||
submission.submitters.each do |submitter|
|
||||
next unless submitter.completed_at?
|
||||
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id,
|
||||
'send_invitation_email' => false)
|
||||
end
|
||||
end
|
||||
|
||||
SearchEntries.enqueue_reindex(submissions)
|
||||
|
||||
submission = submissions.first
|
||||
|
||||
+25
-6
@@ -5,6 +5,22 @@ module Submissions
|
||||
|
||||
module_function
|
||||
|
||||
def maybe_update_completed_at(submission)
|
||||
incomplete_submitter = Submitter.where(submission_id: submission.id, completed_at: nil).select(1)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
Submission.where(id: submission.id, completed_at: nil)
|
||||
.where.not(incomplete_submitter.arel.exists)
|
||||
.update_all(completed_at: max_completed_at)
|
||||
.positive?
|
||||
end
|
||||
|
||||
def search(current_user, submissions, keyword, search_values: false, search_template: false)
|
||||
if Docuseal.fulltext_search?
|
||||
fulltext_search(current_user, submissions, keyword, search_template:)
|
||||
@@ -21,19 +37,22 @@ module Submissions
|
||||
|
||||
arel_table = Submitter.arel_table
|
||||
|
||||
arel = arel_table[:email].lower.matches(term)
|
||||
.or(arel_table[:phone].matches(term))
|
||||
.or(arel_table[:name].lower.matches(term))
|
||||
submitter_arel = arel_table[:email].lower.matches(term)
|
||||
.or(arel_table[:phone].matches(term))
|
||||
.or(arel_table[:name].lower.matches(term))
|
||||
|
||||
arel = arel.or(Arel::Table.new(:submitters)[:values].matches(term)) if search_values
|
||||
submitter_arel = submitter_arel.or(arel_table[:values].matches(term)) if search_values
|
||||
|
||||
arel = Submitter.where(arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||
.where(submitter_arel).select(1).arel.exists
|
||||
|
||||
if search_template
|
||||
submissions = submissions.left_joins(:template)
|
||||
|
||||
arel = arel.or(Template.arel_table[:name].lower.matches("%#{sanitized}%"))
|
||||
arel = arel.or(Template.arel_table[:name].lower.matches(term))
|
||||
end
|
||||
|
||||
submissions.joins(:submitters).where(arel).group(:id)
|
||||
submissions.where(arel)
|
||||
end
|
||||
|
||||
def fulltext_search(current_user, submissions, keyword, search_template: false)
|
||||
|
||||
@@ -127,14 +127,18 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
submission.template_fields = template.fields.deep_dup
|
||||
submission.template_fields = template.fields.deep_dup.filter_map do |field|
|
||||
next field if field['areas'].blank?
|
||||
|
||||
submission.template_fields.each do |field|
|
||||
field['areas'].to_a.each do |area|
|
||||
field['areas'] = field['areas'].filter_map do |area|
|
||||
dynamic_area = areas_index[area['uuid']]
|
||||
|
||||
area.merge!(dynamic_area) if dynamic_area
|
||||
next area.merge(dynamic_area) if dynamic_area
|
||||
|
||||
area if area.key?('page')
|
||||
end
|
||||
|
||||
field if field['areas'].present?
|
||||
end
|
||||
|
||||
submission
|
||||
|
||||
@@ -15,7 +15,7 @@ module Submissions
|
||||
def call(submission)
|
||||
return nil unless submission
|
||||
|
||||
raise NotCompletedYet unless submission.submitters.all?(&:completed_at?)
|
||||
raise NotCompletedYet unless submission.completed_at?
|
||||
|
||||
total_wait_time ||= 0
|
||||
key = [KEY_PREFIX, submission.id].join(':')
|
||||
|
||||
+15
-26
@@ -40,7 +40,6 @@ module Submissions
|
||||
submissions.where(created_by_user_id: user&.id || -1)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def filter_by_status(submissions, filters)
|
||||
case filters[:status]
|
||||
when 'pending'
|
||||
@@ -52,33 +51,24 @@ module Submissions
|
||||
when 'expired'
|
||||
submissions.expired
|
||||
when 'sent'
|
||||
submissions.joins(:submitters)
|
||||
.where(submitters: { opened_at: nil, completed_at: nil, declined_at: nil })
|
||||
.where.not(submitters: { sent_at: nil })
|
||||
.group(:id)
|
||||
submissions.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||
.where(opened_at: nil, completed_at: nil, declined_at: nil)
|
||||
.where.not(sent_at: nil)
|
||||
.limit(1).arel.exists)
|
||||
when 'opened'
|
||||
submissions.joins(:submitters)
|
||||
.where(submitters: { completed_at: nil, declined_at: nil })
|
||||
.where.not(submitters: { opened_at: nil })
|
||||
.group(:id)
|
||||
submissions.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||
.where(completed_at: nil, declined_at: nil)
|
||||
.where.not(opened_at: nil)
|
||||
.limit(1).arel.exists)
|
||||
when 'partially_completed'
|
||||
submissions.joins(:submitters)
|
||||
.group(:id)
|
||||
.having(Arel::Nodes::NamedFunction.new(
|
||||
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||
[Submitter.arel_table[:completed_at].eq(nil),
|
||||
Arel::Nodes.build_quoted(false)])]
|
||||
).gt(0))
|
||||
.having(Arel::Nodes::NamedFunction.new(
|
||||
'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF',
|
||||
[Submitter.arel_table[:completed_at].not_eq(nil),
|
||||
Arel::Nodes.build_quoted(false)])]
|
||||
).gt(0))
|
||||
submissions.where(completed_at: nil)
|
||||
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||
.where.not(completed_at: nil)
|
||||
.limit(1).arel.exists)
|
||||
else
|
||||
submissions
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def filter_by_created_at(submissions, filters)
|
||||
submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present?
|
||||
@@ -104,16 +94,15 @@ module Submissions
|
||||
def filter_by_completed_at(submissions, filters)
|
||||
return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present?
|
||||
|
||||
completed_arel = Submitter.arel_table[:completed_at].maximum
|
||||
submissions = submissions.completed.joins(:submitters).group(:id)
|
||||
submissions = submissions.completed
|
||||
|
||||
if filters[:completed_at_from].present?
|
||||
submissions = submissions.having(completed_arel.gteq(filters[:completed_at_from]))
|
||||
submissions = submissions.where(completed_at: filters[:completed_at_from]..)
|
||||
end
|
||||
|
||||
return submissions if filters[:completed_at_to].blank?
|
||||
|
||||
submissions.having(completed_arel.lteq(filters[:completed_at_to].end_of_day))
|
||||
submissions.where(completed_at: ..filters[:completed_at_to].end_of_day)
|
||||
end
|
||||
|
||||
def normalize_filter_params(params, current_user)
|
||||
|
||||
@@ -37,8 +37,8 @@ module Submissions
|
||||
json['fields'] = submission.template_fields || submission.template&.fields
|
||||
end
|
||||
|
||||
if submitters.all?(&:completed_at?)
|
||||
last_submitter = submitters.max_by(&:completed_at)
|
||||
if submission.completed_at?
|
||||
last_submitter = submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||
|
||||
if with_documents
|
||||
json['documents'] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||
@@ -49,7 +49,7 @@ module Submissions
|
||||
json['combined_document_url'] ||= maybe_build_combined_url(submitters, submission, params, expires_at:)
|
||||
|
||||
json['status'] = 'completed'
|
||||
json['completed_at'] = last_submitter.completed_at.as_json
|
||||
json['completed_at'] = submission.completed_at.as_json
|
||||
else
|
||||
json['documents'] = [] if with_documents
|
||||
json['audit_log_url'] = nil
|
||||
@@ -73,12 +73,12 @@ module Submissions
|
||||
end
|
||||
|
||||
def maybe_build_combined_url(submitters, submission, params, expires_at: nil)
|
||||
return unless submitters.all?(&:completed_at?)
|
||||
return unless submission.completed_at?
|
||||
|
||||
attachment = submission.combined_document_attachment
|
||||
|
||||
if !attachment && params[:include].to_s.include?('combined_document_url')
|
||||
submitter = submitters.max_by(&:completed_at)
|
||||
submitter = submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||
|
||||
attachment = Submissions::EnsureCombinedGenerated.call(submitter)
|
||||
end
|
||||
|
||||
+4
-4
@@ -84,7 +84,7 @@ module Submitters
|
||||
submitter_ids = SearchEntry.where(record_type: 'Submitter')
|
||||
.where(account_id: current_user.account_id)
|
||||
.where(*query)
|
||||
.limit(500)
|
||||
.limit(keyword.length > 2 ? 500 : 5000)
|
||||
.pluck(:record_id)
|
||||
|
||||
submitters.where(id: submitter_ids.first(100))
|
||||
@@ -108,7 +108,7 @@ module Submitters
|
||||
if AccountConfig.exists?(account_id: submitter.submission.account_id,
|
||||
key: AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||
value: true) &&
|
||||
submitter.submission.submitters.all?(&:completed_at?) &&
|
||||
submitter.submission.completed_at? &&
|
||||
submitter.submission.template_fields.none? { |f| f['type'] == 'verification' }
|
||||
return [submitter.submission.combined_document_attachment || Submissions::EnsureCombinedGenerated.call(submitter)]
|
||||
end
|
||||
@@ -207,7 +207,7 @@ module Submitters
|
||||
|
||||
filename = filename.gsub('{document.name}', blob.filename.base)
|
||||
filename = filename.gsub(' - {submission.status}') do
|
||||
if submitter.submission.submitters.all?(&:completed_at?)
|
||||
if submitter.submission.completed_at?
|
||||
status =
|
||||
if submitter.submission.template_fields.any? { |f| f['type'] == 'signature' }
|
||||
I18n.t(:signed)
|
||||
@@ -264,7 +264,7 @@ module Submitters
|
||||
end
|
||||
|
||||
def build_combined_url(submitter, ttl: FILES_TTL)
|
||||
return if submitter.submission.submitters.exists?(completed_at: nil)
|
||||
return unless submitter.submission.completed_at?
|
||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||
|
||||
attachment = submitter.submission.combined_document_attachment
|
||||
|
||||
@@ -93,11 +93,9 @@ module Submitters
|
||||
end
|
||||
|
||||
def build_submission_status(submission)
|
||||
submitters = submission.submitters
|
||||
|
||||
if submitters.all?(&:completed_at?)
|
||||
if submission.completed_at?
|
||||
'completed'
|
||||
elsif submitters.any?(&:declined_at?)
|
||||
elsif submission.submitters.any?(&:declined_at?)
|
||||
'declined'
|
||||
else
|
||||
submission.expired? ? 'expired' : 'pending'
|
||||
|
||||
@@ -32,7 +32,11 @@ module Submitters
|
||||
|
||||
submitter.submission.save!
|
||||
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
|
||||
if submitter.completed_at?
|
||||
is_last = Submissions.maybe_update_completed_at(submitter.submission)
|
||||
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'is_last' => is_last)
|
||||
end
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
@@ -10,6 +10,8 @@ RSpec.describe ProcessSubmitterCompletionJob do
|
||||
before do
|
||||
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||
|
||||
Submissions.maybe_update_completed_at(submitter.submission)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
|
||||
@@ -26,6 +26,7 @@ RSpec.describe 'Submission Preview' do
|
||||
create(:encrypted_config, account:, key: EncryptedConfig::EMAIL_SMTP_KEY, value: '{}')
|
||||
|
||||
submission.submitters.each { |s| s.update(completed_at: 1.day.ago) }
|
||||
Submissions.maybe_update_completed_at(submission)
|
||||
|
||||
visit submissions_preview_path(slug: submission.slug)
|
||||
end
|
||||
@@ -47,6 +48,7 @@ RSpec.describe 'Submission Preview' do
|
||||
|
||||
it "doesn't display the email form if SMTP is not configured" do
|
||||
submission.submitters.each { |s| s.update(completed_at: 1.day.ago) }
|
||||
Submissions.maybe_update_completed_at(submission)
|
||||
|
||||
visit submissions_preview_path(slug: submission.slug)
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe 'Submission' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account:) }
|
||||
let(:template) { create(:template, account:, author: user) }
|
||||
let(:submission) do
|
||||
create(:submission, :with_submitters, template:, created_by_user: user,
|
||||
archived_at: Time.current, completed_at: Time.current)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
submission.submitters.each { |s| s.update!(completed_at: 1.day.ago) }
|
||||
end
|
||||
|
||||
it 'unarchives a completed submission from the download dropdown' do
|
||||
visit submission_path(submission)
|
||||
|
||||
find('label[aria-label="Download"]').click
|
||||
click_button 'Unarchive'
|
||||
|
||||
expect(page).to have_content('Submission has been unarchived.')
|
||||
expect(submission.reload.archived_at).to be_nil
|
||||
end
|
||||
end
|
||||
@@ -207,6 +207,8 @@ RSpec.describe 'Template' do
|
||||
submitter.update!(completed_at: rand(2..5).days.ago)
|
||||
end
|
||||
|
||||
(last_week_submissions + this_week_submissions).each { |s| Submissions.maybe_update_completed_at(s) }
|
||||
|
||||
visit template_path(template)
|
||||
|
||||
(last_week_submissions + this_week_submissions).map(&:submitters).flatten.last(10).uniq.each do |submitter|
|
||||
|
||||
Reference in New Issue
Block a user