Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
154d706be1 | ||
|
|
aaae257c75 | ||
|
|
ec944a2283 | ||
|
|
69d63ecffe | ||
|
|
06a1e2992d | ||
|
|
ec9293bac0 | ||
|
|
8bf727c43c | ||
|
|
708b8afdf5 | ||
|
|
6f7b9b1ebf | ||
|
|
4f48c4fd9f | ||
|
|
9dac90e6cc | ||
|
|
0741a6f485 | ||
|
|
1f338dfd2b | ||
|
|
0310c26edd | ||
|
|
e43570d4c1 | ||
|
|
d7142c2935 | ||
|
|
fa6dc04ef1 | ||
|
|
c5ec7a4b37 | ||
|
|
bd3d643318 | ||
|
|
1ae635963c | ||
|
|
a61c3e798d | ||
|
|
931addedbe | ||
|
|
9aa4a547cf | ||
|
|
82a7df5e3f | ||
|
|
0ff87383d0 | ||
|
|
ea3dec443f |
No files matched your search
@@ -63,7 +63,7 @@ RSpec/MultipleExpectations:
|
|||||||
Max: 25
|
Max: 25
|
||||||
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 50
|
Max: 500
|
||||||
|
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 15
|
Max: 15
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ module Api
|
|||||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
|
submitters.preload(template: { folder: :parent_folder },
|
||||||
|
submission: [:submitters, { audit_trail_attachment: :blob,
|
||||||
combined_document_attachment: :blob }],
|
combined_document_attachment: :blob }],
|
||||||
documents_attachments: :blob, attachments_attachments: :blob),
|
documents_attachments: :blob, attachments_attachments: :blob),
|
||||||
field: :completed_at
|
field: :completed_at
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ module Api
|
|||||||
submissions = filter_submissions(submissions, params)
|
submissions = filter_submissions(submissions, params)
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
template: :folder,
|
template: { folder: :parent_folder },
|
||||||
combined_document_attachment: :blob,
|
combined_document_attachment: :blob,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
@@ -104,9 +104,10 @@ module Api
|
|||||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
if params[:template_folder].present?
|
if params[:template_folder].present?
|
||||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id)
|
folders =
|
||||||
|
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
|
||||||
|
|
||||||
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids })
|
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
|
||||||
end
|
end
|
||||||
|
|
||||||
if params.key?(:archived)
|
if params.key?(:archived)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module Api
|
|||||||
def index
|
def index
|
||||||
templates = filter_templates(@templates, params)
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, :folder))
|
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||||
|
|
||||||
schema_documents =
|
schema_documents =
|
||||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||||
@@ -92,9 +92,9 @@ module Api
|
|||||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
if params[:folder].present?
|
if params[:folder].present?
|
||||||
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id)
|
folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
|
||||||
|
|
||||||
templates = templates.where(folder_id: folder_ids)
|
templates = templates.where(folder_id: folders.pluck(:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
templates
|
templates
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ class StartFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show completed]
|
around_action :with_browser_locale, only: %i[show update completed]
|
||||||
before_action :maybe_redirect_com, only: %i[show completed]
|
before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
before_action :load_resubmit_submitter, only: :update
|
before_action :load_resubmit_submitter, only: :update
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
before_action :authorize_start!, only: :update
|
before_action :authorize_start!, only: :update
|
||||||
|
|
||||||
|
COOKIES_TTL = 12.hours
|
||||||
|
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ class StartFormController < ApplicationController
|
|||||||
.submitters.new(account_id: @template.account_id,
|
.submitters.new(account_id: @template.account_id,
|
||||||
uuid: (filter_undefined_submitters(@template).first ||
|
uuid: (filter_undefined_submitters(@template).first ||
|
||||||
@template.submitters.first)['uuid'])
|
@template.submitters.first)['uuid'])
|
||||||
|
render :email_verification if params[:email_verification]
|
||||||
else
|
else
|
||||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -49,17 +53,10 @@ class StartFormController < ApplicationController
|
|||||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.errors.blank? && @submitter.save
|
if @template.preferences['shared_link_2fa'] == true
|
||||||
if is_new_record
|
handle_require_2fa(@submitter, is_new_record:)
|
||||||
WebhookUrls.enqueue_events(@submitter.submission, 'submission.created')
|
elsif @submitter.errors.blank? && @submitter.save
|
||||||
|
enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||||
SearchEntries.enqueue_reindex(@submitter)
|
|
||||||
|
|
||||||
if @submitter.submission.expire_at?
|
|
||||||
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
|
|
||||||
'submission_id' => @submitter.submission_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
else
|
else
|
||||||
@@ -89,6 +86,16 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def enqueue_new_submitter_jobs(submitter)
|
||||||
|
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submitter)
|
||||||
|
|
||||||
|
return unless submitter.submission.expire_at?
|
||||||
|
|
||||||
|
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
|
||||||
|
end
|
||||||
|
|
||||||
def load_resubmit_submitter
|
def load_resubmit_submitter
|
||||||
@resubmit_submitter =
|
@resubmit_submitter =
|
||||||
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
||||||
@@ -123,7 +130,7 @@ class StartFormController < ApplicationController
|
|||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.where(declined_at: nil)
|
.where(declined_at: nil)
|
||||||
.where(external_id: nil)
|
.where(external_id: nil)
|
||||||
.where(ip: [nil, request.remote_ip])
|
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
|
||||||
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
||||||
.find_or_initialize_by(find_params)
|
.find_or_initialize_by(find_params)
|
||||||
|
|
||||||
@@ -173,7 +180,7 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
return current_user.slice(:email) if params[:selfsign]
|
return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
|
||||||
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
|
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
|
||||||
|
|
||||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
@@ -197,4 +204,39 @@ class StartFormController < ApplicationController
|
|||||||
I18n.t('not_found')
|
I18n.t('not_found')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_require_2fa(submitter, is_new_record:)
|
||||||
|
return render :show, status: :unprocessable_entity if submitter.errors.present?
|
||||||
|
|
||||||
|
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
||||||
|
|
||||||
|
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
|
||||||
|
if submitter.save
|
||||||
|
enqueue_new_submitter_jobs(submitter) if is_new_record
|
||||||
|
|
||||||
|
if is_otp_verified
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
|
||||||
|
|
||||||
|
cookies.encrypted[:email_2fa_slug] =
|
||||||
|
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to submit_form_path(submitter.slug)
|
||||||
|
else
|
||||||
|
render :show, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||||
|
|
||||||
|
render :email_verification
|
||||||
|
end
|
||||||
|
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
||||||
|
redirect_to start_form_path(submitter.submission.template.slug,
|
||||||
|
params: submitter_params.merge(email_verification: true)),
|
||||||
|
alert: e.message
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
redirect_to start_form_path(submitter.submission.template.slug,
|
||||||
|
params: submitter_params.merge(email_verification: true)),
|
||||||
|
alert: I18n.t(:too_many_attempts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StartFormEmail2faSendController < ApplicationController
|
||||||
|
around_action :with_browser_locale
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
@template = Template.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
|
.submitters.new(**submitter_params, account_id: @template.account_id)
|
||||||
|
|
||||||
|
Submitters.send_shared_link_email_verification_code(@submitter, request:)
|
||||||
|
|
||||||
|
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
|
||||||
|
|
||||||
|
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
|
||||||
|
**redir_params
|
||||||
|
rescue Submitters::UnableToSendCode => e
|
||||||
|
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
|
||||||
|
alert: e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
params.require(:submitter).permit(:name, :email, :phone)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,6 +12,7 @@ class SubmissionEventsController < ApplicationController
|
|||||||
'send_2fa_sms' => '2fa',
|
'send_2fa_sms' => '2fa',
|
||||||
'send_sms' => 'send',
|
'send_sms' => 'send',
|
||||||
'phone_verified' => 'phone_check',
|
'phone_verified' => 'phone_check',
|
||||||
|
'email_verified' => 'email_check',
|
||||||
'click_sms' => 'hand_click',
|
'click_sms' => 'hand_click',
|
||||||
'decline_form' => 'x',
|
'decline_form' => 'x',
|
||||||
'start_verification' => 'player_play',
|
'start_verification' => 'player_play',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
before_action :load_submitter, only: %i[show update completed]
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
before_action :maybe_render_locked_page, only: :show
|
before_action :maybe_render_locked_page, only: :show
|
||||||
|
before_action :maybe_require_link_2fa, only: %i[show update]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ class SubmitFormController < ApplicationController
|
|||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.template&.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,6 +81,15 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def maybe_require_link_2fa
|
||||||
|
return if @submitter.submission.source != 'link'
|
||||||
|
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||||
|
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
||||||
|
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
||||||
|
|
||||||
|
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_render_locked_page
|
def maybe_render_locked_page
|
||||||
return render :archived if @submitter.submission.template&.archived_at? ||
|
return render :archived if @submitter.submission.template&.archived_at? ||
|
||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
|
|||||||
@@ -3,14 +3,31 @@
|
|||||||
class TemplateFoldersAutocompleteController < ApplicationController
|
class TemplateFoldersAutocompleteController < ApplicationController
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 30
|
||||||
|
|
||||||
def index
|
def index
|
||||||
templates_query = Template.accessible_by(current_ability).where(archived_at: nil)
|
parent_name, name =
|
||||||
|
if params[:parent_name].present?
|
||||||
|
[params[:parent_name], params[:q]]
|
||||||
|
else
|
||||||
|
params[:q].to_s.split(' /', 2).map(&:squish)
|
||||||
|
end
|
||||||
|
|
||||||
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
|
if name
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
|
||||||
|
else
|
||||||
|
name = parent_name
|
||||||
|
end
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name archived_at])
|
template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
|
||||||
|
Template.accessible_by(current_ability))
|
||||||
|
|
||||||
|
name = name.to_s.downcase
|
||||||
|
|
||||||
|
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
|
||||||
|
|
||||||
|
render json: template_folders.preload(:parent_folder)
|
||||||
|
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
|
||||||
|
.as_json(only: %i[name archived_at], methods: %i[full_name])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -5,13 +5,39 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
helper_method :selected_order
|
helper_method :selected_order
|
||||||
|
|
||||||
|
TEMPLATES_PER_PAGE = 12
|
||||||
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
@templates = Template.active.accessible_by(current_ability)
|
||||||
|
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
||||||
.preload(:author, :template_accesses)
|
.preload(:author, :template_accesses)
|
||||||
|
|
||||||
|
@template_folders =
|
||||||
|
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
||||||
|
|
||||||
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
|
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||||
|
|
||||||
|
if @templates.exists?
|
||||||
@templates = Templates.search(current_user, @templates, params[:q])
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
limit =
|
||||||
|
if @template_folders.size < 4
|
||||||
|
TEMPLATES_PER_PAGE
|
||||||
|
else
|
||||||
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||||
|
|
||||||
|
load_related_submissions if params[:q].present? && @templates.blank?
|
||||||
|
else
|
||||||
|
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||||
|
|
||||||
|
@templates = @templates.none
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
@@ -40,4 +66,21 @@ class TemplateFoldersController < ApplicationController
|
|||||||
def template_folder_params
|
def template_folder_params
|
||||||
params.require(:template_folder).permit(:name)
|
params.require(:template_folder).permit(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_related_submissions
|
||||||
|
@related_submissions =
|
||||||
|
Submission.accessible_by(current_ability)
|
||||||
|
.where(archived_at: nil)
|
||||||
|
.where(template_id: current_account.templates.active
|
||||||
|
.where(folder: [@template_folder, *@template_folder.subfolders])
|
||||||
|
.select(:id))
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
|
@templates = @templates.where.not(archived_at: nil)
|
||||||
|
.preload(:author, :template_accesses, folder: :parent_folder)
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
@templates = Templates.search(current_user, @templates, params[:q])
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||||
|
|
||||||
|
return unless params[:q].present? && @templates.blank?
|
||||||
|
|
||||||
|
@related_submissions =
|
||||||
|
Submission.accessible_by(current_ability)
|
||||||
|
.joins(:template)
|
||||||
|
.where.not(templates: { archived_at: nil })
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -11,10 +11,11 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
helper_method :selected_order
|
helper_method :selected_order
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
@template_folders =
|
||||||
|
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
||||||
|
|
||||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
@template_folders = sort_template_folders(@template_folders, current_user, selected_order)
|
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
@@ -68,40 +69,6 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
Templates.search(current_user, rel, params[:q])
|
Templates.search(current_user, rel, params[:q])
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_template_folders(template_folders, current_user, order)
|
|
||||||
case order
|
|
||||||
when 'used_at'
|
|
||||||
subquery =
|
|
||||||
Template.left_joins(:submissions)
|
|
||||||
.group(:folder_id)
|
|
||||||
.where(account_id: current_user.account_id)
|
|
||||||
.select(
|
|
||||||
:folder_id,
|
|
||||||
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
|
|
||||||
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
|
|
||||||
)
|
|
||||||
|
|
||||||
template_folders = template_folders.joins(
|
|
||||||
Template.arel_table
|
|
||||||
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
|
|
||||||
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
|
|
||||||
.join_sources
|
|
||||||
)
|
|
||||||
|
|
||||||
template_folders.order(
|
|
||||||
Arel::Nodes::Case.new
|
|
||||||
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
|
|
||||||
.then(Template.arel_table[:submission_created_at_max])
|
|
||||||
.else(Template.arel_table[:updated_at_max])
|
|
||||||
.desc
|
|
||||||
)
|
|
||||||
when 'name'
|
|
||||||
template_folders.order(name: :asc)
|
|
||||||
else
|
|
||||||
template_folders.order(id: :desc)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def selected_order
|
def selected_order
|
||||||
@selected_order ||=
|
@selected_order ||=
|
||||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
|
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||||
|
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
require_all_submitters submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa
|
||||||
default_expire_at_duration
|
default_expire_at_duration shared_link_2fa
|
||||||
default_expire_at
|
default_expire_at
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class WebhookEventsController < ApplicationController
|
|||||||
turbo_stream.replace(helpers.dom_id(@webhook_event),
|
turbo_stream.replace(helpers.dom_id(@webhook_event),
|
||||||
partial: 'event_row',
|
partial: 'event_row',
|
||||||
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
|
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
|
||||||
turbo_stream.replace("drawer_events_#{helpers.dom_id(@webhook_event)}",
|
turbo_stream.replace(helpers.dom_id(@webhook_event, :drawer_events),
|
||||||
partial: 'drawer_events',
|
partial: 'drawer_events',
|
||||||
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
|
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
|||||||
import AppTour from './elements/app_tour'
|
import AppTour from './elements/app_tour'
|
||||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||||
|
import PageContainer from './elements/page_container'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ safeRegisterElement('app-tour', AppTour)
|
|||||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
safeRegisterElement('check-on-click', CheckOnClick)
|
safeRegisterElement('check-on-click', CheckOnClick)
|
||||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||||
|
safeRegisterElement('page-container', PageContainer)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
|||||||
@@ -147,3 +147,11 @@ button[disabled] .enabled {
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
outline-color: hsl(var(--bc) / 0.2);
|
outline-color: hsl(var(--bc) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-times {
|
||||||
|
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-courier {
|
||||||
|
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
static [target.static] = [
|
static [target.static] = [
|
||||||
'form',
|
'form',
|
||||||
'fileDropzone',
|
'fileDropzone',
|
||||||
|
'folderDropzone',
|
||||||
'fileDropzoneLoading'
|
'fileDropzoneLoading'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement {
|
|||||||
window.addEventListener('dragleave', this.onWindowDragleave)
|
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
|
||||||
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||||
|
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
|
||||||
|
|
||||||
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||||
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||||
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||||
|
|
||||||
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
el?.addEventListener('dragover', this.onDragover)
|
el?.addEventListener('dragover', this.onDragover)
|
||||||
el?.addEventListener('dragleave', this.onDragleave)
|
el?.addEventListener('dragleave', this.onDragleave)
|
||||||
})
|
})
|
||||||
@@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement {
|
|||||||
onTemplateDragStart = (e) => {
|
onTemplateDragStart = (e) => {
|
||||||
const id = e.target.href.split('/').pop()
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden'))
|
||||||
|
this.folderDropzone?.classList?.remove('hidden')
|
||||||
|
window.flash?.remove()
|
||||||
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -98,13 +104,13 @@ export default targetable(class extends HTMLElement {
|
|||||||
el.classList.add('opacity-50')
|
el.classList.add('opacity-50')
|
||||||
|
|
||||||
if (e.dataTransfer.files.length) {
|
if (e.dataTransfer.files.length) {
|
||||||
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
|
const params = new URLSearchParams({ folder_name: el.innerText.trim() }).toString()
|
||||||
|
|
||||||
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
|
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
|
||||||
} else {
|
} else {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
formData.append('name', el.innerText)
|
formData.append('name', el.dataset.fullName)
|
||||||
|
|
||||||
fetch(`/templates/${templateId}/folder`, {
|
fetch(`/templates/${templateId}/folder`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDropNewFolder (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const templateId = e.dataTransfer.getData('template_id')
|
||||||
|
|
||||||
|
const a = document.createElement('a')
|
||||||
|
|
||||||
|
a.href = `/templates/${templateId}/folder/edit?autocomplete=false`
|
||||||
|
a.dataset.turboFrame = 'modal'
|
||||||
|
a.classList.add('hidden')
|
||||||
|
|
||||||
|
document.body.append(a)
|
||||||
|
|
||||||
|
a.click()
|
||||||
|
|
||||||
|
a.remove()
|
||||||
|
}
|
||||||
|
|
||||||
onDragleave () {
|
onDragleave () {
|
||||||
this.style.backgroundColor = null
|
this.style.backgroundColor = null
|
||||||
|
|
||||||
@@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.isDrag = true
|
this.isDrag = true
|
||||||
|
|
||||||
|
window.flash?.remove()
|
||||||
this.fileDropzone?.classList?.remove('hidden')
|
this.fileDropzone?.classList?.remove('hidden')
|
||||||
|
|
||||||
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||||
@@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
this.isDrag = false
|
this.isDrag = false
|
||||||
|
|
||||||
this.fileDropzone?.classList?.add('hidden')
|
this.fileDropzone?.classList?.add('hidden')
|
||||||
|
this.folderDropzone?.classList?.add('hidden')
|
||||||
|
|
||||||
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import autocomplete from 'autocompleter'
|
|||||||
|
|
||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
|
if (this.dataset.enabled === 'false') return
|
||||||
|
|
||||||
autocomplete({
|
autocomplete({
|
||||||
input: this.input,
|
input: this.input,
|
||||||
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
||||||
@@ -14,12 +16,16 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSelect = (item) => {
|
onSelect = (item) => {
|
||||||
this.input.value = item.name
|
this.input.value = this.dataset.parentName ? item.name : item.full_name
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const queryParams = new URLSearchParams({ q: text })
|
const queryParams = new URLSearchParams({ q: text })
|
||||||
|
|
||||||
|
if (this.dataset.parentName) {
|
||||||
|
queryParams.append('parent_name', this.dataset.parentName)
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
@@ -34,7 +40,7 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
div.setAttribute('dir', 'auto')
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item.name
|
div.textContent = this.dataset.parentName ? item.name : item.full_name
|
||||||
|
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const image = this.querySelector('img')
|
||||||
|
|
||||||
|
image.addEventListener('load', (e) => {
|
||||||
|
image.setAttribute('width', e.target.naturalWidth)
|
||||||
|
image.setAttribute('height', e.target.naturalHeight)
|
||||||
|
|
||||||
|
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import DownloadButton from './elements/download_button'
|
|||||||
import ToggleSubmit from './elements/toggle_submit'
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
import FetchForm from './elements/fetch_form'
|
import FetchForm from './elements/fetch_form'
|
||||||
import ScrollButtons from './elements/scroll_buttons'
|
import ScrollButtons from './elements/scroll_buttons'
|
||||||
|
import PageContainer from './elements/page_container'
|
||||||
|
|
||||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ safeRegisterElement('download-button', DownloadButton)
|
|||||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
safeRegisterElement('fetch-form', FetchForm)
|
safeRegisterElement('fetch-form', FetchForm)
|
||||||
safeRegisterElement('scroll-buttons', ScrollButtons)
|
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||||
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|||||||
@@ -70,3 +70,11 @@ button[disabled] .enabled {
|
|||||||
.base-radio {
|
.base-radio {
|
||||||
@apply radio bg-white radio-sm;
|
@apply radio bg-white radio-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-times {
|
||||||
|
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-courier {
|
||||||
|
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="[computedStyle, fontStyle]"
|
||||||
:class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
:class="{ 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
<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"
|
||||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex overflow-hidden"
|
class="flex overflow-hidden"
|
||||||
:class="isNarrow && (withSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
|
||||||
style="min-height: 50%"
|
style="min-height: 50%"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withSignatureId || field.preferences?.reason_field_uuid"
|
v-if="isShowSignatureId || field.preferences?.reason_field_uuid"
|
||||||
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||||
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||||
>
|
>
|
||||||
@@ -236,6 +236,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
isInlineSize: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
submitter: {
|
submitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -328,6 +333,13 @@ export default {
|
|||||||
verification: this.t('verify_id')
|
verification: this.t('verify_id')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isShowSignatureId () {
|
||||||
|
if ([true, false].includes(this.field.preferences?.with_signature_id)) {
|
||||||
|
return this.field.preferences.with_signature_id
|
||||||
|
} else {
|
||||||
|
return this.withSignatureId
|
||||||
|
}
|
||||||
|
},
|
||||||
alignClasses () {
|
alignClasses () {
|
||||||
if (!this.field.preferences) {
|
if (!this.field.preferences) {
|
||||||
return { 'items-center': true }
|
return { 'items-center': true }
|
||||||
@@ -348,8 +360,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'font-mono': this.field.preferences.font === 'Courier',
|
'font-courier': this.field.preferences.font === 'Courier',
|
||||||
'font-serif': this.field.preferences.font === 'Times',
|
'font-times': this.field.preferences.font === 'Times',
|
||||||
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
||||||
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
||||||
}
|
}
|
||||||
@@ -427,6 +439,34 @@ export default {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fontStyle () {
|
||||||
|
let fontSize = ''
|
||||||
|
|
||||||
|
if (this.isInlineSize) {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
|
||||||
|
} else {
|
||||||
|
fontSize = `${this.fontSizePx / 10}cqmin`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
|
||||||
|
} else {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||||
|
},
|
||||||
|
fontSizePx () {
|
||||||
|
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||||
|
},
|
||||||
|
lineHeight () {
|
||||||
|
return 1.3
|
||||||
|
},
|
||||||
|
fontScale () {
|
||||||
|
return 1000 / 612.0
|
||||||
|
},
|
||||||
computedStyle () {
|
computedStyle () {
|
||||||
const { x, y, w, h } = this.area
|
const { x, y, w, h } = this.area
|
||||||
|
|
||||||
@@ -437,11 +477,6 @@ export default {
|
|||||||
height: h * 100 + '%'
|
height: h * 100 + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.field.preferences?.font_size) {
|
|
||||||
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
|
|
||||||
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.field.preferences?.color) {
|
if (this.field.preferences?.color) {
|
||||||
style.color = this.field.preferences.color
|
style.color = this.field.preferences.color
|
||||||
}
|
}
|
||||||
@@ -456,7 +491,7 @@ export default {
|
|||||||
modelValue () {
|
modelValue () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -464,7 +499,7 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
||||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > (this.$refs.textContainer.clientHeight + 1) ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
:area="area"
|
:area="area"
|
||||||
:submittable="submittable"
|
:submittable="submittable"
|
||||||
:field-index="fieldIndex"
|
:field-index="fieldIndex"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
@@ -110,6 +111,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isInlineSize () {
|
||||||
|
return CSS.supports('container-type: size')
|
||||||
|
},
|
||||||
isMobileContainer () {
|
isMobileContainer () {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
const container = root.body || root.querySelector('div')
|
const container = root.body || root.querySelector('div')
|
||||||
|
|||||||
@@ -95,7 +95,19 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
this.uploadFiles(e.dataTransfer.files)
|
const files = Array.from(e.dataTransfer.files).filter((f) => {
|
||||||
|
if (this.accept === 'image/*') {
|
||||||
|
return f.type.startsWith('image')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.accept === 'image/*' && !files.length) {
|
||||||
|
alert(this.t('please_upload_an_image_file'))
|
||||||
|
} else {
|
||||||
|
this.uploadFiles(files)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSelectFiles (e) {
|
onSelectFiles (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button"
|
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
|
||||||
style="width: 96%; margin-left: 2%"
|
style="width: 96%; margin-left: 2%"
|
||||||
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="isMathLoaded"
|
v-if="isMathLoaded"
|
||||||
:model-value="calculateFormula(field)"
|
:model-value="calculateFormula(field)"
|
||||||
|
:is-inline-size="isInlineSize"
|
||||||
:field="field"
|
:field="field"
|
||||||
:area="area"
|
:area="area"
|
||||||
:submittable="false"
|
:submittable="false"
|
||||||
@@ -54,6 +55,11 @@ export default {
|
|||||||
isMathLoaded: false
|
isMathLoaded: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isInlineSize () {
|
||||||
|
return CSS.supports('container-type: size')
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
const {
|
const {
|
||||||
create,
|
create,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
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.',
|
||||||
verify_id: 'Verify ID',
|
verify_id: 'Verify ID',
|
||||||
@@ -99,6 +100,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
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.',
|
||||||
verify_id: 'Verificar ID',
|
verify_id: 'Verificar ID',
|
||||||
@@ -199,6 +201,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
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à.",
|
||||||
verify_id: 'Verifica ID',
|
verify_id: 'Verifica ID',
|
||||||
@@ -299,6 +302,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
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: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
||||||
verify_id: 'ID überprüfen',
|
verify_id: 'ID überprüfen',
|
||||||
@@ -399,6 +403,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
please_upload_an_image_file: 'Veuillez télécharger un fichier image',
|
||||||
must_be_characters_length: 'Doit contenir {number} caractères',
|
must_be_characters_length: 'Doit contenir {number} caractères',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
||||||
verify_id: "Vérification de l'ID",
|
verify_id: "Vérification de l'ID",
|
||||||
@@ -499,6 +504,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
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.',
|
||||||
verify_id: 'Zweryfikuj ID',
|
verify_id: 'Zweryfikuj ID',
|
||||||
@@ -599,6 +605,7 @@ const pl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
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: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
verify_id: 'Підтвердження ідентичності',
|
verify_id: 'Підтвердження ідентичності',
|
||||||
@@ -699,6 +706,7 @@ const uk = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
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.',
|
||||||
verify_id: 'Ověřit ID',
|
verify_id: 'Ověřit ID',
|
||||||
@@ -799,6 +807,7 @@ const cs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
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.',
|
||||||
verify_id: 'Verificar ID',
|
verify_id: 'Verificar ID',
|
||||||
@@ -899,6 +908,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
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: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
verify_id: 'אמת מזהה',
|
verify_id: 'אמת מזהה',
|
||||||
@@ -999,6 +1009,7 @@ const he = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
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.',
|
||||||
verify_id: 'Verifiëren ID',
|
verify_id: 'Verifiëren ID',
|
||||||
@@ -1099,6 +1110,7 @@ const nl = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
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: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
verify_id: 'تحقق من الهوية',
|
verify_id: 'تحقق من الهوية',
|
||||||
@@ -1199,6 +1211,7 @@ const ar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
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: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
verify_id: '아이디 확인',
|
verify_id: '아이디 확인',
|
||||||
@@ -1299,6 +1312,7 @@ const ko = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ja = {
|
const ja = {
|
||||||
|
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: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
verify_id: '本人確認',
|
verify_id: '本人確認',
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ export default {
|
|||||||
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
|
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug
|
submitter_slug: this.submitterSlug,
|
||||||
|
redirect_url: document.location.href
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
:background-color="'white'"
|
:background-color="'white'"
|
||||||
:with-required="false"
|
:with-required="false"
|
||||||
:with-areas="false"
|
:with-areas="false"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@click-description="isShowDescriptionModal = true"
|
@click-description="isShowDescriptionModal = true"
|
||||||
@@ -172,7 +173,8 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||||
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type), 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
|
||||||
|
:style="fontStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
@@ -315,7 +317,7 @@ export default {
|
|||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'selectedAreaRef', 'save', 't'],
|
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -346,6 +348,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -385,6 +392,34 @@ export default {
|
|||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
fontStyle () {
|
||||||
|
let fontSize = ''
|
||||||
|
|
||||||
|
if (this.isInlineSize) {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
|
||||||
|
} else {
|
||||||
|
fontSize = `${this.fontSizePx / 10}cqmin`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.textOverflowChars) {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
|
||||||
|
} else {
|
||||||
|
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
|
||||||
|
},
|
||||||
|
fontSizePx () {
|
||||||
|
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
||||||
|
},
|
||||||
|
lineHeight () {
|
||||||
|
return 1.3
|
||||||
|
},
|
||||||
|
fontScale () {
|
||||||
|
return 1040 / 612.0
|
||||||
|
},
|
||||||
isDefaultValuePresent () {
|
isDefaultValuePresent () {
|
||||||
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
||||||
return false
|
return false
|
||||||
@@ -413,8 +448,8 @@ export default {
|
|||||||
'justify-center': this.field.preferences.align === 'center',
|
'justify-center': this.field.preferences.align === 'center',
|
||||||
'justify-start': this.field.preferences.align === 'left',
|
'justify-start': this.field.preferences.align === 'left',
|
||||||
'justify-end': this.field.preferences.align === 'right',
|
'justify-end': this.field.preferences.align === 'right',
|
||||||
'font-mono': this.field.preferences.font === 'Courier',
|
'font-courier': this.field.preferences.font === 'Courier',
|
||||||
'font-serif': this.field.preferences.font === 'Times',
|
'font-times': this.field.preferences.font === 'Times',
|
||||||
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
||||||
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
||||||
}
|
}
|
||||||
@@ -491,7 +526,7 @@ export default {
|
|||||||
'field.default_value' () {
|
'field.default_value' () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -499,7 +534,7 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
|
||||||
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -330,6 +330,7 @@
|
|||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:allow-draw="!onlyDefinedFields || drawField"
|
:allow-draw="!onlyDefinedFields || drawField"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:data-document-uuid="document.uuid"
|
:data-document-uuid="document.uuid"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
||||||
@@ -436,6 +437,7 @@
|
|||||||
:default-required-fields="defaultRequiredFields"
|
:default-required-fields="defaultRequiredFields"
|
||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:show-tour-start-form="showTourStartForm"
|
:show-tour-start-form="showTourStartForm"
|
||||||
@@ -541,7 +543,9 @@ export default {
|
|||||||
withPayment: this.withPayment,
|
withPayment: this.withPayment,
|
||||||
isPaymentConnected: this.isPaymentConnected,
|
isPaymentConnected: this.isPaymentConnected,
|
||||||
withFormula: this.withFormula,
|
withFormula: this.withFormula,
|
||||||
|
withSignatureId: this.withSignatureId,
|
||||||
withConditions: this.withConditions,
|
withConditions: this.withConditions,
|
||||||
|
isInlineSize: this.isInlineSize,
|
||||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef),
|
selectedAreaRef: computed(() => this.selectedAreaRef),
|
||||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
|
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
|
||||||
@@ -562,6 +566,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -793,6 +802,9 @@ export default {
|
|||||||
language () {
|
language () {
|
||||||
return this.locale.split('-')[0].toLowerCase()
|
return this.locale.split('-')[0].toLowerCase()
|
||||||
},
|
},
|
||||||
|
isInlineSize () {
|
||||||
|
return CSS.supports('container-type: size')
|
||||||
|
},
|
||||||
isMobile () {
|
isMobile () {
|
||||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
|
||||||
@@ -1064,6 +1076,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||||
|
field.preferences ||= {}
|
||||||
|
field.preferences.with_signature_id = this.withSignatureId
|
||||||
|
}
|
||||||
|
|
||||||
this.template.fields.push(field)
|
this.template.fields.push(field)
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
@@ -1470,6 +1487,11 @@ export default {
|
|||||||
field.preferences ||= {}
|
field.preferences ||= {}
|
||||||
field.preferences.format ||= this.defaultDateFormat
|
field.preferences.format ||= this.defaultDateFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'signature' && [true, false].includes(this.withSignatureId)) {
|
||||||
|
field.preferences ||= {}
|
||||||
|
field.preferences.with_signature_id = this.withSignatureId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldArea = {
|
const fieldArea = {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
:data-page="index"
|
:data-page="index"
|
||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
:allow-draw="allowDraw"
|
:allow-draw="allowDraw"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Page from './page'
|
import Page from './page'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateDocument',
|
name: 'TemplateDocument',
|
||||||
@@ -65,6 +67,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
drawFieldType: {
|
drawFieldType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -123,14 +130,14 @@ export default {
|
|||||||
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
||||||
},
|
},
|
||||||
sortedPreviewImages () {
|
sortedPreviewImages () {
|
||||||
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata
|
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1]?.metadata || { width: 1400, height: 1812 }
|
||||||
|
|
||||||
return [...Array(this.numberOfPages).keys()].map((i) => {
|
return [...Array(this.numberOfPages).keys()].map((i) => {
|
||||||
return this.previewImagesIndex[i] || {
|
return this.previewImagesIndex[i] || reactive({
|
||||||
metadata: lazyloadMetadata,
|
metadata: { ...lazyloadMetadata },
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
previewImagesIndex () {
|
previewImagesIndex () {
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:background-color="dropdownBgColor"
|
:background-color="dropdownBgColor"
|
||||||
@click-formula="isShowFormulaModal = true"
|
@click-formula="isShowFormulaModal = true"
|
||||||
@click-font="isShowFontModal = true"
|
@click-font="isShowFontModal = true"
|
||||||
@@ -302,6 +303,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
withOptions: {
|
withOptions: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -320,6 +320,21 @@
|
|||||||
{{ t('format') }}
|
{{ t('format') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<label class="cursor-pointer py-1.5">
|
||||||
|
<input
|
||||||
|
:checked="field.preferences?.with_signature_id"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, save()]"
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ t('signature_id') }}</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
|
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
|
||||||
@click.stop
|
@click.stop
|
||||||
@@ -525,6 +540,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:default-field="defaultFieldsIndex[field.name]"
|
:default-field="defaultFieldsIndex[field.name]"
|
||||||
:draggable="editable"
|
:draggable="editable"
|
||||||
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||||
@@ -253,6 +254,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
template: {
|
template: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
|
class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
|
||||||
:style="{
|
:style="{
|
||||||
color: preferences.color || 'black',
|
color: preferences.color || 'black',
|
||||||
fontSize: (preferences.font_size || 12) + 'pt',
|
fontSize: (preferences.font_size || 11) + 'pt',
|
||||||
}"
|
}"
|
||||||
:class="textClasses"
|
:class="textClasses"
|
||||||
>
|
>
|
||||||
@@ -222,8 +222,8 @@ export default {
|
|||||||
fonts () {
|
fonts () {
|
||||||
return [
|
return [
|
||||||
{ value: null, label: 'Default' },
|
{ value: null, label: 'Default' },
|
||||||
{ value: 'Times', label: 'Times', class: 'font-serif' },
|
{ value: 'Times', label: 'Times', class: 'font-times' },
|
||||||
{ value: 'Courier', label: 'Courier', class: 'font-mono' }
|
{ value: 'Courier', label: 'Courier', class: 'font-courier' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
types () {
|
types () {
|
||||||
@@ -258,8 +258,8 @@ export default {
|
|||||||
},
|
},
|
||||||
textClasses () {
|
textClasses () {
|
||||||
return {
|
return {
|
||||||
'font-mono': this.preferences.font === 'Courier',
|
'font-courier': this.preferences.font === 'Courier',
|
||||||
'font-serif': this.preferences.font === 'Times',
|
'font-times': this.preferences.font === 'Times',
|
||||||
'justify-center': this.preferences.align === 'center',
|
'justify-center': this.preferences.align === 'center',
|
||||||
'justify-start': this.preferences.align === 'left',
|
'justify-start': this.preferences.align === 'left',
|
||||||
'justify-end': this.preferences.align === 'right',
|
'justify-end': this.preferences.align === 'right',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
signature_id: 'Signature ID',
|
||||||
error_message: 'Error message',
|
error_message: 'Error message',
|
||||||
length: 'Length',
|
length: 'Length',
|
||||||
min: 'Min',
|
min: 'Min',
|
||||||
@@ -172,6 +173,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
signature_id: 'ID de Firma',
|
||||||
error_message: 'Mensaje de error',
|
error_message: 'Mensaje de error',
|
||||||
length: 'Longitud',
|
length: 'Longitud',
|
||||||
min: 'Mín',
|
min: 'Mín',
|
||||||
@@ -345,6 +347,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
signature_id: 'ID firma',
|
||||||
error_message: 'Messaggio di errore',
|
error_message: 'Messaggio di errore',
|
||||||
length: 'Lunghezza',
|
length: 'Lunghezza',
|
||||||
min: 'Min',
|
min: 'Min',
|
||||||
@@ -518,6 +521,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
signature_id: 'ID da Assinatura',
|
||||||
error_message: 'Mensagem de erro',
|
error_message: 'Mensagem de erro',
|
||||||
length: 'Comprimento',
|
length: 'Comprimento',
|
||||||
min: 'Mín',
|
min: 'Mín',
|
||||||
@@ -691,6 +695,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
signature_id: 'ID de signature',
|
||||||
error_message: 'Message d\'erreur',
|
error_message: 'Message d\'erreur',
|
||||||
length: 'Longueur',
|
length: 'Longueur',
|
||||||
min: 'Min',
|
min: 'Min',
|
||||||
@@ -864,6 +869,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
signature_id: 'Signatur-ID',
|
||||||
error_message: 'Fehlermeldung',
|
error_message: 'Fehlermeldung',
|
||||||
length: 'Länge',
|
length: 'Länge',
|
||||||
min: 'Min',
|
min: 'Min',
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="relative select-none"
|
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||||
:class="{ 'cursor-crosshair': allowDraw }"
|
:class="{ 'cursor-crosshair': allowDraw, 'touch-none': !!drawField }"
|
||||||
:style="drawField ? 'touch-action: none' : ''"
|
style="container-type: size"
|
||||||
|
:style="{ aspectRatio: `${width} / ${height}`}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
:src="image.url"
|
:src="image.url"
|
||||||
:width="width"
|
:width="width"
|
||||||
:height="height"
|
:height="height"
|
||||||
class="border rounded mb-4"
|
class="rounded"
|
||||||
@load="onImageLoad"
|
@load="onImageLoad"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
:field="item.field"
|
:field="item.field"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
:default-field="defaultFieldsIndex[item.field.name]"
|
:default-field="defaultFieldsIndex[item.field.name]"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:max-page="totalPages - 1"
|
:max-page="totalPages - 1"
|
||||||
@@ -77,6 +79,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withSignatureId: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
areas: {
|
areas: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -191,8 +198,8 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onImageLoad (e) {
|
onImageLoad (e) {
|
||||||
e.target.setAttribute('width', e.target.naturalWidth)
|
this.image.metadata.width = e.target.naturalWidth
|
||||||
e.target.setAttribute('height', e.target.naturalHeight)
|
this.image.metadata.height = e.target.naturalHeight
|
||||||
},
|
},
|
||||||
setAreaRefs (el) {
|
setAreaRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateMailer < ApplicationMailer
|
||||||
|
def otp_verification_email(template, email:)
|
||||||
|
@template = template
|
||||||
|
|
||||||
|
@otp_code = EmailVerificationCodes.generate([email.downcase.strip, template.slug].join(':'))
|
||||||
|
|
||||||
|
assign_message_metadata('otp_verification_email', template)
|
||||||
|
|
||||||
|
mail(to: email, subject: I18n.t('email_verification'))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -41,8 +41,9 @@ class AccountConfig < ApplicationRecord
|
|||||||
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
FORCE_SSO_AUTH_KEY = 'force_sso_auth'
|
||||||
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
|
||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
|
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
||||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||||
WITH_AUDIT_SUBMITTER_TIMEZONE_KEY = 'with_audit_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'
|
||||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
click_email: 'click_email',
|
click_email: 'click_email',
|
||||||
click_sms: 'click_sms',
|
click_sms: 'click_sms',
|
||||||
phone_verified: 'phone_verified',
|
phone_verified: 'phone_verified',
|
||||||
|
email_verified: 'email_verified',
|
||||||
start_form: 'start_form',
|
start_form: 'start_form',
|
||||||
start_verification: 'start_verification',
|
start_verification: 'start_verification',
|
||||||
complete_verification: 'complete_verification',
|
complete_verification: 'complete_verification',
|
||||||
|
|||||||
@@ -72,12 +72,14 @@ class Template < ApplicationRecord
|
|||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
|
|
||||||
delegate :name, to: :folder, prefix: true
|
|
||||||
|
|
||||||
def application_key
|
def application_key
|
||||||
external_id
|
external_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def folder_name
|
||||||
|
folder.full_name
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def maybe_set_default_folder
|
def maybe_set_default_folder
|
||||||
|
|||||||
@@ -11,29 +11,43 @@
|
|||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :bigint not null
|
# account_id :bigint not null
|
||||||
# author_id :bigint not null
|
# author_id :bigint not null
|
||||||
|
# parent_folder_id :bigint
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_template_folders_on_account_id (account_id)
|
# index_template_folders_on_account_id (account_id)
|
||||||
# index_template_folders_on_author_id (author_id)
|
# index_template_folders_on_author_id (author_id)
|
||||||
|
# index_template_folders_on_parent_folder_id (parent_folder_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
#
|
#
|
||||||
# fk_rails_... (account_id => accounts.id)
|
# fk_rails_... (account_id => accounts.id)
|
||||||
# fk_rails_... (author_id => users.id)
|
# fk_rails_... (author_id => users.id)
|
||||||
|
# fk_rails_... (parent_folder_id => template_folders.id)
|
||||||
#
|
#
|
||||||
class TemplateFolder < ApplicationRecord
|
class TemplateFolder < ApplicationRecord
|
||||||
DEFAULT_NAME = 'Default'
|
DEFAULT_NAME = 'Default'
|
||||||
|
|
||||||
belongs_to :author, class_name: 'User'
|
belongs_to :author, class_name: 'User'
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
belongs_to :parent_folder, class_name: 'TemplateFolder', optional: true
|
||||||
|
|
||||||
has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
|
has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
|
||||||
|
has_many :subfolders, class_name: 'TemplateFolder', foreign_key: :parent_folder_id, inverse_of: :parent_folder,
|
||||||
|
dependent: :destroy
|
||||||
has_many :active_templates, -> { where(archived_at: nil) },
|
has_many :active_templates, -> { where(archived_at: nil) },
|
||||||
class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
|
class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
|
||||||
|
|
||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
|
|
||||||
|
def full_name
|
||||||
|
if parent_folder_id?
|
||||||
|
[parent_folder.name, name].join(' / ')
|
||||||
|
else
|
||||||
|
name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def default?
|
def default?
|
||||||
name == DEFAULT_NAME
|
name == DEFAULT_NAME
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<span class="top-0 right-0 absolute">
|
<span class="top-0 right-0 absolute">
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
</span>
|
</span>
|
||||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<span class="top-0 right-0 absolute">
|
<span class="top-0 right-0 absolute">
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
</span>
|
</span>
|
||||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<span class="top-0 right-0 absolute">
|
<span class="top-0 right-0 absolute">
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
</span>
|
</span>
|
||||||
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%== HighlightCode.call(text, 'Shell', theme: 'base16.dark') %></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 351 B |
@@ -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" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18" /><path d="M8 12l4 4" /><path d="M8 12h8" /><path d="M12 8l-4 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 398 B |
@@ -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" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18" /><path d="M9 12l4 4" /><path d="M13 8l-4 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 378 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M11 19h-6a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v6" />
|
||||||
|
<path d="M3 7l9 6l9 -6" />
|
||||||
|
<path d="M15 19l2 2l4 -4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 420 B |
@@ -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" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 19h-7a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v3.5" /><path d="M16 19h6" /><path d="M19 16v6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 408 B |
@@ -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" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M17 5l-10 14" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 308 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" wi width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M8.243 7.34l-6.38 .925l-.113 .023a1 1 0 0 0 -.44 1.684l4.622 4.499l-1.09 6.355l-.013 .11a1 1 0 0 0 1.464 .944l5.706 -3l5.693 3l.1 .046a1 1 0 0 0 1.352 -1.1l-1.091 -6.355l4.624 -4.5l.078 -.085a1 1 0 0 0 -.633 -1.62l-6.38 -.926l-2.852 -5.78a1 1 0 0 0 -1.794 0l-2.853 5.78z" stroke-width="0" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 604 B |
@@ -2,16 +2,15 @@
|
|||||||
if (!window.customElements.get('autosize-field')) {
|
if (!window.customElements.get('autosize-field')) {
|
||||||
window.customElements.define('autosize-field', class extends HTMLElement {
|
window.customElements.define('autosize-field', class extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
const originalFontValue = this.field.style.fontSize
|
||||||
this.field.classList.remove('text-[1.6vw]', 'lg:text-base');
|
|
||||||
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
|
|
||||||
|
|
||||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
this.field.classList.add('text-[0.8vw]', 'lg:text-[0.50rem]');
|
this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`;
|
||||||
}
|
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`;
|
||||||
|
|
||||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||||
this.field.classList.add('text-[0.7vw]', 'lg:text-[0.45rem]');
|
this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`;
|
||||||
|
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
|
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
|
||||||
<img alt="GitHub Repo stars" src="https://www.docuseal.com/github-badge.svg" style="height: 22px">
|
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
||||||
|
<%= svg_icon('start', class: 'h-3 w-3') %>
|
||||||
|
<span>9k</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<%= render 'shared/title' %>
|
<%= render 'shared/title' %>
|
||||||
</a>
|
</a>
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
<%= render 'shared/github' %>
|
<%= render 'shared/github' if request.path.starts_with?('/settings') %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if signed_in? %>
|
<% if signed_in? %>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<% if request.path != new_user_session_path %>
|
<% if request.path != new_user_session_path && request.path != setup_index_path %>
|
||||||
<%= link_to new_user_session_path({ lang: params[:lang] }.compact_blank), class: 'font-medium text-lg' do %>
|
<%= link_to new_user_session_path({ lang: params[:lang] }.compact_blank), class: 'font-medium text-lg' do %>
|
||||||
<span class="flex items-center justify-center space-x-1">
|
<span class="flex items-center justify-center space-x-1">
|
||||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
|
<% if signed_in? && current_user != true_user %>
|
||||||
|
<%= render 'shared/test_alert' %>
|
||||||
|
<% elsif request.path.starts_with?('/settings') %>
|
||||||
|
<%= link_to "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
|
||||||
<%= t('upgrade') %>
|
<%= t('upgrade') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && current_user != true_user %>
|
|
||||||
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
||||||
<%= render 'shared/test_alert' %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="block w-full md:w-52 flex-none">
|
<div class="block w-full md:w-52 flex-none">
|
||||||
<menu-active>
|
<menu-active>
|
||||||
<ul id="account_settings_menu" class="menu px-0">
|
<ul id="account_settings_menu" class="menu px-0">
|
||||||
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">← <%= t('back') %></a></li>
|
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium flex items-center space-x-0.5"><%= svg_icon('arrow_left', class: 'w-4 h-4 stroke-2') %><span><%= t('back') %></span></a></li>
|
||||||
<li class="menu-title py-0 !bg-transparent">
|
<li class="menu-title py-0 !bg-transparent">
|
||||||
<span class="!bg-transparent"><%= t('settings') %></span>
|
<span class="!bg-transparent"><%= t('settings') %></span>
|
||||||
</li>
|
</li>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
<%= t('plans') %>
|
<%= t('plans') %>
|
||||||
<span class="badge badge-warning"><%= t('new') %></span>
|
<span class="badge badge-warning"><%= t('pro') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
|
||||||
|
<% I18n.with_locale(@template.account.locale) do %>
|
||||||
|
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||||
|
<% end %>
|
||||||
|
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||||
|
<div class="space-y-6 mx-auto">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<%= render 'start_form/banner' %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-3">
|
||||||
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
||||||
|
<p dir="auto" class="text-sm"><%= t('invited_by_html', name: @template.account.name) %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<%= t('we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue') %>
|
||||||
|
</div>
|
||||||
|
<%= form_for '', url: start_form_path(@template.slug), method: :put, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
|
||||||
|
<div dir="auto" class="form-control !mt-0">
|
||||||
|
<%= f.hidden_field 'submitter[name]', value: params[:name] || @submitter&.name %>
|
||||||
|
<%= f.hidden_field 'submitter[email]', value: params[:email] || @submitter&.email %>
|
||||||
|
<%= f.hidden_field 'submitter[phone]', value: params[:phone] || @submitter&.phone %>
|
||||||
|
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %>
|
||||||
|
<div class="flex justify-between items-center mt-1">
|
||||||
|
<span>
|
||||||
|
<% if flash[:alert] %>
|
||||||
|
<span class="text-red-500">
|
||||||
|
<%= flash[:alert] %>
|
||||||
|
</span>
|
||||||
|
<% elsif flash[:notice] %>
|
||||||
|
<%= flash[:notice] %>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<label for="resend_code" id="resend_label" class="link"><%= t(:re_send_code) %></label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<toggle-submit dir="auto" class="form-control">
|
||||||
|
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
|
||||||
|
</toggle-submit>
|
||||||
|
<% end %>
|
||||||
|
<%= button_to t(:re_send_email), start_form_email_2fa_send_index_path, params: { slug: @template.slug, resend: true, submitter: { name: params[:name] || @submitter&.name, email: params[:email] || @submitter&.email, phone: params[:phone] || @submitter&.phone } }.compact, method: :post, id: 'resend_code', class: 'hidden' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
<% color = field.dig('preferences', 'color') %>
|
<% color = field.dig('preferences', 'color') %>
|
||||||
<% font = field.dig('preferences', 'font') %>
|
<% font = field.dig('preferences', 'font') %>
|
||||||
<% font_type = field.dig('preferences', 'font_type') %>
|
<% font_type = field.dig('preferences', 'font_type') %>
|
||||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' 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? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
<% 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? %>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'].to_f / area['h']) > 6 %>
|
||||||
<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' %>">
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
<div class="truncate uppercase">
|
<div class="truncate uppercase">
|
||||||
ID: <%= attachment.uuid %>
|
ID: <%= attachment.uuid %>
|
||||||
</div>
|
</div>
|
||||||
|
<% if local_assigns[:with_signature_id_reason] != false %>
|
||||||
<div>
|
<div>
|
||||||
<% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %>
|
<% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %>
|
||||||
<% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %>
|
<% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %>
|
||||||
@@ -22,8 +24,10 @@
|
|||||||
<<%= submitter.email %>>
|
<<%= submitter.email %>>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
<%= l(attachment.created_at.in_time_zone(local_assigns[:timezone]), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(local_assigns[:timezone], attachment.created_at) %>
|
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
|
||||||
|
<%= l(attachment.created_at.in_time_zone(timezone), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<% if params[:controller] == 'submissions_preview' %>
|
<% if params[:controller] == 'submissions_preview' %>
|
||||||
<%= render 'submissions/preview_tags' %>
|
<%= render 'submissions/preview_tags' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% with_signature_id, is_combined_enabled = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID], value: true).then { |configs| [configs.any? { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }, configs.any? { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }] } %>
|
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
|
||||||
|
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) %>
|
||||||
|
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
|
||||||
|
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true %>
|
||||||
|
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
||||||
|
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
||||||
<div style="max-width: 1600px" class="mx-auto pl-4">
|
<div style="max-width: 1600px" class="mx-auto pl-4">
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
||||||
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
||||||
@@ -94,11 +99,11 @@
|
|||||||
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.first&.metadata || {} %>
|
<% lazyload_metadata = document.preview_images.first&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
||||||
<img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= page.metadata['width'] %>" class="border rounded mb-4" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= width %>" class="rounded" height="<%= height %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', annot: %>
|
<%= render 'submissions/annotation', annot: %>
|
||||||
@@ -107,21 +112,22 @@
|
|||||||
<% value = values[field['uuid']] %>
|
<% value = values[field['uuid']] %>
|
||||||
<% value ||= field['default_value'] if field['type'] == 'heading' %>
|
<% value ||= field['default_value'] if field['type'] == 'heading' %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
|
<% submitter = submitters_index[field['submitter_uuid']] %>
|
||||||
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
|
<% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %>
|
||||||
<span class="group">
|
<span class="group">
|
||||||
<span class="hidden group-hover:inline">
|
<span class="hidden group-hover:inline">
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
|
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
|
||||||
</span>
|
</span>
|
||||||
<span class="group-hover:hidden">
|
<span class="group-hover:hidden">
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
|
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id: %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %>
|
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_signature_id_reason: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</page-container>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
||||||
<div>
|
<div>
|
||||||
<%= link_to root_path do %>
|
<%= link_to root_path, class: 'flex items-center' do %>
|
||||||
←
|
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
|
||||||
<span><%= t('back_to_active') %></span>
|
<span style="margin-left: 3px"><%= t('back_to_active') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row md:items-center mb-4 gap-3">
|
<div class="flex flex-col md:flex-row md:items-center mb-4 gap-3">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
<h1 class="text-2xl md:text-3xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %>
|
<% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %>
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if params[:folder].present? %>
|
||||||
|
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('folder') %>">
|
||||||
|
<%= link_to submissions_filter_path('folder', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
||||||
|
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
|
||||||
|
<span class="font-normal truncate"><%= params[:folder] %></span>
|
||||||
|
<% end %>
|
||||||
|
<%= link_to url_for(params.to_unsafe_h.except(:folder)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% if params[:author].present? %>
|
<% if params[:author].present? %>
|
||||||
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('author') %>">
|
<div class="tooltip tooltip-bottom flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-34 border-neutral-700" data-tip="<%= t('author') %>">
|
||||||
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
<%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
|
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>
|
||||||
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
|
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
|
||||||
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
|
<% schema = Submissions.filtered_conditions_schema(@submitter.submission, values:, include_submitter_uuid: @submitter.uuid) %>
|
||||||
|
<% font_scale = 1000.0 / PdfUtils::US_LETTER_W %>
|
||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
<div id="scrollbox">
|
<div id="scrollbox">
|
||||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||||
@@ -66,11 +67,11 @@
|
|||||||
<div id="document-<%= document.uuid %>">
|
<div id="document-<%= document.uuid %>">
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
|
||||||
<% lazyload_metadata = document.preview_images.last&.metadata || {} %>
|
<% lazyload_metadata = document.preview_images.last&.metadata || Templates::ProcessDocument::US_LETTER_SIZE %>
|
||||||
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
|
||||||
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
|
||||||
<div class="relative my-4 shadow-md">
|
<page-container class="block relative my-4 shadow-md" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
||||||
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" height="<%= height %>">
|
||||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% if annots = document_annots_index[index] %>
|
<% if annots = document_annots_index[index] %>
|
||||||
<%= render 'submit_form/annotations', annots: %>
|
<%= render 'submit_form/annotations', annots: %>
|
||||||
@@ -83,10 +84,10 @@
|
|||||||
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
|
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
|
||||||
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
|
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %>
|
<%= render 'submissions/value', font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</page-container>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<% is_long = folder.name.size > 32 %>
|
<% is_long = folder.name.size > 32 %>
|
||||||
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards">
|
<a href="<%= folder_path(folder) %>" class="flex h-full flex-col justify-between rounded-2xl py-5 px-6 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl relative" data-targets="dashboard-dropzone.folderCards" data-full-name="<%= folder.full_name %>">
|
||||||
<% if !is_long %>
|
<% if !is_long %>
|
||||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
<div>
|
<div>
|
||||||
<%= link_to root_path do %>
|
<%= link_to @template_folder.parent_folder ? folder_path(@template_folder.parent_folder) : root_path, class: 'flex items-center' do %>
|
||||||
←
|
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
|
||||||
<span><%= t('home') %></span>
|
<span style="margin-left: 3px"><%= @template_folder.parent_folder&.name || t('home') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<dashboard-dropzone>
|
<dashboard-dropzone>
|
||||||
<div class="relative flex justify-between items-center w-full mb-4">
|
<div class="relative flex justify-between items-center w-full mb-4">
|
||||||
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>
|
<%= form_for '', url: '', id: form_id = SecureRandom.uuid, method: :post, class: 'hidden', data: { target: 'dashboard-dropzone.form' }, html: { enctype: 'multipart/form-data' } do %>
|
||||||
<input name="form_id" value="<%= form_id %>">
|
<input name="form_id" value="<%= form_id %>">
|
||||||
<input name="folder_name" value="<%= @template_folder.name %>">
|
<input name="folder_name" value="<%= @template_folder.full_name %>">
|
||||||
<button type="submit"></button>
|
<button type="submit"></button>
|
||||||
<input id="dashboard_dropzone_input" name="files[]" type="file" multiple>
|
<input id="dashboard_dropzone_input" name="files[]" type="file" multiple>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'templates/dashboard_dropzone', style: 'height: 137px' %>
|
<%= render 'templates/dashboard_dropzone', style: 'height: 137px' %>
|
||||||
|
<% unless @template_folder.parent_folder %>
|
||||||
|
<%= render 'templates/dashboard_folder_dropzone', style: 'height: 137px' %>
|
||||||
|
<% end %>
|
||||||
<h1 class="text-2xl truncate md:text-3xl font-bold flex items-center flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
|
<h1 class="text-2xl truncate md:text-3xl font-bold flex items-center flex-grow min-w-0 space-x-2 md:flex <%= 'hidden' if params[:q].present? %>">
|
||||||
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
|
<%= svg_icon('folder', class: 'w-9 h-9 flex-shrink-0') %>
|
||||||
<span class="peer truncate">
|
<span class="peer truncate">
|
||||||
<%= @template_folder.name %>
|
<%= @template_folder.name %>
|
||||||
</span>
|
</span>
|
||||||
<% if can?(:update, @template_folder) && @template_folder.name != TemplateFolder::DEFAULT_NAME %>
|
<% if can?(:update, @template_folder) && @template_folder.full_name != TemplateFolder::DEFAULT_NAME %>
|
||||||
<span class="opacity-0 hover:opacity-100 peer-hover:opacity-100">
|
<span class="opacity-0 hover:opacity-100 peer-hover:opacity-100">
|
||||||
<a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal">
|
<a href="<%= edit_folder_path(@template_folder) %>" data-turbo-frame="modal">
|
||||||
<%= svg_icon('pencil', class: 'w-7 h-7') %>
|
<%= svg_icon('pencil', class: 'w-7 h-7') %>
|
||||||
@@ -27,18 +30,23 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<% if params[:q].present? || @pagy.pages > 1 %>
|
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
|
||||||
<%= render 'shared/search_input' %>
|
<%= render 'shared/search_input' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if can?(:create, ::Template) %>
|
<% if can?(:create, ::Template) %>
|
||||||
<%= render 'templates/upload_button', folder_name: @template_folder.name %>
|
<%= render 'templates/upload_button', folder_name: @template_folder.full_name %>
|
||||||
<%= link_to new_template_path(folder_name: @template_folder.name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
<%= link_to new_template_path(folder_name: @template_folder.full_name), class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
|
||||||
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
|
||||||
<span class="hidden md:block"><%= t('create') %></span>
|
<span class="hidden md:block"><%= t('create') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if @template_folders.present? %>
|
||||||
|
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
|
||||||
|
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% if @pagy.count.nil? || @pagy.count > 0 %>
|
<% if @pagy.count.nil? || @pagy.count > 0 %>
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<div class="grid gap-4 md:grid-cols-3">
|
||||||
<%= render partial: 'templates/template', collection: @templates %>
|
<%= render partial: 'templates/template', collection: @templates %>
|
||||||
@@ -48,12 +56,21 @@
|
|||||||
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
|
<%= render 'shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000 && !can?(:manage, :countless), selected_order: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
|
<%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', right_additional_html: templates_order_select_html %>
|
||||||
<% elsif params[:q].present? %>
|
<% elsif params[:q].present? %>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="mt-16 text-3xl font-semibold">
|
<div class="mt-16 text-3xl font-semibold">
|
||||||
<%= t('templates_not_found') %>
|
<%= t('templates_not_found') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if @related_submissions.present? %>
|
||||||
|
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
|
||||||
|
<%= t('submissions') %>
|
||||||
|
</h1>
|
||||||
|
<div class="space-y-4 mt-4">
|
||||||
|
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_path(q: params[:q], folder: @template_folder.full_name) %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</dashboard-dropzone>
|
</dashboard-dropzone>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<p><%= t('your_verification_code_to_access_the_name', name: @template.name) %></p>
|
||||||
|
<p><b><%= @otp_code %></b></p>
|
||||||
|
<p><%= t('please_reply_to_this_email_if_you_didnt_request_this') %></p>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<div class="absolute bottom-0 w-full cursor-pointer rounded-xl bg-base-100 border-2 border-base-300 border-dashed hidden z-50" data-target="dashboard-dropzone.folderDropzone" style="<%= local_assigns[:style] %>">
|
||||||
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex justify-center p-2 items-center pointer-events-none">
|
||||||
|
<div class="flex flex-col items-center text-center" data-target="dashboard-dropzone.toggleLoading">
|
||||||
|
<span class="flex flex-col items-center">
|
||||||
|
<span>
|
||||||
|
<%= svg_icon('folder_plus', class: 'w-9 h-9') %>
|
||||||
|
</span>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
<%= t('create_a_new_folder') %>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span class="flex flex-col items-center hidden" data-target="dashboard-dropzone.fileDropzoneLoading">
|
||||||
|
<%= svg_icon('loader', class: 'w-9 h-9 animate-spin') %>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
<%= t('loading') %>...
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="h-36 relative group">
|
<div class="h-36 relative group">
|
||||||
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl" data-targets="dashboard-dropzone.templateCards">
|
<a href="<%= template_path(template) %>" class="flex h-full flex-col justify-between rounded-2xl pt-6 px-7 w-full bg-base-200 before:border-2 before:border-base-300 before:border-dashed before:absolute before:left-0 before:right-0 before:top-0 before:bottom-0 before:hidden before:rounded-2xl" data-targets="dashboard-dropzone.templateCards">
|
||||||
<div class="text-xl font-semibold" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
<div class="text-xl font-semibold" style="line-height: 1.6rem; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;">
|
||||||
<% if template.template_accesses.present? %>
|
<% if template.template_accesses.present? %>
|
||||||
<%= svg_icon('lock', class: 'w-6 h-6 inline -translate-y-[4px]') %>
|
<%= svg_icon('lock', class: 'w-6 h-6 inline -translate-y-[4px]') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<% if template.archived_at? %>
|
<% if template.archived_at? %>
|
||||||
<span class="flex items-center space-x-1 w-1/2">
|
<span class="flex items-center space-x-1 w-1/2">
|
||||||
<%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %>
|
<%= svg_icon('folder', class: 'w-4 h-4 flex-shrink-0') %>
|
||||||
<span class="truncate"><%= template.folder.name %></span>
|
<span class="truncate"><%= template.folder.full_name %></span>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
<a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer">
|
<a href="<%= folder_path(@template.folder) %>" class="flex items-center space-x-1 mt-1 peer">
|
||||||
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %>
|
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0') %>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
<%= @template.folder.name %>
|
<%= @template.folder.full_name %>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<% if can?(:update, template) %>
|
<% if can?(:update, template) %>
|
||||||
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('move') %>">
|
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('move') %>">
|
||||||
<a href="<%= edit_template_folder_path(template.id) %>" data-turbo-frame="modal">
|
<a href="<%= edit_template_folder_path(template.id, subfolder: @template.folder.parent_folder.present?) %>" data-turbo-frame="modal">
|
||||||
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
|
<%= svg_icon('pencil_share', class: 'w-5 h-5') %>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||||
</a>
|
</a>
|
||||||
<folder-autocomplete class="flex justify-between w-full">
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
<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 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" 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 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
|
||||||
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
|
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="shrink-0 link peer-focus:hidden mr-1.5">
|
||||||
<%= t('change_folder') %>
|
<%= t('change_folder') %>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<div>
|
<div>
|
||||||
<%= link_to root_path do %>
|
<%= link_to root_path, class: 'flex items-center' do %>
|
||||||
←
|
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
|
||||||
<span><%= t('back_to_active') %></span>
|
<span style="margin-left: 3px"><%= t('back_to_active') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between mb-4 items-center">
|
<div class="flex justify-between mb-4 items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('document_templates_html') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
<h1 class="text-2xl md:text-3xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('document_templates_html') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
||||||
</div>
|
</div>
|
||||||
<% if params[:q].present? || @pagy.pages > 1 %>
|
<% if params[:q].present? || @pagy.pages > 1 %>
|
||||||
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
|
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
|
||||||
@@ -22,5 +22,14 @@
|
|||||||
<%= t('templates_not_found') %>
|
<%= t('templates_not_found') %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% if @related_submissions.present? %>
|
||||||
|
<h1 class="text-2xl md:text-3xl sm:text-4xl font-bold mt-8 md:mt-4">
|
||||||
|
<%= t('submissions') %>
|
||||||
|
</h1>
|
||||||
|
<div class="space-y-4 mt-4">
|
||||||
|
<%= render partial: 'templates/submission', collection: @related_submissions, locals: { with_template: true } %>
|
||||||
|
</div>
|
||||||
|
<%= render 'shared/pagination', pagy: @related_submissions_pagy, items_name: 'submissions', next_page_path: submissions_archived_index_path(q: params[:q]) %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %>
|
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates' %>
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
||||||
<% with_filters = @pagy.pages > 1 || params[:q].present? || filter_params.present? %>
|
<% with_filters = @pagy.pages > 1 || params[:q].present? || filter_params.present? %>
|
||||||
<%= render 'templates/title', template: @template %>
|
<%= render 'templates/title', template: @template %>
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center mb-6 gap-3">
|
||||||
|
<div class="flex w-full justify-between md:items-end items-end">
|
||||||
<div>
|
<div>
|
||||||
<%= link_to template_path(@template) do %>
|
<div>
|
||||||
←
|
<%= link_to template_path(@template), class: 'flex items-center' do %>
|
||||||
<span><%= t('back_to_active') %></span>
|
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
|
||||||
|
<span style="margin-left: 3px"><%= t('back_to_active') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row md:items-center mb-6 gap-3">
|
|
||||||
<div class="flex w-full justify-between md:items-end items-center">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-3xl font-bold md:block"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
<h1 class="text-3xl font-bold md:block"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2 justify-end">
|
<div class="flex space-x-2 justify-end">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<% unless show_dropzone %>
|
<% unless show_dropzone %>
|
||||||
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
|
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= render 'templates/dashboard_folder_dropzone', style: 'height: 114px' %>
|
||||||
<div class="flex items-center flex-grow min-w-0">
|
<div class="flex items-center flex-grow min-w-0">
|
||||||
<% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
|
<% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
|
|||||||
@@ -1,7 +1,39 @@
|
|||||||
|
<% with_subfolder = @template.folder.name != TemplateFolder::DEFAULT_NAME && params[:subfolder] != 'false' %>
|
||||||
<%= render 'shared/turbo_modal', title: t('move_into_folder') do %>
|
<%= render 'shared/turbo_modal', title: t('move_into_folder') do %>
|
||||||
<%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
<% if with_subfolder %>
|
||||||
<div class="form-control my-6">
|
<%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'subfolder_form', autocomplete: :off } do |f| %>
|
||||||
<folder-autocomplete class="block" data-submit-on-select="true">
|
<%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %>
|
||||||
|
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative">
|
||||||
|
<div class="flex items-center justify-between mb-2.5">
|
||||||
|
<label for="is_root_folder" class="flex items-center">
|
||||||
|
<%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %>
|
||||||
|
<span class="flex items-center space-x-0.5 mt-1 peer">
|
||||||
|
<%= svg_icon('folder', class: 'w-6 h-5 flex-shrink-0') %>
|
||||||
|
<span class="text-md">
|
||||||
|
<%= @template.folder.parent_folder&.name || @template.folder.name %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('change_parent_folder') %>">
|
||||||
|
<span href="<%= edit_template_folder_path(@template.id) %>" data-turbo-frame="modal">
|
||||||
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</toggle-visible>
|
||||||
|
<div class="form-control mb-6">
|
||||||
|
<folder-autocomplete class="block" data-submit-on-select="true" data-parent-name="<%= @template.folder.parent_folder&.name || @template.folder.name %>" data-enabled="<%= params[:autocomplete] != 'false' %>">
|
||||||
|
<%= f.text_field :name, required: true, placeholder: "#{t('new_subfolder_name')}...", class: 'base-input w-full', autofocus: true %>
|
||||||
|
</folder-autocomplete>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.button button_title(title: t('move'), disabled_with: t('moving')), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'folder_form', autocomplete: :off, class: "mt-6 #{'hidden' if with_subfolder}" } do |f| %>
|
||||||
|
<div class="form-control mb-6">
|
||||||
|
<folder-autocomplete class="block" data-submit-on-select="true" data-enabled="<%= params[:autocomplete] != 'false' %>">
|
||||||
<%= f.text_field :name, required: true, placeholder: "#{t('new_folder_name')}...", class: 'base-input w-full', autofocus: true %>
|
<%= f.text_field :name, required: true, placeholder: "#{t('new_folder_name')}...", class: 'base-input w-full', autofocus: true %>
|
||||||
</folder-autocomplete>
|
</folder-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,5 +41,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
|
||||||
|
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
|
||||||
|
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
|
||||||
|
<label for="template_preferences_shared_link_2fa" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
|
||||||
|
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
|
||||||
|
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
|
||||||
|
</label>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<div id="drawer_events_<%= dom_id(webhook_event) %>">
|
<div id="<%= dom_id(webhook_event, :drawer_events) %>">
|
||||||
<ol class="relative border-s border-base-300 space-y-6 ml-3">
|
<ol class="relative border-s border-base-300 space-y-6 ml-3">
|
||||||
<% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %>
|
<% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %>
|
||||||
<% if webhook_event.status == 'error' %>
|
<% if webhook_event.status == 'error' %>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module Rouge
|
|||||||
|
|
||||||
module Lexers
|
module Lexers
|
||||||
autoload :JSON, 'rouge/lexers/json'
|
autoload :JSON, 'rouge/lexers/json'
|
||||||
|
autoload :Shell, 'rouge/lexers/shell'
|
||||||
end
|
end
|
||||||
|
|
||||||
autoload :Formatter, 'rouge/formatter'
|
autoload :Formatter, 'rouge/formatter'
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ en: &en
|
|||||||
language_ko: 한국어
|
language_ko: 한국어
|
||||||
language_ja: 日本語
|
language_ja: 日本語
|
||||||
hi_there: Hi there
|
hi_there: Hi there
|
||||||
|
pro: Pro
|
||||||
thanks: Thanks
|
thanks: Thanks
|
||||||
private: Private
|
private: Private
|
||||||
default_parties: Default parties
|
default_parties: Default parties
|
||||||
@@ -459,6 +460,10 @@ en: &en
|
|||||||
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.'
|
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: '%{users_count}/%{total_users_count} Pro users limit was reached. To invite additional users, please purchase more Pro user seats via the "Manage plan" button.'
|
||||||
move_into_folder: Move Into Folder
|
move_into_folder: Move Into Folder
|
||||||
new_folder_name: New Folder Name
|
new_folder_name: New Folder Name
|
||||||
|
new_subfolder_name: New Subfolder Name
|
||||||
|
change_parent_folder: Change Parent Folder
|
||||||
|
folder: Folder
|
||||||
|
create_a_new_folder: Create a New Folder
|
||||||
exit_preview: Exit Preview
|
exit_preview: Exit Preview
|
||||||
general: General
|
general: General
|
||||||
recipients: Recipients
|
recipients: Recipients
|
||||||
@@ -769,6 +774,15 @@ en: &en
|
|||||||
there_are_no_events: There are no events
|
there_are_no_events: There are no events
|
||||||
resend: Resend
|
resend: Resend
|
||||||
next_attempt_in_time_in_words: Next attempt in %{time_in_words}
|
next_attempt_in_time_in_words: Next attempt in %{time_in_words}
|
||||||
|
request_email_otp_verification_with_shared_link: Request email OTP verification with shared link
|
||||||
|
sms_rate_limit_exceeded: SMS rate limit exceeded
|
||||||
|
invalid_phone_number: Invalid phone number
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Please contact the requester to specify your phone number for two-factor authentication.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: We've sent a one-time verification code to your email address. Please enter the code below to continue.
|
||||||
|
re_send_code: Re-send Code
|
||||||
|
email_verification: Email verification
|
||||||
|
your_verification_code_to_access_the_name: 'Your verification code to access the "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Please reply to this email if you didn't request this.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Bulk Send
|
bulk: Bulk Send
|
||||||
@@ -786,6 +800,7 @@ en: &en
|
|||||||
click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}'
|
click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}'
|
||||||
click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}'
|
click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}'
|
||||||
phone_verified_by_html: '<b>Phone verified</b> by %{submitter_name}'
|
phone_verified_by_html: '<b>Phone verified</b> by %{submitter_name}'
|
||||||
|
email_verified_by_html: '<b>Email verified</b> by %{submitter_name}'
|
||||||
start_form_by_html: '<b>Submission started</b> by %{submitter_name}'
|
start_form_by_html: '<b>Submission started</b> by %{submitter_name}'
|
||||||
view_form_by_html: '<b>Form viewed</b> by %{submitter_name}'
|
view_form_by_html: '<b>Form viewed</b> by %{submitter_name}'
|
||||||
invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}'
|
invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}'
|
||||||
@@ -874,6 +889,7 @@ en: &en
|
|||||||
range_without_total: "%{from}-%{to} events"
|
range_without_total: "%{from}-%{to} events"
|
||||||
|
|
||||||
es: &es
|
es: &es
|
||||||
|
pro: Pro
|
||||||
default_parties: Partes predeterminadas
|
default_parties: Partes predeterminadas
|
||||||
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
|
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
|
||||||
require_all_recipients: Requerir a todos los destinatarios
|
require_all_recipients: Requerir a todos los destinatarios
|
||||||
@@ -1314,6 +1330,10 @@ es: &es
|
|||||||
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".'
|
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Se alcanzó el límite de %{users_count}/%{total_users_count} usuarios Pro. Para invitar a más usuarios, compra más plazas Pro usando el botón "Gestionar plan".'
|
||||||
move_into_folder: Mover a la carpeta
|
move_into_folder: Mover a la carpeta
|
||||||
new_folder_name: Nuevo nombre de la carpeta
|
new_folder_name: Nuevo nombre de la carpeta
|
||||||
|
new_subfolder_name: Nuevo nombre de subcarpeta
|
||||||
|
change_parent_folder: Cambiar carpeta principal
|
||||||
|
folder: Carpeta
|
||||||
|
create_a_new_folder: Crear una nueva carpeta
|
||||||
exit_preview: Salir de la vista previa
|
exit_preview: Salir de la vista previa
|
||||||
general: General
|
general: General
|
||||||
recipients: Destinatarios
|
recipients: Destinatarios
|
||||||
@@ -1624,6 +1644,15 @@ es: &es
|
|||||||
there_are_no_events: No hay eventos
|
there_are_no_events: No hay eventos
|
||||||
resend: Reenviar
|
resend: Reenviar
|
||||||
next_attempt_in_time_in_words: Próximo intento en %{time_in_words}
|
next_attempt_in_time_in_words: Próximo intento en %{time_in_words}
|
||||||
|
request_email_otp_verification_with_shared_link: Solicitar verificación OTP por correo electrónico con enlace compartido
|
||||||
|
sms_rate_limit_exceeded: Límite de SMS excedido
|
||||||
|
invalid_phone_number: Número de teléfono inválido
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Contacte al solicitante para especificar su número para la autenticación de dos factores.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Enviamos un código de verificación único a su correo electrónico. Ingréselo a continuación para continuar.
|
||||||
|
re_send_code: Reenviar código
|
||||||
|
email_verification: Verificación por correo electrónico
|
||||||
|
your_verification_code_to_access_the_name: 'Su código de verificación para acceder a "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Por favor, responda este correo si no solicitó esto.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envío masivo
|
bulk: Envío masivo
|
||||||
@@ -1641,6 +1670,7 @@ es: &es
|
|||||||
click_email_by_html: '<b>Enlace del correo electrónico clicado</b> por %{submitter_name}'
|
click_email_by_html: '<b>Enlace del correo electrónico clicado</b> por %{submitter_name}'
|
||||||
click_sms_by_html: '<b>Enlace del SMS clicado</b> por %{submitter_name}'
|
click_sms_by_html: '<b>Enlace del SMS clicado</b> por %{submitter_name}'
|
||||||
phone_verified_by_html: '<b>Teléfono verificado</b> por %{submitter_name}'
|
phone_verified_by_html: '<b>Teléfono verificado</b> por %{submitter_name}'
|
||||||
|
email_verified_by_html: '<b>Correo electrónico verificado</b> por %{submitter_name}'
|
||||||
start_form_by_html: '<b>Envío iniciado</b> por %{submitter_name}'
|
start_form_by_html: '<b>Envío iniciado</b> por %{submitter_name}'
|
||||||
view_form_by_html: '<b>Formulario visto</b> por %{submitter_name}'
|
view_form_by_html: '<b>Formulario visto</b> por %{submitter_name}'
|
||||||
invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}'
|
invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}'
|
||||||
@@ -1729,6 +1759,7 @@ es: &es
|
|||||||
range_without_total: "%{from}-%{to} eventos"
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
it: &it
|
it: &it
|
||||||
|
pro: Pro
|
||||||
default_parties: Parti predefiniti
|
default_parties: Parti predefiniti
|
||||||
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
|
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
|
||||||
require_all_recipients: Richiedi tutti i destinatari
|
require_all_recipients: Richiedi tutti i destinatari
|
||||||
@@ -2168,6 +2199,10 @@ it: &it
|
|||||||
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".'
|
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'È stato raggiunto il limite di %{users_count}/%{total_users_count} utenti Pro. Per invitare altri utenti, acquista più posti Pro tramite il pulsante "Gestisci piano".'
|
||||||
move_into_folder: Sposta nella cartella
|
move_into_folder: Sposta nella cartella
|
||||||
new_folder_name: Nuovo nome della cartella
|
new_folder_name: Nuovo nome della cartella
|
||||||
|
new_subfolder_name: Nuovo nome della sottocartella
|
||||||
|
change_parent_folder: Cambia cartella principale
|
||||||
|
folder: Cartella
|
||||||
|
create_a_new_folder: Crea una nuova cartella
|
||||||
exit_preview: "Esci dall'anteprima"
|
exit_preview: "Esci dall'anteprima"
|
||||||
general: Generale
|
general: Generale
|
||||||
recipients: Destinatari
|
recipients: Destinatari
|
||||||
@@ -2477,6 +2512,16 @@ it: &it
|
|||||||
there_are_no_events: Nessun evento
|
there_are_no_events: Nessun evento
|
||||||
resend: Invia di nuovo
|
resend: Invia di nuovo
|
||||||
next_attempt_in_time_in_words: Prossimo tentativo tra %{time_in_words}
|
next_attempt_in_time_in_words: Prossimo tentativo tra %{time_in_words}
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: "Questo modello ha più parti, il che impedisce l'uso di un link di condivisione perché non è chiaro quale parte sia responsabile di campi specifici. Per risolvere, definisci i dettagli predefiniti della parte."
|
||||||
|
request_email_otp_verification_with_shared_link: "Richiedi la verifica OTP tramite email con link condiviso"
|
||||||
|
sms_rate_limit_exceeded: Limite SMS superato
|
||||||
|
invalid_phone_number: Numero di telefono non valido
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Contatta il richiedente per specificare il tuo numero per l'autenticazione a due fattori.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Abbiamo inviato un codice di verifica una tantum alla tua email. Inseriscilo qui sotto per continuare.
|
||||||
|
re_send_code: Invia di nuovo il codice
|
||||||
|
email_verification: Verifica email
|
||||||
|
your_verification_code_to_access_the_name: 'Il tuo codice per accedere a "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Rispondi a questa email se non hai richiesto questo.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Invio massivo
|
bulk: Invio massivo
|
||||||
@@ -2494,6 +2539,7 @@ it: &it
|
|||||||
click_email_by_html: "<b>Link dell'e-mail cliccato</b> da %{submitter_name}"
|
click_email_by_html: "<b>Link dell'e-mail cliccato</b> da %{submitter_name}"
|
||||||
click_sms_by_html: "<b>Link dell'SMS cliccato</b> da %{submitter_name}"
|
click_sms_by_html: "<b>Link dell'SMS cliccato</b> da %{submitter_name}"
|
||||||
phone_verified_by_html: '<b>Telefono verificato</b> da %{submitter_name}'
|
phone_verified_by_html: '<b>Telefono verificato</b> da %{submitter_name}'
|
||||||
|
email_verified_by_html: '<b>Email verificata</b> da %{submitter_name}'
|
||||||
start_form_by_html: '<b>Invio iniziato</b> da %{submitter_name}'
|
start_form_by_html: '<b>Invio iniziato</b> da %{submitter_name}'
|
||||||
view_form_by_html: '<b>Modulo visualizzato</b> da %{submitter_name}'
|
view_form_by_html: '<b>Modulo visualizzato</b> da %{submitter_name}'
|
||||||
invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}'
|
invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}'
|
||||||
@@ -2582,6 +2628,7 @@ it: &it
|
|||||||
range_without_total: "%{from}-%{to} eventi"
|
range_without_total: "%{from}-%{to} eventi"
|
||||||
|
|
||||||
fr: &fr
|
fr: &fr
|
||||||
|
pro: Pro
|
||||||
default_parties: Parties par défaut
|
default_parties: Parties par défaut
|
||||||
authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton
|
authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton
|
||||||
require_all_recipients: Exiger tous les destinataires
|
require_all_recipients: Exiger tous les destinataires
|
||||||
@@ -3023,6 +3070,10 @@ fr: &fr
|
|||||||
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".'
|
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'La limite de %{users_count}/%{total_users_count} utilisateurs Pro a été atteinte. Pour inviter d''autres utilisateurs, veuillez acheter plus de places Pro via le bouton "Gérer le plan".'
|
||||||
move_into_folder: Déplacer dans le dossier
|
move_into_folder: Déplacer dans le dossier
|
||||||
new_folder_name: Nouveau nom du dossier
|
new_folder_name: Nouveau nom du dossier
|
||||||
|
new_subfolder_name: Nouveau nom du sous-dossier
|
||||||
|
change_parent_folder: Changer le dossier parent
|
||||||
|
folder: Dossier
|
||||||
|
create_a_new_folder: Créer un nouveau dossier
|
||||||
exit_preview: "Quitter l'aperçu"
|
exit_preview: "Quitter l'aperçu"
|
||||||
general: Général
|
general: Général
|
||||||
recipients: Destinataires
|
recipients: Destinataires
|
||||||
@@ -3333,6 +3384,16 @@ fr: &fr
|
|||||||
there_are_no_events: Aucun événement
|
there_are_no_events: Aucun événement
|
||||||
resend: Renvoyer
|
resend: Renvoyer
|
||||||
next_attempt_in_time_in_words: Nouvelle tentative dans %{time_in_words}
|
next_attempt_in_time_in_words: Nouvelle tentative dans %{time_in_words}
|
||||||
|
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: "Ce modèle contient plusieurs parties, ce qui empêche l'utilisation d'un lien de partage car il n'est pas clair quelle partie est responsable de certains champs. Pour résoudre cela, définissez les détails de la partie par défaut."
|
||||||
|
request_email_otp_verification_with_shared_link: "Demander une vérification OTP par e-mail avec un lien de partage"
|
||||||
|
sms_rate_limit_exceeded: Limite de SMS dépassée
|
||||||
|
invalid_phone_number: Numéro de téléphone invalide
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Veuillez contacter l'expéditeur pour spécifier votre numéro pour l'authentification à deux facteurs.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Un code de vérification unique a été envoyé à votre adresse email. Veuillez le saisir ci-dessous pour continuer.
|
||||||
|
re_send_code: Renvoyer le code
|
||||||
|
email_verification: Vérification de l'email
|
||||||
|
your_verification_code_to_access_the_name: 'Votre code pour accéder à "%{name}" :'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Veuillez répondre à cet email si vous n'avez pas fait cette demande.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envoi en masse
|
bulk: Envoi en masse
|
||||||
@@ -3350,6 +3411,7 @@ fr: &fr
|
|||||||
click_email_by_html: "<b>Lien de l'e-mail cliqué</b> par %{submitter_name}"
|
click_email_by_html: "<b>Lien de l'e-mail cliqué</b> par %{submitter_name}"
|
||||||
click_sms_by_html: '<b>Lien du SMS cliqué</b> par %{submitter_name}'
|
click_sms_by_html: '<b>Lien du SMS cliqué</b> par %{submitter_name}'
|
||||||
phone_verified_by_html: '<b>Téléphone vérifié</b> par %{submitter_name}'
|
phone_verified_by_html: '<b>Téléphone vérifié</b> par %{submitter_name}'
|
||||||
|
email_verified_by_html: '<b>Email vérifié</b> par %{submitter_name}'
|
||||||
start_form_by_html: '<b>Soumission commencée</b> par %{submitter_name}'
|
start_form_by_html: '<b>Soumission commencée</b> par %{submitter_name}'
|
||||||
view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}'
|
view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}'
|
||||||
invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}'
|
invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}'
|
||||||
@@ -3438,6 +3500,7 @@ fr: &fr
|
|||||||
range_without_total: "%{from} à %{to} événements"
|
range_without_total: "%{from} à %{to} événements"
|
||||||
|
|
||||||
pt: &pt
|
pt: &pt
|
||||||
|
pro: Pro
|
||||||
default_parties: Partes padrão
|
default_parties: Partes padrão
|
||||||
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
|
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
|
||||||
require_all_recipients: Exigir todos os destinatários
|
require_all_recipients: Exigir todos os destinatários
|
||||||
@@ -3878,6 +3941,10 @@ pt: &pt
|
|||||||
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".'
|
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'O limite de %{users_count}/%{total_users_count} usuários Pro foi atingido. Para convidar mais usuários, adquira mais licenças Pro através do botão "Gerenciar plano".'
|
||||||
move_into_folder: Mover para pasta
|
move_into_folder: Mover para pasta
|
||||||
new_folder_name: Novo nome da pasta
|
new_folder_name: Novo nome da pasta
|
||||||
|
new_subfolder_name: Novo nome da subpasta
|
||||||
|
change_parent_folder: Alterar pasta pai
|
||||||
|
folder: Pasta
|
||||||
|
create_a_new_folder: Criar uma nova pasta
|
||||||
exit_preview: Sair da pré-visualização
|
exit_preview: Sair da pré-visualização
|
||||||
general: Geral
|
general: Geral
|
||||||
recipients: Destinatários
|
recipients: Destinatários
|
||||||
@@ -4188,6 +4255,15 @@ pt: &pt
|
|||||||
there_are_no_events: Nenhum evento
|
there_are_no_events: Nenhum evento
|
||||||
resend: Reenviar
|
resend: Reenviar
|
||||||
next_attempt_in_time_in_words: Próxima tentativa em %{time_in_words}
|
next_attempt_in_time_in_words: Próxima tentativa em %{time_in_words}
|
||||||
|
request_email_otp_verification_with_shared_link: Solicitar verificação de OTP por e-mail com link compartilhado
|
||||||
|
sms_rate_limit_exceeded: Limite de SMS excedido
|
||||||
|
invalid_phone_number: Número de telefone inválido
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Entre em contato com o solicitante para especificar seu número para autenticação de dois fatores.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Enviamos um código de verificação único para seu e-mail. Insira-o abaixo para continuar.
|
||||||
|
re_send_code: Reenviar código
|
||||||
|
email_verification: Verificação de e-mail
|
||||||
|
your_verification_code_to_access_the_name: 'Seu código de verificação para acessar "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Responda a este e-mail se você não solicitou isso.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envio em massa
|
bulk: Envio em massa
|
||||||
@@ -4205,6 +4281,7 @@ pt: &pt
|
|||||||
click_email_by_html: '<b>Link do e-mail clicado</b> por %{submitter_name}'
|
click_email_by_html: '<b>Link do e-mail clicado</b> por %{submitter_name}'
|
||||||
click_sms_by_html: '<b>Link do SMS clicado</b> por %{submitter_name}'
|
click_sms_by_html: '<b>Link do SMS clicado</b> por %{submitter_name}'
|
||||||
phone_verified_by_html: '<b>Telefone verificado</b> por %{submitter_name}'
|
phone_verified_by_html: '<b>Telefone verificado</b> por %{submitter_name}'
|
||||||
|
email_verified_by_html: '<b>Email verificado</b> por %{submitter_name}'
|
||||||
start_form_by_html: '<b>Submissão iniciada</b> por %{submitter_name}'
|
start_form_by_html: '<b>Submissão iniciada</b> por %{submitter_name}'
|
||||||
view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}'
|
view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}'
|
||||||
invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}'
|
invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}'
|
||||||
@@ -4294,6 +4371,7 @@ pt: &pt
|
|||||||
range_without_total: "%{from}-%{to} eventos"
|
range_without_total: "%{from}-%{to} eventos"
|
||||||
|
|
||||||
de: &de
|
de: &de
|
||||||
|
pro: Pro
|
||||||
default_parties: Standardparteien
|
default_parties: Standardparteien
|
||||||
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
|
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
|
||||||
require_all_recipients: Alle Empfänger erforderlich
|
require_all_recipients: Alle Empfänger erforderlich
|
||||||
@@ -4734,6 +4812,10 @@ de: &de
|
|||||||
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".'
|
users_count_total_users_count_pro_users_limit_was_reached_to_invite_additional_users_please_purchase_more_pro_user_seats_via_the_manage_plan_button: 'Das Limit von %{users_count}/%{total_users_count} Pro-Benutzern wurde erreicht. Um weitere Benutzer einzuladen, kaufen Sie bitte zusätzliche Pro-Benutzerplätze über die Schaltfläche "Plan verwalten".'
|
||||||
move_into_folder: In Ordner verschieben
|
move_into_folder: In Ordner verschieben
|
||||||
new_folder_name: Neuer Ordnername
|
new_folder_name: Neuer Ordnername
|
||||||
|
new_subfolder_name: Neuer Unterordnername
|
||||||
|
change_parent_folder: Übergeordneten Ordner ändern
|
||||||
|
folder: Ordner
|
||||||
|
create_a_new_folder: Neuen Ordner erstellen
|
||||||
exit_preview: Vorschau beenden
|
exit_preview: Vorschau beenden
|
||||||
general: Allgemein
|
general: Allgemein
|
||||||
recipients: Empfänger
|
recipients: Empfänger
|
||||||
@@ -5044,6 +5126,15 @@ de: &de
|
|||||||
there_are_no_events: Keine Ereignisse vorhanden
|
there_are_no_events: Keine Ereignisse vorhanden
|
||||||
resend: Erneut senden
|
resend: Erneut senden
|
||||||
next_attempt_in_time_in_words: Nächster Versuch in %{time_in_words}
|
next_attempt_in_time_in_words: Nächster Versuch in %{time_in_words}
|
||||||
|
request_email_otp_verification_with_shared_link: Fordern Sie die E-Mail-OTP-Verifizierung mit Freigabelink an
|
||||||
|
sms_rate_limit_exceeded: SMS-Limit überschritten
|
||||||
|
invalid_phone_number: Ungültige Telefonnummer
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Kontaktieren Sie den Absender, um Ihre Telefonnummer für die Zwei-Faktor-Authentifizierung anzugeben.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Wir haben einen einmaligen Verifizierungscode an Ihre E-Mail-Adresse gesendet. Bitte geben Sie ihn unten ein, um fortzufahren.
|
||||||
|
re_send_code: Code erneut senden
|
||||||
|
email_verification: E-Mail-Verifizierung
|
||||||
|
your_verification_code_to_access_the_name: 'Ihr Verifizierungscode für den Zugriff auf "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Antworten Sie auf diese E-Mail, wenn Sie dies nicht angefordert haben.
|
||||||
submission_sources:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Massenversand
|
bulk: Massenversand
|
||||||
@@ -5061,6 +5152,7 @@ de: &de
|
|||||||
click_email_by_html: '<b>E-Mail-Link angeklickt</b> von %{submitter_name}'
|
click_email_by_html: '<b>E-Mail-Link angeklickt</b> von %{submitter_name}'
|
||||||
click_sms_by_html: '<b>SMS-Link angeklickt</b> von %{submitter_name}'
|
click_sms_by_html: '<b>SMS-Link angeklickt</b> von %{submitter_name}'
|
||||||
phone_verified_by_html: '<b>Telefon verifiziert</b> von %{submitter_name}'
|
phone_verified_by_html: '<b>Telefon verifiziert</b> von %{submitter_name}'
|
||||||
|
email_verified_by_html: '<b>Email verifiziert</b> von %{submitter_name}'
|
||||||
start_form_by_html: '<b>Einreichung gestartet</b> von %{submitter_name}'
|
start_form_by_html: '<b>Einreichung gestartet</b> von %{submitter_name}'
|
||||||
view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}'
|
view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}'
|
||||||
invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}'
|
invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}'
|
||||||
@@ -5227,6 +5319,15 @@ pl:
|
|||||||
select_data_residency: Wybierz lokalizację danych
|
select_data_residency: Wybierz lokalizację danych
|
||||||
company_name: Nazwa firmy
|
company_name: Nazwa firmy
|
||||||
optional: opcjonalne
|
optional: opcjonalne
|
||||||
|
submit: Prześlij
|
||||||
|
sms_rate_limit_exceeded: Przekroczono limit wiadomości SMS
|
||||||
|
invalid_phone_number: Nieprawidłowy numer telefonu
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Skontaktuj się z nadawcą, aby podać numer telefonu do uwierzytelniania dwuskładnikowego.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Wysłaliśmy jednorazowy kod weryfikacyjny na Twój adres e-mail. Wprowadź go poniżej, aby kontynuować.
|
||||||
|
re_send_code: Wyślij ponownie kod
|
||||||
|
email_verification: Weryfikacja e-mail
|
||||||
|
your_verification_code_to_access_the_name: 'Twój kod weryfikacyjny do uzyskania dostępu do "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Odpowiedz na ten e-mail, jeśli nie prosiłeś o to.
|
||||||
|
|
||||||
uk:
|
uk:
|
||||||
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
|
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
|
||||||
@@ -5307,6 +5408,15 @@ uk:
|
|||||||
select_data_residency: Виберіть місце зберігання даних
|
select_data_residency: Виберіть місце зберігання даних
|
||||||
company_name: Назва компанії
|
company_name: Назва компанії
|
||||||
optional: необов’язково
|
optional: необов’язково
|
||||||
|
submit: Надіслати
|
||||||
|
sms_rate_limit_exceeded: Перевищено ліміт SMS
|
||||||
|
invalid_phone_number: Невірний номер телефону
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Зв’яжіться з відправником, щоб вказати номер телефону для двофакторної автентифікації.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Ми надіслали одноразовий код підтвердження на вашу електронну пошту. Введіть його нижче, щоб продовжити.
|
||||||
|
re_send_code: Надіслати код повторно
|
||||||
|
email_verification: Підтвердження електронної пошти
|
||||||
|
your_verification_code_to_access_the_name: 'Ваш код доступу до "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Відповідайте на цей лист, якщо ви цього не запитували.
|
||||||
|
|
||||||
cs:
|
cs:
|
||||||
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
|
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
|
||||||
@@ -5387,6 +5497,15 @@ cs:
|
|||||||
select_data_residency: Vyberte umístění dat
|
select_data_residency: Vyberte umístění dat
|
||||||
company_name: Název společnosti
|
company_name: Název společnosti
|
||||||
optional: volitelné
|
optional: volitelné
|
||||||
|
submit: Odeslat
|
||||||
|
sms_rate_limit_exceeded: Překročena hranice SMS
|
||||||
|
invalid_phone_number: Neplatné telefonní číslo
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Kontaktujte odesílatele kvůli zadání vašeho čísla pro dvoufázové ověření.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: Poslali jsme jednorázový ověřovací kód na váš e-mail. Zadejte ho níže pro pokračování.
|
||||||
|
re_send_code: Znovu odeslat kód
|
||||||
|
email_verification: Ověření e-mailu
|
||||||
|
your_verification_code_to_access_the_name: 'Váš ověřovací kód pro přístup k "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Odpovězte na tento e-mail, pokud jste o to nežádali.
|
||||||
|
|
||||||
he:
|
he:
|
||||||
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
|
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
|
||||||
@@ -5467,6 +5586,15 @@ he:
|
|||||||
select_data_residency: בחר מיקום נתונים
|
select_data_residency: בחר מיקום נתונים
|
||||||
company_name: שם החברה
|
company_name: שם החברה
|
||||||
optional: אופציונלי
|
optional: אופציונלי
|
||||||
|
submit: 'שלח'
|
||||||
|
sms_rate_limit_exceeded: 'חריגה ממגבלת SMS'
|
||||||
|
invalid_phone_number: 'מספר טלפון לא תקין'
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 'פנה לשולח כדי לציין את מספרך לאימות דו-שלבי.'
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 'שלחנו קוד אימות חד-פעמי לדוא"ל שלך. הזן את הקוד למטה כדי להמשיך.'
|
||||||
|
re_send_code: 'שלח קוד מחדש'
|
||||||
|
email_verification: 'אימות דוא"ל'
|
||||||
|
your_verification_code_to_access_the_name: 'קוד האימות שלך לגישה ל-%{name}:'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: 'השב למייל זה אם לא ביקשת זאת.'
|
||||||
|
|
||||||
nl:
|
nl:
|
||||||
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
|
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
|
||||||
@@ -5547,6 +5675,15 @@ nl:
|
|||||||
select_data_residency: Selecteer gegevenslocatie
|
select_data_residency: Selecteer gegevenslocatie
|
||||||
company_name: Bedrijfsnaam
|
company_name: Bedrijfsnaam
|
||||||
optional: optioneel
|
optional: optioneel
|
||||||
|
submit: Verzenden
|
||||||
|
sms_rate_limit_exceeded: SMS-limiet overschreden
|
||||||
|
invalid_phone_number: Ongeldig telefoonnummer
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: Neem contact op met de aanvrager om uw nummer voor twee-factor-authenticatie op te geven.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: We hebben een eenmalige verificatiecode naar je e-mailadres gestuurd. Voer de code hieronder in om verder te gaan.
|
||||||
|
re_send_code: Code opnieuw verzenden
|
||||||
|
email_verification: E-mailverificatie
|
||||||
|
your_verification_code_to_access_the_name: 'Je verificatiecode voor toegang tot "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: Reageer op deze e-mail als je dit niet hebt aangevraagd.
|
||||||
|
|
||||||
ar:
|
ar:
|
||||||
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
|
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
|
||||||
@@ -5627,6 +5764,15 @@ ar:
|
|||||||
select_data_residency: اختر موقع تخزين البيانات
|
select_data_residency: اختر موقع تخزين البيانات
|
||||||
company_name: اسم الشركة
|
company_name: اسم الشركة
|
||||||
optional: اختياري
|
optional: اختياري
|
||||||
|
submit: 'إرسال'
|
||||||
|
sms_rate_limit_exceeded: 'تم تجاوز حد رسائل SMS'
|
||||||
|
invalid_phone_number: 'رقم الهاتف غير صالح'
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 'يرجى الاتصال بالمرسل لتحديد رقم هاتفك للمصادقة الثنائية.'
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 'أرسلنا رمز تحقق لمرة واحدة إلى بريدك الإلكتروني. الرجاء إدخاله أدناه للمتابعة.'
|
||||||
|
re_send_code: 'إعادة إرسال الرمز'
|
||||||
|
email_verification: 'التحقق من البريد الإلكتروني'
|
||||||
|
your_verification_code_to_access_the_name: 'رمز التحقق الخاص بك للوصول إلى "%{name}":'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: 'يرجى الرد على هذا البريد إذا لم تطلب ذلك.'
|
||||||
|
|
||||||
ko:
|
ko:
|
||||||
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
|
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
|
||||||
@@ -5707,6 +5853,15 @@ ko:
|
|||||||
select_data_residency: 데이터 저장 위치 선택
|
select_data_residency: 데이터 저장 위치 선택
|
||||||
company_name: 회사 이름
|
company_name: 회사 이름
|
||||||
optional: 선택 사항
|
optional: 선택 사항
|
||||||
|
submit: 제출
|
||||||
|
sms_rate_limit_exceeded: SMS 제한 초과
|
||||||
|
invalid_phone_number: 잘못된 전화번호입니다
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 이중 인증을 위해 전화번호를 지정하려면 요청자에게 문의하세요.
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: 일회용 인증 코드를 이메일로 보냈습니다. 계속하려면 아래에 입력하세요.
|
||||||
|
re_send_code: 코드 재전송
|
||||||
|
email_verification: 이메일 인증
|
||||||
|
your_verification_code_to_access_the_name: '"%{name}"에 액세스하기 위한 인증 코드:'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: 요청하지 않았다면 이 이메일에 회신하세요.
|
||||||
|
|
||||||
ja:
|
ja:
|
||||||
require_phone_2fa_to_open: 電話による2段階認証が必要です
|
require_phone_2fa_to_open: 電話による2段階認証が必要です
|
||||||
@@ -5787,6 +5942,15 @@ ja:
|
|||||||
select_data_residency: データ保存場所を選択
|
select_data_residency: データ保存場所を選択
|
||||||
company_name: 会社名
|
company_name: 会社名
|
||||||
optional: 任意
|
optional: 任意
|
||||||
|
submit: 送信
|
||||||
|
sms_rate_limit_exceeded: SMSの送信制限を超えました
|
||||||
|
invalid_phone_number: 無効な電話番号です
|
||||||
|
please_contact_the_requester_to_specify_your_phone_number_for_two_factor_authentication: 二要素認証のため、リクエスターに電話番号を指定してください。
|
||||||
|
we_sent_a_one_time_verification_code_to_your_email_address_please_enter_the_code_below_to_continue: ワンタイム認証コードをメールアドレスに送信しました。続行するには、以下にコードを入力してください。
|
||||||
|
re_send_code: コードを再送信
|
||||||
|
email_verification: メール認証
|
||||||
|
your_verification_code_to_access_the_name: '"%{name}"へのアクセスコード:'
|
||||||
|
please_reply_to_this_email_if_you_didnt_request_this: このリクエストを行っていない場合は、このメールに返信してください。
|
||||||
|
|
||||||
en-US:
|
en-US:
|
||||||
<<: *en
|
<<: *en
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
resource :resubmit_form, controller: 'start_form', only: :update
|
resource :resubmit_form, controller: 'start_form', only: :update
|
||||||
|
resources :start_form_email_2fa_send, only: :create
|
||||||
|
|
||||||
resources :submit_form, only: %i[], path: '' do
|
resources :submit_form, only: %i[], path: '' do
|
||||||
get :success, on: :collection
|
get :success, on: :collection
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddParentFolderIdToTemplateFolders < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_reference :template_folders, :parent_folder, foreign_key: { to_table: :template_folders }, index: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do
|
ActiveRecord::Schema[8.0].define(version: 2025_07_18_121133) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -356,8 +356,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do
|
|||||||
t.datetime "archived_at"
|
t.datetime "archived_at"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.bigint "parent_folder_id"
|
||||||
t.index ["account_id"], name: "index_template_folders_on_account_id"
|
t.index ["account_id"], name: "index_template_folders_on_account_id"
|
||||||
t.index ["author_id"], name: "index_template_folders_on_author_id"
|
t.index ["author_id"], name: "index_template_folders_on_author_id"
|
||||||
|
t.index ["parent_folder_id"], name: "index_template_folders_on_parent_folder_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "template_sharings", force: :cascade do |t|
|
create_table "template_sharings", force: :cascade do |t|
|
||||||
@@ -498,6 +500,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_130628) do
|
|||||||
add_foreign_key "submitters", "submissions"
|
add_foreign_key "submitters", "submissions"
|
||||||
add_foreign_key "template_accesses", "templates"
|
add_foreign_key "template_accesses", "templates"
|
||||||
add_foreign_key "template_folders", "accounts"
|
add_foreign_key "template_folders", "accounts"
|
||||||
|
add_foreign_key "template_folders", "template_folders", column: "parent_folder_id"
|
||||||
add_foreign_key "template_folders", "users", column: "author_id"
|
add_foreign_key "template_folders", "users", column: "author_id"
|
||||||
add_foreign_key "template_sharings", "templates"
|
add_foreign_key "template_sharings", "templates"
|
||||||
add_foreign_key "templates", "accounts"
|
add_foreign_key "templates", "accounts"
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ module Docuseal
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pdf_format
|
||||||
|
@pdf_format ||= ENV['PDF_FORMAT'].to_s.downcase
|
||||||
|
end
|
||||||
|
|
||||||
def trusted_certs
|
def trusted_certs
|
||||||
@trusted_certs ||=
|
@trusted_certs ||=
|
||||||
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64|
|
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EmailVerificationCodes
|
||||||
|
DRIFT_BEHIND = 5.minutes
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def generate(value)
|
||||||
|
totp = ROTP::TOTP.new(build_totp_secret(value))
|
||||||
|
|
||||||
|
totp.at(Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify(code, value)
|
||||||
|
totp = ROTP::TOTP.new(build_totp_secret(value))
|
||||||
|
|
||||||
|
totp.verify(code, drift_behind: DRIFT_BEHIND)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_totp_secret(value)
|
||||||
|
ROTP::Base32.encode(
|
||||||
|
Digest::SHA1.digest(
|
||||||
|
[Rails.application.secret_key_base, value].join(':')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module PdfUtils
|
module PdfUtils
|
||||||
|
DEFAULT_DPI = 72
|
||||||
|
US_LETTER_W = DEFAULT_DPI * 8.5
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def encrypted?(data, password: nil)
|
def encrypted?(data, password: nil)
|
||||||
|
|||||||
@@ -132,12 +132,14 @@ module SearchEntries
|
|||||||
return if submitter.email.blank? && submitter.phone.blank? && submitter.name.blank?
|
return if submitter.email.blank? && submitter.phone.blank? && submitter.name.blank?
|
||||||
|
|
||||||
email_phone_name = [
|
email_phone_name = [
|
||||||
[submitter.email.to_s, submitter.email.to_s.split('@').last].join(' ').delete("\0"),
|
submitter.email.to_s.then { |e| [e, e.split('@').last] }.join(' ').delete("\0"),
|
||||||
[submitter.phone.to_s.gsub(/\D/, ''),
|
submitter.phone.to_s.then { |e| [e.gsub(/\D/, ''), e.gsub(PhoneCodes::REGEXP, '').gsub(/\D/, '')] }
|
||||||
submitter.phone.to_s.gsub(PhoneCodes::REGEXP, '').gsub(/\D/, '')].uniq.join(' ').delete("\0"),
|
.uniq.join(' ').delete("\0"),
|
||||||
TextUtils.transliterate(submitter.name).delete("\0")
|
TextUtils.transliterate(submitter.name).delete("\0")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
values_string = build_submitter_values_string(submitter)
|
||||||
|
|
||||||
sql = SearchEntry.sanitize_sql_array(
|
sql = SearchEntry.sanitize_sql_array(
|
||||||
[
|
[
|
||||||
"SELECT setweight(to_tsvector(?), 'A') || setweight(to_tsvector(?), 'B') ||
|
"SELECT setweight(to_tsvector(?), 'A') || setweight(to_tsvector(?), 'B') ||
|
||||||
@@ -145,9 +147,7 @@ module SearchEntries
|
|||||||
setweight(to_tsvector('simple', ?), 'A') ||
|
setweight(to_tsvector('simple', ?), 'A') ||
|
||||||
setweight(to_tsvector('simple', ?), 'B') ||
|
setweight(to_tsvector('simple', ?), 'B') ||
|
||||||
setweight(to_tsvector('simple', ?), 'C') as ngram".squish,
|
setweight(to_tsvector('simple', ?), 'C') as ngram".squish,
|
||||||
*email_phone_name,
|
*email_phone_name, values_string, *email_phone_name
|
||||||
build_submitter_values_string(submitter),
|
|
||||||
*email_phone_name
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,6 +155,9 @@ module SearchEntries
|
|||||||
|
|
||||||
entry.account_id = submitter.account_id
|
entry.account_id = submitter.account_id
|
||||||
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
|
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
|
||||||
|
|
||||||
|
add_hyphens(entry, values_string)
|
||||||
|
|
||||||
entry.ngram = build_ngram(ngram)
|
entry.ngram = build_ngram(ngram)
|
||||||
|
|
||||||
return if entry.tsvector.blank?
|
return if entry.tsvector.blank?
|
||||||
@@ -189,11 +192,7 @@ module SearchEntries
|
|||||||
entry.account_id = template.account_id
|
entry.account_id = template.account_id
|
||||||
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
|
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
|
||||||
|
|
||||||
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
|
add_hyphens(entry, text)
|
||||||
|
|
||||||
hyphens.uniq.each_with_index do |item, index|
|
|
||||||
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
|
|
||||||
end
|
|
||||||
|
|
||||||
entry.ngram = build_ngram(ngram)
|
entry.ngram = build_ngram(ngram)
|
||||||
|
|
||||||
@@ -220,11 +219,7 @@ module SearchEntries
|
|||||||
entry.account_id = submission.account_id
|
entry.account_id = submission.account_id
|
||||||
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
|
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
|
||||||
|
|
||||||
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
|
add_hyphens(entry, text)
|
||||||
|
|
||||||
hyphens.uniq.each_with_index do |item, index|
|
|
||||||
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
|
|
||||||
end
|
|
||||||
|
|
||||||
entry.ngram = build_ngram(ngram)
|
entry.ngram = build_ngram(ngram)
|
||||||
|
|
||||||
@@ -239,6 +234,16 @@ module SearchEntries
|
|||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_hyphens(entry, text)
|
||||||
|
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
|
||||||
|
|
||||||
|
hyphens.uniq.each_with_index do |item, index|
|
||||||
|
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
|
||||||
|
end
|
||||||
|
|
||||||
|
entry
|
||||||
|
end
|
||||||
|
|
||||||
def build_ngram(ngram)
|
def build_ngram(ngram)
|
||||||
ngrams =
|
ngrams =
|
||||||
ngram.split(/\s(?=')/).each_with_object([]) do |item, acc|
|
ngram.split(/\s(?=')/).each_with_object([]) do |item, acc|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module Submissions
|
|||||||
ALLOWED_PARAMS = %w[
|
ALLOWED_PARAMS = %w[
|
||||||
author
|
author
|
||||||
status
|
status
|
||||||
|
folder
|
||||||
completed_at_from
|
completed_at_from
|
||||||
completed_at_to
|
completed_at_to
|
||||||
created_at_from
|
created_at_from
|
||||||
@@ -24,6 +25,7 @@ module Submissions
|
|||||||
filters = normalize_filter_params(params, current_user)
|
filters = normalize_filter_params(params, current_user)
|
||||||
|
|
||||||
submissions = filter_by_author(submissions, filters, current_user)
|
submissions = filter_by_author(submissions, filters, current_user)
|
||||||
|
submissions = filter_by_folder(submissions, filters, current_user)
|
||||||
submissions = filter_by_status(submissions, filters)
|
submissions = filter_by_status(submissions, filters)
|
||||||
submissions = filter_by_created_at(submissions, filters)
|
submissions = filter_by_created_at(submissions, filters)
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ module Submissions
|
|||||||
return submissions if filters[:author].blank?
|
return submissions if filters[:author].blank?
|
||||||
|
|
||||||
user = current_user.account.users.find_by(email: filters[:author])
|
user = current_user.account.users.find_by(email: filters[:author])
|
||||||
|
|
||||||
submissions.where(created_by_user_id: user&.id || -1)
|
submissions.where(created_by_user_id: user&.id || -1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -87,6 +90,17 @@ module Submissions
|
|||||||
submissions
|
submissions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_by_folder(submissions, filters, current_user)
|
||||||
|
return submissions if filters[:folder].blank?
|
||||||
|
|
||||||
|
folders =
|
||||||
|
TemplateFolders.filter_by_full_name(current_user.account.template_folders, filters[:folder])
|
||||||
|
|
||||||
|
folders += folders.preload(:subfolders).flat_map(&:subfolders)
|
||||||
|
|
||||||
|
submissions.joins(:template).where(templates: { folder_id: folders.map(&:id) })
|
||||||
|
end
|
||||||
|
|
||||||
def filter_by_completed_at(submissions, filters)
|
def filter_by_completed_at(submissions, filters)
|
||||||
return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present?
|
return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present?
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,12 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
composer = HexaPDF::Composer.new(skip_page_creation: true)
|
composer = HexaPDF::Composer.new(skip_page_creation: true)
|
||||||
composer.document.task(:pdfa) if FONT_NAME == 'GoNotoKurrent'
|
|
||||||
|
if Docuseal.pdf_format == 'pdf/a-3b'
|
||||||
|
composer.document.task(:pdfa, level: '3b')
|
||||||
|
elsif FONT_NAME == 'GoNotoKurrent'
|
||||||
|
composer.document.task(:pdfa)
|
||||||
|
end
|
||||||
|
|
||||||
composer.document.config['font.map'] = {
|
composer.document.config['font.map'] = {
|
||||||
'Helvetica' => {
|
'Helvetica' => {
|
||||||
@@ -108,17 +113,16 @@ module Submissions
|
|||||||
|
|
||||||
configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY,
|
configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::WITH_AUDIT_SUBMITTER_TIMEZONE_KEY])
|
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY])
|
||||||
|
|
||||||
last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
|
last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
|
||||||
|
|
||||||
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||||
with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false
|
with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false
|
||||||
with_audit_submitter_timezone =
|
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
||||||
configs.find { |c| c.key == AccountConfig::WITH_AUDIT_SUBMITTER_TIMEZONE_KEY }&.value == true
|
|
||||||
|
|
||||||
timezone = account.timezone
|
timezone = account.timezone
|
||||||
timezone = last_submitter.timezone || account.timezone if with_audit_submitter_timezone
|
timezone = last_submitter.timezone || account.timezone if with_submitter_timezone
|
||||||
|
|
||||||
composer.page_style(:default, page_size:) do |canvas, style|
|
composer.page_style(:default, page_size:) do |canvas, style|
|
||||||
box = canvas.context.box(:media)
|
box = canvas.context.box(:media)
|
||||||
@@ -240,18 +244,23 @@ module Submissions
|
|||||||
|
|
||||||
click_email_event =
|
click_email_event =
|
||||||
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.click_email? }
|
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.click_email? }
|
||||||
|
|
||||||
|
verify_email_event =
|
||||||
|
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.phone_verified? }
|
||||||
|
|
||||||
is_phone_verified =
|
is_phone_verified =
|
||||||
submission.template_fields.any? do |e|
|
submission.template_fields.any? do |e|
|
||||||
e['type'] == 'phone' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
|
e['type'] == 'phone' && e['submitter_uuid'] == submitter.uuid && submitter.values[e['uuid']].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
verify_phone_event =
|
||||||
|
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.phone_verified? }
|
||||||
|
|
||||||
is_id_verified =
|
is_id_verified =
|
||||||
submission.template_fields.any? do |e|
|
submission.template_fields.any? do |e|
|
||||||
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
|
||||||
|
|
||||||
submitter_field_counters = Hash.new { 0 }
|
|
||||||
|
|
||||||
info_rows = [
|
info_rows = [
|
||||||
[
|
[
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
@@ -266,10 +275,10 @@ module Submissions
|
|||||||
[
|
[
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
[
|
[
|
||||||
submitter.email && click_email_event && {
|
submitter.email && (click_email_event || verify_email_event) && {
|
||||||
text: "#{I18n.t('email_verification')}: #{I18n.t('verified')}\n"
|
text: "#{I18n.t('email_verification')}: #{I18n.t('verified')}\n"
|
||||||
},
|
},
|
||||||
submitter.phone && is_phone_verified && {
|
submitter.phone && (is_phone_verified || verify_phone_event) && {
|
||||||
text: "#{I18n.t('phone_verification')}: #{I18n.t('verified')}\n"
|
text: "#{I18n.t('phone_verification')}: #{I18n.t('verified')}\n"
|
||||||
},
|
},
|
||||||
is_id_verified && {
|
is_id_verified && {
|
||||||
@@ -287,25 +296,53 @@ module Submissions
|
|||||||
|
|
||||||
composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } })
|
composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } })
|
||||||
|
|
||||||
|
submitter_field_counters = Hash.new { 0 }
|
||||||
|
grouped_value_field_names = {}
|
||||||
|
skip_grouped_field_uuids = {}
|
||||||
|
|
||||||
|
submission.template_fields.each do |field|
|
||||||
|
next unless field['type'].in?(%w[signature initials])
|
||||||
|
|
||||||
|
submitter_field_counters[field['type']] += 1
|
||||||
|
|
||||||
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
|
field_name = field['title'].presence || field['name'].presence ||
|
||||||
|
"#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}"
|
||||||
|
|
||||||
|
if grouped_value_field_names[value]
|
||||||
|
skip_grouped_field_uuids[field['uuid']] = true
|
||||||
|
|
||||||
|
grouped_value_field_names[value] += ", #{field_name}"
|
||||||
|
else
|
||||||
|
grouped_value_field_names[value] = field_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter_field_counters = Hash.new { 0 }
|
||||||
|
|
||||||
submission.template_fields.filter_map do |field|
|
submission.template_fields.filter_map do |field|
|
||||||
|
submitter_field_counters[field['type']] += 1
|
||||||
|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
next if field['type'] == 'heading'
|
next if field['type'] == 'heading'
|
||||||
next if !with_audit_values && !field['type'].in?(%w[signature initials])
|
next if !with_audit_values && !field['type'].in?(%w[signature initials])
|
||||||
|
next if skip_grouped_field_uuids[field['uuid']]
|
||||||
submitter_field_counters[field['type']] += 1
|
|
||||||
|
|
||||||
value = submitter.values[field['uuid']]
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
next if Array.wrap(value).compact_blank.blank?
|
next if Array.wrap(value).compact_blank.blank?
|
||||||
|
|
||||||
field_name = field['title'].presence || field['name'].to_s
|
field_name = grouped_value_field_names[value].presence || field['title'].presence || field['name'].to_s
|
||||||
|
|
||||||
[
|
[
|
||||||
composer.formatted_text_box(
|
composer.formatted_text_box(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence ||
|
text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence ||
|
||||||
"#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}\n".upcase,
|
"#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}".upcase,
|
||||||
font_size: 6
|
font_size: 6
|
||||||
}
|
}
|
||||||
].compact_blank,
|
].compact_blank,
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ module Submissions
|
|||||||
|
|
||||||
pdf.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
|
pdf.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
|
||||||
|
|
||||||
|
if Docuseal.pdf_format == 'pdf/a-3b'
|
||||||
|
pdf.task(:pdfa, level: '3b')
|
||||||
|
pdf.config['font.map'] = GenerateResultAttachments::PDFA_FONT_MAP
|
||||||
|
end
|
||||||
|
|
||||||
if pkcs
|
if pkcs
|
||||||
sign_params = {
|
sign_params = {
|
||||||
reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter),
|
reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter),
|
||||||
|
|||||||
@@ -13,10 +13,15 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID])
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
|
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||||
|
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
|
||||||
|
|
||||||
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||||
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
||||||
|
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
||||||
|
with_signature_id_reason =
|
||||||
|
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
||||||
|
|
||||||
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten)
|
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten)
|
||||||
|
|
||||||
@@ -28,7 +33,9 @@ module Submissions
|
|||||||
|
|
||||||
submitters.preload(attachments_attachments: :blob).each_with_index do |s, index|
|
submitters.preload(attachments_attachments: :blob).each_with_index do |s, index|
|
||||||
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
|
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
|
||||||
with_signature_id:, is_flatten:, with_headings: index.zero?)
|
with_signature_id:, is_flatten:, with_headings: index.zero?,
|
||||||
|
with_submitter_timezone:,
|
||||||
|
with_signature_id_reason:)
|
||||||
end
|
end
|
||||||
|
|
||||||
template = submission.template
|
template = submission.template
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ module Submissions
|
|||||||
bold_italic: FONT_BOLD_ITALIC_NAME
|
bold_italic: FONT_BOLD_ITALIC_NAME
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
PDFA_FONT_VARIANS = {
|
||||||
|
none: FONT_NAME,
|
||||||
|
bold: FONT_BOLD_NAME,
|
||||||
|
italic: FONT_NAME,
|
||||||
|
bold_italic: FONT_BOLD_NAME
|
||||||
|
}.freeze
|
||||||
|
|
||||||
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
|
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
|
||||||
|
|
||||||
RTL_REGEXP = TextUtils::RTL_REGEXP
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||||
@@ -43,11 +50,18 @@ module Submissions
|
|||||||
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
|
TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING'
|
||||||
DEFAULT_FONTS = %w[Times Helvetica Courier].freeze
|
DEFAULT_FONTS = %w[Times Helvetica Courier].freeze
|
||||||
FONTS_LINE_HEIGHT = {
|
FONTS_LINE_HEIGHT = {
|
||||||
'Times' => 1.4,
|
'Times' => 1.5,
|
||||||
'Helvetica' => 1.4,
|
'Helvetica' => 1.5,
|
||||||
'Courier' => 1.6
|
'Courier' => 1.6
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
PDFA_FONT_MAP = {
|
||||||
|
FONT_NAME => PDFA_FONT_VARIANS,
|
||||||
|
'Helvetica' => PDFA_FONT_VARIANS,
|
||||||
|
'Times' => PDFA_FONT_VARIANS,
|
||||||
|
'Courier' => PDFA_FONT_VARIANS
|
||||||
|
}.freeze
|
||||||
|
|
||||||
MISSING_GLYPH_REPLACE = {
|
MISSING_GLYPH_REPLACE = {
|
||||||
'▪' => '-',
|
'▪' => '-',
|
||||||
'✔️' => 'V',
|
'✔️' => 'V',
|
||||||
@@ -124,10 +138,15 @@ module Submissions
|
|||||||
|
|
||||||
def generate_pdfs(submitter)
|
def generate_pdfs(submitter)
|
||||||
configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID])
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
|
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||||
|
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
|
||||||
|
|
||||||
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||||
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
||||||
|
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
||||||
|
with_signature_id_reason =
|
||||||
|
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
||||||
|
|
||||||
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten)
|
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten)
|
||||||
|
|
||||||
@@ -171,10 +190,13 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:)
|
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:,
|
||||||
|
with_submitter_timezone:,
|
||||||
|
with_signature_id_reason:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil)
|
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
||||||
|
with_submitter_timezone: false, with_signature_id_reason: true)
|
||||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
||||||
|
|
||||||
attachments_data_cache = {}
|
attachments_data_cache = {}
|
||||||
@@ -256,6 +278,10 @@ module Submissions
|
|||||||
field_type = 'file' if field_type == 'image' &&
|
field_type = 'file' if field_type == 'image' &&
|
||||||
!submitter.attachments.find { |a| a.uuid == value }.image?
|
!submitter.attachments.find { |a| a.uuid == value }.image?
|
||||||
|
|
||||||
|
if field_type == 'signature' && field.dig('preferences', 'with_signature_id').in?([true, false])
|
||||||
|
with_signature_id = field['preferences']['with_signature_id']
|
||||||
|
end
|
||||||
|
|
||||||
case field_type
|
case field_type
|
||||||
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
|
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
@@ -274,10 +300,18 @@ module Submissions
|
|||||||
|
|
||||||
reason_string =
|
reason_string =
|
||||||
I18n.with_locale(locale) do
|
I18n.with_locale(locale) do
|
||||||
|
timezone = submitter.account.timezone
|
||||||
|
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
|
||||||
|
|
||||||
|
if with_signature_id_reason
|
||||||
"#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
|
"#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
|
||||||
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
|
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
|
||||||
"#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \
|
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
|
||||||
"#{TimeUtils.timezone_abbr(submitter.account.timezone, attachment.created_at)}"
|
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
|
||||||
|
else
|
||||||
|
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
|
||||||
|
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
|
reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
|
||||||
@@ -599,6 +633,11 @@ module Submissions
|
|||||||
|
|
||||||
pdf.trailer.info[:Creator] = info_creator
|
pdf.trailer.info[:Creator] = info_creator
|
||||||
|
|
||||||
|
if Docuseal.pdf_format == 'pdf/a-3b'
|
||||||
|
pdf.task(:pdfa, level: '3b')
|
||||||
|
pdf.config['font.map'] = PDFA_FONT_MAP
|
||||||
|
end
|
||||||
|
|
||||||
sign_reason = fetch_sign_reason(submitter)
|
sign_reason = fetch_sign_reason(submitter)
|
||||||
|
|
||||||
if sign_reason && pkcs
|
if sign_reason && pkcs
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ module Submitters
|
|||||||
'values' => 'D'
|
'values' => 'D'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
UnableToSendCode = Class.new(StandardError)
|
||||||
|
InvalidOtp = Class.new(StandardError)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def search(current_user, submitters, keyword)
|
def search(current_user, submitters, keyword)
|
||||||
@@ -194,4 +197,27 @@ module Submitters
|
|||||||
|
|
||||||
"#{filename}.#{blob.filename.extension}"
|
"#{filename}.#{blob.filename.extension}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_shared_link_email_verification_code(submitter, request:)
|
||||||
|
RateLimit.call("send-otp-code-#{request.remote_ip}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
TemplateMailer.otp_verification_email(submitter.submission.template, email: submitter.email).deliver_later!
|
||||||
|
rescue RateLimit::LimitApproached
|
||||||
|
Rollbar.warning("Limit verification code for template: #{submitter.submission.template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
raise UnableToSendCode, I18n.t('too_many_attempts')
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_link_otp!(otp, submitter)
|
||||||
|
return false if otp.blank?
|
||||||
|
|
||||||
|
RateLimit.call("verify-2fa-code-#{Digest::MD5.base64digest(submitter.email)}",
|
||||||
|
limit: 2, ttl: 45.seconds, enabled: true)
|
||||||
|
|
||||||
|
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
|
||||||
|
|
||||||
|
raise InvalidOtp, I18n.t(:invalid_code) unless EmailVerificationCodes.verify(otp, link_2fa_key)
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -13,6 +13,8 @@ module Submitters
|
|||||||
AccountConfig::REUSE_SIGNATURE_KEY,
|
AccountConfig::REUSE_SIGNATURE_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_SIGNATURE_ID_REASON_KEY,
|
||||||
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
|
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
@@ -31,20 +33,15 @@ module Submitters
|
|||||||
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
|
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
|
||||||
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
|
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
|
||||||
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_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_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:,
|
attrs = { completed_button:, with_typed_signature:, with_confetti:,
|
||||||
with_typed_signature:,
|
reuse_signature:, with_decline:, with_partial_download:,
|
||||||
with_confetti:,
|
policy_links:, enforce_signing_order:, completed_message:,
|
||||||
reuse_signature:,
|
require_signing_reason:, prefill_signature:, with_submitter_timezone:,
|
||||||
with_decline:,
|
with_signature_id_reason:, with_signature_id: }
|
||||||
with_partial_download:,
|
|
||||||
policy_links:,
|
|
||||||
enforce_signing_order:,
|
|
||||||
completed_message:,
|
|
||||||
require_signing_reason:,
|
|
||||||
prefill_signature:,
|
|
||||||
with_signature_id: }
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -3,15 +3,87 @@
|
|||||||
module TemplateFolders
|
module TemplateFolders
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
def filter_by_full_name(template_folders, name)
|
||||||
|
parent_name, name = name.split(' / ', 2).map(&:squish)
|
||||||
|
|
||||||
|
if name.present?
|
||||||
|
parent_folder = template_folders.where(parent_folder_id: nil).find_by(name: parent_name)
|
||||||
|
else
|
||||||
|
name = parent_name
|
||||||
|
end
|
||||||
|
|
||||||
|
template_folders.where(name:, parent_folder:)
|
||||||
|
end
|
||||||
|
|
||||||
def search(folders, keyword)
|
def search(folders, keyword)
|
||||||
return folders if keyword.blank?
|
return folders if keyword.blank?
|
||||||
|
|
||||||
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_active_folders(template_folders, templates)
|
||||||
|
folder_exists =
|
||||||
|
templates.active.where(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
|
||||||
|
.select(1).limit(1).arel.exists
|
||||||
|
|
||||||
|
subfolders_arel = TemplateFolder.arel_table.alias('subfolders')
|
||||||
|
|
||||||
|
subfolder_exists =
|
||||||
|
TemplateFolder.from(subfolders_arel)
|
||||||
|
.where(subfolders_arel[:parent_folder_id].eq(TemplateFolder.arel_table[:id]))
|
||||||
|
.where(
|
||||||
|
templates.active.where(Template.arel_table[:folder_id].eq(subfolders_arel[:id])).arel.exists
|
||||||
|
).select(1).limit(1).arel.exists
|
||||||
|
|
||||||
|
template_folders.where(folder_exists).or(template_folders.where(subfolder_exists))
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort(template_folders, current_user, order)
|
||||||
|
case order
|
||||||
|
when 'used_at'
|
||||||
|
subquery =
|
||||||
|
Template.left_joins(:submissions)
|
||||||
|
.group(:folder_id)
|
||||||
|
.where(account_id: current_user.account_id)
|
||||||
|
.select(
|
||||||
|
:folder_id,
|
||||||
|
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
|
||||||
|
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
|
||||||
|
)
|
||||||
|
|
||||||
|
template_folders = template_folders.joins(
|
||||||
|
Template.arel_table
|
||||||
|
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
|
||||||
|
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
|
||||||
|
.join_sources
|
||||||
|
)
|
||||||
|
|
||||||
|
template_folders.order(
|
||||||
|
Arel::Nodes::Case.new
|
||||||
|
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
|
||||||
|
.then(Template.arel_table[:submission_created_at_max])
|
||||||
|
.else(Template.arel_table[:updated_at_max])
|
||||||
|
.desc
|
||||||
|
)
|
||||||
|
when 'name'
|
||||||
|
template_folders.order(name: :asc)
|
||||||
|
else
|
||||||
|
template_folders.order(id: :desc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def find_or_create_by_name(author, name)
|
def find_or_create_by_name(author, name)
|
||||||
return author.account.default_template_folder if name.blank? || name == TemplateFolder::DEFAULT_NAME
|
return author.account.default_template_folder if name.blank? || name == TemplateFolder::DEFAULT_NAME
|
||||||
|
|
||||||
author.account.template_folders.create_with(author:, account: author.account).find_or_create_by(name:)
|
parent_name, name = name.split(' / ', 2).map(&:squish)
|
||||||
|
|
||||||
|
if name.present?
|
||||||
|
parent_folder = author.account.template_folders.create_with(author:)
|
||||||
|
.find_or_create_by(name: parent_name, parent_folder_id: nil)
|
||||||
|
else
|
||||||
|
name = parent_name
|
||||||
|
end
|
||||||
|
|
||||||
|
author.account.template_folders.create_with(author:).find_or_create_by(name:, parent_folder:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -15,6 +15,7 @@ module Templates
|
|||||||
MAX_NUMBER_OF_PAGES_PROCESSED = 15
|
MAX_NUMBER_OF_PAGES_PROCESSED = 15
|
||||||
MAX_FLATTEN_FILE_SIZE = 20.megabytes
|
MAX_FLATTEN_FILE_SIZE = 20.megabytes
|
||||||
GENERATE_PREVIEW_SIZE_LIMIT = 50.megabytes
|
GENERATE_PREVIEW_SIZE_LIMIT = 50.megabytes
|
||||||
|
US_LETTER_SIZE = { 'width' => MAX_WIDTH, 'height' => 1812 }.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"@babel/plugin-transform-runtime": "7.21.4",
|
"@babel/plugin-transform-runtime": "7.21.4",
|
||||||
"@babel/preset-env": "7.21.5",
|
"@babel/preset-env": "7.21.5",
|
||||||
"@babel/runtime": "7.21.5",
|
"@babel/runtime": "7.21.5",
|
||||||
"@eid-easy/eideasy-widget": "^2.159.0",
|
"@eid-easy/eideasy-widget": "^2.163.4",
|
||||||
"@github/catalyst": "^2.0.0-beta",
|
"@github/catalyst": "^2.0.0-beta",
|
||||||
"@hotwired/turbo": "https://github.com/docusealco/turbo#main",
|
"@hotwired/turbo": "https://github.com/docusealco/turbo#main",
|
||||||
"@hotwired/turbo-rails": "^7.3.0",
|
"@hotwired/turbo-rails": "^7.3.0",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateMailerPreview < ActionMailer::Preview
|
||||||
|
def otp_verification_email
|
||||||
|
template = Template.active.last
|
||||||
|
|
||||||
|
TemplateMailer.otp_verification_email(template, email: 'john.doe@example.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -161,7 +161,6 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(field_value(submitter, 'Cell code')).to eq '123'
|
expect(field_value(submitter, 'Cell code')).to eq '123'
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop:disable RSpec/ExampleLength
|
|
||||||
it 'completes the form when name, email, and phone are required' do
|
it 'completes the form when name, email, and phone are required' do
|
||||||
template.update(preferences: { link_form_fields: %w[email name phone] })
|
template.update(preferences: { link_form_fields: %w[email name phone] })
|
||||||
|
|
||||||
@@ -250,7 +249,93 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(field_value(submitter, 'Attachment')).to be_present
|
expect(field_value(submitter, 'Attachment')).to be_present
|
||||||
expect(field_value(submitter, 'Cell code')).to eq '123'
|
expect(field_value(submitter, 'Cell code')).to eq '123'
|
||||||
end
|
end
|
||||||
# rubocop:enable RSpec/ExampleLength
|
|
||||||
|
it 'completes the form when identity verification with a 2FA code is enabled', sidekiq: :inline do
|
||||||
|
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
|
||||||
|
value: GenerateCertificate.call.transform_values(&:to_pem))
|
||||||
|
|
||||||
|
template.update(preferences: { link_form_fields: %w[email name], shared_link_2fa: true })
|
||||||
|
|
||||||
|
visit start_form_path(slug: template.slug)
|
||||||
|
|
||||||
|
fill_in 'Email', with: 'john.dou@example.com'
|
||||||
|
fill_in 'Name', with: 'John Doe'
|
||||||
|
|
||||||
|
expect do
|
||||||
|
click_button 'Start'
|
||||||
|
end.to change { ActionMailer::Base.deliveries.count }.by(1)
|
||||||
|
|
||||||
|
email = ActionMailer::Base.deliveries.last
|
||||||
|
code = email.body.encoded[%r{<b>(.*?)</b>}, 1]
|
||||||
|
|
||||||
|
fill_in 'one_time_code', with: code
|
||||||
|
|
||||||
|
click_button 'Submit'
|
||||||
|
|
||||||
|
fill_in 'First Name', with: 'John'
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d')
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
check 'Do you agree?'
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
choose 'Boy'
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
draw_canvas
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
fill_in 'House number', with: '123'
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
%w[Red Blue].each { |color| check color }
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
select 'Male', from: 'Gender'
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
draw_canvas
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
find('#dropzone').click
|
||||||
|
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
find('#dropzone').click
|
||||||
|
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
fill_in 'Cell code', with: '123'
|
||||||
|
click_on 'Complete'
|
||||||
|
|
||||||
|
expect(page).to have_button('Download')
|
||||||
|
expect(page).to have_content('Document has been signed!')
|
||||||
|
|
||||||
|
submitter = template.submissions.last.submitters.last
|
||||||
|
|
||||||
|
expect(submitter.email).to eq('john.dou@example.com')
|
||||||
|
expect(submitter.name).to eq('John Doe')
|
||||||
|
expect(submitter.ip).to eq('127.0.0.1')
|
||||||
|
expect(submitter.ua).to be_present
|
||||||
|
expect(submitter.opened_at).to be_present
|
||||||
|
expect(submitter.completed_at).to be_present
|
||||||
|
expect(submitter.declined_at).to be_nil
|
||||||
|
|
||||||
|
expect(field_value(submitter, 'First Name')).to eq 'John'
|
||||||
|
expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d')
|
||||||
|
expect(field_value(submitter, 'Do you agree?')).to be_truthy
|
||||||
|
expect(field_value(submitter, 'First child')).to eq 'Boy'
|
||||||
|
expect(field_value(submitter, 'Signature')).to be_present
|
||||||
|
expect(field_value(submitter, 'House number')).to eq 123
|
||||||
|
expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue')
|
||||||
|
expect(field_value(submitter, 'Gender')).to eq 'Male'
|
||||||
|
expect(field_value(submitter, 'Initials')).to be_present
|
||||||
|
expect(field_value(submitter, 'Avatar')).to be_present
|
||||||
|
expect(field_value(submitter, 'Attachment')).to be_present
|
||||||
|
expect(field_value(submitter, 'Cell code')).to eq '123'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the submitter form link is opened' do
|
context 'when the submitter form link is opened' do
|
||||||
|
|||||||
@@ -1035,22 +1035,22 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||||
|
|
||||||
"@eid-easy/eideasy-browser-client@2.124.0":
|
"@eid-easy/eideasy-browser-client@2.127.0":
|
||||||
version "2.124.0"
|
version "2.127.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.124.0.tgz#940ccb9d4d853f0bd32e49cc455f2c7b13f3bbd1"
|
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.127.0.tgz#d1b09e3634e94d7e32f593209a63f962fd326731"
|
||||||
integrity sha512-hZuUeg0CcyvgJSseRBQixRStAIr58bulmNcikcASBo6z8wv+/R8nAnUoE7qdNW1l4ZyYxLmVOwDs6+W+FHy6hQ==
|
integrity sha512-2iosVqkF1C0hQWc6TVBe1SK7b/z8ZnvHnulGXfQf6VrrpEJMgnzK95h6LFDqDQyetfIwEGGoeOiUij2hYA1ZPA==
|
||||||
dependencies:
|
dependencies:
|
||||||
axios "1.8.2"
|
axios "1.8.2"
|
||||||
jsencrypt "3.2.1"
|
jsencrypt "3.2.1"
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
serialize-error "^9.1.1"
|
serialize-error "^9.1.1"
|
||||||
|
|
||||||
"@eid-easy/eideasy-widget@^2.159.0":
|
"@eid-easy/eideasy-widget@^2.163.4":
|
||||||
version "2.159.0"
|
version "2.163.4"
|
||||||
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.159.0.tgz#f2291dad292bd5f7941496f0db0aa0a180226b6b"
|
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.163.4.tgz#4a8ada2c61f032527a08f9f8d9d1adf3fa7b5334"
|
||||||
integrity sha512-527uCNrN5MVY/PaOUoZ3J2XZ0C+xt6057sJ4xSO0/FPYj2cxOn+qyCJODQjOhjBL7GcnpShWPpYAfv1OIStYwQ==
|
integrity sha512-7OQ1bm+KSG99wUI6szXT25Icq67CEQRK38VGj8fynW0ctqJTiFXRop7dqgR9JXLlJ1s1Z++El7igYxph7Dq5Aw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eid-easy/eideasy-browser-client" "2.124.0"
|
"@eid-easy/eideasy-browser-client" "2.127.0"
|
||||||
core-js "^3.8.3"
|
core-js "^3.8.3"
|
||||||
i18n-iso-countries "^6.7.0"
|
i18n-iso-countries "^6.7.0"
|
||||||
lodash.defaultsdeep "^4.6.1"
|
lodash.defaultsdeep "^4.6.1"
|
||||||
|
|||||||