mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9011e3f4b4 | ||
|
|
0b45ce5c0c | ||
|
|
a8ab93a4cc | ||
|
|
d1768be636 | ||
|
|
b2cd7c82a7 | ||
|
|
b88d99afe0 | ||
|
|
4523d38551 | ||
|
|
c4ba892559 | ||
|
|
40f8093e00 | ||
|
|
4375459856 | ||
|
|
b3eb8a70bf | ||
|
|
6245a487b6 | ||
|
|
a89f4c1b10 | ||
|
|
7df729b8b5 | ||
|
|
f1db7c7e82 | ||
|
|
00af69cbf2 | ||
|
|
c6819a19bd | ||
|
|
f5cbcb3d24 | ||
|
|
6ca75c53ef | ||
|
|
e0ec443e10 | ||
|
|
9eed2fc2ef | ||
|
|
b9c6d3ff9f | ||
|
|
03c25a89b2 | ||
|
|
f5b4a0c5ab | ||
|
|
b10ed46ccc | ||
|
|
c55036a1b1 | ||
|
|
962a5dade5 | ||
|
|
369a09a6df | ||
|
|
e9a39e2571 | ||
|
|
1cf9cc18f1 | ||
|
|
1984d4ec8d | ||
|
|
fa99b4ebdc | ||
|
|
83c793a2a5 | ||
|
|
d078727070 | ||
|
|
0d8e2c5ff0 | ||
|
|
ea062d7494 | ||
|
|
d525e0597a | ||
|
|
964b5d4e74 | ||
|
|
abf17eb09f | ||
|
|
178809dad7 | ||
|
|
881d189715 | ||
|
|
48a8973910 |
No files matched your search
@@ -48,6 +48,9 @@ Style/MultipleComparison:
|
|||||||
Style/NumericPredicate:
|
Style/NumericPredicate:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MinMaxComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Naming/PredicateMethod:
|
Naming/PredicateMethod:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -9,7 +9,7 @@ RUN apk --no-cache add fontforge wget && \
|
|||||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.1.0/model_704_int8.onnx" && \
|
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.1.0/model_704_int8.onnx" && \
|
||||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||||
@@ -53,6 +53,8 @@ WORKDIR /app
|
|||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
|
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
[provider_sect]\n\
|
[provider_sect]\n\
|
||||||
@@ -92,7 +94,10 @@ COPY --from=webpack /app/public/packs ./public/packs
|
|||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
|
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
|
||||||
|
|
||||||
|
RUN chown -R docuseal:docuseal /app
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
|
ENV HOME=/home/docuseal
|
||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
+12
-9
@@ -77,24 +77,27 @@ GEM
|
|||||||
annotaterb (4.14.0)
|
annotaterb (4.14.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1027.0)
|
aws-partitions (1.1197.0)
|
||||||
aws-sdk-core (3.214.0)
|
aws-sdk-core (3.240.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.96.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-kms (1.118.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.239.1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.176.1)
|
aws-sdk-s3 (1.208.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.234.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-secretsmanager (1.110.0)
|
aws-sdk-secretsmanager (1.110.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.10.1)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-storage-blob (2.0.3)
|
||||||
azure-storage-common (~> 2.0)
|
azure-storage-common (~> 2.0)
|
||||||
@@ -584,7 +587,7 @@ GEM
|
|||||||
unicode-emoji (~> 4.1)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.1.0)
|
unicode-emoji (4.1.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
uri (1.0.3)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
|
|||||||
@@ -5,34 +5,17 @@ module Api
|
|||||||
load_and_authorize_resource :submission
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
is_merge = params[:merge] == 'true' &&
|
||||||
|
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||||
|
|
||||||
documents =
|
documents =
|
||||||
if @submission.submitters.all?(&:completed_at?)
|
if @submission.submitters.all?(&:completed_at?)
|
||||||
last_submitter = @submission.submitters.max_by(&:completed_at)
|
build_completed_documents(@submission, merge: is_merge)
|
||||||
|
|
||||||
if last_submitter.documents_attachments.blank?
|
|
||||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
|
||||||
end
|
|
||||||
|
|
||||||
last_submitter.documents_attachments
|
|
||||||
else
|
else
|
||||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
build_preview_documents(@submission, merge: is_merge)
|
||||||
|
|
||||||
if @submission.preview_documents.present? &&
|
|
||||||
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
|
||||||
@submission.preview_documents
|
|
||||||
else
|
|
||||||
ApplicationRecord.no_touching do
|
|
||||||
@submission.preview_documents.each(&:destroy)
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||||
records: documents,
|
|
||||||
associations: [:blob]
|
|
||||||
).call
|
|
||||||
|
|
||||||
expires_at = Accounts.link_expires_at(current_account)
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
@@ -43,5 +26,50 @@ module Api
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_completed_documents(submission, merge: false)
|
||||||
|
last_submitter = submission.submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
|
if merge
|
||||||
|
if submission.merged_document_attachment.blank?
|
||||||
|
submission.merged_document_attachment =
|
||||||
|
Submissions::GenerateCombinedAttachment.call(last_submitter, with_audit: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
[submission.merged_document_attachment]
|
||||||
|
else
|
||||||
|
if last_submitter.documents_attachments.blank?
|
||||||
|
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_submitter.documents_attachments
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_preview_documents(submission, merge: false)
|
||||||
|
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(submission)
|
||||||
|
|
||||||
|
if merge
|
||||||
|
if submission.preview_merged_document_attachment.present? &&
|
||||||
|
submission.preview_merged_document_attachment.metadata['values_hash'] == values_hash
|
||||||
|
[submission.preview_merged_document_attachment]
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching { submission.preview_merged_document_attachment&.destroy }
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(submission, values_hash:, merge: true)
|
||||||
|
end
|
||||||
|
elsif submission.preview_documents.present? &&
|
||||||
|
submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||||
|
submission.preview_documents
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching do
|
||||||
|
submission.preview_documents.each(&:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(submission, values_hash:)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -17,6 +17,7 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
helper_method :button_title,
|
helper_method :button_title,
|
||||||
:current_account,
|
:current_account,
|
||||||
|
:true_ability,
|
||||||
:form_link_host,
|
:form_link_host,
|
||||||
:svg_icon
|
:svg_icon
|
||||||
|
|
||||||
@@ -102,6 +103,10 @@ class ApplicationController < ActionController::Base
|
|||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def true_ability
|
||||||
|
@true_ability ||= Ability.new(true_user)
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_redirect_to_setup
|
def maybe_redirect_to_setup
|
||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class SubmissionsController < ApplicationController
|
|||||||
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
||||||
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
||||||
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
||||||
'payment' => 'credit_card', 'verification' => 'id'
|
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -53,14 +53,15 @@ class SubmissionsController < ApplicationController
|
|||||||
else
|
else
|
||||||
submissions_attrs = submissions_params[:submission].to_h.values
|
submissions_attrs = submissions_params[:submission].to_h.values
|
||||||
|
|
||||||
submissions_attrs, =
|
submissions_attrs, _, new_fields =
|
||||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
submissions_attrs:,
|
submissions_attrs:,
|
||||||
|
new_fields:,
|
||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCloneController < ApplicationController
|
||||||
|
load_and_authorize_resource :template, instance_name: :base_template
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize!(:create, Template)
|
||||||
|
|
||||||
|
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@base_template],
|
||||||
|
associations: [schema_documents: :preview_images_attachments]
|
||||||
|
).call
|
||||||
|
|
||||||
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
|
name: params.dig(:template, :name),
|
||||||
|
folder_name: params[:folder_name])
|
||||||
|
|
||||||
|
authorize!(:create, @template)
|
||||||
|
|
||||||
|
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.author = true_user if true_user.account_id == @template.account_id
|
||||||
|
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
|
if @template.save
|
||||||
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
|
maybe_redirect_to_template(@template)
|
||||||
|
else
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, partial: 'templates_clone/form'), status: :unprocessable_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
class TemplatesController < ApplicationController
|
class TemplatesController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
before_action :load_base_template, only: %i[new create]
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
@@ -26,9 +24,7 @@ class TemplatesController < ApplicationController
|
|||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new; end
|
||||||
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
|
|
||||||
end
|
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
@@ -48,37 +44,18 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @base_template
|
@template.author = current_user
|
||||||
ActiveRecord::Associations::Preloader.new(
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
records: [@base_template],
|
@template.account = current_account
|
||||||
associations: [schema_documents: :preview_images_attachments]
|
|
||||||
).call
|
|
||||||
|
|
||||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
|
||||||
name: params.dig(:template, :name),
|
|
||||||
folder_name: params[:folder_name])
|
|
||||||
else
|
|
||||||
@template.author = current_user
|
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
|
||||||
@template.account_id = params[:account_id]
|
|
||||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
|
||||||
else
|
|
||||||
@template.account = current_account
|
|
||||||
end
|
|
||||||
|
|
||||||
Templates.maybe_assign_access(@template)
|
Templates.maybe_assign_access(@template)
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
redirect_to(edit_template_path(@template))
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -132,23 +109,4 @@ class TemplatesController < ApplicationController
|
|||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorized_clone_account_id?(account_id)
|
|
||||||
true_user.account_id.to_s == account_id.to_s ||
|
|
||||||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_to_template(template)
|
|
||||||
if template.account == current_account
|
|
||||||
redirect_to(edit_template_path(@template))
|
|
||||||
else
|
|
||||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_base_template
|
|
||||||
return if params[:base_template_id].blank?
|
|
||||||
|
|
||||||
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -52,6 +52,7 @@ import AutosizeField from './elements/autosize_field'
|
|||||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||||
import OpenModal from './elements/open_modal'
|
import OpenModal from './elements/open_modal'
|
||||||
import BarChart from './elements/bar_chart'
|
import BarChart from './elements/bar_chart'
|
||||||
|
import FieldCondition from './elements/field_condition'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -142,6 +143,7 @@ safeRegisterElement('autosize-field', AutosizeField)
|
|||||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||||
safeRegisterElement('open-modal', OpenModal)
|
safeRegisterElement('open-modal', OpenModal)
|
||||||
safeRegisterElement('bar-chart', BarChart)
|
safeRegisterElement('bar-chart', BarChart)
|
||||||
|
safeRegisterElement('field-condition', FieldCondition)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -157,6 +159,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||||
|
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.targetId = this.dataset.targetId
|
||||||
|
this.fieldId = this.dataset.fieldId
|
||||||
|
this.action = (this.dataset.action || '').trim()
|
||||||
|
this.expectedValue = this.dataset.value
|
||||||
|
|
||||||
|
this.targetEl = document.getElementById(this.targetId)
|
||||||
|
this.sourceEl = document.getElementById(this.fieldId)
|
||||||
|
|
||||||
|
this.bindListeners()
|
||||||
|
|
||||||
|
this.evaluateAndApply()
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.unbindListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
bindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.addEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindListeners () {
|
||||||
|
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||||
|
this.sourceEl.removeEventListener(ev, this.evaluateAndApply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsFor (el) {
|
||||||
|
if (!el) return []
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'textarea') return ['input']
|
||||||
|
if (tag === 'input') return ['input', 'change']
|
||||||
|
|
||||||
|
return ['change']
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluateAndApply = () => {
|
||||||
|
const fieldConditions = document.querySelectorAll(`field-condition[data-target-id="${this.targetId}"]`)
|
||||||
|
|
||||||
|
const result = [...fieldConditions].reduce((acc, cond) => {
|
||||||
|
if (cond.dataset.operation === 'or') {
|
||||||
|
acc.push(acc.pop() || cond.checkCondition())
|
||||||
|
} else {
|
||||||
|
acc.push(cond.checkCondition())
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
this.apply(!result.includes(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCondition () {
|
||||||
|
const action = this.action
|
||||||
|
const actual = this.getSourceValue()
|
||||||
|
const expected = this.expectedValue
|
||||||
|
|
||||||
|
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||||
|
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||||
|
|
||||||
|
if (action === 'equal') {
|
||||||
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
|
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'contains') return this.contains(actual, expected)
|
||||||
|
|
||||||
|
if (action === 'not_equal') {
|
||||||
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
|
return !list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'does_not_contain') return !this.contains(actual, expected)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getSourceValue () {
|
||||||
|
const el = this.sourceEl
|
||||||
|
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const tag = el.tagName.toLowerCase()
|
||||||
|
const type = (el.getAttribute('type') || '').toLowerCase()
|
||||||
|
|
||||||
|
if (tag === 'select') return el.value
|
||||||
|
if (tag === 'textarea') return el.value
|
||||||
|
if (tag === 'input' && type === 'checkbox') return el.checked ? (el.value || '1') : null
|
||||||
|
if (tag === 'input') return el.value
|
||||||
|
|
||||||
|
return el.value ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty (obj) {
|
||||||
|
if (obj == null) return true
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return obj.trim().length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
return Object.keys(obj).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj === false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
contains (actual, expected) {
|
||||||
|
if (expected === null || expected === undefined) return false
|
||||||
|
|
||||||
|
const exp = String(expected)
|
||||||
|
|
||||||
|
if (Array.isArray(actual)) return actual.filter((v) => v !== null && v !== undefined).map(String).includes(exp)
|
||||||
|
|
||||||
|
if (typeof actual === 'string') return actual.includes(exp)
|
||||||
|
|
||||||
|
return actual !== null && actual !== undefined && String(actual) === exp
|
||||||
|
}
|
||||||
|
|
||||||
|
apply (passed) {
|
||||||
|
const controls = this.targetEl.matches('input, select, textarea, button')
|
||||||
|
? [this.targetEl]
|
||||||
|
: Array.from(this.targetEl.querySelectorAll('input, select, textarea, button'))
|
||||||
|
|
||||||
|
if (passed) {
|
||||||
|
this.targetEl.style.display = ''
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = '' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = false))
|
||||||
|
} else {
|
||||||
|
this.targetEl.style.display = 'none'
|
||||||
|
this.targetEl.labels.forEach((label) => { label.style.display = 'none' })
|
||||||
|
|
||||||
|
controls.forEach((c) => (c.disabled = true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
|||||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
|
withFieldLabels: this.dataset.withFieldLabels !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
|||||||
@@ -60,6 +60,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="stamp.url"
|
:src="stamp.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'kba' && kba"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="kba.url"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
class="flex justify-between h-full gap-1 overflow-hidden w-full"
|
||||||
@@ -272,7 +277,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId, IconUserScan } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -391,7 +396,8 @@ export default {
|
|||||||
stamp: this.t('stamp'),
|
stamp: this.t('stamp'),
|
||||||
payment: this.t('payment'),
|
payment: this.t('payment'),
|
||||||
phone: this.t('phone'),
|
phone: this.t('phone'),
|
||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id'),
|
||||||
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
strikethroughWidth () {
|
strikethroughWidth () {
|
||||||
@@ -454,7 +460,8 @@ export default {
|
|||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
verification: IconId
|
verification: IconId,
|
||||||
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -471,6 +478,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
kba () {
|
||||||
|
if (this.field.type === 'kba') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
@@ -560,7 +574,7 @@ export default {
|
|||||||
return style
|
return style
|
||||||
},
|
},
|
||||||
isNarrow () {
|
isNarrow () {
|
||||||
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
return this.area.h > 0 && ((this.area.w * this.pageWidth) / (this.area.h * this.pageHeight)) > 4.5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
:scroll-el="scrollEl"
|
:scroll-el="scrollEl"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:with-label="!isAnonymousChecboxes && showFieldNames"
|
:with-label="withFieldLabels && !isAnonymousChecboxes && showFieldNames"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
@@ -465,6 +465,17 @@
|
|||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="!isSubmitting && submitStep()"
|
@submit="!isSubmitting && submitStep()"
|
||||||
/>
|
/>
|
||||||
|
<KbaStep
|
||||||
|
v-else-if="currentField.type === 'kba'"
|
||||||
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
:submitter="submitter"
|
||||||
|
:empty-value-required-step="emptyValueRequiredStep"
|
||||||
|
:field="currentField"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
:values="values"
|
||||||
|
@submit="!isSubmitting && submitStep()"
|
||||||
|
/>
|
||||||
<VerificationStep
|
<VerificationStep
|
||||||
v-else-if="currentField.type === 'verification'"
|
v-else-if="currentField.type === 'verification'"
|
||||||
ref="currentStep"
|
ref="currentStep"
|
||||||
@@ -480,7 +491,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
|
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification' && currentField.type !== 'kba') || submittedValues[currentField.uuid]"
|
||||||
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
|
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -569,6 +580,7 @@ import MultiSelectStep from './multi_select_step'
|
|||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
import PaymentStep from './payment_step'
|
import PaymentStep from './payment_step'
|
||||||
import VerificationStep from './verification_step'
|
import VerificationStep from './verification_step'
|
||||||
|
import KbaStep from './kba_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
import NumberStep from './number_step'
|
import NumberStep from './number_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
@@ -613,6 +625,7 @@ export default {
|
|||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
VerificationStep,
|
VerificationStep,
|
||||||
|
KbaStep,
|
||||||
InviteForm,
|
InviteForm,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
@@ -737,6 +750,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withFieldLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -999,7 +1017,7 @@ export default {
|
|||||||
const verificationFields = []
|
const verificationFields = []
|
||||||
|
|
||||||
const sortedFields = this.fields.reduce((acc, f) => {
|
const sortedFields = this.fields.reduce((acc, f) => {
|
||||||
if (f.type === 'verification') {
|
if (f.type === 'verification' || f.type === 'kba') {
|
||||||
verificationFields.push(f)
|
verificationFields.push(f)
|
||||||
} else if (!f.readonly) {
|
} else if (!f.readonly) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
@@ -1396,7 +1414,7 @@ export default {
|
|||||||
|
|
||||||
const submitStep = this.currentStep
|
const submitStep = this.currentStep
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Please upload an image file',
|
please_upload_an_image_file: 'Please upload an image file',
|
||||||
must_be_characters_length: 'Must be {number} characters length',
|
must_be_characters_length: 'Must be {number} characters length',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||||
@@ -100,6 +101,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||||
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||||
@@ -201,6 +203,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Per favore carica un file immagine',
|
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||||
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||||
@@ -302,6 +305,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
||||||
@@ -403,6 +407,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||||
must_be_characters_length: 'Doit comporter {number} caractères',
|
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
||||||
@@ -504,6 +509,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||||
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||||
@@ -605,6 +611,7 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||||
must_be_characters_length: 'Має містити {number} символів',
|
must_be_characters_length: 'Має містити {number} символів',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
@@ -706,6 +713,7 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||||
must_be_characters_length: 'Musí mít délku {number} znaků',
|
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||||
@@ -807,6 +815,7 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||||
must_be_characters_length: 'Deve ter {number} caracteres',
|
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||||
@@ -908,6 +917,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||||
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
@@ -1009,6 +1019,7 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
@@ -1110,6 +1121,7 @@ const nl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||||
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
@@ -1211,6 +1223,7 @@ const ar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||||
must_be_characters_length: '{number}자여야 합니다',
|
must_be_characters_length: '{number}자여야 합니다',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
@@ -1312,6 +1325,7 @@ const ko = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ja = {
|
const ja = {
|
||||||
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||||
must_be_characters_length: '{number}文字でなければなりません',
|
must_be_characters_length: '{number}文字でなければなりません',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
|
|||||||
@@ -0,0 +1,576 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="!error"
|
||||||
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>{{ field.name || 'Knowledge Based Authentication' }}</template>
|
||||||
|
<span
|
||||||
|
v-if="questions"
|
||||||
|
class="float-right font-normal text-neutral-500 mt-1 whitespace-nowrap text-right text-sm"
|
||||||
|
>
|
||||||
|
<span>Question {{ currentQuestionIndex + 1 }} / {{ questions.length }}</span>
|
||||||
|
<span
|
||||||
|
v-if="timeLeftSeconds !== null"
|
||||||
|
class="block"
|
||||||
|
>
|
||||||
|
Time left: {{ formattedTimeLeft }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-4 px-1 field-description-text"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isRequiredFieldEmpty"
|
||||||
|
class="px-1 field-description-text"
|
||||||
|
>
|
||||||
|
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="mb-4 text-center"
|
||||||
|
>
|
||||||
|
<div class="text-xl mb-4">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="base-button w-full flex justify-center submit-form-button"
|
||||||
|
@click="restartKba"
|
||||||
|
>
|
||||||
|
{{ questions ? 'Restart KBA' : 'Retry' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="w-full flex space-x-2 justify-center mb-2"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
width="40"
|
||||||
|
class="animate-spin h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="questions && !error">
|
||||||
|
<form @submit.prevent="nextQuestion">
|
||||||
|
<div class="mb-6 px-1">
|
||||||
|
<p class="font-semibold mb-4 text-lg">
|
||||||
|
{{ currentQuestion.prompt }}
|
||||||
|
</p>
|
||||||
|
<div class="space-y-3.5 mx-auto">
|
||||||
|
<div
|
||||||
|
v-for="(answer, index) in currentQuestion.answers"
|
||||||
|
:key="`${currentQuestion.id}_${index}`"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="`${currentQuestion.id}_${index}`"
|
||||||
|
class="flex items-center space-x-3 radio-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`${currentQuestion.id}_${index}`"
|
||||||
|
v-model="answers[currentQuestion.id]"
|
||||||
|
type="radio"
|
||||||
|
:name="currentQuestion.id"
|
||||||
|
:value="answer.text"
|
||||||
|
class="base-radio !h-7 !w-7"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<span class="text-xl">{{ answer.text }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="base-button w-full flex justify-center submit-form-button"
|
||||||
|
:disabled="isSubmitting || !answers[currentQuestion.id]"
|
||||||
|
>
|
||||||
|
<span class="flex">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isSubmitting"
|
||||||
|
class="mr-1 animate-spin"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ isLastQuestion ? (isSubmitting ? t('submitting_') : t('complete')) : t('next') }}
|
||||||
|
</span><span
|
||||||
|
v-if="isSubmitting"
|
||||||
|
class="w-6 flex justify-start mr-1"
|
||||||
|
><span>...</span></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!error && !isRequiredFieldEmpty">
|
||||||
|
<form @submit.prevent="startKba">
|
||||||
|
<div class="grid grid-cols-6 gap-x-2 md:gap-x-4 md:gap-y-2 mb-4">
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_fn"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>First Name</label>
|
||||||
|
<input
|
||||||
|
id="kba_fn"
|
||||||
|
v-model="form.fn"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_ln"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Last Name</label>
|
||||||
|
<input
|
||||||
|
id="kba_ln"
|
||||||
|
v-model="form.ln"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-6">
|
||||||
|
<label
|
||||||
|
for="kba_addr"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Address</label>
|
||||||
|
<input
|
||||||
|
id="kba_addr"
|
||||||
|
v-model="form.addr"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label
|
||||||
|
for="kba_city"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>City</label>
|
||||||
|
<input
|
||||||
|
id="kba_city"
|
||||||
|
v-model="form.city"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label
|
||||||
|
for="kba_state"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>State</label>
|
||||||
|
<select
|
||||||
|
id="kba_state"
|
||||||
|
v-model="form.state"
|
||||||
|
class="select select-bordered !h-10 min-h-[2.5rem] w-full bg-white font-normal !text-base"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Select State
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="state in states"
|
||||||
|
:key="state.code"
|
||||||
|
:value="state.code"
|
||||||
|
>
|
||||||
|
{{ state.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label
|
||||||
|
for="kba_zip"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Zip</label>
|
||||||
|
<input
|
||||||
|
id="kba_zip"
|
||||||
|
v-model="form.zip"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_phone"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Phone</label>
|
||||||
|
<input
|
||||||
|
id="kba_phone"
|
||||||
|
v-model="form.phone"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_email"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>Email</label>
|
||||||
|
<input
|
||||||
|
id="kba_email"
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_dob"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>DOB</label>
|
||||||
|
<input
|
||||||
|
id="kba_dob"
|
||||||
|
v-model="form.dob"
|
||||||
|
type="date"
|
||||||
|
class="input input-bordered !h-10 md:w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label
|
||||||
|
for="kba_ssn"
|
||||||
|
class="label text-sm md:text-base"
|
||||||
|
>SSN</label>
|
||||||
|
<input
|
||||||
|
id="kba_ssn"
|
||||||
|
v-model="form.ssn"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered !h-10 w-full bg-white"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="base-button w-full flex justify-center submit-form-button"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<span class="flex">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isLoading"
|
||||||
|
class="mr-1 animate-spin"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ isLoading ? 'Loading...' : 'Start Verification' }}
|
||||||
|
</span><span
|
||||||
|
v-if="isLoading"
|
||||||
|
class="w-6 flex justify-start mr-1"
|
||||||
|
><span>...</span></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
import { IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KbaStep',
|
||||||
|
components: {
|
||||||
|
MarkdownContent,
|
||||||
|
IconInnerShadowTop
|
||||||
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
emptyValueRequiredStep: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['submit'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
isSubmitting: false,
|
||||||
|
questions: null,
|
||||||
|
currentQuestionIndex: 0,
|
||||||
|
token: null,
|
||||||
|
reference: null,
|
||||||
|
answers: {},
|
||||||
|
error: null,
|
||||||
|
timeLeftSeconds: null,
|
||||||
|
countdownIntervalId: null,
|
||||||
|
form: {
|
||||||
|
fn: '',
|
||||||
|
ln: '',
|
||||||
|
addr: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
zip: '',
|
||||||
|
dob: '',
|
||||||
|
ssn: '',
|
||||||
|
phone: '',
|
||||||
|
email: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentQuestion () {
|
||||||
|
return this.questions ? this.questions[this.currentQuestionIndex] : null
|
||||||
|
},
|
||||||
|
isRequiredFieldEmpty () {
|
||||||
|
return this.emptyValueRequiredStep && this.emptyValueRequiredStep[0] !== this.field
|
||||||
|
},
|
||||||
|
kbaTimeLimitSeconds () {
|
||||||
|
return 90
|
||||||
|
},
|
||||||
|
states () {
|
||||||
|
return [
|
||||||
|
{ code: 'AL', name: 'Alabama' },
|
||||||
|
{ code: 'AK', name: 'Alaska' },
|
||||||
|
{ code: 'AZ', name: 'Arizona' },
|
||||||
|
{ code: 'AR', name: 'Arkansas' },
|
||||||
|
{ code: 'CA', name: 'California' },
|
||||||
|
{ code: 'CO', name: 'Colorado' },
|
||||||
|
{ code: 'CT', name: 'Connecticut' },
|
||||||
|
{ code: 'DE', name: 'Delaware' },
|
||||||
|
{ code: 'DC', name: 'District of Columbia' },
|
||||||
|
{ code: 'FL', name: 'Florida' },
|
||||||
|
{ code: 'GA', name: 'Georgia' },
|
||||||
|
{ code: 'HI', name: 'Hawaii' },
|
||||||
|
{ code: 'ID', name: 'Idaho' },
|
||||||
|
{ code: 'IL', name: 'Illinois' },
|
||||||
|
{ code: 'IN', name: 'Indiana' },
|
||||||
|
{ code: 'IA', name: 'Iowa' },
|
||||||
|
{ code: 'KS', name: 'Kansas' },
|
||||||
|
{ code: 'KY', name: 'Kentucky' },
|
||||||
|
{ code: 'LA', name: 'Louisiana' },
|
||||||
|
{ code: 'ME', name: 'Maine' },
|
||||||
|
{ code: 'MD', name: 'Maryland' },
|
||||||
|
{ code: 'MA', name: 'Massachusetts' },
|
||||||
|
{ code: 'MI', name: 'Michigan' },
|
||||||
|
{ code: 'MN', name: 'Minnesota' },
|
||||||
|
{ code: 'MS', name: 'Mississippi' },
|
||||||
|
{ code: 'MO', name: 'Missouri' },
|
||||||
|
{ code: 'MT', name: 'Montana' },
|
||||||
|
{ code: 'NE', name: 'Nebraska' },
|
||||||
|
{ code: 'NV', name: 'Nevada' },
|
||||||
|
{ code: 'NH', name: 'New Hampshire' },
|
||||||
|
{ code: 'NJ', name: 'New Jersey' },
|
||||||
|
{ code: 'NM', name: 'New Mexico' },
|
||||||
|
{ code: 'NY', name: 'New York' },
|
||||||
|
{ code: 'NC', name: 'North Carolina' },
|
||||||
|
{ code: 'ND', name: 'North Dakota' },
|
||||||
|
{ code: 'OH', name: 'Ohio' },
|
||||||
|
{ code: 'OK', name: 'Oklahoma' },
|
||||||
|
{ code: 'OR', name: 'Oregon' },
|
||||||
|
{ code: 'PA', name: 'Pennsylvania' },
|
||||||
|
{ code: 'RI', name: 'Rhode Island' },
|
||||||
|
{ code: 'SC', name: 'South Carolina' },
|
||||||
|
{ code: 'SD', name: 'South Dakota' },
|
||||||
|
{ code: 'TN', name: 'Tennessee' },
|
||||||
|
{ code: 'TX', name: 'Texas' },
|
||||||
|
{ code: 'UT', name: 'Utah' },
|
||||||
|
{ code: 'VT', name: 'Vermont' },
|
||||||
|
{ code: 'VA', name: 'Virginia' },
|
||||||
|
{ code: 'WA', name: 'Washington' },
|
||||||
|
{ code: 'WV', name: 'West Virginia' },
|
||||||
|
{ code: 'WI', name: 'Wisconsin' },
|
||||||
|
{ code: 'WY', name: 'Wyoming' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
isLastQuestion () {
|
||||||
|
return this.questions && this.currentQuestionIndex === this.questions.length - 1
|
||||||
|
},
|
||||||
|
formattedTimeLeft () {
|
||||||
|
if (this.timeLeftSeconds === null) return ''
|
||||||
|
|
||||||
|
const minutes = Math.floor(this.timeLeftSeconds / 60)
|
||||||
|
const seconds = this.timeLeftSeconds % 60
|
||||||
|
|
||||||
|
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.clearCountdown()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearCountdown () {
|
||||||
|
if (this.countdownIntervalId) {
|
||||||
|
clearInterval(this.countdownIntervalId)
|
||||||
|
this.countdownIntervalId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeLeftSeconds = null
|
||||||
|
},
|
||||||
|
startCountdown () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.timeLeftSeconds = this.kbaTimeLimitSeconds
|
||||||
|
|
||||||
|
this.countdownIntervalId = setInterval(() => {
|
||||||
|
if (this.timeLeftSeconds === null) return
|
||||||
|
|
||||||
|
this.timeLeftSeconds -= 1
|
||||||
|
|
||||||
|
if (this.timeLeftSeconds <= 0) {
|
||||||
|
this.handleTimeout()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
handleTimeout () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.questions = null
|
||||||
|
this.token = null
|
||||||
|
this.reference = null
|
||||||
|
this.answers = {}
|
||||||
|
this.currentQuestionIndex = 0
|
||||||
|
this.error = `Knowledge Based Authentication timed out. You only have ${this.kbaTimeLimitSeconds} seconds to complete the verification. Please retry.`
|
||||||
|
},
|
||||||
|
nextQuestion () {
|
||||||
|
if (this.isLastQuestion) {
|
||||||
|
this.$emit('submit')
|
||||||
|
} else {
|
||||||
|
this.currentQuestionIndex++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restartKba () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.questions = null
|
||||||
|
this.token = null
|
||||||
|
this.reference = null
|
||||||
|
this.answers = {}
|
||||||
|
this.currentQuestionIndex = 0
|
||||||
|
this.error = null
|
||||||
|
},
|
||||||
|
async startKba () {
|
||||||
|
this.isLoading = true
|
||||||
|
this.error = null
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = { ...this.form, submitter_slug: this.submitterSlug }
|
||||||
|
|
||||||
|
if (payload.dob) {
|
||||||
|
payload.dob = payload.dob.replace(/-/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.ssn) {
|
||||||
|
payload.ssn = payload.ssn.replace(/\D/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.phone) {
|
||||||
|
payload.phone = payload.phone.replace(/^\+1/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(this.baseUrl + '/api/kba', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!resp.ok) throw new Error('Failed to start KBA')
|
||||||
|
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (data.result && data.result.action === 'FAIL') {
|
||||||
|
if (data.result.detail === 'NO MATCH') {
|
||||||
|
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.result.detail || 'KBA Start Failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.output && data.output.questions && data.output.questions.questions) {
|
||||||
|
this.questions = data.output.questions.questions
|
||||||
|
this.token = data.continuations.questions.template.token
|
||||||
|
this.reference = data.meta.reference
|
||||||
|
|
||||||
|
this.questions.forEach(q => {
|
||||||
|
this.answers[q.id] = null
|
||||||
|
})
|
||||||
|
|
||||||
|
this.startCountdown()
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid KBA response')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.error = e.message
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit () {
|
||||||
|
this.clearCountdown()
|
||||||
|
|
||||||
|
this.isSubmitting = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
const formattedAnswers = Object.keys(this.answers).reduce((acc, key) => {
|
||||||
|
acc[key] = [this.answers[key]]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(this.baseUrl + `/api/kba/${this.field.uuid}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: this.token,
|
||||||
|
answers: formattedAnswers,
|
||||||
|
reference: this.reference,
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (data.result?.action !== 'PASS') {
|
||||||
|
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||||
|
|
||||||
|
throw new Error('Knowledge Based Authentication Failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
this.error = 'Failed to submit answers'
|
||||||
|
|
||||||
|
throw new Error('Failed to submit answers')
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="phone"
|
ref="phone"
|
||||||
:value="phoneValue"
|
:value="defaultValue && detectedPhoneValueDialCode ? phoneValue.split('+' + detectedPhoneValueDialCode).slice(-1).join('') : phoneValue"
|
||||||
:readonly="!!defaultValue"
|
:readonly="!!defaultValue"
|
||||||
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
|
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
|
|||||||
@@ -583,6 +583,7 @@ export default {
|
|||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
withVerification: this.withVerification,
|
withVerification: this.withVerification,
|
||||||
|
withKba: this.withKba,
|
||||||
withPayment: this.withPayment,
|
withPayment: this.withPayment,
|
||||||
isPaymentConnected: this.isPaymentConnected,
|
isPaymentConnected: this.isPaymentConnected,
|
||||||
withFormula: this.withFormula,
|
withFormula: this.withFormula,
|
||||||
@@ -794,6 +795,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withKba: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
withPayment: {
|
withPayment: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -1110,14 +1116,17 @@ export default {
|
|||||||
const sortArea = (aArea, bArea) => {
|
const sortArea = (aArea, bArea) => {
|
||||||
if (aArea.attachment_uuid === bArea.attachment_uuid) {
|
if (aArea.attachment_uuid === bArea.attachment_uuid) {
|
||||||
if (aArea.page === bArea.page) {
|
if (aArea.page === bArea.page) {
|
||||||
if (Math.abs(aArea.y - bArea.y) < 0.01) {
|
const aY = aArea.y + aArea.h
|
||||||
|
const bY = bArea.y + bArea.h
|
||||||
|
|
||||||
|
if (Math.abs(aY - bY) < 0.01 || (aArea.h < bArea.h ? (aArea.y >= bArea.y && aY <= bY) : (bArea.y >= aArea.y && bY <= aY))) {
|
||||||
if (aArea.x === bArea.x) {
|
if (aArea.x === bArea.x) {
|
||||||
return 0
|
return 0
|
||||||
} else {
|
} else {
|
||||||
return aArea.x - bArea.x
|
return aArea.x - bArea.x
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return aArea.y - bArea.y
|
return (aArea.y + aArea.h) - (bArea.y + bArea.h)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return aArea.page - bArea.page
|
return aArea.page - bArea.page
|
||||||
@@ -1167,6 +1176,72 @@ export default {
|
|||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
findFieldInsertIndex (field) {
|
||||||
|
if (!field.areas?.length) return -1
|
||||||
|
|
||||||
|
const area = field.areas[0]
|
||||||
|
|
||||||
|
const attachmentUuidsIndex = this.template.schema.reduce((acc, e, index) => {
|
||||||
|
acc[e.attachment_uuid] = index
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const compareAreas = (a, b) => {
|
||||||
|
const aAttIdx = attachmentUuidsIndex[a.attachment_uuid]
|
||||||
|
const bAttIdx = attachmentUuidsIndex[b.attachment_uuid]
|
||||||
|
|
||||||
|
if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx
|
||||||
|
if (a.page !== b.page) return a.page - b.page
|
||||||
|
|
||||||
|
const aY = a.y + a.h
|
||||||
|
const bY = b.y + b.h
|
||||||
|
|
||||||
|
if (Math.abs(aY - bY) < 0.01) return a.x - b.x
|
||||||
|
if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x
|
||||||
|
|
||||||
|
return aY - bY
|
||||||
|
}
|
||||||
|
|
||||||
|
let closestBeforeIndex = -1
|
||||||
|
let closestBeforeArea = null
|
||||||
|
let closestAfterIndex = -1
|
||||||
|
let closestAfterArea = null
|
||||||
|
|
||||||
|
this.template.fields.forEach((f, index) => {
|
||||||
|
if (f.submitter_uuid === field.submitter_uuid) {
|
||||||
|
(f.areas || []).forEach((a) => {
|
||||||
|
const cmp = compareAreas(a, area)
|
||||||
|
|
||||||
|
if (cmp < 0) {
|
||||||
|
if (!closestBeforeArea || (compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) {
|
||||||
|
closestBeforeIndex = index
|
||||||
|
closestBeforeArea = a
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!closestAfterArea || (compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) {
|
||||||
|
closestAfterIndex = index
|
||||||
|
closestAfterArea = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (closestBeforeIndex !== -1) return closestBeforeIndex + 1
|
||||||
|
if (closestAfterIndex !== -1) return closestAfterIndex
|
||||||
|
|
||||||
|
return -1
|
||||||
|
},
|
||||||
|
insertField (field) {
|
||||||
|
const insertIndex = this.findFieldInsertIndex(field)
|
||||||
|
|
||||||
|
if (insertIndex !== -1) {
|
||||||
|
this.template.fields.splice(insertIndex, 0, field)
|
||||||
|
} else {
|
||||||
|
this.template.fields.push(field)
|
||||||
|
}
|
||||||
|
},
|
||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
},
|
},
|
||||||
@@ -1220,7 +1295,7 @@ export default {
|
|||||||
field.preferences.with_signature_id = this.withSignatureId
|
field.preferences.with_signature_id = this.withSignatureId
|
||||||
}
|
}
|
||||||
|
|
||||||
this.template.fields.push(field)
|
this.insertField(field)
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
@@ -1449,11 +1524,13 @@ export default {
|
|||||||
|
|
||||||
field.areas.push(area)
|
field.areas.push(area)
|
||||||
} else {
|
} else {
|
||||||
this.template.fields.push({
|
const newField = {
|
||||||
...JSON.parse(JSON.stringify(field)),
|
...JSON.parse(JSON.stringify(field)),
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
areas: [area]
|
areas: [area]
|
||||||
})
|
}
|
||||||
|
|
||||||
|
this.insertField(newField)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
@@ -1476,13 +1553,13 @@ export default {
|
|||||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
||||||
|
|
||||||
if (type === 'checkbox') {
|
if (type === 'checkbox' || type === 'radio' || type === 'multiple') {
|
||||||
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
|
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
|
||||||
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
} else if (type === 'image') {
|
} else if (type === 'image') {
|
||||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
} else if (type === 'signature' || type === 'stamp' || type === 'verification') {
|
} else if (type === 'signature' || type === 'stamp' || type === 'verification' || type === 'kba') {
|
||||||
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
|
||||||
} else if (type === 'initials') {
|
} else if (type === 'initials') {
|
||||||
@@ -1538,7 +1615,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.template.fields.indexOf(this.drawField) === -1) {
|
if (this.template.fields.indexOf(this.drawField) === -1) {
|
||||||
this.template.fields.push(this.drawField)
|
this.insertField(this.drawField)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
@@ -1566,8 +1643,8 @@ export default {
|
|||||||
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
||||||
|
|
||||||
if (previousArea || area.w) {
|
if (previousArea || area.w) {
|
||||||
const areaW = previousArea?.w || (30 / pageMask.clientWidth)
|
const areaW = previousArea?.w || area.w || (30 / pageMask.clientWidth)
|
||||||
const areaH = previousArea?.h || (30 / pageMask.clientHeight)
|
const areaH = previousArea?.h || area.h || (30 / pageMask.clientHeight)
|
||||||
|
|
||||||
if ((pageMask.clientWidth * area.w) < 5) {
|
if ((pageMask.clientWidth * area.w) < 5) {
|
||||||
area.x = area.x - (areaW / 2)
|
area.x = area.x - (areaW / 2)
|
||||||
@@ -1677,7 +1754,7 @@ export default {
|
|||||||
this.selectedAreaRef.value = fieldArea
|
this.selectedAreaRef.value = fieldArea
|
||||||
|
|
||||||
if (this.template.fields.indexOf(field) === -1) {
|
if (this.template.fields.indexOf(field) === -1) {
|
||||||
this.template.fields.push(field)
|
this.insertField(field)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
@@ -1707,7 +1784,7 @@ export default {
|
|||||||
} else if (previousField?.areas?.length) {
|
} else if (previousField?.areas?.length) {
|
||||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||||
} else {
|
} else {
|
||||||
if (['checkbox'].includes(fieldType)) {
|
if (['checkbox', 'radio', 'multiple'].includes(fieldType)) {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 30 / area.maskW,
|
w: area.maskW / 30 / area.maskW,
|
||||||
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
||||||
@@ -1717,7 +1794,7 @@ export default {
|
|||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (fieldType === 'signature' || fieldType === 'stamp' || fieldType === 'verification') {
|
} else if (fieldType === 'signature' || fieldType === 'stamp' || fieldType === 'verification' || fieldType === 'kba') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||||
@@ -1839,7 +1916,7 @@ export default {
|
|||||||
attachment.metadata.pdf.fields.forEach((field) => {
|
attachment.metadata.pdf.fields.forEach((field) => {
|
||||||
field.submitter_uuid = this.selectedSubmitter.uuid
|
field.submitter_uuid = this.selectedSubmitter.uuid
|
||||||
|
|
||||||
this.template.fields.push(field)
|
this.insertField(field)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -154,15 +154,22 @@
|
|||||||
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
|
||||||
ref="options"
|
ref="options"
|
||||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||||
draggable="true"
|
@dragover="onOptionDragover"
|
||||||
@dragstart.prevent.stop
|
@drop="reorderOptions"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
class="flex space-x-1.5 items-center"
|
class="flex space-x-1.5 items-center"
|
||||||
|
:data-option-uuid="option.uuid"
|
||||||
>
|
>
|
||||||
<span class="text-sm w-3.5">
|
<span
|
||||||
|
class="text-sm w-3.5 cursor-grab select-none"
|
||||||
|
:draggable="editable && !defaultField"
|
||||||
|
@dragstart.stop="onOptionDragstart($event, option)"
|
||||||
|
@dragend.stop="optionDragRef = null"
|
||||||
|
@dragover.prevent.stop="onOptionDragover"
|
||||||
|
>
|
||||||
{{ index + 1 }}.
|
{{ index + 1 }}.
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
@@ -176,6 +183,7 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
required
|
required
|
||||||
:placeholder="`${t('option')} ${index + 1}`"
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
|
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -198,6 +206,7 @@
|
|||||||
:readonly="!editable || defaultField"
|
:readonly="!editable || defaultField"
|
||||||
required
|
required
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||||
@focus="maybeFocusOnOptionArea(option)"
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
@@ -217,7 +226,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="field.options && editable && !defaultField"
|
v-else-if="field.options && editable && !defaultField"
|
||||||
class="field-add-option text-center text-sm w-full pb-1"
|
class="field-add-option text-center text-sm w-full pb-1"
|
||||||
@click="addOption"
|
@click="addOptionAt(field.options.length)"
|
||||||
>
|
>
|
||||||
+ {{ t('add_option') }}
|
+ {{ t('add_option') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -363,7 +372,8 @@ export default {
|
|||||||
isShowFontModal: false,
|
isShowFontModal: false,
|
||||||
isShowConditionsModal: false,
|
isShowConditionsModal: false,
|
||||||
isShowDescriptionModal: false,
|
isShowDescriptionModal: false,
|
||||||
renderDropdown: false
|
renderDropdown: false,
|
||||||
|
optionDragRef: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -450,15 +460,17 @@ export default {
|
|||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
},
|
},
|
||||||
addOption () {
|
addOptionAt (index) {
|
||||||
this.isExpandOptions = true
|
this.isExpandOptions = true
|
||||||
|
|
||||||
this.field.options.push({ value: '', uuid: v4() })
|
const insertAt = index ?? this.field.options.length
|
||||||
|
|
||||||
|
this.field.options.splice(insertAt, 0, { value: '', uuid: v4() })
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const inputs = this.$refs.options.querySelectorAll('input')
|
const inputs = this.$refs.options.querySelectorAll('input')
|
||||||
|
|
||||||
inputs[inputs.length - 1]?.focus()
|
inputs[insertAt]?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
@@ -515,6 +527,70 @@ export default {
|
|||||||
this.isNameFocus = false
|
this.isNameFocus = false
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
|
},
|
||||||
|
onOptionDragstart (event, option) {
|
||||||
|
this.optionDragRef = option
|
||||||
|
|
||||||
|
const root = this.$el.getRootNode()
|
||||||
|
const hiddenEl = document.createElement('div')
|
||||||
|
|
||||||
|
hiddenEl.style.width = '1px'
|
||||||
|
hiddenEl.style.height = '1px'
|
||||||
|
hiddenEl.style.opacity = '0'
|
||||||
|
hiddenEl.style.position = 'fixed'
|
||||||
|
|
||||||
|
root.querySelector('#docuseal_modal_container')?.appendChild(hiddenEl)
|
||||||
|
event.dataTransfer?.setDragImage(hiddenEl, 0, 0)
|
||||||
|
|
||||||
|
setTimeout(() => { hiddenEl.remove() }, 1000)
|
||||||
|
|
||||||
|
event.dataTransfer.effectAllowed = 'move'
|
||||||
|
},
|
||||||
|
onOptionDragover (e) {
|
||||||
|
if (!this.optionDragRef) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const targetRow = e.target.closest('[data-option-uuid]')
|
||||||
|
|
||||||
|
if (!targetRow) return
|
||||||
|
|
||||||
|
const dragRow = this.$refs.options?.querySelector(`[data-option-uuid="${this.optionDragRef.uuid}"]`)
|
||||||
|
|
||||||
|
if (!dragRow) return
|
||||||
|
if (targetRow === dragRow) return
|
||||||
|
|
||||||
|
const rows = Array.from(this.$refs.options.querySelectorAll('[data-option-uuid]'))
|
||||||
|
|
||||||
|
const currentIndex = rows.indexOf(dragRow)
|
||||||
|
const targetIndex = rows.indexOf(targetRow)
|
||||||
|
|
||||||
|
if (currentIndex < targetIndex) {
|
||||||
|
targetRow.after(dragRow)
|
||||||
|
} else {
|
||||||
|
targetRow.before(dragRow)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reorderOptions (e) {
|
||||||
|
if (!this.optionDragRef) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const rows = Array.from(this.$refs.options.querySelectorAll('[data-option-uuid]'))
|
||||||
|
|
||||||
|
const newOrder = rows
|
||||||
|
.map((el) => this.field.options.find((opt) => opt.uuid === el.dataset.optionUuid))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
if (newOrder.length === this.field.options.length) {
|
||||||
|
this.field.options.splice(0, this.field.options.length, ...newOrder)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.optionDragRef = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -51,11 +51,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId, IconCalendarCheck, IconStrikethrough, IconUserScan } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
|
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 't', 'fieldTypes'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -114,7 +114,8 @@ export default {
|
|||||||
stamp: this.t('stamp'),
|
stamp: this.t('stamp'),
|
||||||
payment: this.t('payment'),
|
payment: this.t('payment'),
|
||||||
phone: this.t('phone'),
|
phone: this.t('phone'),
|
||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id'),
|
||||||
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldLabels () {
|
fieldLabels () {
|
||||||
@@ -134,7 +135,8 @@ export default {
|
|||||||
stamp: this.t('stamp_field'),
|
stamp: this.t('stamp_field'),
|
||||||
payment: this.t('payment_field'),
|
payment: this.t('payment_field'),
|
||||||
phone: this.t('phone_field'),
|
phone: this.t('phone_field'),
|
||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id'),
|
||||||
|
kba: this.t('kba')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -157,7 +159,8 @@ export default {
|
|||||||
stamp: IconRubberStamp,
|
stamp: IconRubberStamp,
|
||||||
payment: IconCreditCard,
|
payment: IconCreditCard,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
verification: IconId
|
verification: IconId,
|
||||||
|
kba: IconUserScan
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
skipTypes () {
|
skipTypes () {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
ref="fields"
|
ref="fields"
|
||||||
class="fields mb-1 mt-2"
|
class="fields mb-1 mt-2"
|
||||||
@dragover.prevent="onFieldDragover"
|
@dragover.prevent="onFieldDragover"
|
||||||
@drop="reorderFields"
|
@drop="fieldsDragFieldRef.value ? reorderFields() : null"
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
v-for="field in submitterFields"
|
v-for="field in submitterFields"
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
|
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))"
|
||||||
:id="`${type}_type_field_button`"
|
:id="`${type}_type_field_button`"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
||||||
@dragstart="onDragstart($event, { type: type })"
|
@dragstart="onDragstart($event, { type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
@click="['file', 'payment', 'verification', 'kba'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-console transition-all cursor-grab h-full absolute left-0"
|
class="flex items-console transition-all cursor-grab h-full absolute left-0"
|
||||||
@@ -283,7 +283,7 @@ export default {
|
|||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -579,7 +579,7 @@ export default {
|
|||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
if (field.submitter_uuid === submitter.uuid) {
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
this.removeField(field)
|
this.removeField(field, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -591,7 +591,7 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
removeField (field) {
|
removeField (field, save = true) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
this.fields.forEach((f) => {
|
this.fields.forEach((f) => {
|
||||||
@@ -610,7 +610,9 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.save()
|
if (save) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
form="sync_form"
|
:form="`sync_form_${inputId}`"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
>
|
>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form
|
<form
|
||||||
id="sync_form"
|
:id="`sync_form_${inputId}`"
|
||||||
ref="form"
|
ref="form"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@submit.prevent="upload({ path: uploadUrl })"
|
@submit.prevent="upload({ path: uploadUrl })"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
kba: 'KBA',
|
||||||
analyzing_: 'Analyzing...',
|
analyzing_: 'Analyzing...',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
downloading_: 'Downloading...',
|
downloading_: 'Downloading...',
|
||||||
@@ -188,6 +189,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Autodetectar campos',
|
autodetect_fields: 'Autodetectar campos',
|
||||||
analyzing_: 'Analizando...',
|
analyzing_: 'Analizando...',
|
||||||
download: 'Descargar',
|
download: 'Descargar',
|
||||||
@@ -377,6 +379,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Rileva campi',
|
autodetect_fields: 'Rileva campi',
|
||||||
analyzing_: 'Analisi...',
|
analyzing_: 'Analisi...',
|
||||||
download: 'Scarica',
|
download: 'Scarica',
|
||||||
@@ -566,6 +569,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Detectar campos',
|
autodetect_fields: 'Detectar campos',
|
||||||
analyzing_: 'Analisando...',
|
analyzing_: 'Analisando...',
|
||||||
download: 'Baixar',
|
download: 'Baixar',
|
||||||
@@ -755,6 +759,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Détecter les champs',
|
autodetect_fields: 'Détecter les champs',
|
||||||
analyzing_: 'Analyse...',
|
analyzing_: 'Analyse...',
|
||||||
download: 'Télécharger',
|
download: 'Télécharger',
|
||||||
@@ -944,6 +949,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Felder erkennen',
|
autodetect_fields: 'Felder erkennen',
|
||||||
analyzing_: 'Analysiere...',
|
analyzing_: 'Analysiere...',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
@@ -1133,6 +1139,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
kba: 'KBA',
|
||||||
autodetect_fields: 'Velden detecteren',
|
autodetect_fields: 'Velden detecteren',
|
||||||
analyzing_: 'Analyseren...',
|
analyzing_: 'Analyseren...',
|
||||||
download: 'Downloaden',
|
download: 'Downloaden',
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default {
|
|||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
if (field.submitter_uuid === submitter.uuid) {
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
this.removeField(field)
|
this.removeField(field, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -89,10 +89,12 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
removeField (field) {
|
removeField (field, save = true) {
|
||||||
this.fields.splice(this.fields.indexOf(field), 1)
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
|
||||||
this.save()
|
if (save) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification') && (withKba || type != 'kba'))">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -89,7 +89,7 @@ export default {
|
|||||||
IconPlus,
|
IconPlus,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'],
|
inject: ['withPhone', 'withPayment', 'withVerification', 'withKba', 'backgroundColor', 't'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -216,6 +216,9 @@ export default {
|
|||||||
queryParams () {
|
queryParams () {
|
||||||
return new URLSearchParams(window.location.search)
|
return new URLSearchParams(window.location.search)
|
||||||
},
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
|
},
|
||||||
googleDriveOauthPath () {
|
googleDriveOauthPath () {
|
||||||
const params = {
|
const params = {
|
||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
@@ -279,7 +282,7 @@ export default {
|
|||||||
async upload ({ path } = {}) {
|
async upload ({ path } = {}) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
return this.baseFetch(path || `/templates/${this.templateId}/documents`, {
|
return this.baseFetch(path || this.uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
|
|||||||
@@ -39,11 +39,18 @@ class ProcessSubmitterCompletionJob
|
|||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
|
|
||||||
complete_verification_events, sms_events =
|
complete_verification_events, sms_events =
|
||||||
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification])
|
submitter.submission_events.where(event_type: %i[send_sms send_2fa_sms complete_verification complete_kba])
|
||||||
.partition { |e| e.event_type == 'complete_verification' }
|
.partition { |e| e.event_type == 'complete_verification' || e.event_type == 'complete_kba' }
|
||||||
|
|
||||||
complete_verification_event = complete_verification_events.first
|
complete_verification_event = complete_verification_events.first
|
||||||
|
|
||||||
|
verification_method =
|
||||||
|
if complete_verification_event&.event_type == 'complete_kba'
|
||||||
|
'kba'
|
||||||
|
elsif complete_verification_event
|
||||||
|
complete_verification_event.data['method']
|
||||||
|
end
|
||||||
|
|
||||||
completed_submitter.assign_attributes(
|
completed_submitter.assign_attributes(
|
||||||
submission_id: submitter.submission_id,
|
submission_id: submitter.submission_id,
|
||||||
account_id: submission.account_id,
|
account_id: submission.account_id,
|
||||||
@@ -51,7 +58,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
template_id: submission.template_id,
|
template_id: submission.template_id,
|
||||||
source: submission.source,
|
source: submission.source,
|
||||||
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
|
||||||
verification_method: complete_verification_event&.data&.dig('method'),
|
verification_method:,
|
||||||
completed_at: submitter.completed_at
|
completed_at: submitter.completed_at
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class SendTestWebhookRequestJob
|
|||||||
|
|
||||||
USER_AGENT = 'DocuSeal.com Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
|
HttpsError = Class.new(StandardError)
|
||||||
|
LocalhostError = Class.new(StandardError)
|
||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find_by(id: params['submitter_id'])
|
submitter = Submitter.find_by(id: params['submitter_id'])
|
||||||
|
|
||||||
@@ -16,6 +19,17 @@ class SendTestWebhookRequestJob
|
|||||||
|
|
||||||
return unless webhook_url
|
return unless webhook_url
|
||||||
|
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
uri = begin
|
||||||
|
URI(webhook_url.url)
|
||||||
|
rescue URI::Error
|
||||||
|
Addressable::URI.parse(webhook_url.url).normalize
|
||||||
|
end
|
||||||
|
|
||||||
|
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https'
|
||||||
|
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
||||||
|
end
|
||||||
|
|
||||||
Faraday.post(webhook_url.url,
|
Faraday.post(webhook_url.url,
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ class AccountConfig < ApplicationRecord
|
|||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
WITH_FILE_LINKS_KEY = 'with_file_links'
|
WITH_FILE_LINKS_KEY = 'with_file_links'
|
||||||
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
||||||
|
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
|
||||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||||
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
||||||
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
||||||
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
||||||
|
WITH_FIELD_LABELS_KEY = 'with_field_labels'
|
||||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||||
POLICY_LINKS_KEY = 'policy_links'
|
POLICY_LINKS_KEY = 'policy_links'
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class Submission < ApplicationRecord
|
|||||||
|
|
||||||
has_one_attached :audit_trail
|
has_one_attached :audit_trail
|
||||||
has_one_attached :combined_document
|
has_one_attached :combined_document
|
||||||
|
has_one_attached :merged_document
|
||||||
|
has_one_attached :preview_merged_document
|
||||||
|
|
||||||
has_many_attached :preview_documents
|
has_many_attached :preview_documents
|
||||||
has_many_attached :documents
|
has_many_attached :documents
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
start_form: 'start_form',
|
start_form: 'start_form',
|
||||||
start_verification: 'start_verification',
|
start_verification: 'start_verification',
|
||||||
complete_verification: 'complete_verification',
|
complete_verification: 'complete_verification',
|
||||||
|
start_kba: 'start_kba',
|
||||||
|
complete_kba: 'complete_kba',
|
||||||
|
fail_kba: 'fail_kba',
|
||||||
view_form: 'view_form',
|
view_form: 'view_form',
|
||||||
invite_party: 'invite_party',
|
invite_party: 'invite_party',
|
||||||
complete_form: 'complete_form',
|
complete_form: 'complete_form',
|
||||||
|
|||||||
@@ -235,7 +235,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:manage, :personalization_advanced) %>
|
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
|
||||||
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::WITH_FILE_LINKS_KEY) %>
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::WITH_FILE_LINKS_KEY) %>
|
||||||
<% if can?(:manage, account_config) %>
|
<% if can?(:manage, account_config) %>
|
||||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M10 9a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M8 16a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 525 B |
@@ -13,10 +13,10 @@
|
|||||||
<meta property="og:image" content="">
|
<meta property="og:image" content="">
|
||||||
<meta name="twitter:image" content="">
|
<meta name="twitter:image" content="">
|
||||||
<% else %>
|
<% else %>
|
||||||
<meta property="og:image" content="<%= root_url %>preview.png">
|
<meta property="og:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>">
|
||||||
<meta property="og:image:width" content="800">
|
<meta property="og:image:width" content="800">
|
||||||
<meta property="og:image:height" content="800">
|
<meta property="og:image:height" content="800">
|
||||||
<meta name="twitter:image" content="<%= root_url %>preview.png">
|
<meta name="twitter:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:creator" content="@docusealco">
|
<meta name="twitter:creator" content="@docusealco">
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
|
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
|
||||||
<% submitters.each_with_index do |item, index| %>
|
<% submitters.each_with_index do |item, index| %>
|
||||||
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] } %>
|
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] }.presence %>
|
||||||
|
<% prefillable_fields ||= local_assigns[:recipient_form_fields].presence %>
|
||||||
<submitter-item class="form-control">
|
<submitter-item class="form-control">
|
||||||
<% if submitters.size > 1 %>
|
<% if submitters.size > 1 %>
|
||||||
<label class="label pt-0 pb-1">
|
<label class="label pt-0 pb-1">
|
||||||
@@ -29,14 +30,14 @@
|
|||||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
|
||||||
<submitters-autocomplete data-field="email">
|
<submitters-autocomplete data-field="email">
|
||||||
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
|
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<% has_phone_field = true %>
|
<% has_phone_field = true %>
|
||||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
|
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</custom-validation>
|
</custom-validation>
|
||||||
@@ -59,17 +60,23 @@
|
|||||||
</custom-validation>
|
</custom-validation>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% prefillable_fields.each do |field| %>
|
<% prefillable_fields.each do |field| %>
|
||||||
|
<% field_id = "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}" %>
|
||||||
<% if field['type'] == 'checkbox' %>
|
<% if field['type'] == 'checkbox' %>
|
||||||
<label for="detailed_field_<%= field['uuid'] %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
|
<label for="<%= field_id %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
|
||||||
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
|
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
|
||||||
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid']}]", id: "detailed_field_#{field['uuid']}", class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
|
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", id: field_id, class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
|
||||||
</label>
|
</label>
|
||||||
<% elsif field['type'] == 'select' || field['type'] == 'radio' %>
|
<% elsif field['type'] == 'select' || field['type'] == 'radio' %>
|
||||||
<%= select_tag "submission[1][submitters][][values][#{field['uuid']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: "detailed_field_#{field['uuid']}", class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
|
<%= select_tag "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: field_id, class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
|
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: field_id, required: field['required'] %>
|
||||||
<% elsif field['type'] != 'phone' %>
|
<% elsif field['type'] != 'phone' %>
|
||||||
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
|
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: field_id, required: field['required'] %>
|
||||||
|
<% end %>
|
||||||
|
<% field['conditions']&.each do |condition| %>
|
||||||
|
<% if (condition_field = prefillable_fields.find { |f| f['uuid'] == condition['field_uuid'] || f['name'] == condition['field_name'] }) %>
|
||||||
|
<field-condition data-target-id="<%= field_id %>" data-field-id="<%= "detailed_field_#{index}_#{condition['field_uuid'] || condition['field_name'].parameterize}" %>" data-action="<%= condition['action'] %>" data-value="<%= condition_field['options'].present? ? condition_field['options'].find { |o| o['uuid'] == condition['value'] }&.dig('value') || condition['value'] : condition['value'] %>" data-operation="<%= condition['operation'] %>"></field-condition>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -79,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? %>
|
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? %>
|
||||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||||
<span><%= t('add_new') %></span>
|
<span><%= t('add_new') %></span>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
||||||
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
||||||
<% if field['type'] == 'signature' %>
|
<% if field['type'] == 'signature' %>
|
||||||
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
|
<% is_narrow = area['h'].positive? && ((area['w'] * local_assigns[:page_width]).to_f / (area['h'] * local_assigns[:page_height])) > 4.5 %>
|
||||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
||||||
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>">
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'initials', 'stamp']) && attachments_index[value].image? %>
|
<% elsif field['type'].in?(['image', 'initials', 'stamp', 'kba']) && attachments_index[value].image? %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
||||||
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
||||||
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
||||||
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? %>
|
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
||||||
|
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
||||||
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
||||||
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
|
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
|
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
|
||||||
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields: %>
|
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields: %>
|
||||||
</div>
|
</div>
|
||||||
<div id="list" class="hidden">
|
<div id="list" class="hidden">
|
||||||
<%= render 'list_form', template: @template %>
|
<%= render 'list_form', template: @template %>
|
||||||
|
|||||||
@@ -262,7 +262,7 @@
|
|||||||
<div class="w-full bg-base-300 py-1">
|
<div class="w-full bg-base-300 py-1">
|
||||||
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'].in?(['image', 'stamp']) && attachments_index[value].image? %>
|
<% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
|
||||||
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
|
<% data_fields = Submissions.filtered_conditions_fields(submitter).to_json %>
|
||||||
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
||||||
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
<% optional_invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['optional_invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-schema="<%= schema.to_json %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-field-labels="<%= configs[:with_field_labels] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template&.preferences&.dig('completed_redirect_url') %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template&.preferences&.dig('completed_message') || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-optional-invite-submitters="<%= optional_invite_submitters %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
<%= form_for @template, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
<%= form_for @template, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
||||||
<% if @base_template %>
|
|
||||||
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
|
||||||
<% end %>
|
|
||||||
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.linked_accounts.active.accessible_by(current_ability).exists? %>
|
|
||||||
<div class="form-control -mb-2 mt-2">
|
|
||||||
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.active.accessible_by(current_ability)].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control mt-4">
|
<div class="form-control mt-4">
|
||||||
<%= f.text_field :name, required: true, placeholder: t('document_name'), class: 'base-input', dir: 'auto' %>
|
<%= f.text_field :name, required: true, placeholder: t('document_name'), class: 'base-input', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +8,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<folder-autocomplete class="flex justify-between w-full">
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
<set-value data-on="blur" data-value="<%= TemplateFolder::DEFAULT_NAME %>" data-empty-only="true" class="peer w-full whitespace-nowrap">
|
<set-value data-on="blur" data-value="<%= TemplateFolder::DEFAULT_NAME %>" data-empty-only="true" class="peer w-full whitespace-nowrap">
|
||||||
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
|
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1" name="folder_name" value="<%= params[:folder_name].presence || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
|
||||||
</set-value>
|
</set-value>
|
||||||
<set-value data-on="click" data-value="" data-input-id="folder_name" class="peer-focus-within:hidden whitespace-nowrap">
|
<set-value data-on="click" data-value="" data-input-id="folder_name" class="peer-focus-within:hidden whitespace-nowrap">
|
||||||
<label for="folder_name" data-clear-on-focus="true" class="shrink-0 link mr-1.5 cursor-pointer">
|
<label for="folder_name" data-clear-on-focus="true" class="shrink-0 link mr-1.5 cursor-pointer">
|
||||||
@@ -26,6 +18,6 @@
|
|||||||
</folder-autocomplete>
|
</folder-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: @base_template ? t('submit') : t('create'), disabled_with: t('creating')), class: 'base-button' %>
|
<%= f.button button_title(title: t('create'), disabled_with: t('creating')), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:create, template) %>
|
<% if can?(:create, template) %>
|
||||||
<span class="tooltip tooltip-left" data-tip="<%= t('clone') %>">
|
<span class="tooltip tooltip-left" data-tip="<%= t('clone') %>">
|
||||||
<a href="<%= new_template_path(base_template_id: template.id) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
|
<a href="<%= new_template_clone_path(template) %>" data-turbo-frame="modal" class="btn btn-xs hover:btn-outline bg-base-200 btn-circle">
|
||||||
<%= svg_icon('copy', class: 'w-4 h-4') %>
|
<%= svg_icon('copy', class: 'w-4 h-4') %>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving'), title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete, data: { turbo_confirm: t('are_you_sure_') } %>
|
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving'), title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete, data: { turbo_confirm: t('are_you_sure_') } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:create, current_account.templates.new(author: current_user)) %>
|
<% if can?(:create, current_account.templates.new(author: current_user)) %>
|
||||||
<%= link_to new_template_path(base_template_id: template.id), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %>
|
<%= link_to new_template_clone_path(template), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %>
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<span class="flex items-center justify-center space-x-2">
|
||||||
<%= svg_icon('copy', class: 'w-6 h-6') %>
|
<%= svg_icon('copy', class: 'w-6 h-6') %>
|
||||||
<span class="inline"><%= t('clone') %></span>
|
<span class="inline"><%= t('clone') %></span>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<%= form_for @template, url: template_clone_index_path(@base_template), data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
||||||
|
<% accounts = Account.accessible_by(true_ability).where.not(id: true_user.account.testing_accounts).where(User.where(User.arel_table[:account_id].eq(Account.arel_table[:id])).arel.exists).active %>
|
||||||
|
<% if (can?(:manage, :tenants) || true_user != current_user) && accounts.where.not(id: current_account.id).exists? %>
|
||||||
|
<div class="form-control -mb-2 mt-2">
|
||||||
|
<%= select_tag :account_id, options_for_select([current_account, *accounts.order(:name)].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<%= f.text_field :name, required: true, placeholder: t('document_name'), class: 'base-input', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 mb-4 flex items-center justify-between">
|
||||||
|
<label for="folder_name" class="cursor-pointer">
|
||||||
|
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||||
|
</label>
|
||||||
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
|
<set-value data-on="blur" data-value="<%= TemplateFolder::DEFAULT_NAME %>" data-empty-only="true" class="peer w-full whitespace-nowrap">
|
||||||
|
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
|
||||||
|
</set-value>
|
||||||
|
<set-value data-on="click" data-value="" data-input-id="folder_name" class="peer-focus-within:hidden whitespace-nowrap">
|
||||||
|
<label for="folder_name" data-clear-on-focus="true" class="shrink-0 link mr-1.5 cursor-pointer">
|
||||||
|
<%= t('change_folder') %>
|
||||||
|
</label>
|
||||||
|
</set-value>
|
||||||
|
</folder-autocomplete>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.button button_title(title: t('submit'), disabled_with: t('creating')), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: t('clone_template') do %>
|
||||||
|
<%= render 'templates_clone/form' %>
|
||||||
|
<% end %>
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
if ENV['RAILS_ENV'] == 'production'
|
if ENV['RAILS_ENV'] == 'production'
|
||||||
|
if Process.uid.zero?
|
||||||
|
begin
|
||||||
|
workdir = ENV.fetch('WORKDIR', '.')
|
||||||
|
|
||||||
|
if File.exist?(workdir) && File.stat(workdir).uid != 2000
|
||||||
|
puts 'Changing the owner of the docuseal directory...' unless Dir.empty?(workdir)
|
||||||
|
|
||||||
|
FileUtils.chown_R(2000, 2000, workdir)
|
||||||
|
end
|
||||||
|
rescue StandardError
|
||||||
|
puts 'Unable to change docuseal directory owner'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if !ENV['AWS_SECRET_MANAGER_ID'].to_s.empty?
|
if !ENV['AWS_SECRET_MANAGER_ID'].to_s.empty?
|
||||||
require 'aws-sdk-secretsmanager'
|
require 'aws-sdk-secretsmanager'
|
||||||
|
|
||||||
@@ -30,12 +44,30 @@ if ENV['RAILS_ENV'] == 'production'
|
|||||||
File.write(dotenv_path, default_env)
|
File.write(dotenv_path, default_env)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if Process.uid.zero?
|
||||||
|
begin
|
||||||
|
File.chown(0, 0, dotenv_path)
|
||||||
|
File.chmod(0o600, dotenv_path)
|
||||||
|
rescue StandardError
|
||||||
|
puts 'Unable to set dotenv mod'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
database_url = ENV.fetch('DATABASE_URL', nil)
|
database_url = ENV.fetch('DATABASE_URL', nil)
|
||||||
|
|
||||||
Dotenv.load(dotenv_path)
|
Dotenv.load(dotenv_path)
|
||||||
|
|
||||||
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
|
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless Process.uid == 2000
|
||||||
|
begin
|
||||||
|
Process::Sys.setgid(2000)
|
||||||
|
Process::Sys.setuid(2000)
|
||||||
|
rescue StandardError
|
||||||
|
puts 'Unable to run as 2000:2000'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
|
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
|
||||||
|
|||||||
@@ -677,6 +677,9 @@ en: &en
|
|||||||
payment_field: Payment Field
|
payment_field: Payment Field
|
||||||
phone_field: Phone Field
|
phone_field: Phone Field
|
||||||
verification_field: Identity Verification
|
verification_field: Identity Verification
|
||||||
|
knowledge_based_authentication: Knowledge Based Authentication
|
||||||
|
kba_field: KBA
|
||||||
|
passed: Passed
|
||||||
identity_verification: Identity verification
|
identity_verification: Identity verification
|
||||||
paid_price: 'Paid %{price}'
|
paid_price: 'Paid %{price}'
|
||||||
verified: Verified
|
verified: Verified
|
||||||
@@ -910,6 +913,9 @@ en: &en
|
|||||||
complete_form_by_html: '<b>Submission completed</b> by %{submitter_name}'
|
complete_form_by_html: '<b>Submission completed</b> by %{submitter_name}'
|
||||||
start_verification_by_html: '<b>Identity verification started</b> by %{submitter_name}'
|
start_verification_by_html: '<b>Identity verification started</b> by %{submitter_name}'
|
||||||
complete_verification_by_html: '<b>Identity verification completed</b> by %{submitter_name} with %{provider}'
|
complete_verification_by_html: '<b>Identity verification completed</b> by %{submitter_name} with %{provider}'
|
||||||
|
start_kba_by_html: '<b>KBA started</b> by %{submitter_name}'
|
||||||
|
complete_kba_by_html: '<b>KBA completed</b> by %{submitter_name}'
|
||||||
|
fail_kba_by_html: '<b>KBA failed</b> by %{submitter_name}'
|
||||||
api_complete_form_by_html: '<b>Submission completed via API</b> by %{submitter_name}'
|
api_complete_form_by_html: '<b>Submission completed via API</b> by %{submitter_name}'
|
||||||
import_list:
|
import_list:
|
||||||
select_worksheet: Select Worksheet
|
select_worksheet: Select Worksheet
|
||||||
@@ -992,6 +998,9 @@ en: &en
|
|||||||
range_without_total: "%{from}-%{to} events"
|
range_without_total: "%{from}-%{to} events"
|
||||||
|
|
||||||
es: &es
|
es: &es
|
||||||
|
knowledge_based_authentication: Autenticación basada en el conocimiento
|
||||||
|
kba_field: KBA
|
||||||
|
passed: Aprobado
|
||||||
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Las plantillas que requieren autenticación en dos pasos por correo electrónico o teléfono no se pueden usar mediante un enlace compartido.
|
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Las plantillas que requieren autenticación en dos pasos por correo electrónico o teléfono no se pueden usar mediante un enlace compartido.
|
||||||
make_owner: Hacer propietario
|
make_owner: Hacer propietario
|
||||||
billing: Facturación
|
billing: Facturación
|
||||||
@@ -1883,6 +1892,9 @@ es: &es
|
|||||||
api_complete_form_by_html: '<b>Envío completado vía API</b> por %{submitter_name}'
|
api_complete_form_by_html: '<b>Envío completado vía API</b> por %{submitter_name}'
|
||||||
start_verification_by_html: '<b>Verificación de identidad iniciada</b> por %{submitter_name}'
|
start_verification_by_html: '<b>Verificación de identidad iniciada</b> por %{submitter_name}'
|
||||||
complete_verification_by_html: '<b>Verificación de identidad completada</b> por %{submitter_name} con %{provider}'
|
complete_verification_by_html: '<b>Verificación de identidad completada</b> por %{submitter_name} con %{provider}'
|
||||||
|
start_kba_by_html: '<b>KBA iniciado</b> por %{submitter_name}'
|
||||||
|
complete_kba_by_html: '<b>KBA completado</b> por %{submitter_name}'
|
||||||
|
fail_kba_by_html: '<b>KBA fallido</b> por %{submitter_name}'
|
||||||
import_list:
|
import_list:
|
||||||
select_worksheet: Seleccionar hoja de cálculo
|
select_worksheet: Seleccionar hoja de cálculo
|
||||||
open: Abrir
|
open: Abrir
|
||||||
@@ -1964,6 +1976,9 @@ es: &es
|
|||||||
range_without_total: "%{from}-%{to} eventos"
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
it: &it
|
it: &it
|
||||||
|
knowledge_based_authentication: Autenticazione basata sulla conoscenza
|
||||||
|
kba_field: KBA
|
||||||
|
passed: Superato
|
||||||
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: I modelli che richiedono 2FA via email o telefono non possono essere utilizzati tramite un link condiviso.
|
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: I modelli che richiedono 2FA via email o telefono non possono essere utilizzati tramite un link condiviso.
|
||||||
make_owner: Rendi proprietario
|
make_owner: Rendi proprietario
|
||||||
billing: Fatturazione
|
billing: Fatturazione
|
||||||
@@ -2856,6 +2871,9 @@ it: &it
|
|||||||
api_complete_form_by_html: '<b>Invio completato tramite API</b> da %{submitter_name}'
|
api_complete_form_by_html: '<b>Invio completato tramite API</b> da %{submitter_name}'
|
||||||
start_verification_by_html: "<b>Verifica dell'identità iniziata</b> da %{submitter_name}"
|
start_verification_by_html: "<b>Verifica dell'identità iniziata</b> da %{submitter_name}"
|
||||||
complete_verification_by_html: "<b>Verifica dell'identità completata</b> da %{submitter_name} con %{provider}"
|
complete_verification_by_html: "<b>Verifica dell'identità completata</b> da %{submitter_name} con %{provider}"
|
||||||
|
start_kba_by_html: "<b>KBA avviata</b> da %{submitter_name}"
|
||||||
|
complete_kba_by_html: "<b>KBA completata</b> da %{submitter_name}"
|
||||||
|
fail_kba_by_html: "<b>KBA non riuscita</b> da %{submitter_name}"
|
||||||
import_list:
|
import_list:
|
||||||
select_worksheet: Seleziona il foglio di lavoro
|
select_worksheet: Seleziona il foglio di lavoro
|
||||||
open: Apri
|
open: Apri
|
||||||
@@ -2937,6 +2955,9 @@ it: &it
|
|||||||
range_without_total: "%{from}-%{to} eventi"
|
range_without_total: "%{from}-%{to} eventi"
|
||||||
|
|
||||||
fr: &fr
|
fr: &fr
|
||||||
|
knowledge_based_authentication: Authentification basée sur la connaissance
|
||||||
|
kba_field: KBA
|
||||||
|
passed: Réussi
|
||||||
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Les modèles nécessitant une authentification à deux facteurs par e-mail ou téléphone ne peuvent pas être utilisés via un lien partagé.
|
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Les modèles nécessitant une authentification à deux facteurs par e-mail ou téléphone ne peuvent pas être utilisés via un lien partagé.
|
||||||
make_owner: Rendre propriétaire
|
make_owner: Rendre propriétaire
|
||||||
billing: Facturation
|
billing: Facturation
|
||||||
@@ -3825,6 +3846,9 @@ fr: &fr
|
|||||||
start_verification_by_html: "<b>Vérification d’identité démarrée</b> par %{submitter_name}"
|
start_verification_by_html: "<b>Vérification d’identité démarrée</b> par %{submitter_name}"
|
||||||
complete_verification_by_html: "<b>Vérification d’identité terminée</b> par %{submitter_name} avec %{provider}"
|
complete_verification_by_html: "<b>Vérification d’identité terminée</b> par %{submitter_name} avec %{provider}"
|
||||||
api_complete_form_by_html: "<b>Soumission terminée via API</b> par %{submitter_name}"
|
api_complete_form_by_html: "<b>Soumission terminée via API</b> par %{submitter_name}"
|
||||||
|
start_kba_by_html: "<b>KBA démarré</b> par %{submitter_name}"
|
||||||
|
complete_kba_by_html: "<b>KBA terminé</b> par %{submitter_name}"
|
||||||
|
fail_kba_by_html: "<b>KBA échoué</b> par %{submitter_name}"
|
||||||
import_list:
|
import_list:
|
||||||
select_worksheet: Sélectionner une feuille
|
select_worksheet: Sélectionner une feuille
|
||||||
open: Ouvrir
|
open: Ouvrir
|
||||||
@@ -3906,6 +3930,9 @@ fr: &fr
|
|||||||
range_without_total: "%{from}-%{to} événements"
|
range_without_total: "%{from}-%{to} événements"
|
||||||
|
|
||||||
pt: &pt
|
pt: &pt
|
||||||
|
knowledge_based_authentication: Autenticação baseada em conhecimento
|
||||||
|
kba_field: KBA
|
||||||
|
passed: Aprovado
|
||||||
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Modelos que exigem 2FA por e-mail ou telefone não podem ser usados por meio de um link compartilhado.
|
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Modelos que exigem 2FA por e-mail ou telefone não podem ser usados por meio de um link compartilhado.
|
||||||
make_owner: Tornar proprietário
|
make_owner: Tornar proprietário
|
||||||
billing: Pagamentos
|
billing: Pagamentos
|
||||||
@@ -4797,6 +4824,9 @@ pt: &pt
|
|||||||
start_verification_by_html: '<b>Verificação de identidade iniciada</b> por %{submitter_name}'
|
start_verification_by_html: '<b>Verificação de identidade iniciada</b> por %{submitter_name}'
|
||||||
complete_verification_by_html: '<b>Verificação de identidade concluída</b> por %{submitter_name} com %{provider}'
|
complete_verification_by_html: '<b>Verificação de identidade concluída</b> por %{submitter_name} com %{provider}'
|
||||||
api_complete_form_by_html: '<b>Submissão concluída via API</b> por %{submitter_name}'
|
api_complete_form_by_html: '<b>Submissão concluída via API</b> por %{submitter_name}'
|
||||||
|
start_kba_by_html: '<b>KBA iniciada</b> por %{submitter_name}'
|
||||||
|
complete_kba_by_html: '<b>KBA concluída</b> por %{submitter_name}'
|
||||||
|
fail_kba_by_html: '<b>KBA reprovada</b> por %{submitter_name}'
|
||||||
import_list:
|
import_list:
|
||||||
select_worksheet: Selecionar planilha
|
select_worksheet: Selecionar planilha
|
||||||
open: Abrir
|
open: Abrir
|
||||||
@@ -4878,6 +4908,9 @@ pt: &pt
|
|||||||
range_without_total: "%{from}-%{to} eventos"
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
de: &de
|
de: &de
|
||||||
|
knowledge_based_authentication: Wissensbasierte Authentifizierung
|
||||||
|
kba_field: KBA
|
||||||
|
passed: Bestanden
|
||||||
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Vorlagen, die eine Zwei-Faktor-Authentifizierung per E-Mail oder Telefon erfordern, können nicht über einen geteilten Link verwendet werden.
|
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Vorlagen, die eine Zwei-Faktor-Authentifizierung per E-Mail oder Telefon erfordern, können nicht über einen geteilten Link verwendet werden.
|
||||||
make_owner: Eigentümer machen
|
make_owner: Eigentümer machen
|
||||||
billing: Abrechnung
|
billing: Abrechnung
|
||||||
@@ -5769,6 +5802,9 @@ de: &de
|
|||||||
start_verification_by_html: '<b>Identitätsüberprüfung gestartet</b> von %{submitter_name}'
|
start_verification_by_html: '<b>Identitätsüberprüfung gestartet</b> von %{submitter_name}'
|
||||||
complete_verification_by_html: '<b>Identitätsüberprüfung abgeschlossen</b> von %{submitter_name} mit %{provider}'
|
complete_verification_by_html: '<b>Identitätsüberprüfung abgeschlossen</b> von %{submitter_name} mit %{provider}'
|
||||||
api_complete_form_by_html: '<b>Einreichung über API abgeschlossen</b> von %{submitter_name}'
|
api_complete_form_by_html: '<b>Einreichung über API abgeschlossen</b> von %{submitter_name}'
|
||||||
|
start_kba_by_html: '<b>KBA gestartet</b> von %{submitter_name}'
|
||||||
|
complete_kba_by_html: '<b>KBA abgeschlossen</b> von %{submitter_name}'
|
||||||
|
fail_kba_by_html: '<b>KBA fehlgeschlagen</b> von %{submitter_name}'
|
||||||
import_list:
|
import_list:
|
||||||
select_worksheet: Arbeitsblatt auswählen
|
select_worksheet: Arbeitsblatt auswählen
|
||||||
open: Öffnen
|
open: Öffnen
|
||||||
@@ -6238,6 +6274,9 @@ he:
|
|||||||
too_many_requests_try_again_later: יותר מדי בקשות. נסה שוב מאוחר יותר.
|
too_many_requests_try_again_later: יותר מדי בקשות. נסה שוב מאוחר יותר.
|
||||||
|
|
||||||
nl: &nl
|
nl: &nl
|
||||||
|
knowledge_based_authentication: Kennisgebaseerde authenticatie
|
||||||
|
kba_field: KBA
|
||||||
|
passed: Geslaagd
|
||||||
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Sjablonen waarvoor e-mail- of telefoon-2FA vereist is, kunnen niet via een gedeelde link worden gebruikt.
|
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Sjablonen waarvoor e-mail- of telefoon-2FA vereist is, kunnen niet via een gedeelde link worden gebruikt.
|
||||||
make_owner: Eigenaar maken
|
make_owner: Eigenaar maken
|
||||||
billing: Facturatie
|
billing: Facturatie
|
||||||
@@ -7126,6 +7165,9 @@ nl: &nl
|
|||||||
start_verification_by_html: "<b>Identiteitsverificatie gestart</b> door %{submitter_name}"
|
start_verification_by_html: "<b>Identiteitsverificatie gestart</b> door %{submitter_name}"
|
||||||
complete_verification_by_html: "<b>Identiteitsverificatie voltooid</b> door %{submitter_name} met %{provider}"
|
complete_verification_by_html: "<b>Identiteitsverificatie voltooid</b> door %{submitter_name} met %{provider}"
|
||||||
api_complete_form_by_html: "<b>Inzending via API voltooid</b> door %{submitter_name}"
|
api_complete_form_by_html: "<b>Inzending via API voltooid</b> door %{submitter_name}"
|
||||||
|
start_kba_by_html: "<b>KBA gestart</b> door %{submitter_name}"
|
||||||
|
complete_kba_by_html: "<b>KBA voltooid</b> door %{submitter_name}"
|
||||||
|
fail_kba_by_html: "<b>KBA mislukt</b> door %{submitter_name}"
|
||||||
import_list:
|
import_list:
|
||||||
select_worksheet: Werkblad selecteren
|
select_worksheet: Werkblad selecteren
|
||||||
open: Openen
|
open: Openen
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ Rails.application.routes.draw do
|
|||||||
resources :templates, only: %i[index], controller: 'templates_dashboard'
|
resources :templates, only: %i[index], controller: 'templates_dashboard'
|
||||||
resources :submissions_filters, only: %i[show], param: 'name'
|
resources :submissions_filters, only: %i[show], param: 'name'
|
||||||
resources :templates, only: %i[new create edit update show destroy] do
|
resources :templates, only: %i[new create edit update show destroy] do
|
||||||
|
resources :clone, only: %i[new create], controller: 'templates_clone'
|
||||||
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
|
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
|
||||||
resources :documents, only: %i[index create], controller: 'template_documents'
|
resources :documents, only: %i[index create], controller: 'template_documents'
|
||||||
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
|
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ aws_s3:
|
|||||||
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
|
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
|
||||||
region: <%= ENV['AWS_REGION'] || 'us-east-1' %>
|
region: <%= ENV['AWS_REGION'] || 'us-east-1' %>
|
||||||
bucket: <%= ENV['S3_ATTACHMENTS_BUCKET'] %>
|
bucket: <%= ENV['S3_ATTACHMENTS_BUCKET'] %>
|
||||||
|
<% if !ENV['S3_ENDPOINT'].to_s.empty? %>
|
||||||
|
endpoint: <%= ENV['S3_ENDPOINT'] %>
|
||||||
|
force_path_style: true
|
||||||
|
<% end %>
|
||||||
public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %>
|
public: <%= ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' %>
|
||||||
upload:
|
upload:
|
||||||
cache_control: 'public, max-age=31536000'
|
cache_control: 'public, max-age=31536000'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class CreateLockEvents < ActiveRecord::Migration[8.0]
|
|||||||
t.string :key, index: true, null: false
|
t.string :key, index: true, null: false
|
||||||
t.string :event_name, null: false
|
t.string :event_name, null: false
|
||||||
|
|
||||||
t.index %i[event_name key], unique: true, where: "event_name IN ('start', 'complete')"
|
t.index %i[event_name key], unique: adapter_name != 'Mysql2', where: "event_name IN ('start', 'complete')"
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ module Accounts
|
|||||||
new_template
|
new_template
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_recipient_form_fields(_account)
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
def load_signing_pkcs(account)
|
def load_signing_pkcs(account)
|
||||||
cert_data =
|
cert_data =
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module SendWebhookRequest
|
module SendWebhookRequest
|
||||||
USER_AGENT = 'DocuSeal.com Webhook'
|
USER_AGENT = 'DocuSeal.com Webhook'
|
||||||
|
|
||||||
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
|
LOCALHOSTS = DownloadUtils::LOCALHOSTS
|
||||||
|
|
||||||
MANUAL_ATTEMPT = 99_999
|
MANUAL_ATTEMPT = 99_999
|
||||||
AUTOMATED_RETRY_RANGE = 1..(MANUAL_ATTEMPT - 1)
|
AUTOMATED_RETRY_RANGE = 1..(MANUAL_ATTEMPT - 1)
|
||||||
|
|||||||
+2
-2
@@ -133,9 +133,9 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_from_submitters(template:, user:, submissions_attrs:, source:, with_template: true,
|
def create_from_submitters(template:, user:, submissions_attrs:, source:, with_template: true,
|
||||||
submitters_order: DEFAULT_SUBMITTERS_ORDER, params: {})
|
submitters_order: DEFAULT_SUBMITTERS_ORDER, params: {}, new_fields: nil)
|
||||||
Submissions::CreateFromSubmitters.call(
|
Submissions::CreateFromSubmitters.call(
|
||||||
template:, user:, submissions_attrs:, source:, submitters_order:, params:, with_template:
|
template:, user:, submissions_attrs:, source:, submitters_order:, params:, with_template:, new_fields:
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ module Submissions
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {}, with_template: true)
|
def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {}, with_template: true,
|
||||||
|
new_fields: nil)
|
||||||
preferences = Submitters.normalize_preferences(user.account, user, params)
|
preferences = Submitters.normalize_preferences(user.account, user, params)
|
||||||
|
|
||||||
submissions = Array.wrap(submissions_attrs).filter_map do |attrs|
|
submissions = Array.wrap(submissions_attrs).filter_map do |attrs|
|
||||||
@@ -67,7 +68,7 @@ module Submissions
|
|||||||
preferences: preferences.merge(submission_preferences))
|
preferences: preferences.merge(submission_preferences))
|
||||||
end
|
end
|
||||||
|
|
||||||
maybe_set_template_fields(submission, attrs[:submitters], with_template:)
|
maybe_set_template_fields(submission, attrs[:submitters], with_template:, new_fields:)
|
||||||
|
|
||||||
if submission.submitters.size > template.submitters.size
|
if submission.submitters.size > template.submitters.size
|
||||||
raise BaseError, 'Defined more signing parties than in template'
|
raise BaseError, 'Defined more signing parties than in template'
|
||||||
@@ -92,7 +93,6 @@ module Submissions
|
|||||||
|
|
||||||
submissions
|
submissions
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
|
||||||
|
|
||||||
def maybe_enqueue_expire_at(submissions)
|
def maybe_enqueue_expire_at(submissions)
|
||||||
submissions.each do |submission|
|
submissions.each do |submission|
|
||||||
@@ -135,7 +135,8 @@ module Submissions
|
|||||||
}.compact_blank
|
}.compact_blank
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil, with_template: true)
|
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil, with_template: true,
|
||||||
|
new_fields: nil)
|
||||||
template_fields = (submission.template_fields || submission.template.fields).deep_dup
|
template_fields = (submission.template_fields || submission.template.fields).deep_dup
|
||||||
|
|
||||||
submitters = submission.template_submitters || submission.template.submitters
|
submitters = submission.template_submitters || submission.template.submitters
|
||||||
@@ -149,9 +150,9 @@ module Submissions
|
|||||||
process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid)
|
process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid)
|
||||||
end
|
end
|
||||||
|
|
||||||
if template_fields != (submission.template_fields || submission.template.fields) ||
|
if template_fields != (submission.template_fields || submission.template.fields) || new_fields.present? ||
|
||||||
submitters_attrs.any? { |e| e[:completed].present? } || !with_template || submission.variables.present?
|
submitters_attrs.any? { |e| e[:completed].present? } || !with_template || submission.variables.present?
|
||||||
submission.template_fields = template_fields
|
submission.template_fields = new_fields ? new_fields + template_fields : template_fields
|
||||||
submission.template_schema = submission.template.schema if submission.template_schema.blank?
|
submission.template_schema = submission.template.schema if submission.template_schema.blank?
|
||||||
submission.variables_schema = submission.template.variables_schema if submission.template &&
|
submission.variables_schema = submission.template.variables_schema if submission.template &&
|
||||||
submission.variables_schema.blank?
|
submission.variables_schema.blank?
|
||||||
@@ -159,6 +160,7 @@ module Submissions
|
|||||||
|
|
||||||
submission
|
submission
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def merge_submitters_and_fields(submitter_attrs, template_submitters, template_fields)
|
def merge_submitters_and_fields(submitter_attrs, template_submitters, template_fields)
|
||||||
selected_submitters = submitter_attrs[:roles].map do |role|
|
selected_submitters = submitter_attrs[:roles].map do |role|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ module Submissions
|
|||||||
|
|
||||||
raise NotCompletedYet unless submission.submitters.all?(&:completed_at?)
|
raise NotCompletedYet unless submission.submitters.all?(&:completed_at?)
|
||||||
|
|
||||||
|
total_wait_time ||= 0
|
||||||
key = [KEY_PREFIX, submission.id].join(':')
|
key = [KEY_PREFIX, submission.id].join(':')
|
||||||
|
|
||||||
if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||||
@@ -39,7 +40,9 @@ module Submissions
|
|||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
sleep WAIT_FOR_RETRY
|
sleep WAIT_FOR_RETRY
|
||||||
|
|
||||||
retry
|
total_wait_time += WAIT_FOR_RETRY
|
||||||
|
|
||||||
|
total_wait_time > CHECK_COMPLETE_TIMEOUT ? raise : retry
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
Rails.logger.error(e)
|
Rails.logger.error(e)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ module Submissions
|
|||||||
|
|
||||||
raise NotCompletedYet unless submitter.completed_at?
|
raise NotCompletedYet unless submitter.completed_at?
|
||||||
|
|
||||||
|
total_wait_time ||= 0
|
||||||
key = [KEY_PREFIX, submitter.id].join(':')
|
key = [KEY_PREFIX, submitter.id].join(':')
|
||||||
|
|
||||||
if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||||
@@ -38,8 +39,9 @@ module Submissions
|
|||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
sleep WAIT_FOR_RETRY
|
sleep WAIT_FOR_RETRY
|
||||||
|
total_wait_time += WAIT_FOR_RETRY
|
||||||
|
|
||||||
retry
|
total_wait_time > CHECK_COMPLETE_TIMEOUT ? raise : retry
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
Rails.logger.error(e)
|
Rails.logger.error(e)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ module Submissions
|
|||||||
|
|
||||||
raise NotCompletedYet unless submitter.completed_at?
|
raise NotCompletedYet unless submitter.completed_at?
|
||||||
|
|
||||||
|
total_wait_time ||= 0
|
||||||
key = ['result_attachments', submitter.id].join(':')
|
key = ['result_attachments', submitter.id].join(':')
|
||||||
|
|
||||||
return submitter.documents if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
return submitter.documents if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||||
@@ -35,8 +36,9 @@ module Submissions
|
|||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
sleep WAIT_FOR_RETRY
|
sleep WAIT_FOR_RETRY
|
||||||
|
total_wait_time += WAIT_FOR_RETRY
|
||||||
|
|
||||||
retry
|
total_wait_time > CHECK_COMPLETE_TIMEOUT ? raise : retry
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
Rails.logger.error(e)
|
Rails.logger.error(e)
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ module Submissions
|
|||||||
e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
|
e['type'] == 'verification' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
is_kba_passed =
|
||||||
|
submission.template_fields.any? do |e|
|
||||||
|
e['type'] == 'kba' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
|
||||||
|
end
|
||||||
|
|
||||||
info_rows = [
|
info_rows = [
|
||||||
[
|
[
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
@@ -288,6 +293,9 @@ module Submissions
|
|||||||
is_id_verified && {
|
is_id_verified && {
|
||||||
text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n"
|
text: "#{I18n.t('identity_verification')}: #{I18n.t('verified')}\n"
|
||||||
},
|
},
|
||||||
|
is_kba_passed && {
|
||||||
|
text: "#{I18n.t('knowledge_based_authentication')}: #{I18n.t('passed')}\n"
|
||||||
|
},
|
||||||
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
|
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
|
||||||
completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
|
completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
|
||||||
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
|
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
|
||||||
@@ -353,7 +361,7 @@ module Submissions
|
|||||||
text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
line_spacing: 1.3, padding: [0, 0, 2, 0]
|
line_spacing: 1.3, padding: [0, 0, 2, 0]
|
||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials stamp]) &&
|
if field['type'].in?(%w[image signature initials stamp kba]) &&
|
||||||
(attachment = submitter.attachments.find { |a| a.uuid == value }) &&
|
(attachment = submitter.attachments.find { |a| a.uuid == value }) &&
|
||||||
attachment.image?
|
attachment.image?
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ module Submissions
|
|||||||
module GenerateCombinedAttachment
|
module GenerateCombinedAttachment
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submitter)
|
def call(submitter, with_audit: true)
|
||||||
pdf = build_combined_pdf(submitter)
|
pdf = build_combined_pdf(submitter, with_audit:)
|
||||||
|
|
||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
account = submission.account
|
account = submission.account
|
||||||
@@ -39,7 +39,7 @@ module Submissions
|
|||||||
blob: ActiveStorage::Blob.create_and_upload!(
|
blob: ActiveStorage::Blob.create_and_upload!(
|
||||||
io: io.tap(&:rewind), filename: "#{submission.name || submission.template.name}.pdf"
|
io: io.tap(&:rewind), filename: "#{submission.name || submission.template.name}.pdf"
|
||||||
),
|
),
|
||||||
name: 'combined_document',
|
name: with_audit ? 'combined_document' : 'merged_document',
|
||||||
record: submission
|
record: submission
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -58,14 +58,16 @@ module Submissions
|
|||||||
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_combined_pdf(submitter)
|
def build_combined_pdf(submitter, with_audit:)
|
||||||
pdfs_index = Submissions::GenerateResultAttachments.generate_pdfs(submitter)
|
pdfs_index = Submissions::GenerateResultAttachments.generate_pdfs(submitter)
|
||||||
|
|
||||||
audit_trail = I18n.with_locale(submitter.account.locale) do
|
if with_audit
|
||||||
Submissions::GenerateAuditTrail.build_audit_trail(submitter.submission)
|
audit_trail = I18n.with_locale(submitter.account.locale) do
|
||||||
end
|
Submissions::GenerateAuditTrail.build_audit_trail(submitter.submission)
|
||||||
|
end
|
||||||
|
|
||||||
audit_trail.dispatch_message(:complete_objects)
|
audit_trail.dispatch_message(:complete_objects)
|
||||||
|
end
|
||||||
|
|
||||||
result = HexaPDF::Document.new
|
result = HexaPDF::Document.new
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ module Submissions
|
|||||||
pdf.pages.each { |page| result.pages << result.import(page) }
|
pdf.pages.each { |page| result.pages << result.import(page) }
|
||||||
end
|
end
|
||||||
|
|
||||||
audit_trail.pages.each { |page| result.pages << result.import(page) }
|
audit_trail&.pages&.each { |page| result.pages << result.import(page) }
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Submissions
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(submission, values_hash: nil, submitter: nil)
|
def call(submission, values_hash: nil, submitter: nil, merge: false)
|
||||||
values_hash ||= if submitter
|
values_hash ||= if submitter
|
||||||
build_submitter_values_hash(submitter)
|
build_submitter_values_hash(submitter)
|
||||||
else
|
else
|
||||||
@@ -42,48 +42,74 @@ module Submissions
|
|||||||
|
|
||||||
template = submission.template
|
template = submission.template
|
||||||
|
|
||||||
image_pdfs = []
|
if merge
|
||||||
original_documents = submission.schema_documents.preload(:blob)
|
result = HexaPDF::Document.new
|
||||||
|
|
||||||
result_attachments =
|
(submission.template_schema || template.schema).each do |item|
|
||||||
(submission.template_schema || template.schema).filter_map do |item|
|
|
||||||
pdf = pdfs_index[item['attachment_uuid']]
|
pdf = pdfs_index[item['attachment_uuid']]
|
||||||
|
|
||||||
next if pdf.nil?
|
next unless pdf
|
||||||
|
|
||||||
if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
pdf.dispatch_message(:complete_objects)
|
||||||
pdf = GenerateResultAttachments.normalize_image_pdf(pdf)
|
|
||||||
|
|
||||||
image_pdfs << pdf
|
pdf.pages.each { |page| result.pages << result.import(page) }
|
||||||
end
|
|
||||||
|
|
||||||
build_pdf_attachment(pdf:, submission:, submitter:,
|
|
||||||
uuid: item['attachment_uuid'],
|
|
||||||
values_hash:,
|
|
||||||
name: item['name'])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
|
attachment = build_pdf_attachment(
|
||||||
|
pdf: result,
|
||||||
images_pdf =
|
|
||||||
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
|
||||||
pdf.pages.each { |page| doc.pages << doc.import(page) }
|
|
||||||
end
|
|
||||||
|
|
||||||
images_pdf = GenerateResultAttachments.normalize_image_pdf(images_pdf)
|
|
||||||
|
|
||||||
images_pdf_attachment =
|
|
||||||
build_pdf_attachment(
|
|
||||||
pdf: images_pdf,
|
|
||||||
submission:,
|
submission:,
|
||||||
submitter:,
|
|
||||||
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
|
|
||||||
values_hash:,
|
values_hash:,
|
||||||
name: submission.name || template.name
|
name: 'preview_merged_document',
|
||||||
|
filename: "#{submission.name || template.name}.pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
ApplicationRecord.no_touching do
|
ApplicationRecord.no_touching { attachment.save! }
|
||||||
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
|
||||||
|
[attachment]
|
||||||
|
else
|
||||||
|
image_pdfs = []
|
||||||
|
original_documents = submission.schema_documents.preload(:blob)
|
||||||
|
|
||||||
|
result_attachments =
|
||||||
|
(submission.template_schema || template.schema).filter_map do |item|
|
||||||
|
pdf = pdfs_index[item['attachment_uuid']]
|
||||||
|
|
||||||
|
next if pdf.nil?
|
||||||
|
|
||||||
|
if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
||||||
|
pdf = GenerateResultAttachments.normalize_image_pdf(pdf)
|
||||||
|
|
||||||
|
image_pdfs << pdf
|
||||||
|
end
|
||||||
|
|
||||||
|
build_pdf_attachment(pdf:, submission:, submitter:,
|
||||||
|
uuid: item['attachment_uuid'],
|
||||||
|
values_hash:,
|
||||||
|
filename: "#{item['name']}.pdf")
|
||||||
|
end
|
||||||
|
|
||||||
|
return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
|
||||||
|
|
||||||
|
images_pdf =
|
||||||
|
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
||||||
|
pdf.pages.each { |page| doc.pages << doc.import(page) }
|
||||||
|
end
|
||||||
|
|
||||||
|
images_pdf = GenerateResultAttachments.normalize_image_pdf(images_pdf)
|
||||||
|
|
||||||
|
images_pdf_attachment =
|
||||||
|
build_pdf_attachment(
|
||||||
|
pdf: images_pdf,
|
||||||
|
submission:,
|
||||||
|
submitter:,
|
||||||
|
uuid: GenerateResultAttachments.images_pdf_uuid(original_documents.select(&:image?)),
|
||||||
|
values_hash:,
|
||||||
|
filename: "#{submission.name || template.name}.pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
ApplicationRecord.no_touching do
|
||||||
|
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -102,7 +128,8 @@ module Submissions
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_pdf_attachment(pdf:, submission:, submitter:, uuid:, name:, values_hash:)
|
def build_pdf_attachment(pdf:, submission:, filename:, values_hash:, submitter: nil, uuid: nil,
|
||||||
|
name: 'preview_documents')
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@@ -114,13 +141,13 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
ActiveStorage::Attachment.new(
|
ActiveStorage::Attachment.new(
|
||||||
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename: "#{name}.pdf"),
|
blob: ActiveStorage::Blob.create_and_upload!(io: io.tap(&:rewind), filename:),
|
||||||
io_data: io.string,
|
io_data: io.string,
|
||||||
metadata: { original_uuid: uuid,
|
metadata: { original_uuid: uuid,
|
||||||
values_hash:,
|
values_hash:,
|
||||||
analyzed: true,
|
analyzed: true,
|
||||||
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
|
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) }.compact,
|
||||||
name: 'preview_documents',
|
name: name,
|
||||||
record: submitter || submission
|
record: submitter || submission
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -333,12 +333,12 @@ module Submissions
|
|||||||
|
|
||||||
result = nil
|
result = nil
|
||||||
|
|
||||||
if area['h']&.positive? && (area['w'].to_f / area['h']) > 6
|
area_x = area['x'] * width
|
||||||
area_x = area['x'] * width
|
area_y = area['y'] * height
|
||||||
area_y = area['y'] * height
|
area_w = area['w'] * width
|
||||||
area_w = area['w'] * width
|
area_h = area['h'] * height
|
||||||
area_h = area['h'] * height
|
|
||||||
|
|
||||||
|
if area_h.positive? && (area_w.to_f / area_h) > 4.5
|
||||||
half_width = area_w / 2.0
|
half_width = area_w / 2.0
|
||||||
scale = [half_width / image.width, area_h / image.height].min
|
scale = [half_width / image.width, area_h / image.height].min
|
||||||
image_width = image.width * scale
|
image_width = image.width * scale
|
||||||
@@ -396,7 +396,7 @@ module Submissions
|
|||||||
font:,
|
font:,
|
||||||
font_size: base_font_size)
|
font_size: base_font_size)
|
||||||
|
|
||||||
result = layouter.fit([text], area['w'] * width, base_font_size / 0.65)
|
result = layouter.fit([text], area_w, base_font_size / 0.65)
|
||||||
|
|
||||||
break if result.status == :success
|
break if result.status == :success
|
||||||
|
|
||||||
@@ -405,36 +405,36 @@ module Submissions
|
|||||||
break if id_string.length < 8
|
break if id_string.length < 8
|
||||||
end
|
end
|
||||||
|
|
||||||
reason_result = layouter.fit([reason_text], area['w'] * width, height)
|
reason_result = layouter.fit([reason_text], area_w, height)
|
||||||
text_height = result.lines.sum(&:height) + reason_result.lines.sum(&:height)
|
text_height = result.lines.sum(&:height) + reason_result.lines.sum(&:height)
|
||||||
|
|
||||||
image_height = (area['h'] * height) - text_height
|
image_height = area_h - text_height
|
||||||
image_height = (area['h'] * height) / 2 if image_height < (area['h'] * height) / 2
|
image_height = area_h / 2 if image_height < area_h / 2
|
||||||
|
|
||||||
scale = [(area['w'] * width) / image.width, image_height / image.height].min
|
scale = [area_w / image.width, image_height / image.height].min
|
||||||
|
|
||||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
||||||
|
|
||||||
layouter.fit([text], area['w'] * width, base_font_size / 0.65)
|
layouter.fit([text], area_w, base_font_size / 0.65)
|
||||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
.draw(canvas, area_x + TEXT_LEFT_MARGIN,
|
||||||
height - (area['y'] * height) - TEXT_TOP_MARGIN - image_height)
|
height - area_y - TEXT_TOP_MARGIN - image_height)
|
||||||
|
|
||||||
layouter.fit([reason_text], area['w'] * width, reason_result.lines.sum(&:height))
|
layouter.fit([reason_text], area_w, reason_result.lines.sum(&:height))
|
||||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
.draw(canvas, area_x + TEXT_LEFT_MARGIN,
|
||||||
height - (area['y'] * height) - TEXT_TOP_MARGIN -
|
height - area_y - TEXT_TOP_MARGIN -
|
||||||
result.lines.sum(&:height) - image_height)
|
result.lines.sum(&:height) - image_height)
|
||||||
|
|
||||||
canvas.image(
|
canvas.image(
|
||||||
io,
|
io,
|
||||||
at: [
|
at: [
|
||||||
(area['x'] * width) + (area['w'] * width / 2) - ((image.width * scale) / 2),
|
area_x + (area_w / 2) - ((image.width * scale) / 2),
|
||||||
height - (area['y'] * height) - (image.height * scale / 2) - (image_height / 2)
|
height - area_y - (image.height * scale / 2) - (image_height / 2)
|
||||||
],
|
],
|
||||||
width: image.width * scale,
|
width: image.width * scale,
|
||||||
height: image.height * scale
|
height: image.height * scale
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
when 'image', 'signature', 'initials', 'stamp'
|
when 'image', 'signature', 'initials', 'stamp', 'kba'
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
|
|
||||||
image =
|
image =
|
||||||
@@ -648,7 +648,7 @@ module Submissions
|
|||||||
text_params = { font:, fill_color:, font_size: }
|
text_params = { font:, fill_color:, font_size: }
|
||||||
text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
|
text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1)
|
||||||
|
|
||||||
text = HexaPDF::Layout::TextFragment.create(value, **text_params)
|
text = HexaPDF::Layout::TextFragment.create(value.tr("\u00A0", ' '), **text_params)
|
||||||
|
|
||||||
lines = layouter.fit([text], area['w'] * width, height).lines
|
lines = layouter.fit([text], area['w'] * width, height).lines
|
||||||
box_height = lines.sum(&:height)
|
box_height = lines.sum(&:height)
|
||||||
|
|||||||
@@ -4,21 +4,23 @@ module Submissions
|
|||||||
module NormalizeParamUtils
|
module NormalizeParamUtils
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def normalize_submissions_params!(submissions_params, template)
|
def normalize_submissions_params!(submissions_params, template, add_fields: false)
|
||||||
attachments = []
|
attachments = []
|
||||||
|
fields = []
|
||||||
|
|
||||||
Array.wrap(submissions_params).each do |submission|
|
Array.wrap(submissions_params).each do |submission|
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
submission[:submitters].each_with_index do |submitter, index|
|
||||||
_, new_attachments = normalize_submitter_params!(submitter, template, index)
|
_, new_attachments, new_fields = normalize_submitter_params!(submitter, template, index, add_fields:)
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
|
fields.push(*new_fields)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
[submissions_params, attachments]
|
[submissions_params, attachments, fields]
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil)
|
def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil, add_fields: false)
|
||||||
with_values = submitter_params[:values].present?
|
with_values = submitter_params[:values].present?
|
||||||
|
|
||||||
default_values = with_values ? submitter_params[:values] : {}
|
default_values = with_values ? submitter_params[:values] : {}
|
||||||
@@ -30,18 +32,23 @@ module Submissions
|
|||||||
|
|
||||||
return submitter_params if default_values.blank?
|
return submitter_params if default_values.blank?
|
||||||
|
|
||||||
values, new_attachments =
|
values, new_attachments, new_fields =
|
||||||
Submitters::NormalizeValues.call(template,
|
Submitters::NormalizeValues.call(
|
||||||
default_values,
|
template,
|
||||||
submitter_name: submitter_params[:role] ||
|
default_values,
|
||||||
template.submitters.dig(index, 'name'),
|
submitter_name: submitter_params[:role] ||
|
||||||
role_names: submitter_params[:roles],
|
(submitter_params[:uuid] &&
|
||||||
for_submitter:,
|
template.submitters.find { |s| s['uuid'] == submitter_params[:uuid] }&.dig('name')) ||
|
||||||
throw_errors: !with_values)
|
template.submitters.dig(index, 'name'),
|
||||||
|
role_names: submitter_params[:roles],
|
||||||
|
for_submitter:,
|
||||||
|
add_fields:,
|
||||||
|
throw_errors: !with_values
|
||||||
|
)
|
||||||
|
|
||||||
submitter_params[:values] = values
|
submitter_params[:values] = values
|
||||||
|
|
||||||
[submitter_params, new_attachments]
|
[submitter_params, new_attachments, new_fields]
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_default_value_attachments!(attachments, submitters)
|
def save_default_value_attachments!(attachments, submitters)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module Submitters
|
|||||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||||
AccountConfig::REUSE_SIGNATURE_KEY,
|
AccountConfig::REUSE_SIGNATURE_KEY,
|
||||||
|
AccountConfig::WITH_FIELD_LABELS_KEY,
|
||||||
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||||
@@ -35,13 +36,14 @@ module Submitters
|
|||||||
enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true
|
enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true
|
||||||
with_submitter_timezone = find_safe_value(configs, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY) == true
|
with_submitter_timezone = find_safe_value(configs, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY) == true
|
||||||
with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false
|
with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false
|
||||||
|
with_field_labels = find_safe_value(configs, AccountConfig::WITH_FIELD_LABELS_KEY) != false
|
||||||
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
|
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
|
||||||
|
|
||||||
attrs = { completed_button:, with_typed_signature:, with_confetti:,
|
attrs = { completed_button:, with_typed_signature:, with_confetti:,
|
||||||
reuse_signature:, with_decline:, with_partial_download:,
|
reuse_signature:, with_decline:, with_partial_download:,
|
||||||
policy_links:, enforce_signing_order:, completed_message:,
|
policy_links:, enforce_signing_order:, completed_message:,
|
||||||
require_signing_reason:, prefill_signature:, with_submitter_timezone:,
|
require_signing_reason:, prefill_signature:, with_submitter_timezone:,
|
||||||
with_signature_id_reason:, with_signature_id: }
|
with_signature_id_reason:, with_signature_id:, with_field_labels: }
|
||||||
|
|
||||||
keys.each do |key|
|
keys.each do |key|
|
||||||
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value
|
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ module Submitters
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template, values, submitter_name: nil, role_names: nil, for_submitter: nil, throw_errors: false)
|
# rubocop:disable Metrics
|
||||||
|
def call(template, values, submitter_name: nil, role_names: nil, for_submitter: nil, throw_errors: false,
|
||||||
|
add_fields: false)
|
||||||
fields =
|
fields =
|
||||||
if role_names.present?
|
if role_names.present?
|
||||||
fetch_roles_fields(template, roles: role_names)
|
fetch_roles_fields(template, roles: role_names)
|
||||||
@@ -29,6 +31,8 @@ module Submitters
|
|||||||
fields_name_index = build_fields_index(fields)
|
fields_name_index = build_fields_index(fields)
|
||||||
|
|
||||||
attachments = []
|
attachments = []
|
||||||
|
new_fields = []
|
||||||
|
recipient_form_fields = nil
|
||||||
|
|
||||||
normalized_values = values.to_h.each_with_object({}) do |(key, value), acc|
|
normalized_values = values.to_h.each_with_object({}) do |(key, value), acc|
|
||||||
next if key.blank?
|
next if key.blank?
|
||||||
@@ -40,7 +44,23 @@ module Submitters
|
|||||||
if value_fields.blank?
|
if value_fields.blank?
|
||||||
value_fields = fields_name_index[key].presence || fields_name_index[key.to_s.downcase]
|
value_fields = fields_name_index[key].presence || fields_name_index[key.to_s.downcase]
|
||||||
|
|
||||||
raise(UnknownFieldName, "Unknown field: #{key}") if value_fields.blank? && throw_errors
|
if value_fields.blank?
|
||||||
|
if add_fields && (recipient_form_fields ||= Accounts.load_recipient_form_fields(template.account))
|
||||||
|
new_field = recipient_form_fields.to_a.find { |e| e['name'] == key }.deep_dup
|
||||||
|
|
||||||
|
if new_field && fields.present?
|
||||||
|
new_field = new_field.except('conditions')
|
||||||
|
.merge('uuid' => SecureRandom.uuid,
|
||||||
|
'readonly' => true,
|
||||||
|
'submitter_uuid' => fields.first['submitter_uuid'])
|
||||||
|
|
||||||
|
new_fields.push(new_field)
|
||||||
|
value_fields = [new_field]
|
||||||
|
end
|
||||||
|
elsif throw_errors
|
||||||
|
raise(UnknownFieldName, "Unknown field: #{key}")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
next if value_fields.blank?
|
next if value_fields.blank?
|
||||||
@@ -59,8 +79,9 @@ module Submitters
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
[normalized_values, attachments]
|
[normalized_values, attachments, new_fields]
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def normalize_value(field, value)
|
def normalize_value(field, value)
|
||||||
if field['type'] == 'checkbox'
|
if field['type'] == 'checkbox'
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetch_field_value(field, value, attachments_index, expires_at: nil)
|
def fetch_field_value(field, value, attachments_index, expires_at: nil)
|
||||||
if field['type'].in?(%w[image signature initials stamp payment])
|
if field['type'].in?(%w[image signature initials stamp payment kba])
|
||||||
rails_storage_proxy_url(attachments_index[value], expires_at:)
|
rails_storage_proxy_url(attachments_index[value], expires_at:)
|
||||||
elsif field['type'] == 'file'
|
elsif field['type'] == 'file'
|
||||||
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) }
|
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e], expires_at:) }
|
||||||
|
|||||||
@@ -59,25 +59,25 @@ module Templates
|
|||||||
CHECKBOXES = ['☐', '□'].freeze
|
CHECKBOXES = ['☐', '□'].freeze
|
||||||
|
|
||||||
# rubocop:disable Metrics, Style
|
# rubocop:disable Metrics, Style
|
||||||
def call(io, attachment: nil, confidence: 0.3, temperature: 1, inference: Templates::ImageToFields,
|
def call(io, attachment: nil, confidence: 0.3, temperature: 1, inference: Templates::ImageToFields, nms: 0.1,
|
||||||
nms: 0.1, split_page: false, aspect_ratio: true, padding: 20, regexp_type: true, &)
|
nmm: 0.5, split_page: false, aspect_ratio: true, padding: 20, regexp_type: true, &)
|
||||||
fields, head_node =
|
fields, head_node =
|
||||||
if attachment&.image?
|
if attachment&.image?
|
||||||
process_image_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
process_image_attachment(io, attachment:, confidence:, nms:, nmm:, split_page:, inference:,
|
||||||
temperature:, aspect_ratio:, padding:, &)
|
temperature:, aspect_ratio:, padding:, &)
|
||||||
else
|
else
|
||||||
process_pdf_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
process_pdf_attachment(io, attachment:, confidence:, nms:, nmm:, split_page:, inference:,
|
||||||
temperature:, aspect_ratio:, regexp_type:, padding:, &)
|
temperature:, aspect_ratio:, regexp_type:, padding:, &)
|
||||||
end
|
end
|
||||||
|
|
||||||
[fields, head_node]
|
[fields, head_node]
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_image_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
|
def process_image_attachment(io, attachment:, confidence:, nms:, nmm:, temperature:, inference:,
|
||||||
split_page: false, aspect_ratio: false, padding: nil)
|
split_page: false, aspect_ratio: false, padding: nil)
|
||||||
image = Vips::Image.new_from_buffer(io.read, '')
|
image = Vips::Image.new_from_buffer(io.read, '')
|
||||||
|
|
||||||
fields = inference.call(image, confidence:, nms:, split_page:,
|
fields = inference.call(image, confidence:, nms:, nmm:, split_page:,
|
||||||
temperature:, aspect_ratio:, padding:)
|
temperature:, aspect_ratio:, padding:)
|
||||||
|
|
||||||
fields = sort_fields(fields, y_threshold: 10.0 / image.height)
|
fields = sort_fields(fields, y_threshold: 10.0 / image.height)
|
||||||
@@ -104,7 +104,7 @@ module Templates
|
|||||||
[fields, nil]
|
[fields, nil]
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_pdf_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
|
def process_pdf_attachment(io, attachment:, confidence:, nms:, nmm:, temperature:, inference:,
|
||||||
split_page: false, aspect_ratio: false, padding: nil, regexp_type: false)
|
split_page: false, aspect_ratio: false, padding: nil, regexp_type: false)
|
||||||
doc = Pdfium::Document.open_bytes(io.read)
|
doc = Pdfium::Document.open_bytes(io.read)
|
||||||
|
|
||||||
@@ -114,11 +114,14 @@ module Templates
|
|||||||
fields = doc.page_count.times.flat_map do |page_number|
|
fields = doc.page_count.times.flat_map do |page_number|
|
||||||
page = doc.get_page(page_number)
|
page = doc.get_page(page_number)
|
||||||
|
|
||||||
data, width, height = page.render_to_bitmap(width: inference::RESOLUTION * 1.5)
|
size_key = page.width > page.height ? :width : :height
|
||||||
|
size = padding ? inference.resolution * 1.5 : inference.resolution
|
||||||
|
|
||||||
|
data, width, height = page.render_to_bitmap(size_key => size)
|
||||||
|
|
||||||
image = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
image = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
||||||
|
|
||||||
fields = inference.call(image, confidence: confidence / 4.0, nms:, split_page:,
|
fields = inference.call(image, confidence: confidence / 3.0, nms:, nmm:, split_page:,
|
||||||
temperature:, aspect_ratio:, padding:)
|
temperature:, aspect_ratio:, padding:)
|
||||||
|
|
||||||
text_fields = extract_text_fields_from_page(page)
|
text_fields = extract_text_fields_from_page(page)
|
||||||
@@ -126,8 +129,8 @@ module Templates
|
|||||||
|
|
||||||
fields = sort_fields(fields, y_threshold: 10.0 / image.height)
|
fields = sort_fields(fields, y_threshold: 10.0 / image.height)
|
||||||
|
|
||||||
fields = increase_confidence_for_overlapping_fields(fields, text_fields)
|
fields = increase_confidence_for_overlapping_fields(fields, text_fields, confidence:)
|
||||||
fields = increase_confidence_for_overlapping_fields(fields, line_fields)
|
fields = increase_confidence_for_overlapping_fields(fields, line_fields, confidence:)
|
||||||
|
|
||||||
fields = fields.reject { |f| f.confidence < confidence }
|
fields = fields.reject { |f| f.confidence < confidence }
|
||||||
|
|
||||||
@@ -477,10 +480,11 @@ module Templates
|
|||||||
!(box1.endx < box2.x || box2.endx < box1.x || box1.endy < box2.y || box2.endy < box1.y)
|
!(box1.endx < box2.x || box2.endx < box1.x || box1.endy < box2.y || box2.endy < box1.y)
|
||||||
end
|
end
|
||||||
|
|
||||||
def increase_confidence_for_overlapping_fields(image_fields, text_fields, by: 1.0)
|
def increase_confidence_for_overlapping_fields(image_fields, text_fields, confidence: 1, by: 1.0)
|
||||||
return image_fields if text_fields.blank?
|
return image_fields if text_fields.blank?
|
||||||
|
|
||||||
image_fields.map do |image_field|
|
image_fields.map do |image_field|
|
||||||
|
next if image_field.confidence >= confidence
|
||||||
next if image_field.type != 'text'
|
next if image_field.type != 'text'
|
||||||
|
|
||||||
text_fields.each do |text_field|
|
text_fields.each do |text_field|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module Templates
|
|||||||
|
|
||||||
MODEL_PATH = Rails.root.join('tmp/model.onnx')
|
MODEL_PATH = Rails.root.join('tmp/model.onnx')
|
||||||
|
|
||||||
RESOLUTION = 704
|
INPUT_NAMES = %w[images input].freeze
|
||||||
|
|
||||||
ID_TO_CLASS = %w[text checkbox].freeze
|
ID_TO_CLASS = %w[text checkbox].freeze
|
||||||
|
|
||||||
@@ -26,13 +26,15 @@ module Templates
|
|||||||
CPU_THREADS = Etc.nprocessors
|
CPU_THREADS = Etc.nprocessors
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(image, confidence: 0.3, nms: 0.1, temperature: 1,
|
def call(image, confidence: 0.3, nms: 0.1, nmm: 0.9, temperature: 1,
|
||||||
split_page: false, aspect_ratio: true, padding: nil, resolution: RESOLUTION)
|
split_page: false, aspect_ratio: true, padding: nil, resolution: self.resolution)
|
||||||
base_image = image.extract_band(0, n: 3)
|
image = image.extract_band(0, n: 3) if image.bands > 3
|
||||||
|
|
||||||
trimmed_base, base_offset_x, base_offset_y = trim_image_with_padding(base_image, padding)
|
trimmed_base, base_offset_x, base_offset_y = trim_image_with_padding(image, padding)
|
||||||
|
|
||||||
if split_page && image.height > image.width
|
if model_v2?
|
||||||
|
detections = call_v2(trimmed_base, base_offset_x, base_offset_y, split_page, confidence:, resolution:)
|
||||||
|
elsif split_page && image.height > image.width
|
||||||
regions = build_split_image_regions(trimmed_base)
|
regions = build_split_image_regions(trimmed_base)
|
||||||
|
|
||||||
detections = { xyxy: Numo::SFloat[], confidence: Numo::SFloat[], class_id: Numo::Int32[] }
|
detections = { xyxy: Numo::SFloat[], confidence: Numo::SFloat[], class_id: Numo::Int32[] }
|
||||||
@@ -66,11 +68,132 @@ module Templates
|
|||||||
detections = postprocess_outputs(boxes, logits, transform_info, confidence:, temperature:, resolution:)
|
detections = postprocess_outputs(boxes, logits, transform_info, confidence:, temperature:, resolution:)
|
||||||
end
|
end
|
||||||
|
|
||||||
detections = apply_nms(detections, nms)
|
detections = apply_nms_nmm(detections, nms_threshold: nms, nmm_threshold: nmm)
|
||||||
|
|
||||||
build_fields_from_detections(detections, image)
|
build_fields_from_detections(detections, image)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def call_v2(image, offset_x, offset_y, split_page, confidence:, resolution:)
|
||||||
|
if split_page && image.height > image.width
|
||||||
|
regions = build_split_image_regions(image)
|
||||||
|
|
||||||
|
detections = { xyxy: Numo::SFloat[], confidence: Numo::SFloat[], class_id: Numo::Int32[] }
|
||||||
|
|
||||||
|
regions.reduce(detections) do |acc, r|
|
||||||
|
next acc if r[:img].height <= 0 || r[:img].width <= 0
|
||||||
|
|
||||||
|
input_tensor, orig_size_tensor, transform_info = preprocess_image_v2(r[:img], resolution)
|
||||||
|
|
||||||
|
outputs = model.predict({ 'images' => input_tensor, 'orig_target_sizes' => orig_size_tensor },
|
||||||
|
output_type: :numo)
|
||||||
|
|
||||||
|
boxes = outputs['boxes'][0, true, true]
|
||||||
|
labels = outputs['labels'][0, true]
|
||||||
|
scores = outputs['scores'][0, true]
|
||||||
|
|
||||||
|
postprocess_outputs_v2(boxes, labels, scores, acc,
|
||||||
|
offset_x:, offset_y: offset_y + r[:offset_y],
|
||||||
|
confidence:, transform_info:)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
input_tensor, orig_size_tensor, transform_info = preprocess_image_v2(image, resolution)
|
||||||
|
|
||||||
|
outputs = model.predict({ 'images' => input_tensor, 'orig_target_sizes' => orig_size_tensor },
|
||||||
|
output_type: :numo)
|
||||||
|
|
||||||
|
boxes = outputs['boxes'][0, true, true]
|
||||||
|
labels = outputs['labels'][0, true]
|
||||||
|
scores = outputs['scores'][0, true]
|
||||||
|
|
||||||
|
postprocess_outputs_v2(boxes, labels, scores, offset_x:, offset_y:,
|
||||||
|
confidence:, transform_info:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def preprocess_image_v2(image, resolution)
|
||||||
|
image = image.extract_band(0, n: 3) if image.bands > 3
|
||||||
|
|
||||||
|
ratio = [resolution.to_f / image.width, resolution.to_f / image.height].min
|
||||||
|
new_width = (image.width * ratio).to_i
|
||||||
|
new_height = (image.height * ratio).to_i
|
||||||
|
|
||||||
|
image = image.resize(ratio, vscale: ratio, kernel: :linear) if ratio != 1
|
||||||
|
|
||||||
|
pad_w = (resolution - new_width) / 2
|
||||||
|
pad_h = (resolution - new_height) / 2
|
||||||
|
|
||||||
|
padded = image.embed(pad_w, pad_h, resolution, resolution, background: [255, 255, 255])
|
||||||
|
|
||||||
|
padded /= 255.0
|
||||||
|
|
||||||
|
img_array = Numo::SFloat.from_binary(padded.write_to_memory, [resolution, resolution, 3])
|
||||||
|
|
||||||
|
img_array = img_array.transpose(2, 0, 1)
|
||||||
|
|
||||||
|
input_tensor = img_array.reshape(1, 3, resolution, resolution)
|
||||||
|
|
||||||
|
orig_size_tensor = Numo::Int64[[resolution, resolution]]
|
||||||
|
|
||||||
|
transform_info = { ratio: ratio, pad_w: pad_w, pad_h: pad_h }
|
||||||
|
|
||||||
|
[input_tensor, orig_size_tensor, transform_info]
|
||||||
|
end
|
||||||
|
|
||||||
|
def postprocess_outputs_v2(boxes, labels, scores, detections = nil, offset_x:, offset_y:, confidence:,
|
||||||
|
transform_info:)
|
||||||
|
keep_mask = scores.gt(confidence)
|
||||||
|
keep_indices = keep_mask.where
|
||||||
|
|
||||||
|
if keep_indices.empty?
|
||||||
|
detections || {
|
||||||
|
xyxy: Numo::SFloat[],
|
||||||
|
confidence: Numo::SFloat[],
|
||||||
|
class_id: Numo::Int32[]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
scores = scores[keep_indices]
|
||||||
|
labels = labels[keep_indices]
|
||||||
|
boxes_xyxy = boxes[keep_indices, true]
|
||||||
|
|
||||||
|
ratio = transform_info[:ratio]
|
||||||
|
pad_w = transform_info[:pad_w]
|
||||||
|
pad_h = transform_info[:pad_h]
|
||||||
|
|
||||||
|
boxes_xyxy[true, 0] = ((boxes_xyxy[true, 0] - pad_w) / ratio) + offset_x
|
||||||
|
boxes_xyxy[true, 1] = ((boxes_xyxy[true, 1] - pad_h) / ratio) + offset_y
|
||||||
|
boxes_xyxy[true, 2] = ((boxes_xyxy[true, 2] - pad_w) / ratio) + offset_x
|
||||||
|
boxes_xyxy[true, 3] = ((boxes_xyxy[true, 3] - pad_h) / ratio) + offset_y
|
||||||
|
|
||||||
|
if detections
|
||||||
|
existing_n = detections[:xyxy].shape[0]
|
||||||
|
new_n = boxes_xyxy.shape[0]
|
||||||
|
total = existing_n + new_n
|
||||||
|
|
||||||
|
xyxy = Numo::SFloat.zeros(total, 4)
|
||||||
|
conf = Numo::SFloat.zeros(total)
|
||||||
|
cls = Numo::Int32.zeros(total)
|
||||||
|
|
||||||
|
if existing_n.positive?
|
||||||
|
xyxy[0...existing_n, true] = detections[:xyxy]
|
||||||
|
conf[0...existing_n] = detections[:confidence]
|
||||||
|
cls[0...existing_n] = detections[:class_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
xyxy[existing_n...(existing_n + new_n), true] = boxes_xyxy
|
||||||
|
conf[existing_n...(existing_n + new_n)] = scores
|
||||||
|
cls[existing_n...(existing_n + new_n)] = labels
|
||||||
|
|
||||||
|
{ xyxy: xyxy, confidence: conf, class_id: cls }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
xyxy: boxes_xyxy,
|
||||||
|
confidence: scores,
|
||||||
|
class_id: labels
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def build_split_image_regions(image)
|
def build_split_image_regions(image)
|
||||||
half_h = image.height / 2
|
half_h = image.height / 2
|
||||||
top_h = half_h
|
top_h = half_h
|
||||||
@@ -122,6 +245,8 @@ module Templates
|
|||||||
|
|
||||||
left, top, trim_width, trim_height = image.find_trim(threshold: 10, background: [255, 255, 255])
|
left, top, trim_width, trim_height = image.find_trim(threshold: 10, background: [255, 255, 255])
|
||||||
|
|
||||||
|
trim_width = [trim_width, image.width - (left * 1.9)].max
|
||||||
|
|
||||||
padded_left = [left - padding, 0].max
|
padded_left = [left - padding, 0].max
|
||||||
padded_top = [top - padding, 0].max
|
padded_top = [top - padding, 0].max
|
||||||
padded_right = [left + trim_width + padding, image.width].min
|
padded_right = [left + trim_width + padding, image.width].min
|
||||||
@@ -172,7 +297,10 @@ module Templates
|
|||||||
[img_array.reshape(1, 3, resolution, resolution), transform_info]
|
[img_array.reshape(1, 3, resolution, resolution), transform_info]
|
||||||
end
|
end
|
||||||
|
|
||||||
def nms(boxes, scores, iou_threshold = 0.5)
|
def nms(detections, iou_threshold = 0.5)
|
||||||
|
boxes = detections[:xyxy]
|
||||||
|
scores = detections[:confidence]
|
||||||
|
|
||||||
return Numo::Int32[] if boxes.shape[0].zero?
|
return Numo::Int32[] if boxes.shape[0].zero?
|
||||||
|
|
||||||
x1 = boxes[true, 0]
|
x1 = boxes[true, 0]
|
||||||
@@ -208,11 +336,77 @@ module Templates
|
|||||||
order = order[inds + 1]
|
order = order[inds + 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
Numo::Int32.cast(keep)
|
{
|
||||||
|
xyxy: detections[:xyxy][keep, true],
|
||||||
|
confidence: detections[:confidence][keep],
|
||||||
|
class_id: detections[:class_id][keep]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def nmm(detections, overlap_threshold = 0.9, confidence: 0.3)
|
||||||
|
boxes = detections[:xyxy]
|
||||||
|
scores = detections[:confidence]
|
||||||
|
classes = detections[:class_id]
|
||||||
|
|
||||||
|
return detections if boxes.shape[0].zero?
|
||||||
|
|
||||||
|
x1 = boxes[true, 0]
|
||||||
|
y1 = boxes[true, 1]
|
||||||
|
x2 = boxes[true, 2]
|
||||||
|
y2 = boxes[true, 3]
|
||||||
|
|
||||||
|
areas = (x2 - x1) * (y2 - y1)
|
||||||
|
order = areas.sort_index.reverse
|
||||||
|
|
||||||
|
keep = []
|
||||||
|
|
||||||
|
while order.size.positive?
|
||||||
|
i = order[0]
|
||||||
|
keep << i
|
||||||
|
break if order.size == 1
|
||||||
|
|
||||||
|
xx1 = Numo::SFloat.maximum(x1[i], x1[order[1..]])
|
||||||
|
yy1 = Numo::SFloat.maximum(y1[i], y1[order[1..]])
|
||||||
|
xx2 = Numo::SFloat.minimum(x2[i], x2[order[1..]])
|
||||||
|
yy2 = Numo::SFloat.minimum(y2[i], y2[order[1..]])
|
||||||
|
|
||||||
|
w = Numo::SFloat.maximum(0.0, xx2 - xx1)
|
||||||
|
h = Numo::SFloat.maximum(0.0, yy2 - yy1)
|
||||||
|
|
||||||
|
intersection = w * h
|
||||||
|
|
||||||
|
overlap = intersection / areas[order[1..]]
|
||||||
|
|
||||||
|
merge_mask = scores[i] > confidence ? (overlap.gt(overlap_threshold) & classes[order[1..]].eq(classes[i])) : nil
|
||||||
|
|
||||||
|
if merge_mask && (merge_inds = merge_mask.where).size.positive?
|
||||||
|
candidates = order[merge_inds + 1]
|
||||||
|
|
||||||
|
scores[i] = [scores[i], scores[candidates].max].max
|
||||||
|
|
||||||
|
x1[i] = [x1[i], x1[candidates].min].min
|
||||||
|
y1[i] = [y1[i], y1[candidates].min].min
|
||||||
|
x2[i] = [x2[i], x2[candidates].max].max
|
||||||
|
y2[i] = [y2[i], y2[candidates].max].max
|
||||||
|
end
|
||||||
|
|
||||||
|
if merge_mask
|
||||||
|
inds = (~merge_mask).where
|
||||||
|
order = order[inds + 1]
|
||||||
|
else
|
||||||
|
order = order[1..]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
xyxy: detections[:xyxy][keep, true],
|
||||||
|
confidence: detections[:confidence][keep],
|
||||||
|
class_id: detections[:class_id][keep]
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def postprocess_outputs(boxes, logits, transform_info, detections = nil, confidence: 0.3, temperature: 1,
|
def postprocess_outputs(boxes, logits, transform_info, detections = nil, confidence: 0.3, temperature: 1,
|
||||||
resolution: RESOLUTION)
|
resolution: self.resolution)
|
||||||
scaled_logits = logits / temperature
|
scaled_logits = logits / temperature
|
||||||
|
|
||||||
probs = 1.0 / (1.0 + Numo::NMath.exp(-scaled_logits))
|
probs = 1.0 / (1.0 + Numo::NMath.exp(-scaled_logits))
|
||||||
@@ -304,28 +498,32 @@ module Templates
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_nms(detections, threshold = 0.5)
|
def apply_nms_nmm(detections, nms_threshold: 0.5, nmm_threshold: 0.7, confidence: 0.3)
|
||||||
return detections if detections[:xyxy].shape[0].zero?
|
return detections if detections[:xyxy].shape[0].zero?
|
||||||
|
|
||||||
keep_indices = nms(detections[:xyxy], detections[:confidence], threshold)
|
nms_result = nms(detections, nms_threshold)
|
||||||
|
|
||||||
{
|
nmm(nms_result, nmm_threshold, confidence:)
|
||||||
xyxy: detections[:xyxy][keep_indices, true],
|
|
||||||
confidence: detections[:confidence][keep_indices],
|
|
||||||
class_id: detections[:class_id][keep_indices]
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def model
|
def model
|
||||||
@model ||= OnnxRuntime::Model.new(
|
@model ||= OnnxRuntime::Model.new(
|
||||||
MODEL_PATH.to_s,
|
MODEL_PATH.to_s,
|
||||||
inter_op_num_threads: CPU_THREADS,
|
inter_op_num_threads: 1,
|
||||||
intra_op_num_threads: CPU_THREADS,
|
intra_op_num_threads: CPU_THREADS,
|
||||||
enable_mem_pattern: false,
|
enable_mem_pattern: false,
|
||||||
enable_cpu_mem_arena: false,
|
enable_cpu_mem_arena: Docuseal.multitenant? || Rails.env.development?,
|
||||||
providers: ['CPUExecutionProvider']
|
providers: ['CPUExecutionProvider']
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resolution
|
||||||
|
@resolution ||= model.inputs.find { |i| INPUT_NAMES.include?(i[:name]) }.dig(:shape, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
def model_v2?
|
||||||
|
@model_v2 ||= model.inputs.pluck(:name).include?('orig_target_sizes')
|
||||||
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user