mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528a1216f8 | ||
|
|
f2479bd259 | ||
|
|
135f0826d2 | ||
|
|
5710f0177b | ||
|
|
1a3a0528ba | ||
|
|
59793ff374 | ||
|
|
3c4ed42419 | ||
|
|
6a17b61550 | ||
|
|
76659497b7 | ||
|
|
ee50c957cb | ||
|
|
a9dd200919 | ||
|
|
1fa953104a | ||
|
|
50eb5b070e | ||
|
|
10fd624bec | ||
|
|
04ec2f8260 | ||
|
|
d828d79574 | ||
|
|
01dd3fefe5 | ||
|
|
45ae954c0c | ||
|
|
1304849b55 | ||
|
|
0875faa079 | ||
|
|
3aa15d6ea6 | ||
|
|
6557329e97 | ||
|
|
be27ce4161 | ||
|
|
a3391a970e | ||
|
|
744d45d2c5 | ||
|
|
310b16f54a | ||
|
|
6d3ad4a869 | ||
|
|
1ce2448820 | ||
|
|
07b4ada5be | ||
|
|
f70bf530ba | ||
|
|
a26aa0f411 | ||
|
|
584577c21f | ||
|
|
1503375eed | ||
|
|
2dec099f20 | ||
|
|
bf6eed4ea2 | ||
|
|
e0dc33cfb4 | ||
|
|
9c9dc27537 | ||
|
|
7305637b8c | ||
|
|
a6e22adf2e | ||
|
|
809c6b1aa8 | ||
|
|
1536ded2a6 | ||
|
|
76d54971ce | ||
|
|
6dec56198c | ||
|
|
d96d252df3 | ||
|
|
3fca76a716 | ||
|
|
a184fa11f1 | ||
|
|
e9c0b6a703 | ||
|
|
ec7df08496 | ||
|
|
6627d7eac5 | ||
|
|
1f8945d710 | ||
|
|
daaa289a5c | ||
|
|
81a6a48d70 | ||
|
|
b93c7cd261 | ||
|
|
570482e12d | ||
|
|
39407557f2 | ||
|
|
3b040d558d | ||
|
|
56ba3c2a52 | ||
|
|
c7afe33c72 | ||
|
|
82500c2d7d | ||
|
|
fb1c1f117b | ||
|
|
7c6a7513fd | ||
|
|
6d13119ee0 | ||
|
|
1e2c752937 | ||
|
|
e82faf8320 | ||
|
|
4adb1001b0 | ||
|
|
992a1b26c0 | ||
|
|
c07bc6687c |
No files matched your search
+8
-8
@@ -179,7 +179,7 @@ GEM
|
|||||||
dotenv (3.2.0)
|
dotenv (3.2.0)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb (6.0.2)
|
erb (6.0.4)
|
||||||
erb_lint (0.9.0)
|
erb_lint (0.9.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
@@ -257,7 +257,7 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.2.1)
|
hashdiff (1.2.1)
|
||||||
hexapdf (1.6.0)
|
hexapdf (1.7.0)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
@@ -318,7 +318,7 @@ GEM
|
|||||||
multi_json (1.19.1)
|
multi_json (1.19.1)
|
||||||
net-http (0.9.1)
|
net-http (0.9.1)
|
||||||
uri (>= 0.11.1)
|
uri (>= 0.11.1)
|
||||||
net-imap (0.6.3)
|
net-imap (0.6.4)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@@ -328,15 +328,15 @@ GEM
|
|||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.19.2-aarch64-linux-gnu)
|
nokogiri (1.19.3-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.2-aarch64-linux-musl)
|
nokogiri (1.19.3-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.2-arm64-darwin)
|
nokogiri (1.19.3-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.2-x86_64-linux-gnu)
|
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.2-x86_64-linux-musl)
|
nokogiri (1.19.3-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray-alt (0.10.3)
|
numo-narray-alt (0.10.3)
|
||||||
oj (3.16.16)
|
oj (3.16.16)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- Company logo and white-label
|
- Company logo and white-label
|
||||||
- User roles
|
- User roles
|
||||||
- Automated reminders
|
- Automated reminders
|
||||||
- Invitation and identify verification via SMS
|
- Invitation and identity verification via SMS
|
||||||
- Conditional fields and formulas
|
- Conditional fields and formulas
|
||||||
- Bulk send with CSV, XLSX spreadsheet import
|
- Bulk send with CSV, XLSX spreadsheet import
|
||||||
- SSO / SAML
|
- SSO / SAML
|
||||||
@@ -74,7 +74,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
|
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
|
||||||
```
|
```
|
||||||
|
|
||||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||||
|
|
||||||
#### Docker Compose
|
#### Docker Compose
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,16 @@ module Api
|
|||||||
else
|
else
|
||||||
http_cache_forever public: true do
|
http_cache_forever public: true do
|
||||||
response.headers['Accept-Ranges'] = 'bytes'
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
|
||||||
|
|
||||||
|
if request.head?
|
||||||
|
response.headers['Content-Type'] = blob.content_type_for_serving
|
||||||
|
head :ok
|
||||||
|
else
|
||||||
send_blob_stream blob, disposition: params[:disposition]
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,8 +63,6 @@ module Api
|
|||||||
return if !require_ttl && !require_auth
|
return if !require_ttl && !require_auth
|
||||||
end
|
end
|
||||||
|
|
||||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
|
||||||
|
|
||||||
raise CanCan::AccessDenied
|
raise CanCan::AccessDenied
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ module Api
|
|||||||
COOKIE_STORE_LIMIT = 10
|
COOKIE_STORE_LIMIT = 10
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
unless can_upload?(submitter)
|
unless can_upload?(@submitter)
|
||||||
Rollbar.error("Can't upload: #{submitter.id}") if defined?(Rollbar)
|
Rollbar.error("Can't upload: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -20,23 +20,23 @@ module Api
|
|||||||
image = Vips::Image.new_from_file(params[:file].path)
|
image = Vips::Image.new_from_file(params[:file].path)
|
||||||
|
|
||||||
if ImageUtils.blank?(image)
|
if ImageUtils.blank?(image)
|
||||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
if ImageUtils.error?(image)
|
if ImageUtils.error?(image)
|
||||||
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||||
status: :unprocessable_content
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attachment = Submitters.create_attachment!(submitter, params)
|
attachment = Submitters.create_attachment!(@submitter, params)
|
||||||
|
|
||||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
|
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
|
|||||||
@@ -161,9 +161,7 @@ module Api
|
|||||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.values = submitter.values.transform_values do |v|
|
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
|
||||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
@@ -205,10 +203,15 @@ module Api
|
|||||||
|
|
||||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||||
|
|
||||||
if submitter_preferences.key?('require_phone_2fa')
|
if submitter_preferences.key?('require_phone_2fa')
|
||||||
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if submitter_preferences.key?('require_email_2fa')
|
||||||
|
submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_2fa']
|
||||||
|
end
|
||||||
|
|
||||||
if submitter_preferences.key?('go_to_last')
|
if submitter_preferences.key?('go_to_last')
|
||||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_document_tempfile_path(attachment)
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
|
||||||
|
|
||||||
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||||
f.flock(File::LOCK_EX)
|
f.flock(File::LOCK_EX)
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class RevealAccessTokenController < ApplicationController
|
class RevealAccessTokenController < ApplicationController
|
||||||
|
rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda {
|
||||||
|
Rollbar.error('Rate limit api key') if defined?(Rollbar)
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||||
|
locals: { error_message: I18n.t(:too_many_attempts) }),
|
||||||
|
status: :unprocessable_content
|
||||||
|
}
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize!(:manage, current_user.access_token)
|
authorize!(:manage, current_user.access_token)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
template = Template.find_by!(slug: params[:template_slug])
|
template = Template.find_by!(slug: params[:template_slug])
|
||||||
|
|
||||||
@submitter =
|
@submitter =
|
||||||
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
||||||
elsif params[:submission_slug]
|
elsif params[:submission_slug]
|
||||||
submission = Submission.find_by(slug: params[:submission_slug])
|
submission = Submission.find_by(slug: params[:submission_slug])
|
||||||
|
|
||||||
@@ -27,9 +27,11 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @submitter
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||||
|
end
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return head :unprocessable_content unless can_invite?(submitter)
|
return head :unprocessable_content unless can_invite?(@submitter)
|
||||||
|
|
||||||
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
invite_submitters = filter_invite_submitters(@submitter, 'invite_by_uuid')
|
||||||
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
optional_invite_submitters = filter_invite_submitters(@submitter, 'optional_invite_by_uuid')
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
(invite_submitters + optional_invite_submitters).each do |item|
|
(invite_submitters + optional_invite_submitters).each do |item|
|
||||||
@@ -21,18 +21,18 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
|
|
||||||
email = Submissions.normalize_email(attrs[:email])
|
email = Submissions.normalize_email(attrs[:email])
|
||||||
|
|
||||||
submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: submitter.account_id)
|
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid })
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.submission.update!(submitters_order: :preserved)
|
@submitter.submission.update!(submitters_order: :preserved)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.submission.submitters.reload
|
@submitter.submission.submitters.reload
|
||||||
|
|
||||||
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
if invite_submitters.all? { |s| @submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||||
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
|
Submitters::SubmitValues.call(@submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ class SubmitFormMetadataController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return head :not_found if submitter.declined_at? ||
|
return head :not_found if @submitter.declined_at? ||
|
||||||
submitter.completed_at? ||
|
@submitter.completed_at? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
submitter.account.archived_at? ||
|
@submitter.account.archived_at? ||
|
||||||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
submission = submitter.submission
|
submission = @submitter.submission
|
||||||
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||||
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: submitter.uuid)
|
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
documents = schema.filter_map do |item|
|
documents = schema.filter_map do |item|
|
||||||
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ class SubmitFormValuesController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return render json: {} if submitter.completed_at? ||
|
return render json: {} if @submitter.completed_at? ||
|
||||||
submitter.declined_at? ||
|
@submitter.declined_at? ||
|
||||||
submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
value = submitter.values[params['field_uuid']]
|
value = @submitter.values[params['field_uuid']]
|
||||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
attachment = @submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
value:,
|
value:,
|
||||||
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
}, head: :ok
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TemplatesController < ApplicationController
|
class TemplatesController < ApplicationController
|
||||||
|
TEMPLATE_FIELDS = %i[id author_id folder_id external_id name slug
|
||||||
|
schema fields submitters variables_schema preferences
|
||||||
|
shared_link source archived_at created_at updated_at].freeze
|
||||||
|
|
||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -33,10 +37,11 @@ class TemplatesController < ApplicationController
|
|||||||
).call
|
).call
|
||||||
|
|
||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json(only: TEMPLATE_FIELDS).merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
|
only: %i[id uuid],
|
||||||
methods: %i[metadata signed_key],
|
methods: %i[metadata signed_key],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
|
|
||||||
@@ -72,6 +77,8 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
|
||||||
|
TemplateVersions.find_or_create_for(@template, author: current_user) if params[:revision]
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesVersionsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def index
|
||||||
|
versions = @template.template_versions.order(id: :desc).preload(:author)
|
||||||
|
|
||||||
|
render json: versions.as_json(TemplateVersions::SERIALIZE_PARAMS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
version = @template.template_versions.find(params[:id])
|
||||||
|
|
||||||
|
render json: TemplateVersions.serialize(version)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
class TestingAccountsController < ApplicationController
|
class TestingAccountsController < ApplicationController
|
||||||
skip_authorization_check only: :destroy
|
skip_authorization_check only: :destroy
|
||||||
|
|
||||||
def show
|
def create
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
authorize!(:manage, current_user)
|
authorize!(:manage, current_user)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookHmacController < ApplicationController
|
||||||
|
load_and_authorize_resource :webhook_url, parent: false
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
end
|
||||||
@@ -169,6 +169,9 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||||
|
withDetectExistingFields: this.dataset.withDetectExistingFields === 'true',
|
||||||
|
withRevisions: true,
|
||||||
|
withRevisionsMenu: this.dataset.withRevisionsMenu === 'true',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
withCustomFields: true,
|
withCustomFields: true,
|
||||||
@@ -180,6 +183,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
withDynamicDocuments: this.dataset.withDynamicDocuments === 'true',
|
withDynamicDocuments: this.dataset.withDynamicDocuments === 'true',
|
||||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
|
pagePreviewFormat: this.dataset.pagePreviewFormat || '.jpg',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
withDownload: true,
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!this.canvas) return
|
if (!this.canvas) return
|
||||||
|
if (!this.canvas.parentNode?.clientWidth) return
|
||||||
|
|
||||||
const { width, height } = this.canvas
|
const { width, height } = this.canvas
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redrawCanvas (oldWidth, oldHeight) {
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||||
const sx = this.canvas.width / oldWidth
|
const sx = this.canvas.width / oldWidth
|
||||||
const sy = this.canvas.height / oldHeight
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!this.canvas) return
|
if (!this.canvas) return
|
||||||
|
if (!this.canvas.parentNode?.clientWidth) return
|
||||||
|
|
||||||
const { width, height } = this.canvas
|
const { width, height } = this.canvas
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redrawCanvas (oldWidth, oldHeight) {
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && this.canvas.width > 0 && this.canvas.height > 0) {
|
||||||
const sx = this.canvas.width / oldWidth
|
const sx = this.canvas.width / oldWidth
|
||||||
const sy = this.canvas.height / oldHeight
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,25 @@ export default {
|
|||||||
const areas = {}
|
const areas = {}
|
||||||
|
|
||||||
this.field.areas?.forEach((area) => {
|
this.field.areas?.forEach((area) => {
|
||||||
areas[area.attachment_uuid + area.page] ||= area
|
areas[area.attachment_uuid] ||= []
|
||||||
|
areas[area.attachment_uuid].push(area)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Object.values(areas).slice(0, 6)
|
const sortedAreas = Object.values(areas).reduce((acc, group) => {
|
||||||
|
const seen = {}
|
||||||
|
const sortedGroup = [...group].sort((a, b) => a.page - b.page)
|
||||||
|
|
||||||
|
sortedGroup.forEach((area) => {
|
||||||
|
if (!seen[area.page]) {
|
||||||
|
seen[area.page] = true
|
||||||
|
acc.push(area)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return sortedAreas.slice(0, 6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -527,7 +527,8 @@ export default {
|
|||||||
try {
|
try {
|
||||||
return this.formatDate(
|
return this.formatDate(
|
||||||
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'),
|
||||||
|
{ withTimePlaceholders: this.modelValue === '{{date}}' }
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return this.modelValue
|
return this.modelValue
|
||||||
@@ -646,36 +647,55 @@ export default {
|
|||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format, { withTimePlaceholders = false } = {}) {
|
||||||
const monthFormats = {
|
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||||
M: 'numeric',
|
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||||
MM: '2-digit',
|
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||||
MMM: 'short',
|
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||||
MMMM: 'long'
|
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||||
}
|
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||||
|
|
||||||
const dayFormats = {
|
const hasTime = /[HhAasz]/.test(format)
|
||||||
D: 'numeric',
|
|
||||||
DD: '2-digit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const yearFormats = {
|
const opts = {
|
||||||
YYYY: 'numeric',
|
|
||||||
YYY: 'numeric',
|
|
||||||
YY: '2-digit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = new Intl.DateTimeFormat([], {
|
|
||||||
day: dayFormats[format.match(/D+/)],
|
day: dayFormats[format.match(/D+/)],
|
||||||
month: monthFormats[format.match(/M+/)],
|
month: monthFormats[format.match(/M+/)],
|
||||||
year: yearFormats[format.match(/Y+/)],
|
year: yearFormats[format.match(/Y+/)]
|
||||||
timeZone: 'UTC'
|
}
|
||||||
}).formatToParts(date)
|
|
||||||
|
|
||||||
return format
|
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||||
|
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||||
|
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||||
|
if (!hasTime) opts.timeZone = 'UTC'
|
||||||
|
|
||||||
|
const partTypes = {
|
||||||
|
M: 'month',
|
||||||
|
D: 'day',
|
||||||
|
Y: 'year',
|
||||||
|
H: 'hour',
|
||||||
|
h: 'hour',
|
||||||
|
m: 'minute',
|
||||||
|
s: 'second',
|
||||||
|
z: 'timeZoneName',
|
||||||
|
A: 'dayPeriod',
|
||||||
|
a: 'dayPeriod'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||||
|
|
||||||
|
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||||
|
if (withTimePlaceholders && /^(HH|hh|H|h|mm|m|ss|s|A|a)$/.test(token)) return '--'
|
||||||
|
|
||||||
|
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||||
|
|
||||||
|
if (token === 'A') return (value || '').toUpperCase()
|
||||||
|
if (token === 'a') return (value || '').toLowerCase()
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
},
|
},
|
||||||
updateMultipleSelectValue (value) {
|
updateMultipleSelectValue (value) {
|
||||||
if (this.modelValue?.includes(value)) {
|
if (this.modelValue?.includes(value)) {
|
||||||
|
|||||||
@@ -56,12 +56,18 @@
|
|||||||
class="base-input !text-2xl text-center w-full"
|
class="base-input !text-2xl text-center w-full"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||||
type="date"
|
:type="inputType"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="formatType === 'datetime' ? undefined : `values[${field.uuid}]`"
|
||||||
@keydown.enter="onEnter"
|
@keydown.enter="onEnter"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
@paste="onPaste"
|
@paste="onPaste"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-if="formatType === 'datetime'"
|
||||||
|
type="hidden"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
:value="modelValue"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -97,14 +103,19 @@ export default {
|
|||||||
},
|
},
|
||||||
emits: ['update:model-value', 'focus', 'submit'],
|
emits: ['update:model-value', 'focus', 'submit'],
|
||||||
computed: {
|
computed: {
|
||||||
|
formatType () {
|
||||||
|
const format = this.field.preferences?.format || ''
|
||||||
|
|
||||||
|
if (/[HhAasz]/.test(format)) return 'datetime'
|
||||||
|
if (format && !/[Dd]/.test(format)) return 'month'
|
||||||
|
|
||||||
|
return 'date'
|
||||||
|
},
|
||||||
|
inputType () {
|
||||||
|
return { datetime: 'datetime-local', month: 'month', date: 'date' }[this.formatType]
|
||||||
|
},
|
||||||
dateNowString () {
|
dateNowString () {
|
||||||
const today = new Date()
|
return this.formatDateValue(new Date())
|
||||||
|
|
||||||
const yyyy = today.getFullYear()
|
|
||||||
const mm = String(today.getMonth() + 1).padStart(2, '0')
|
|
||||||
const dd = String(today.getDate()).padStart(2, '0')
|
|
||||||
|
|
||||||
return `${yyyy}-${mm}-${dd}`
|
|
||||||
},
|
},
|
||||||
validationMin () {
|
validationMin () {
|
||||||
if (this.field.validation?.min) {
|
if (this.field.validation?.min) {
|
||||||
@@ -121,6 +132,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
withToday () {
|
withToday () {
|
||||||
|
if (this.formatType === 'datetime') return false
|
||||||
|
|
||||||
const todayDate = new Date().setHours(0, 0, 0, 0)
|
const todayDate = new Date().setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
if (this.validationMin) {
|
if (this.validationMin) {
|
||||||
@@ -137,9 +150,25 @@ export default {
|
|||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
set (value) {
|
set (value) {
|
||||||
|
if (this.formatType === 'datetime' && value) {
|
||||||
|
const d = new Date(value)
|
||||||
|
|
||||||
|
if (!isNaN(d)) {
|
||||||
|
this.$emit('update:model-value', d.toISOString())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
},
|
},
|
||||||
get () {
|
get () {
|
||||||
|
if (this.formatType === 'datetime') {
|
||||||
|
const d = new Date(this.modelValue)
|
||||||
|
|
||||||
|
return isNaN(d) ? '' : this.formatDateValue(d)
|
||||||
|
}
|
||||||
|
|
||||||
return this.modelValue
|
return this.modelValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,20 +192,32 @@ export default {
|
|||||||
|
|
||||||
const parsedDate = new Date(pasteData)
|
const parsedDate = new Date(pasteData)
|
||||||
|
|
||||||
if (!isNaN(parsedDate)) {
|
if (isNaN(parsedDate)) return
|
||||||
const inputEl = this.$refs.input
|
|
||||||
|
|
||||||
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
|
this.setInputValue(parsedDate)
|
||||||
|
|
||||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setCurrentDate () {
|
setCurrentDate () {
|
||||||
|
this.setInputValue(new Date())
|
||||||
|
},
|
||||||
|
setInputValue (date) {
|
||||||
const inputEl = this.$refs.input
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
if (this.formatType === 'date') {
|
||||||
|
inputEl.valueAsDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||||
|
} else {
|
||||||
|
inputEl.value = this.formatDateValue(date)
|
||||||
|
}
|
||||||
|
|
||||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
},
|
||||||
|
formatDateValue (date) {
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
|
|
||||||
|
if (this.formatType === 'month') return ymd.slice(0, 7)
|
||||||
|
if (this.formatType === 'datetime') return `${ymd}T${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||||
|
|
||||||
|
return ymd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const es = {
|
|||||||
verification_code_is_invalid: 'El código de verificación no es válido',
|
verification_code_is_invalid: 'El código de verificación no es válido',
|
||||||
sign_on_the_touchscreen: 'Firmar en pantalla táctil',
|
sign_on_the_touchscreen: 'Firmar en pantalla táctil',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
||||||
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta la',
|
||||||
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||||
esignature_disclosure: 'Divulgación de eFirma',
|
esignature_disclosure: 'Divulgación de eFirma',
|
||||||
already_paid: 'Ya pagado',
|
already_paid: 'Ya pagado',
|
||||||
@@ -245,7 +245,7 @@ const it = {
|
|||||||
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
||||||
sign_on_the_touchscreen: 'Firma su schermo tattile',
|
sign_on_the_touchscreen: 'Firma su schermo tattile',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
||||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti la',
|
||||||
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||||
esignature_disclosure: 'Divulgazione della eFirma',
|
esignature_disclosure: 'Divulgazione della eFirma',
|
||||||
minimize: 'Minimizza',
|
minimize: 'Minimizza',
|
||||||
@@ -258,7 +258,7 @@ const it = {
|
|||||||
date: 'Data',
|
date: 'Data',
|
||||||
number: 'Numero',
|
number: 'Numero',
|
||||||
image: 'Immagine',
|
image: 'Immagine',
|
||||||
pay: 'Pagamento',
|
pay: 'Paga',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
select: 'Seleziona',
|
select: 'Seleziona',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
@@ -574,7 +574,7 @@ const pl = {
|
|||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
||||||
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||||
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||||
esignature_disclosure: 'Ujawnienie ePodpisu',
|
esignature_disclosure: 'informację o e-podpisie',
|
||||||
minimize: 'Zminimalizuj',
|
minimize: 'Zminimalizuj',
|
||||||
text: 'Tekst',
|
text: 'Tekst',
|
||||||
already_paid: 'Już zapłacono',
|
already_paid: 'Już zapłacono',
|
||||||
@@ -642,7 +642,7 @@ const pl = {
|
|||||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany',
|
email_has_been_sent: 'E-mail został wysłany',
|
||||||
processing: 'Przetwarzanie',
|
processing: 'Przetwarzanie',
|
||||||
pay_with_stripe: 'Płatność za pomocą Stripe',
|
pay_with_stripe: 'Zapłać za pomocą Stripe',
|
||||||
reupload: 'Ponowne przesłanie',
|
reupload: 'Ponowne przesłanie',
|
||||||
upload: 'Przesyłanie',
|
upload: 'Przesyłanie',
|
||||||
files: 'Pliki',
|
files: 'Pliki',
|
||||||
@@ -678,12 +678,12 @@ const uk = {
|
|||||||
authored_by: 'Автор',
|
authored_by: 'Автор',
|
||||||
select_a_reason: 'Виберіть причину',
|
select_a_reason: 'Виберіть причину',
|
||||||
value_is_invalid: 'Значення є неправильним',
|
value_is_invalid: 'Значення є неправильним',
|
||||||
verification_code_is_invalid: 'Код підтвердження є неправильним',
|
verification_code_is_invalid: 'Невірний код підтвердження',
|
||||||
sign_on_the_touchscreen: 'Підписати на сенсорному екрані',
|
sign_on_the_touchscreen: 'Підписати на сенсорному екрані',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
||||||
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||||
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
electronic_signature_disclosure: 'інформацією про е-підпис',
|
||||||
esignature_disclosure: 'Розголошення еПідпису',
|
esignature_disclosure: 'інформацією про е-підпис',
|
||||||
minimize: 'Зменшити',
|
minimize: 'Зменшити',
|
||||||
text: 'Текст',
|
text: 'Текст',
|
||||||
already_paid: 'Вже оплачено',
|
already_paid: 'Вже оплачено',
|
||||||
@@ -751,7 +751,7 @@ const uk = {
|
|||||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений',
|
email_has_been_sent: 'Електронний лист був відправлений',
|
||||||
processing: 'Обробка',
|
processing: 'Обробка',
|
||||||
pay_with_strip: 'Сплатити за допомогою Stripe',
|
pay_with_stripe: 'Сплатити за допомогою Stripe',
|
||||||
reupload: 'Перезавантажити',
|
reupload: 'Перезавантажити',
|
||||||
upload: 'Завантажити',
|
upload: 'Завантажити',
|
||||||
files: 'Файли',
|
files: 'Файли',
|
||||||
@@ -774,7 +774,7 @@ const cs = {
|
|||||||
verify_id: 'Ověřit ID',
|
verify_id: 'Ověřit ID',
|
||||||
identity_verification: 'Ověření identity',
|
identity_verification: 'Ověření identity',
|
||||||
complete: 'Dokončit',
|
complete: 'Dokončit',
|
||||||
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
|
fill_all_required_fields_to_complete: 'Vyplňte povinná pole pro dokončení',
|
||||||
sign_and_complete: 'Podepsat a dokončit',
|
sign_and_complete: 'Podepsat a dokončit',
|
||||||
invite: 'Pozvat',
|
invite: 'Pozvat',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -782,8 +782,8 @@ const cs = {
|
|||||||
reviewed: 'Zkontrolováno',
|
reviewed: 'Zkontrolováno',
|
||||||
other: 'Jiné',
|
other: 'Jiné',
|
||||||
authored_by_me: 'Autorem jsem já',
|
authored_by_me: 'Autorem jsem já',
|
||||||
approved_by: 'Schváleno kým',
|
approved_by: 'Schválil(a)',
|
||||||
reviewed_by: 'Zkontrolováno kým',
|
reviewed_by: 'Zkontroloval(a)',
|
||||||
authored_by: 'Autorem',
|
authored_by: 'Autorem',
|
||||||
select_a_reason: 'Vyberte důvod',
|
select_a_reason: 'Vyberte důvod',
|
||||||
value_is_invalid: 'Hodnota je neplatná',
|
value_is_invalid: 'Hodnota je neplatná',
|
||||||
@@ -792,7 +792,7 @@ const cs = {
|
|||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
||||||
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||||
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||||
esignature_disclosure: 'Zveřejnění ePodpisu',
|
esignature_disclosure: 'informací o e-podpisu',
|
||||||
already_paid: 'Už zaplaceno',
|
already_paid: 'Už zaplaceno',
|
||||||
minimize: 'Minimalizovat',
|
minimize: 'Minimalizovat',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
@@ -860,7 +860,7 @@ const cs = {
|
|||||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán',
|
email_has_been_sent: 'E-mail byl odeslán',
|
||||||
processing: 'Zpracování',
|
processing: 'Zpracování',
|
||||||
pay_with_stripe: 'Zaplacení přes Stripe',
|
pay_with_stripe: 'Zaplatit přes Stripe',
|
||||||
reupload: 'Znovu nahrát',
|
reupload: 'Znovu nahrát',
|
||||||
upload: 'Nahrát',
|
upload: 'Nahrát',
|
||||||
files: 'Soubory',
|
files: 'Soubory',
|
||||||
@@ -890,18 +890,18 @@ const pt = {
|
|||||||
approved: 'Aprovado',
|
approved: 'Aprovado',
|
||||||
reviewed: 'Revisado',
|
reviewed: 'Revisado',
|
||||||
other: 'Outro',
|
other: 'Outro',
|
||||||
authored_by_me: 'Autorizado por mim',
|
authored_by_me: 'Escrito por mim',
|
||||||
approved_by: 'Aprovado por',
|
approved_by: 'Aprovado por',
|
||||||
reviewed_by: 'Revisado por',
|
reviewed_by: 'Revisado por',
|
||||||
authored_by: 'Autorizado por',
|
authored_by: 'Escrito por',
|
||||||
select_a_reason: 'Selecione um motivo',
|
select_a_reason: 'Selecione um motivo',
|
||||||
value_is_invalid: 'Valor é inválido',
|
value_is_invalid: 'Valor é inválido',
|
||||||
verification_code_is_invalid: 'Código de verificação é inválido',
|
verification_code_is_invalid: 'Código de verificação é inválido',
|
||||||
sign_on_the_touchscreen: 'Assinar na tela sensível',
|
sign_on_the_touchscreen: 'Assinar na tela sensível',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
||||||
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com a',
|
||||||
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||||
esignature_disclosure: 'Divulgação da eAssinatura',
|
esignature_disclosure: 'Informação sobre eAssinatura',
|
||||||
already_paid: 'Já pago',
|
already_paid: 'Já pago',
|
||||||
minimize: 'Minimizar',
|
minimize: 'Minimizar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
@@ -970,7 +970,7 @@ const pt = {
|
|||||||
email_has_been_sent: 'Email enviado',
|
email_has_been_sent: 'Email enviado',
|
||||||
processing: 'Processamento',
|
processing: 'Processamento',
|
||||||
pay_with_stripe: 'Pagar com Stripe',
|
pay_with_stripe: 'Pagar com Stripe',
|
||||||
reupload: 'Reenviar',
|
reupload: 'Recarregar',
|
||||||
upload: 'Carregar',
|
upload: 'Carregar',
|
||||||
files: 'Arquivos',
|
files: 'Arquivos',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||||
@@ -1010,8 +1010,8 @@ const he = {
|
|||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
||||||
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||||
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||||
esignature_disclosure: 'חשיפת ה-eחתימה',
|
esignature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||||
minimize: 'לקטן',
|
minimize: 'מזער',
|
||||||
text: 'טקסט',
|
text: 'טקסט',
|
||||||
already_paid: 'כבר שולם',
|
already_paid: 'כבר שולם',
|
||||||
signature: 'חתימה',
|
signature: 'חתימה',
|
||||||
@@ -1022,9 +1022,9 @@ const he = {
|
|||||||
number: 'מספר',
|
number: 'מספר',
|
||||||
image: 'תמונה',
|
image: 'תמונה',
|
||||||
file: 'קובץ',
|
file: 'קובץ',
|
||||||
pay: 'תשלום',
|
pay: 'שלם',
|
||||||
select: 'בחר',
|
select: 'בחר',
|
||||||
checkbox: 'תיק בחירה',
|
checkbox: 'סימון',
|
||||||
multiple: 'רב ערכים',
|
multiple: 'רב ערכים',
|
||||||
radio: 'רדיו',
|
radio: 'רדיו',
|
||||||
cells: 'תאים',
|
cells: 'תאים',
|
||||||
@@ -1051,9 +1051,9 @@ const he = {
|
|||||||
download: 'הורדה',
|
download: 'הורדה',
|
||||||
clear: 'נקה',
|
clear: 'נקה',
|
||||||
redraw: 'צייר מחדש',
|
redraw: 'צייר מחדש',
|
||||||
draw_initials: 'צייר ציוני ראשי תיבות',
|
draw_initials: 'צייר ראשי תיבות',
|
||||||
type_signature_here: 'הקלד חתימה כאן',
|
type_signature_here: 'הקלד חתימה כאן',
|
||||||
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
|
type_initial_here: 'הקלד ראשי תיבות כאן',
|
||||||
form_has_been_completed: 'הטופס הושלם',
|
form_has_been_completed: 'הטופס הושלם',
|
||||||
document_has_been_signed: 'המסמך נחתם!',
|
document_has_been_signed: 'המסמך נחתם!',
|
||||||
documents_have_been_signed: 'המסמכים נחתמו!',
|
documents_have_been_signed: 'המסמכים נחתמו!',
|
||||||
@@ -1063,7 +1063,7 @@ const he = {
|
|||||||
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
||||||
verified_phone_number: 'אימות מספר טלפון',
|
verified_phone_number: 'אימות מספר טלפון',
|
||||||
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
||||||
six_digits_code: 'קוד משתמש שש ספרות',
|
six_digits_code: 'קוד שש ספרות',
|
||||||
change_phone_number: 'שינוי מספר טלפון',
|
change_phone_number: 'שינוי מספר טלפון',
|
||||||
sending: 'שולח',
|
sending: 'שולח',
|
||||||
resend_code: 'שלח מחדש קוד',
|
resend_code: 'שלח מחדש קוד',
|
||||||
@@ -1072,7 +1072,7 @@ const he = {
|
|||||||
set_today: 'קבע היום',
|
set_today: 'קבע היום',
|
||||||
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
|
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
|
||||||
draw_signature: 'צייר חתימה',
|
draw_signature: 'צייר חתימה',
|
||||||
type_initial: 'הקלד ציוני ראשי תיבות',
|
type_initial: 'הקלד ראשי תיבות',
|
||||||
draw: 'צייר',
|
draw: 'צייר',
|
||||||
type: 'הקלד',
|
type: 'הקלד',
|
||||||
type_text: 'הקלד טקסט',
|
type_text: 'הקלד טקסט',
|
||||||
@@ -1098,7 +1098,7 @@ const nl = {
|
|||||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
verify_id: 'Verifiëren ID',
|
verify_id: 'ID verifiëren',
|
||||||
identity_verification: 'Identiteitsverificatie',
|
identity_verification: 'Identiteitsverificatie',
|
||||||
complete: 'Voltooien',
|
complete: 'Voltooien',
|
||||||
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
|
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
|
||||||
@@ -1146,7 +1146,7 @@ const nl = {
|
|||||||
continue: 'Doorgaan',
|
continue: 'Doorgaan',
|
||||||
sign_now: 'Nu ondertekenen',
|
sign_now: 'Nu ondertekenen',
|
||||||
type_here_: 'Typ hier...',
|
type_here_: 'Typ hier...',
|
||||||
optional: 'Optioneel',
|
optional: 'optioneel',
|
||||||
option: 'Optie',
|
option: 'Optie',
|
||||||
appears_on: 'Verschijnt op',
|
appears_on: 'Verschijnt op',
|
||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
@@ -1170,11 +1170,11 @@ const nl = {
|
|||||||
powered_by: 'Aangedreven door',
|
powered_by: 'Aangedreven door',
|
||||||
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
|
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
|
||||||
open_source_documents_software: 'Open source documenten software',
|
open_source_documents_software: 'Open source documenten software',
|
||||||
verified_phone_number: 'Geverifieerd telefoonnummer',
|
verified_phone_number: 'Telefoonnummer verifiëren',
|
||||||
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
||||||
six_digits_code: '6-cijferige code',
|
six_digits_code: '6-cijferige code',
|
||||||
change_phone_number: 'Wijzig telefoonnummer',
|
change_phone_number: 'Wijzig telefoonnummer',
|
||||||
sending: 'Voltooien...',
|
sending: 'Verzenden...',
|
||||||
resend_code: 'Code opnieuw verzenden',
|
resend_code: 'Code opnieuw verzenden',
|
||||||
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
|
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
|
||||||
please_fill_all_required_fields: 'Vul alle verplichte velden in',
|
please_fill_all_required_fields: 'Vul alle verplichte velden in',
|
||||||
@@ -1232,7 +1232,7 @@ const ar = {
|
|||||||
esignature_disclosure: 'كشف التوقيع الإلكتروني',
|
esignature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
text: 'نص',
|
text: 'نص',
|
||||||
signature: 'توقيع',
|
signature: 'توقيع',
|
||||||
initials: 'الاختصارات',
|
initials: 'حروف أولى',
|
||||||
date: 'تاريخ',
|
date: 'تاريخ',
|
||||||
number: 'رقم',
|
number: 'رقم',
|
||||||
digitally_signed_by: 'تم التوقيع رقميًا بواسطة',
|
digitally_signed_by: 'تم التوقيع رقميًا بواسطة',
|
||||||
@@ -1269,9 +1269,9 @@ const ar = {
|
|||||||
download: 'تحميل',
|
download: 'تحميل',
|
||||||
clear: 'مسح',
|
clear: 'مسح',
|
||||||
redraw: 'إعادة الرسم',
|
redraw: 'إعادة الرسم',
|
||||||
draw_initials: 'ارسم الاختصارات',
|
draw_initials: 'ارسم حروف أولى',
|
||||||
type_signature_here: 'اكتب التوقيع هنا',
|
type_signature_here: 'اكتب التوقيع هنا',
|
||||||
type_initial_here: 'اكتب الاختصارات هنا',
|
type_initial_here: 'اكتب حروف أولى هنا',
|
||||||
form_has_been_completed: 'تم إكمال النموذج!',
|
form_has_been_completed: 'تم إكمال النموذج!',
|
||||||
document_has_been_signed: 'تم توقيع الوثيقة!',
|
document_has_been_signed: 'تم توقيع الوثيقة!',
|
||||||
documents_have_been_signed: 'تم توقيع الوثائق!',
|
documents_have_been_signed: 'تم توقيع الوثائق!',
|
||||||
@@ -1290,13 +1290,13 @@ const ar = {
|
|||||||
set_today: 'تعيين اليوم',
|
set_today: 'تعيين اليوم',
|
||||||
toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
|
toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
|
||||||
draw_signature: 'ارسم التوقيع',
|
draw_signature: 'ارسم التوقيع',
|
||||||
type_initial: 'اكتب الاختصارات',
|
type_initial: 'اكتب حروف أولى',
|
||||||
draw: 'ارسم',
|
draw: 'ارسم',
|
||||||
type: 'اكتب',
|
type: 'اكتب',
|
||||||
type_text: 'اكتب نصًا',
|
type_text: 'اكتب نصًا',
|
||||||
email_has_been_sent: 'تم إرسال البريد الإلكتروني',
|
email_has_been_sent: 'تم إرسال البريد الإلكتروني',
|
||||||
processing: 'جارٍ المعالجة',
|
processing: 'جارٍ المعالجة',
|
||||||
pay_with_strip: 'الدفع بواسطة Stripe',
|
pay_with_stripe: 'الدفع بواسطة Stripe',
|
||||||
reupload: 'إعادة التحميل',
|
reupload: 'إعادة التحميل',
|
||||||
upload: 'تحميل',
|
upload: 'تحميل',
|
||||||
files: 'الملفات',
|
files: 'الملفات',
|
||||||
@@ -1343,10 +1343,10 @@ const ko = {
|
|||||||
signature: '서명',
|
signature: '서명',
|
||||||
initials: '이니셜',
|
initials: '이니셜',
|
||||||
date: '날짜',
|
date: '날짜',
|
||||||
digitally_signed_by: '디지털 서명이 완료되었습니다',
|
digitally_signed_by: '디지털 서명자:',
|
||||||
reason: '이유',
|
reason: '이유',
|
||||||
number: '숫자',
|
number: '숫자',
|
||||||
pay: '급여',
|
pay: '결제',
|
||||||
image: '이미지',
|
image: '이미지',
|
||||||
take_photo: '사진 찍기',
|
take_photo: '사진 찍기',
|
||||||
file: '파일',
|
file: '파일',
|
||||||
|
|||||||
@@ -544,6 +544,7 @@ export default {
|
|||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!this.$refs.canvas) return
|
if (!this.$refs.canvas) return
|
||||||
|
if (!this.$refs.canvas.parentNode?.clientWidth) return
|
||||||
|
|
||||||
const { width, height } = this.$refs.canvas
|
const { width, height } = this.$refs.canvas
|
||||||
|
|
||||||
@@ -586,7 +587,7 @@ export default {
|
|||||||
redrawCanvas (oldWidth, oldHeight) {
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
|
|
||||||
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0 && canvas.width > 0 && canvas.height > 0) {
|
||||||
const sx = canvas.width / oldWidth
|
const sx = canvas.width / oldWidth
|
||||||
const sy = canvas.height / oldHeight
|
const sy = canvas.height / oldHeight
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
@remove="$emit('remove')"
|
@remove="$emit('remove')"
|
||||||
@scroll-to="$emit('scroll-to', $event)"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
@add-custom-field="$emit('add-custom-field')"
|
@add-custom-field="$emit('add-custom-field')"
|
||||||
|
@click-title="$emit('click-title')"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="touchValueTarget"
|
ref="touchValueTarget"
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
:style="{ color: field.preferences?.color }"
|
:style="{ color: isConditionMatch ? field.preferences?.color : '#9ca3af' }"
|
||||||
:class="{ 'w-full h-full': isWFullType }"
|
:class="{ 'w-full h-full': isWFullType }"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
@@ -131,13 +132,13 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<span
|
<span
|
||||||
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
v-else-if="field.type === 'number' && !isContenteditable && (displayValue || displayValue == 0)"
|
||||||
class="whitespace-pre-wrap"
|
class="whitespace-pre-wrap"
|
||||||
>{{ formatNumber(field.default_value, field.preferences?.format) }}</span>
|
>{{ formatNumber(displayValue, field.preferences?.format) }}</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="field.default_value === '{{date}}'"
|
v-else-if="field.default_value === '{{date}}'"
|
||||||
>
|
>
|
||||||
{{ t('signing_date') }}
|
{{ /[HhAasz]/.test(field.preferences?.format || '') ? t('signing_date_and_time') : t('signing_date') }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells' && field.default_value"
|
v-else-if="field.type === 'cells' && field.default_value"
|
||||||
@@ -183,12 +184,12 @@
|
|||||||
:contenteditable="isValueInput"
|
:contenteditable="isValueInput"
|
||||||
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
||||||
:class="{ 'cursor-text': isValueInput }"
|
:class="{ 'cursor-text': isValueInput }"
|
||||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (isConditionMatch ? (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value')) : '')"
|
||||||
@blur="onDefaultValueBlur"
|
@blur="onDefaultValueBlur"
|
||||||
@focus="selectedAreasRef.value = [area]"
|
@focus="selectedAreasRef.value = [area]"
|
||||||
@paste.prevent="onPaste"
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter="onDefaultValueEnter"
|
@keydown.enter="onDefaultValueEnter"
|
||||||
>{{ field.default_value }}</span>
|
>{{ displayValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
@@ -241,6 +242,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
conditionalFieldIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaValuesIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
isDraw: {
|
isDraw: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -307,7 +318,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
|
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isContenteditable: false,
|
isContenteditable: false,
|
||||||
@@ -323,11 +334,27 @@ 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,
|
||||||
|
isConditionMatch () {
|
||||||
|
return !this.inputMode || this.conditionalFieldIndex[this.field.uuid] !== false
|
||||||
|
},
|
||||||
|
displayValue () {
|
||||||
|
if (this.field.preferences?.formula && this.field.type !== 'payment') {
|
||||||
|
const computed = this.formulaValuesIndex[this.field.uuid]
|
||||||
|
|
||||||
|
if (computed != null) {
|
||||||
|
return computed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.field.default_value
|
||||||
|
},
|
||||||
bgClasses () {
|
bgClasses () {
|
||||||
if (this.field.type === 'heading') {
|
if (this.field.type === 'heading') {
|
||||||
return 'bg-gray-50'
|
return 'bg-gray-50'
|
||||||
} else if (this.field.type === 'strikethrough') {
|
} else if (this.field.type === 'strikethrough') {
|
||||||
return 'bg-transparent'
|
return 'bg-transparent'
|
||||||
|
} else if (!this.isConditionMatch) {
|
||||||
|
return 'bg-gray-100'
|
||||||
} else {
|
} else {
|
||||||
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
return this.bgColors[this.submitterIndex % this.bgColors.length]
|
||||||
}
|
}
|
||||||
@@ -337,6 +364,8 @@ export default {
|
|||||||
return ''
|
return ''
|
||||||
} else if (this.field.type === 'strikethrough') {
|
} else if (this.field.type === 'strikethrough') {
|
||||||
return 'border-dashed border-gray-300'
|
return 'border-dashed border-gray-300'
|
||||||
|
} else if (!this.isConditionMatch) {
|
||||||
|
return 'border-gray-300'
|
||||||
} else {
|
} else {
|
||||||
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
||||||
}
|
}
|
||||||
@@ -390,7 +419,7 @@ export default {
|
|||||||
return this.basePageWidth / 612.0
|
return this.basePageWidth / 612.0
|
||||||
},
|
},
|
||||||
isDefaultValuePresent () {
|
isDefaultValuePresent () {
|
||||||
return this.field?.default_value || this.field?.default_value === 0
|
return this.field?.default_value || this.field?.default_value === 0 || this.displayValue || this.displayValue === 0
|
||||||
},
|
},
|
||||||
isSelectInput () {
|
isSelectInput () {
|
||||||
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
|
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
|
||||||
@@ -399,6 +428,8 @@ export default {
|
|||||||
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
|
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
|
||||||
},
|
},
|
||||||
isValueInput () {
|
isValueInput () {
|
||||||
|
if (this.inputMode && this.field.preferences?.formula) return false
|
||||||
|
|
||||||
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
|
||||||
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
|
||||||
},
|
},
|
||||||
@@ -511,7 +542,7 @@ export default {
|
|||||||
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||||
},
|
},
|
||||||
maybeToggleDefaultValue () {
|
maybeToggleDefaultValue () {
|
||||||
if (!this.editable || this.isCmdKeyRef.value) {
|
if (!this.editable || this.isCmdKeyRef.value || this.field.preferences?.formula) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +590,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
focusValueInput (e) {
|
focusValueInput (e) {
|
||||||
|
if (this.inputMode && this.field.type === 'number' && !this.isContenteditable && !this.field.preferences?.formula) {
|
||||||
|
this.isContenteditable = true
|
||||||
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
|
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
|
||||||
this.$refs.defaultValue.focus()
|
this.$refs.defaultValue.focus()
|
||||||
@@ -624,6 +659,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDefaultValueBlur (e) {
|
onDefaultValueBlur (e) {
|
||||||
|
if (this.field.preferences?.formula) {
|
||||||
|
this.isContenteditable = false
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const text = this.$refs.defaultValue.innerText.trim()
|
const text = this.$refs.defaultValue.innerText.trim()
|
||||||
|
|
||||||
this.isContenteditable = false
|
this.isContenteditable = false
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
||||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
||||||
style="top: -25px; height: 25px"
|
style="top: -25px; height: 25px"
|
||||||
@mousedown.stop
|
@mousedown.stop="$emit('click-title')"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
>
|
>
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
@@ -279,7 +279,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['remove', 'scroll-to', 'add-custom-field', 'change'],
|
emits: ['remove', 'scroll-to', 'add-custom-field', 'change', 'click-title'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isShowFormulaModal: false,
|
isShowFormulaModal: false,
|
||||||
|
|||||||
@@ -51,6 +51,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="beforeRevisionSnapshot"
|
||||||
|
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
|
||||||
|
>
|
||||||
|
<div class="alert border-base-content/30 py-2 px-2.5">
|
||||||
|
<IconInfoCircle class="stroke-info shrink-0 w-6 h-6" />
|
||||||
|
<span>{{ t('viewing_revision_from').replace('{date}', formatRevisionTime(beforeRevisionSnapshot.revision.created_at)) }}</span>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm"
|
||||||
|
@click.prevent="cancelRevision"
|
||||||
|
>
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="editable"
|
||||||
|
class="btn btn-sm btn-neutral text-white"
|
||||||
|
@click.prevent="applyRevision"
|
||||||
|
>
|
||||||
|
{{ t('apply') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$slots.buttons || withTitle"
|
v-if="$slots.buttons || withTitle"
|
||||||
id="title_container"
|
id="title_container"
|
||||||
@@ -213,6 +237,18 @@
|
|||||||
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="withRevisionsMenu">
|
||||||
|
<button
|
||||||
|
class="flex space-x-2"
|
||||||
|
@click.prevent="openRevisionsModal"
|
||||||
|
@mouseenter="preloadRevisions"
|
||||||
|
>
|
||||||
|
<span class="w-6 h-6 flex-shrink-0 flex items-center justify-center">
|
||||||
|
<IconHistory class="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
<span class="whitespace-nowrap">{{ t('revisions') }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li v-if="withDownload">
|
<li v-if="withDownload">
|
||||||
<button
|
<button
|
||||||
class="flex space-x-2"
|
class="flex space-x-2"
|
||||||
@@ -381,6 +417,9 @@
|
|||||||
:document="document"
|
:document="document"
|
||||||
:is-drag="!!dragField"
|
:is-drag="!!dragField"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
|
:conditional-field-index="conditionalFieldIndex"
|
||||||
|
:formula-values-index="formulaValuesIndex"
|
||||||
|
:page-preview-format="pagePreviewFormat"
|
||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
:allow-draw="!onlyDefinedFields || drawField || drawCustomField"
|
:allow-draw="!onlyDefinedFields || drawField || drawCustomField"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
@@ -504,11 +543,14 @@
|
|||||||
:with-custom-fields="withCustomFields"
|
:with-custom-fields="withCustomFields"
|
||||||
:with-fields-search="withFieldsSearch"
|
:with-fields-search="withFieldsSearch"
|
||||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||||
|
:with-custom-fields-tab="withCustomFieldsTab"
|
||||||
:template="template"
|
:template="template"
|
||||||
:default-required-fields="defaultRequiredFields"
|
:default-required-fields="defaultRequiredFields"
|
||||||
|
:detect-custom-fields-index="detectCustomFieldsIndex"
|
||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
:with-fields-detection="withFieldsDetection"
|
:with-fields-detection="withFieldsDetection"
|
||||||
|
:with-detect-existing-fields="withDetectExistingFields"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
@@ -594,8 +636,17 @@
|
|||||||
<div
|
<div
|
||||||
id="docuseal_modal_container"
|
id="docuseal_modal_container"
|
||||||
class="modal-container"
|
class="modal-container"
|
||||||
|
>
|
||||||
|
<RevisionsModal
|
||||||
|
v-if="isRevisionsModalOpen"
|
||||||
|
:template="template"
|
||||||
|
:revisions="revisions"
|
||||||
|
:locale="locale"
|
||||||
|
@close="isRevisionsModalOpen = false"
|
||||||
|
@apply="onRevisionApply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -612,11 +663,22 @@ import DocumentPreview from './preview'
|
|||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import MobileFields from './mobile_fields'
|
import MobileFields from './mobile_fields'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
|
import RevisionsModal from './revisions_modal'
|
||||||
|
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload, IconHistory } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
|
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
|
||||||
import * as i18n from './i18n'
|
import * as i18n from './i18n'
|
||||||
|
|
||||||
|
const isEmpty = (obj) => {
|
||||||
|
if (obj == null) return true
|
||||||
|
if (Array.isArray(obj)) return obj.length === 0
|
||||||
|
if (typeof obj === 'string') return obj.trim().length === 0
|
||||||
|
if (typeof obj === 'object') return Object.keys(obj).length === 0
|
||||||
|
if (obj === false) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
components: {
|
components: {
|
||||||
@@ -642,7 +704,9 @@ export default {
|
|||||||
IconDownload,
|
IconDownload,
|
||||||
IconAdjustments,
|
IconAdjustments,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconDeviceFloppy
|
IconHistory,
|
||||||
|
IconDeviceFloppy,
|
||||||
|
RevisionsModal
|
||||||
},
|
},
|
||||||
provide () {
|
provide () {
|
||||||
return {
|
return {
|
||||||
@@ -654,6 +718,7 @@ export default {
|
|||||||
locale: this.locale,
|
locale: this.locale,
|
||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
fieldTypes: this.fieldTypes,
|
fieldTypes: this.fieldTypes,
|
||||||
|
dateFormats: this.dateFormats,
|
||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
withVerification: this.withVerification,
|
withVerification: this.withVerification,
|
||||||
@@ -738,6 +803,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withDetectExistingFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withCustomFields: {
|
withCustomFields: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -778,6 +848,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
withCustomFieldsTab: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withSelectedFieldType: {
|
withSelectedFieldType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -798,6 +873,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
dateFormats: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -808,6 +888,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
pagePreviewFormat: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '.jpg'
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -943,6 +1028,16 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
withRevisions: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withRevisionsMenu: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -953,6 +1048,7 @@ export default {
|
|||||||
isLoadingBlankPage: false,
|
isLoadingBlankPage: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
isDetectingPageFields: false,
|
isDetectingPageFields: false,
|
||||||
|
detectFieldsQueue: [],
|
||||||
detectingAnalyzingProgress: null,
|
detectingAnalyzingProgress: null,
|
||||||
detectingFieldsAddedCount: null,
|
detectingFieldsAddedCount: null,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
@@ -963,7 +1059,11 @@ export default {
|
|||||||
drawCustomField: null,
|
drawCustomField: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null,
|
dragField: null,
|
||||||
isDragFile: false
|
isDragFile: false,
|
||||||
|
isMathLoaded: false,
|
||||||
|
isRevisionsModalOpen: false,
|
||||||
|
revisions: [],
|
||||||
|
beforeRevisionSnapshot: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -973,6 +1073,13 @@ export default {
|
|||||||
fieldsDragFieldRef: () => ref(),
|
fieldsDragFieldRef: () => ref(),
|
||||||
customDragFieldRef: () => ref(),
|
customDragFieldRef: () => ref(),
|
||||||
selectedAreasRef: () => ref([]),
|
selectedAreasRef: () => ref([]),
|
||||||
|
attachmentUuidsIndex () {
|
||||||
|
return this.template.schema.reduce((acc, e, index) => {
|
||||||
|
acc[e.attachment_uuid] = index
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
language () {
|
language () {
|
||||||
return this.locale.split('-')[0].toLowerCase()
|
return this.locale.split('-')[0].toLowerCase()
|
||||||
},
|
},
|
||||||
@@ -1003,6 +1110,8 @@ export default {
|
|||||||
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
|
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
},
|
},
|
||||||
defaultDateFormat () {
|
defaultDateFormat () {
|
||||||
|
if (this.dateFormats.length) return this.dateFormats[0]
|
||||||
|
|
||||||
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
|
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
|
||||||
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
|
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
|
||||||
|
|
||||||
@@ -1044,6 +1153,43 @@ export default {
|
|||||||
|
|
||||||
return map
|
return map
|
||||||
},
|
},
|
||||||
|
fieldsUuidIndex () {
|
||||||
|
return this.template.fields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = f
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
conditionalFieldIndex () {
|
||||||
|
if (!this.inputMode) return {}
|
||||||
|
|
||||||
|
const cache = {}
|
||||||
|
|
||||||
|
return this.template.fields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = this.checkFieldConditions(f, cache)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
formulaValuesIndex () {
|
||||||
|
const formulaFields = this.template.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.hasFormulaDependencyValue(f))
|
||||||
|
|
||||||
|
if (!formulaFields.length) return {}
|
||||||
|
|
||||||
|
if (!this.isMathLoaded) {
|
||||||
|
this.loadCalculator()
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formulaFields.reduce((acc, f) => {
|
||||||
|
if (this.conditionalFieldIndex[f.uuid] !== false) {
|
||||||
|
acc[f.uuid] = this.calculateFormula(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
isAllRequiredFieldsAdded () {
|
isAllRequiredFieldsAdded () {
|
||||||
return !this.defaultRequiredFields?.some((f) => {
|
return !this.defaultRequiredFields?.some((f) => {
|
||||||
return !this.template.fields?.some((field) => field.name === f.name)
|
return !this.template.fields?.some((field) => field.name === f.name)
|
||||||
@@ -1052,6 +1198,39 @@ export default {
|
|||||||
selectedField () {
|
selectedField () {
|
||||||
return this.template.fields.find((f) => f.areas?.includes(this.lastSelectedArea))
|
return this.template.fields.find((f) => f.areas?.includes(this.lastSelectedArea))
|
||||||
},
|
},
|
||||||
|
detectFieldsIndex () {
|
||||||
|
const submittersByUuid = {}
|
||||||
|
|
||||||
|
this.template.submitters.forEach((s) => {
|
||||||
|
submittersByUuid[s.uuid] = s
|
||||||
|
})
|
||||||
|
|
||||||
|
const index = {}
|
||||||
|
|
||||||
|
this.template.fields.forEach((f) => {
|
||||||
|
if (!f.name) return
|
||||||
|
|
||||||
|
const role = submittersByUuid[f.submitter_uuid]?.name
|
||||||
|
const key = [f.name, role].filter(Boolean).join(':').toLowerCase()
|
||||||
|
|
||||||
|
if (!index[key]) index[key] = f
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
|
detectCustomFieldsIndex () {
|
||||||
|
const index = {}
|
||||||
|
|
||||||
|
;[...this.customFields, ...this.defaultRequiredFields, ...this.defaultFields].forEach((c) => {
|
||||||
|
if (!c.name) return
|
||||||
|
|
||||||
|
const key = [c.name, c.role].filter(Boolean).join(':').toLowerCase()
|
||||||
|
|
||||||
|
if (!index[key]) index[key] = c
|
||||||
|
})
|
||||||
|
|
||||||
|
return index
|
||||||
|
},
|
||||||
sortedDocuments () {
|
sortedDocuments () {
|
||||||
return this.template.schema.map((item) => {
|
return this.template.schema.map((item) => {
|
||||||
return this.template.documents.find(doc => doc.uuid === item.attachment_uuid)
|
return this.template.documents.find(doc => doc.uuid === item.attachment_uuid)
|
||||||
@@ -1147,6 +1326,132 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toRaw,
|
toRaw,
|
||||||
|
applyCustomFieldAttributes: Fields.methods.applyCustomFieldAttributes,
|
||||||
|
buildExistingFields: Fields.methods.buildExistingFields,
|
||||||
|
async loadCalculator () {
|
||||||
|
if (this.math) return
|
||||||
|
|
||||||
|
const { Calculator } = await import('../submission_form/calculator')
|
||||||
|
|
||||||
|
this.math = new Calculator()
|
||||||
|
this.isMathLoaded = true
|
||||||
|
},
|
||||||
|
optionValue (option, index) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkFieldConditions (field, cache = {}) {
|
||||||
|
const cacheKey = field.uuid || field.attachment_uuid
|
||||||
|
|
||||||
|
if (cache[cacheKey] !== undefined) {
|
||||||
|
return cache[cacheKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.conditions?.length) {
|
||||||
|
const result = field.conditions.reduce((acc, cond) => {
|
||||||
|
if (cond.operation === 'or') {
|
||||||
|
acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
|
||||||
|
} else {
|
||||||
|
acc.push(this.checkFieldCondition(cond, cache))
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
cache[cacheKey] = !result.includes(false)
|
||||||
|
} else {
|
||||||
|
cache[cacheKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache[cacheKey]
|
||||||
|
},
|
||||||
|
checkFieldCondition (condition, cache = {}) {
|
||||||
|
const field = this.fieldsUuidIndex[condition.field_uuid]
|
||||||
|
|
||||||
|
if (['not_empty', 'checked', 'equal', 'contains', 'greater_than', 'less_than'].includes(condition.action) && field && !this.checkFieldConditions(field, cache)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = !field || isEmpty(field.default_value) ? null : field.default_value
|
||||||
|
|
||||||
|
if (['empty', 'unchecked'].includes(condition.action)) {
|
||||||
|
return isEmpty(defaultValue)
|
||||||
|
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||||
|
return !isEmpty(defaultValue)
|
||||||
|
} else if (field?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)) {
|
||||||
|
const value = defaultValue
|
||||||
|
|
||||||
|
if (isEmpty(value) || isEmpty(condition.value)) return false
|
||||||
|
|
||||||
|
const actual = parseFloat(value)
|
||||||
|
const expected = parseFloat(condition.value)
|
||||||
|
|
||||||
|
if (Number.isNaN(actual) || Number.isNaN(expected)) return false
|
||||||
|
|
||||||
|
if (condition.action === 'equal') return Math.abs(actual - expected) < Number.EPSILON
|
||||||
|
if (condition.action === 'not_equal') return Math.abs(actual - expected) > Number.EPSILON
|
||||||
|
if (condition.action === 'greater_than') return actual > expected
|
||||||
|
if (condition.action === 'less_than') return actual < expected
|
||||||
|
|
||||||
|
return false
|
||||||
|
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||||
|
if (field.options) {
|
||||||
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
const values = [defaultValue].flat()
|
||||||
|
|
||||||
|
return values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [defaultValue].flat().includes(condition.value)
|
||||||
|
}
|
||||||
|
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
|
||||||
|
if (field.options) {
|
||||||
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
const values = [defaultValue].flat()
|
||||||
|
|
||||||
|
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeFormula (formula, depth = 0) {
|
||||||
|
if (depth > 10) return formula
|
||||||
|
|
||||||
|
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
if (this.fieldsUuidIndex[uuid]?.preferences?.formula) {
|
||||||
|
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
|
||||||
|
} else {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
calculateFormula (field) {
|
||||||
|
const transformedFormula = this.normalizeFormula(field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
return this.fieldsUuidIndex[uuid]?.default_value || 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
},
|
||||||
|
hasFormulaDependencyValue (field) {
|
||||||
|
const normalized = this.normalizeFormula(field.preferences.formula)
|
||||||
|
|
||||||
|
return [...normalized.matchAll(/{{(.*?)}}/g)].some(([, uuid]) => !isEmpty(this.fieldsUuidIndex[uuid]?.default_value))
|
||||||
|
},
|
||||||
addCustomField (field) {
|
addCustomField (field) {
|
||||||
return this.$refs.fields.addCustomField(field)
|
return this.$refs.fields.addCustomField(field)
|
||||||
},
|
},
|
||||||
@@ -1426,20 +1731,9 @@ export default {
|
|||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
findFieldInsertIndex (field) {
|
compareAreas (a, b) {
|
||||||
if (!field.areas?.length) return -1
|
const aAttIdx = this.attachmentUuidsIndex[a.attachment_uuid]
|
||||||
|
const bAttIdx = this.attachmentUuidsIndex[b.attachment_uuid]
|
||||||
const area = field.areas[0]
|
|
||||||
|
|
||||||
const attachmentUuidsIndex = this.template.schema.reduce((acc, e, index) => {
|
|
||||||
acc[e.attachment_uuid] = index
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const compareAreas = (a, b) => {
|
|
||||||
const aAttIdx = attachmentUuidsIndex[a.attachment_uuid]
|
|
||||||
const bAttIdx = attachmentUuidsIndex[b.attachment_uuid]
|
|
||||||
|
|
||||||
if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx
|
if (aAttIdx !== bAttIdx) return aAttIdx - bAttIdx
|
||||||
if (a.page !== b.page) return a.page - b.page
|
if (a.page !== b.page) return a.page - b.page
|
||||||
@@ -1451,7 +1745,11 @@ export default {
|
|||||||
if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x
|
if (a.h < b.h ? a.y >= b.y && aY <= bY : b.y >= a.y && bY <= aY) return a.x - b.x
|
||||||
|
|
||||||
return aY - bY
|
return aY - bY
|
||||||
}
|
},
|
||||||
|
findFieldInsertIndex (field) {
|
||||||
|
if (!field.areas?.length) return -1
|
||||||
|
|
||||||
|
const area = field.areas[0]
|
||||||
|
|
||||||
let closestBeforeIndex = -1
|
let closestBeforeIndex = -1
|
||||||
let closestBeforeArea = null
|
let closestBeforeArea = null
|
||||||
@@ -1461,15 +1759,15 @@ export default {
|
|||||||
this.template.fields.forEach((f, index) => {
|
this.template.fields.forEach((f, index) => {
|
||||||
if (f.submitter_uuid === field.submitter_uuid) {
|
if (f.submitter_uuid === field.submitter_uuid) {
|
||||||
(f.areas || []).forEach((a) => {
|
(f.areas || []).forEach((a) => {
|
||||||
const cmp = compareAreas(a, area)
|
const cmp = this.compareAreas(a, area)
|
||||||
|
|
||||||
if (cmp < 0) {
|
if (cmp < 0) {
|
||||||
if (!closestBeforeArea || (compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) {
|
if (!closestBeforeArea || (this.compareAreas(a, closestBeforeArea) > 0 && closestBeforeIndex < index)) {
|
||||||
closestBeforeIndex = index
|
closestBeforeIndex = index
|
||||||
closestBeforeArea = a
|
closestBeforeArea = a
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!closestAfterArea || (compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) {
|
if (!closestAfterArea || (this.compareAreas(a, closestAfterArea) < 0 && closestAfterIndex > index)) {
|
||||||
closestAfterIndex = index
|
closestAfterIndex = index
|
||||||
closestAfterArea = a
|
closestAfterArea = a
|
||||||
}
|
}
|
||||||
@@ -1492,9 +1790,111 @@ export default {
|
|||||||
this.template.fields.push(field)
|
this.template.fields.push(field)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
insertArea (field, area) {
|
||||||
|
field.areas ||= []
|
||||||
|
|
||||||
|
const insertIndex = field.areas.findIndex((a) => this.compareAreas(a, area) > 0)
|
||||||
|
|
||||||
|
if (insertIndex === -1) {
|
||||||
|
field.areas.push(area)
|
||||||
|
} else {
|
||||||
|
field.areas.splice(insertIndex, 0, area)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insertDetectedField (field) {
|
||||||
|
if (!this.withDetectExistingFields || !field.name) {
|
||||||
|
this.insertField(field)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = this.template.submitters.find((s) => s.uuid === field.submitter_uuid)?.name
|
||||||
|
const nameKey = field.name.toLowerCase()
|
||||||
|
const indexKey = [field.name, role].filter(Boolean).join(':').toLowerCase()
|
||||||
|
|
||||||
|
const existingField = this.detectFieldsIndex[indexKey]
|
||||||
|
|
||||||
|
if (existingField) {
|
||||||
|
existingField.areas = existingField.areas || []
|
||||||
|
field.areas.forEach((area) => this.insertArea(existingField, area))
|
||||||
|
} else {
|
||||||
|
const customField = this.detectCustomFieldsIndex[indexKey] || this.detectCustomFieldsIndex[nameKey]
|
||||||
|
|
||||||
|
if (customField) this.applyCustomFieldAttributes(field, customField)
|
||||||
|
|
||||||
|
this.insertField(field)
|
||||||
|
}
|
||||||
|
},
|
||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
},
|
},
|
||||||
|
preloadRevisions () {
|
||||||
|
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
|
||||||
|
},
|
||||||
|
openRevisionsModal () {
|
||||||
|
this.closeDropdown()
|
||||||
|
|
||||||
|
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
|
||||||
|
|
||||||
|
this.loadRevisionsPromise.then(async (resp) => {
|
||||||
|
this.revisions = await resp.json()
|
||||||
|
|
||||||
|
this.isRevisionsModalOpen = true
|
||||||
|
}).finally(() => {
|
||||||
|
this.loadRevisionsPromise = null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRevisionApply (revision) {
|
||||||
|
this.beforeRevisionSnapshot = {
|
||||||
|
template: JSON.parse(JSON.stringify(this.template)),
|
||||||
|
dynamicDocuments: JSON.parse(JSON.stringify(this.dynamicDocuments)),
|
||||||
|
revision
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dynamic_documents: nextDynamicDocs = [], ...nextTemplate } = revision.data
|
||||||
|
|
||||||
|
Object.assign(this.template, nextTemplate)
|
||||||
|
|
||||||
|
this.dynamicDocuments.splice(0, this.dynamicDocuments.length, ...nextDynamicDocs)
|
||||||
|
|
||||||
|
this.$nextTick(() => this.reloadDynamicDocumentContent())
|
||||||
|
|
||||||
|
this.isRevisionsModalOpen = false
|
||||||
|
},
|
||||||
|
cancelRevision () {
|
||||||
|
Object.assign(this.template, this.beforeRevisionSnapshot.template)
|
||||||
|
|
||||||
|
this.dynamicDocuments.splice(0, this.dynamicDocuments.length, ...this.beforeRevisionSnapshot.dynamicDocuments)
|
||||||
|
|
||||||
|
this.beforeRevisionSnapshot = null
|
||||||
|
|
||||||
|
this.$nextTick(() => this.reloadDynamicDocumentContent())
|
||||||
|
},
|
||||||
|
applyRevision () {
|
||||||
|
this.beforeRevisionSnapshot = null
|
||||||
|
|
||||||
|
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
||||||
|
|
||||||
|
dynamicDocumentRefs.forEach((ref) => ref.update())
|
||||||
|
|
||||||
|
this.rebuildVariablesSchema({ disable: false })
|
||||||
|
|
||||||
|
return Promise.all([this.save({ force: true }), ...dynamicDocumentRefs.map((ref) => ref.saveBody())])
|
||||||
|
},
|
||||||
|
reloadDynamicDocumentContent () {
|
||||||
|
this.documentRefs.forEach((ref) => {
|
||||||
|
if (ref.isDynamic) ref.reloadContent()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatRevisionTime (string) {
|
||||||
|
return new Date(string).toLocaleString(this.locale || undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
},
|
||||||
t (key) {
|
t (key) {
|
||||||
return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key
|
return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key
|
||||||
},
|
},
|
||||||
@@ -1988,7 +2388,7 @@ export default {
|
|||||||
|
|
||||||
fieldUuidIndex[field.uuid] = newField
|
fieldUuidIndex[field.uuid] = newField
|
||||||
|
|
||||||
newField.areas.push(newArea)
|
this.insertArea(newField, newArea)
|
||||||
newAreas.push(newArea)
|
newAreas.push(newArea)
|
||||||
|
|
||||||
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
||||||
@@ -2101,17 +2501,7 @@ export default {
|
|||||||
area.y -= area.h / 2
|
area.y -= area.h / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawField.areas ||= []
|
this.insertArea(this.drawField, area)
|
||||||
|
|
||||||
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
|
|
||||||
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
|
|
||||||
})
|
|
||||||
|
|
||||||
if (insertBeforeAreaIndex !== -1) {
|
|
||||||
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
|
|
||||||
} else {
|
|
||||||
this.drawField.areas.push(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.template.fields.indexOf(this.drawField) === -1) {
|
if (this.template.fields.indexOf(this.drawField) === -1) {
|
||||||
this.insertField(this.drawField)
|
this.insertField(this.drawField)
|
||||||
@@ -2252,9 +2642,7 @@ export default {
|
|||||||
delete field.height
|
delete field.height
|
||||||
}
|
}
|
||||||
|
|
||||||
field.areas ||= []
|
this.insertArea(field, fieldArea)
|
||||||
|
|
||||||
field.areas.push(fieldArea)
|
|
||||||
|
|
||||||
if (this.selectedAreasRef.value.length < 2) {
|
if (this.selectedAreasRef.value.length < 2) {
|
||||||
this.selectedAreasRef.value = [fieldArea]
|
this.selectedAreasRef.value = [fieldArea]
|
||||||
@@ -2324,7 +2712,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
field.areas.push(fieldArea)
|
this.insertArea(field, fieldArea)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const fieldArea = {
|
const fieldArea = {
|
||||||
@@ -2753,7 +3141,7 @@ export default {
|
|||||||
|
|
||||||
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
||||||
|
|
||||||
Promise.all([this.save(), ...dynamicDocumentSaves]).then(() => {
|
Promise.all([this.save({ force: true, revision: this.withRevisions }), ...dynamicDocumentSaves]).then(() => {
|
||||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
@@ -2783,6 +3171,12 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
detectFieldsForPage ({ page, attachmentUuid }) {
|
detectFieldsForPage ({ page, attachmentUuid }) {
|
||||||
|
if (this.isDetectingPageFields) {
|
||||||
|
this.detectFieldsQueue.push({ page, attachmentUuid })
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.isDetectingPageFields = true
|
this.isDetectingPageFields = true
|
||||||
this.detectingAnalyzingProgress = null
|
this.detectingAnalyzingProgress = null
|
||||||
this.detectingFieldsAddedCount = null
|
this.detectingFieldsAddedCount = null
|
||||||
@@ -2821,7 +3215,11 @@ export default {
|
|||||||
this.baseFetch(`/templates/${this.template.id}/detect_fields`, {
|
this.baseFetch(`/templates/${this.template.id}/detect_fields`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ attachment_uuid: attachmentUuid, page })
|
body: JSON.stringify({
|
||||||
|
attachment_uuid: attachmentUuid,
|
||||||
|
page,
|
||||||
|
...(this.withDetectExistingFields ? { fields: this.buildExistingFields() } : {})
|
||||||
|
})
|
||||||
}).then(async (response) => {
|
}).then(async (response) => {
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
const decoder = new TextDecoder('utf-8')
|
const decoder = new TextDecoder('utf-8')
|
||||||
@@ -2850,7 +3248,7 @@ export default {
|
|||||||
if (!f.submitter_uuid) {
|
if (!f.submitter_uuid) {
|
||||||
f.submitter_uuid = this.template.submitters[0].uuid
|
f.submitter_uuid = this.template.submitters[0].uuid
|
||||||
}
|
}
|
||||||
this.insertField(f)
|
this.insertDetectedField(f)
|
||||||
})
|
})
|
||||||
|
|
||||||
totalFieldsAdded += errorFields.length
|
totalFieldsAdded += errorFields.length
|
||||||
@@ -2879,7 +3277,7 @@ export default {
|
|||||||
|
|
||||||
const nonOverlappingFields = filterNonOverlappingFields(finalFields)
|
const nonOverlappingFields = filterNonOverlappingFields(finalFields)
|
||||||
|
|
||||||
nonOverlappingFields.forEach((f) => this.insertField(f))
|
nonOverlappingFields.forEach((f) => this.insertDetectedField(f))
|
||||||
totalFieldsAdded += nonOverlappingFields.length
|
totalFieldsAdded += nonOverlappingFields.length
|
||||||
|
|
||||||
if (nonOverlappingFields.length) {
|
if (nonOverlappingFields.length) {
|
||||||
@@ -2917,7 +3315,7 @@ export default {
|
|||||||
|
|
||||||
const nonOverlappingFields = filterNonOverlappingFields(finalFields)
|
const nonOverlappingFields = filterNonOverlappingFields(finalFields)
|
||||||
|
|
||||||
nonOverlappingFields.forEach((f) => this.insertField(f))
|
nonOverlappingFields.forEach((f) => this.insertDetectedField(f))
|
||||||
totalFieldsAdded += nonOverlappingFields.length
|
totalFieldsAdded += nonOverlappingFields.length
|
||||||
|
|
||||||
if (nonOverlappingFields.length) {
|
if (nonOverlappingFields.length) {
|
||||||
@@ -2935,7 +3333,7 @@ export default {
|
|||||||
|
|
||||||
const nonOverlappingFields = filterNonOverlappingFields(finalFields)
|
const nonOverlappingFields = filterNonOverlappingFields(finalFields)
|
||||||
|
|
||||||
nonOverlappingFields.forEach((f) => this.insertField(f))
|
nonOverlappingFields.forEach((f) => this.insertDetectedField(f))
|
||||||
totalFieldsAdded += nonOverlappingFields.length
|
totalFieldsAdded += nonOverlappingFields.length
|
||||||
|
|
||||||
if (nonOverlappingFields.length) {
|
if (nonOverlappingFields.length) {
|
||||||
@@ -2968,11 +3366,19 @@ export default {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.detectingFieldsAddedCount = null
|
this.detectingFieldsAddedCount = null
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
if (this.detectFieldsQueue.length) {
|
||||||
|
this.detectFieldsForPage(this.detectFieldsQueue.shift())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save ({ force } = { force: false }) {
|
save ({ force = false, revision = false } = {}) {
|
||||||
this.pendingFieldAttachmentUuids = []
|
this.pendingFieldAttachmentUuids = []
|
||||||
|
|
||||||
|
if (this.beforeRevisionSnapshot) {
|
||||||
|
this.beforeRevisionSnapshot = null
|
||||||
|
}
|
||||||
|
|
||||||
if (this.onChange) {
|
if (this.onChange) {
|
||||||
this.onChange(this.template)
|
this.onChange(this.template)
|
||||||
}
|
}
|
||||||
@@ -2998,7 +3404,8 @@ export default {
|
|||||||
submitters: this.template.submitters,
|
submitters: this.template.submitters,
|
||||||
fields: this.template.fields,
|
fields: this.template.fields,
|
||||||
variables_schema: this.template.variables_schema
|
variables_schema: this.template.variables_schema
|
||||||
}
|
},
|
||||||
|
...(revision ? { revision: true } : {})
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
class="absolute top-0 bottom-0 right-0 left-0"
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
@click.prevent="$emit('close')"
|
@click.prevent="$emit('close')"
|
||||||
/>
|
/>
|
||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
|
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
:key="image.id"
|
:key="image.id"
|
||||||
:ref="setPageRefs"
|
:ref="setPageRefs"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
|
:conditional-field-index="conditionalFieldIndex"
|
||||||
|
:formula-values-index="formulaValuesIndex"
|
||||||
:number="index"
|
:number="index"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:data-page="index"
|
:data-page="index"
|
||||||
@@ -64,6 +66,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
conditionalFieldIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaValuesIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
areasIndex: {
|
areasIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -138,6 +150,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
pagePreviewFormat: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '.jpg'
|
||||||
|
},
|
||||||
withFieldsDetection: {
|
withFieldsDetection: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -168,7 +185,7 @@ export default {
|
|||||||
return this.previewImagesIndex[i] || reactive({
|
return this.previewImagesIndex[i] || reactive({
|
||||||
metadata: { ...lazyloadMetadata },
|
metadata: { ...lazyloadMetadata },
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.signed_key || this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_key || this.document.signed_uuid || this.document.uuid}/${i}${this.pagePreviewFormat}`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ export default {
|
|||||||
this.sectionRefs.push(ref)
|
this.sectionRefs.push(ref)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
reloadContent () {
|
||||||
|
this.sectionRefs.forEach((ref) => ref.reloadContent())
|
||||||
|
},
|
||||||
onBeforeUnload (event) {
|
onBeforeUnload (event) {
|
||||||
if (this.saveTimer) {
|
if (this.saveTimer) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
reloadContent () {
|
||||||
|
this.editor.commands.setContent(this.section.innerHTML, { emitUpdate: false })
|
||||||
|
},
|
||||||
findAreaNodePos (areaUuid) {
|
findAreaNodePos (areaUuid) {
|
||||||
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
@change="[schema.format = $event.target.value, save()]"
|
@change="[schema.format = $event.target.value, save()]"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="format in dateFormats"
|
v-for="format in availableDateFormats"
|
||||||
:key="format"
|
:key="format"
|
||||||
:value="format"
|
:value="format"
|
||||||
>{{ formatDate(new Date(), format) }}</option>
|
>{{ formatDate(new Date(), format) }}</option>
|
||||||
@@ -248,7 +248,7 @@ export default {
|
|||||||
FieldType,
|
FieldType,
|
||||||
IconSettings
|
IconSettings
|
||||||
},
|
},
|
||||||
inject: ['t', 'save', 'backgroundColor'],
|
inject: ['t', 'save', 'backgroundColor', 'dateFormats'],
|
||||||
provide () {
|
provide () {
|
||||||
return {
|
return {
|
||||||
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
|
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
|
||||||
@@ -318,8 +318,10 @@ export default {
|
|||||||
'space'
|
'space'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
dateFormats () {
|
availableDateFormats () {
|
||||||
const formats = [
|
const formats = this.dateFormats.length
|
||||||
|
? [...this.dateFormats]
|
||||||
|
: [
|
||||||
'MM/DD/YYYY',
|
'MM/DD/YYYY',
|
||||||
'DD/MM/YYYY',
|
'DD/MM/YYYY',
|
||||||
'YYYY-MM-DD',
|
'YYYY-MM-DD',
|
||||||
@@ -327,11 +329,12 @@ export default {
|
|||||||
'DD.MM.YYYY',
|
'DD.MM.YYYY',
|
||||||
'MMM D, YYYY',
|
'MMM D, YYYY',
|
||||||
'MMMM D, YYYY',
|
'MMMM D, YYYY',
|
||||||
|
'MMMM YYYY',
|
||||||
'D MMM YYYY',
|
'D MMM YYYY',
|
||||||
'D MMMM YYYY'
|
'D MMMM YYYY'
|
||||||
]
|
]
|
||||||
|
|
||||||
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
if (!this.dateFormats.length && (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko'))) {
|
||||||
formats.push('YYYY년 MM월 DD일')
|
formats.push('YYYY년 MM월 DD일')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,18 +404,47 @@ export default {
|
|||||||
formatDate (date, format) {
|
formatDate (date, format) {
|
||||||
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||||
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||||
const yearFormats = { YYYY: 'numeric', YY: '2-digit' }
|
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||||
|
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||||
|
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||||
|
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||||
|
|
||||||
const parts = new Intl.DateTimeFormat([], {
|
const opts = {
|
||||||
day: dayFormats[format.match(/D+/)],
|
day: dayFormats[format.match(/D+/)],
|
||||||
month: monthFormats[format.match(/M+/)],
|
month: monthFormats[format.match(/M+/)],
|
||||||
year: yearFormats[format.match(/Y+/)]
|
year: yearFormats[format.match(/Y+/)]
|
||||||
}).formatToParts(date)
|
}
|
||||||
|
|
||||||
return format
|
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||||
|
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||||
|
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||||
|
|
||||||
|
const partTypes = {
|
||||||
|
M: 'month',
|
||||||
|
D: 'day',
|
||||||
|
Y: 'year',
|
||||||
|
H: 'hour',
|
||||||
|
h: 'hour',
|
||||||
|
m: 'minute',
|
||||||
|
s: 'second',
|
||||||
|
z: 'timeZoneName',
|
||||||
|
A: 'dayPeriod',
|
||||||
|
a: 'dayPeriod'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||||
|
|
||||||
|
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||||
|
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||||
|
|
||||||
|
if (token === 'A') return (value || '').toUpperCase()
|
||||||
|
if (token === 'a') return (value || '').toLowerCase()
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
},
|
},
|
||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
this.$el.getRootNode().activeElement.blur()
|
this.$el.getRootNode().activeElement.blur()
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ export default {
|
|||||||
IconMathFunction,
|
IconMathFunction,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
|
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex', 'dateFormats'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -428,6 +428,7 @@ export default {
|
|||||||
|
|
||||||
if (this.field.type === 'date') {
|
if (this.field.type === 'date') {
|
||||||
this.field.preferences.format ||=
|
this.field.preferences.format ||=
|
||||||
|
this.dateFormats[0] ||
|
||||||
({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY'))
|
({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ export default {
|
|||||||
ContextSubmenu,
|
ContextSubmenu,
|
||||||
ContextModal
|
ContextModal
|
||||||
},
|
},
|
||||||
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies'],
|
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats'],
|
||||||
props: {
|
props: {
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -580,7 +580,7 @@ export default {
|
|||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
validationOptions: FieldSettings.computed.validations,
|
validationOptions: FieldSettings.computed.validations,
|
||||||
dateFormats: FieldSettings.computed.dateFormats,
|
availableDateFormats: FieldSettings.computed.availableDateFormats,
|
||||||
numberFormats: FieldSettings.computed.numberFormats,
|
numberFormats: FieldSettings.computed.numberFormats,
|
||||||
prefillableFieldTypes: FieldSettings.computed.prefillableFieldTypes,
|
prefillableFieldTypes: FieldSettings.computed.prefillableFieldTypes,
|
||||||
verificationMethods: FieldSettings.computed.verificationMethods,
|
verificationMethods: FieldSettings.computed.verificationMethods,
|
||||||
@@ -686,7 +686,7 @@ export default {
|
|||||||
},
|
},
|
||||||
formatOptions () {
|
formatOptions () {
|
||||||
switch (this.field.type) {
|
switch (this.field.type) {
|
||||||
case 'date': return this.dateFormats.map(f => ({ value: f, label: this.formatDate(new Date(), f) }))
|
case 'date': return this.availableDateFormats.map(f => ({ value: f, label: this.formatDate(new Date(), f) }))
|
||||||
case 'number': return this.numberFormats.map(f => ({ value: f, label: this.formatNumber(123456789.567, f) }))
|
case 'number': return this.numberFormats.map(f => ({ value: f, label: this.formatNumber(123456789.567, f) }))
|
||||||
case 'signature': return this.signatureFormats.map(f => ({ value: f, label: this.t(f) }))
|
case 'signature': return this.signatureFormats.map(f => ({ value: f, label: this.t(f) }))
|
||||||
default: return []
|
default: return []
|
||||||
|
|||||||
@@ -290,7 +290,7 @@
|
|||||||
@change="$emit('save')"
|
@change="$emit('save')"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="format in dateFormats"
|
v-for="format in availableDateFormats"
|
||||||
:key="format"
|
:key="format"
|
||||||
:value="format"
|
:value="format"
|
||||||
>
|
>
|
||||||
@@ -610,7 +610,7 @@ export default {
|
|||||||
IconTypography,
|
IconTypography,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 't'],
|
inject: ['template', 't', 'dateFormats'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -701,8 +701,10 @@ export default {
|
|||||||
'space'
|
'space'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
dateFormats () {
|
availableDateFormats () {
|
||||||
const formats = [
|
const formats = this.dateFormats.length
|
||||||
|
? [...this.dateFormats]
|
||||||
|
: [
|
||||||
'MM/DD/YYYY',
|
'MM/DD/YYYY',
|
||||||
'DD/MM/YYYY',
|
'DD/MM/YYYY',
|
||||||
'YYYY-MM-DD',
|
'YYYY-MM-DD',
|
||||||
@@ -710,11 +712,12 @@ export default {
|
|||||||
'DD.MM.YYYY',
|
'DD.MM.YYYY',
|
||||||
'MMM D, YYYY',
|
'MMM D, YYYY',
|
||||||
'MMMM D, YYYY',
|
'MMMM D, YYYY',
|
||||||
|
'MMMM YYYY',
|
||||||
'D MMM YYYY',
|
'D MMM YYYY',
|
||||||
'D MMMM YYYY'
|
'D MMMM YYYY'
|
||||||
]
|
]
|
||||||
|
|
||||||
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
|
if (!this.dateFormats.length && (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko'))) {
|
||||||
formats.push('YYYY년 MM월 DD일')
|
formats.push('YYYY년 MM월 DD일')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -783,17 +786,27 @@ export default {
|
|||||||
},
|
},
|
||||||
copyToAllPages (field) {
|
copyToAllPages (field) {
|
||||||
const areaString = JSON.stringify(field.areas[0])
|
const areaString = JSON.stringify(field.areas[0])
|
||||||
|
const newAreas = []
|
||||||
|
const existingAreasIndex = field.areas.reduce((acc, area) => {
|
||||||
|
acc[`${area.attachment_uuid}-${area.page}`] = area
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
this.template.schema.forEach((item) => {
|
||||||
|
const attachment = this.template.documents.find((d) => d.uuid === item.attachment_uuid)
|
||||||
|
|
||||||
this.template.documents.forEach((attachment) => {
|
|
||||||
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
|
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
|
||||||
|
|
||||||
for (let page = 0; page <= numberOfPages - 1; page++) {
|
for (let page = 0; page <= numberOfPages - 1; page++) {
|
||||||
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
|
const existing = existingAreasIndex[`${attachment.uuid}-${page}`]
|
||||||
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
|
|
||||||
}
|
newAreas.push(existing || { ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
field.areas = newAreas
|
||||||
|
|
||||||
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
|
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
|
||||||
|
|
||||||
this.$emit('save')
|
this.$emit('save')
|
||||||
@@ -819,33 +832,49 @@ export default {
|
|||||||
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
|
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
|
||||||
},
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format) {
|
||||||
const monthFormats = {
|
const monthFormats = { M: 'numeric', MM: '2-digit', MMM: 'short', MMMM: 'long' }
|
||||||
M: 'numeric',
|
const dayFormats = { D: 'numeric', DD: '2-digit' }
|
||||||
MM: '2-digit',
|
const yearFormats = { YYYY: 'numeric', YYY: 'numeric', YY: '2-digit' }
|
||||||
MMM: 'short',
|
const hourFormats = { H: 'numeric', HH: '2-digit', h: 'numeric', hh: '2-digit' }
|
||||||
MMMM: 'long'
|
const minuteFormats = { m: 'numeric', mm: '2-digit' }
|
||||||
}
|
const secondFormats = { s: 'numeric', ss: '2-digit' }
|
||||||
|
|
||||||
const dayFormats = {
|
const opts = {
|
||||||
D: 'numeric',
|
|
||||||
DD: '2-digit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const yearFormats = {
|
|
||||||
YYYY: 'numeric',
|
|
||||||
YY: '2-digit'
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = new Intl.DateTimeFormat([], {
|
|
||||||
day: dayFormats[format.match(/D+/)],
|
day: dayFormats[format.match(/D+/)],
|
||||||
month: monthFormats[format.match(/M+/)],
|
month: monthFormats[format.match(/M+/)],
|
||||||
year: yearFormats[format.match(/Y+/)]
|
year: yearFormats[format.match(/Y+/)]
|
||||||
}).formatToParts(date)
|
}
|
||||||
|
|
||||||
return format
|
if (format.match(/H+/)) { opts.hour = hourFormats[format.match(/H+/)[0]]; opts.hour12 = false }
|
||||||
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
if (format.match(/h+/)) { opts.hour = hourFormats[format.match(/h+/)[0]]; opts.hour12 = true }
|
||||||
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
if (/[Aa]/.test(format) && opts.hour12 === undefined) opts.hour12 = true
|
||||||
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
if (format.match(/m+/)) opts.minute = minuteFormats[format.match(/m+/)[0]]
|
||||||
|
if (format.match(/s+/)) opts.second = secondFormats[format.match(/s+/)[0]]
|
||||||
|
if (/z/.test(format)) opts.timeZoneName = 'short'
|
||||||
|
|
||||||
|
const partTypes = {
|
||||||
|
M: 'month',
|
||||||
|
D: 'day',
|
||||||
|
Y: 'year',
|
||||||
|
H: 'hour',
|
||||||
|
h: 'hour',
|
||||||
|
m: 'minute',
|
||||||
|
s: 'second',
|
||||||
|
z: 'timeZoneName',
|
||||||
|
A: 'dayPeriod',
|
||||||
|
a: 'dayPeriod'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], opts).formatToParts(date)
|
||||||
|
|
||||||
|
return format.replace(/MMMM|MMM|MM|M|DD|D|YYYY|YYY|YY|HH|hh|H|h|mm|m|ss|s|A|a|z/g, (token) => {
|
||||||
|
const value = parts.find((p) => p.type === partTypes[token[0]])?.value
|
||||||
|
|
||||||
|
if (token === 'A') return (value || '').toUpperCase()
|
||||||
|
if (token === 'a') return (value || '').toLowerCase()
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2 group justify-between items-center"
|
class="flex px-2 text-sm group justify-between items-center"
|
||||||
:class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
|
:class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
|
||||||
@click.prevent="selectSubmitter(submitter)"
|
@click.prevent="selectSubmitter(submitter)"
|
||||||
>
|
>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2 text-sm"
|
||||||
@click.prevent="addSubmitter"
|
@click.prevent="addSubmitter"
|
||||||
>
|
>
|
||||||
<IconUserPlus
|
<IconUserPlus
|
||||||
|
|||||||
@@ -65,13 +65,33 @@
|
|||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isShowVariables && submitterDefaultFields.length && editable">
|
<div
|
||||||
<hr class="mb-2">
|
v-if="editable && withCustomFieldsTab"
|
||||||
|
class="tabs w-full mb-1.5 custom-fields-tabs"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="tab tab-bordered w-1/2 border-base-300 custom-fields-tab"
|
||||||
|
:class="{ 'tab-active': !showCustomTab }"
|
||||||
|
:style="{ '--tab-border': showCustomTab ? '0px' : '0.5px' }"
|
||||||
|
@click="setFieldsTab('default')"
|
||||||
|
>{{ t('default') }}</a>
|
||||||
|
<a
|
||||||
|
class="tab tab-bordered w-1/2 border-base-300 custom-fields-tab"
|
||||||
|
:class="{ 'tab-active': showCustomTab }"
|
||||||
|
:style="{ '--tab-border': showCustomTab ? '0.5px' : '0px' }"
|
||||||
|
@click="setFieldsTab('custom')"
|
||||||
|
>{{ t('custom') }}</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isShowVariables && submitterDefaultFields.length && editable && (!withCustomFieldsTab || showCustomTab)">
|
||||||
|
<hr
|
||||||
|
v-if="!withCustomFieldsTab"
|
||||||
|
class="mb-1.5"
|
||||||
|
>
|
||||||
<template v-if="isShowFieldSearch">
|
<template v-if="isShowFieldSearch">
|
||||||
<input
|
<input
|
||||||
v-model="defaultFieldsSearch"
|
v-model="defaultFieldsSearch"
|
||||||
:placeholder="t('search_field')"
|
:placeholder="t('search_field')"
|
||||||
class="input input-ghost input-xs px-0 text-base mb-2 !outline-0 !rounded bg-transparent w-full"
|
class="input input-ghost input-xs px-0 text-base mb-1.5 !outline-0 !rounded bg-transparent w-full"
|
||||||
>
|
>
|
||||||
<hr class="mb-2">
|
<hr class="mb-2">
|
||||||
</template>
|
</template>
|
||||||
@@ -218,7 +238,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isShowVariables && editable && !onlyDefinedFields && (!showCustomTab || (!customFields.length && !newCustomField))"
|
v-if="!isShowVariables && editable && !onlyDefinedFields && (!showCustomTab || (!customFields.length && !newCustomField)) && (!withCustomFieldsTab || !showCustomTab)"
|
||||||
id="field-types-grid"
|
id="field-types-grid"
|
||||||
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
||||||
>
|
>
|
||||||
@@ -363,7 +383,7 @@
|
|||||||
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
||||||
>
|
>
|
||||||
<div class="text-center text-sm">
|
<div class="text-center text-sm">
|
||||||
{{ t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') }}
|
{{ t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<label
|
<label
|
||||||
@@ -418,6 +438,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withCustomFieldsTab: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withFieldsSearch: {
|
withFieldsSearch: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -428,6 +453,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withDetectExistingFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -495,6 +525,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
detectCustomFieldsIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
showTourStartForm: {
|
showTourStartForm: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -557,7 +592,8 @@ export default {
|
|||||||
},
|
},
|
||||||
submitterDefaultFields () {
|
submitterDefaultFields () {
|
||||||
return this.defaultFields.filter((f) => {
|
return this.defaultFields.filter((f) => {
|
||||||
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
return (this.withCustomFieldsTab ? true : !this.submitterFields.find((field) => field.name === f.name)) &&
|
||||||
|
(!f.role || f.role === this.selectedSubmitter.name)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
filteredSubmitterDefaultFields () {
|
filteredSubmitterDefaultFields () {
|
||||||
@@ -656,6 +692,78 @@ export default {
|
|||||||
this.customFields.splice(0, this.customFields.length, ...fields)
|
this.customFields.splice(0, this.customFields.length, ...fields)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
buildExistingFields () {
|
||||||
|
const existing = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
const submittersByUuid = this.template.submitters.reduce((acc, s) => {
|
||||||
|
acc[s.uuid] = s
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const add = (field, role) => {
|
||||||
|
if (!field?.name) return
|
||||||
|
|
||||||
|
const key = field.name.toLowerCase() + ':' + (role || '').toLowerCase()
|
||||||
|
|
||||||
|
if (seen.has(key)) return
|
||||||
|
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
const item = { name: field.name, type: field.type || 'text' }
|
||||||
|
|
||||||
|
if (role) item.role = role
|
||||||
|
|
||||||
|
const optionValues = Array.isArray(field.options)
|
||||||
|
? field.options.map((o) => (typeof o === 'string' ? o : o?.value)).filter(Boolean)
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (optionValues.length) item.options = optionValues
|
||||||
|
|
||||||
|
existing.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.template.fields.forEach((f) => add(f, submittersByUuid[f.submitter_uuid]?.name))
|
||||||
|
this.defaultRequiredFields.forEach((f) => add(f, f.role))
|
||||||
|
this.defaultFields.forEach((f) => add(f, f.role))
|
||||||
|
this.customFields.forEach((f) => add(f, f.role))
|
||||||
|
|
||||||
|
return existing
|
||||||
|
},
|
||||||
|
enrichDetectedField (field) {
|
||||||
|
if (!this.withDetectExistingFields || !field.name) return field
|
||||||
|
|
||||||
|
const role = this.template.submitters.find((s) => s.uuid === field.submitter_uuid)?.name
|
||||||
|
const nameKey = field.name.toLowerCase()
|
||||||
|
const indexKey = [field.name, role].filter(Boolean).join(':').toLowerCase()
|
||||||
|
|
||||||
|
const customField = this.detectCustomFieldsIndex[indexKey] || this.detectCustomFieldsIndex[nameKey]
|
||||||
|
|
||||||
|
if (customField) this.applyCustomFieldAttributes(field, customField)
|
||||||
|
|
||||||
|
return field
|
||||||
|
},
|
||||||
|
applyCustomFieldAttributes (field, customField) {
|
||||||
|
const skipKeys = new Set(['uuid', 'areas', 'submitter_uuid', 'conditions', 'prefillable', 'role'])
|
||||||
|
|
||||||
|
Object.entries(customField).forEach(([key, value]) => {
|
||||||
|
if (skipKeys.has(key)) return
|
||||||
|
if (value === null || value === undefined) return
|
||||||
|
|
||||||
|
if (key === 'options') {
|
||||||
|
if (Array.isArray(value) && !Array.isArray(field.options)) {
|
||||||
|
field.options = value.map((o) => (
|
||||||
|
typeof o === 'string' ? { value: o, uuid: v4() } : { ...o, uuid: v4() }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
field[key] = JSON.parse(JSON.stringify(value))
|
||||||
|
} else {
|
||||||
|
field[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
detectFields () {
|
detectFields () {
|
||||||
const fields = []
|
const fields = []
|
||||||
|
|
||||||
@@ -665,7 +773,10 @@ export default {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
},
|
||||||
|
...(this.withDetectExistingFields
|
||||||
|
? { body: JSON.stringify({ fields: this.buildExistingFields() }) }
|
||||||
|
: {})
|
||||||
}).then(async (response) => {
|
}).then(async (response) => {
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
const decoder = new TextDecoder('utf-8')
|
const decoder = new TextDecoder('utf-8')
|
||||||
@@ -687,7 +798,7 @@ export default {
|
|||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
if ((data.fields || fields).length) {
|
if ((data.fields || fields).length) {
|
||||||
this.template.fields = data.fields || fields
|
this.template.fields = (data.fields || fields).map((f) => this.enrichDetectedField(f))
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
} else {
|
} else {
|
||||||
@@ -705,7 +816,7 @@ export default {
|
|||||||
this.$emit('select-submitter', this.template.submitters[0])
|
this.$emit('select-submitter', this.template.submitters[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.template.fields = data.fields || fields
|
this.template.fields = (data.fields || fields).map((f) => this.enrichDetectedField(f))
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const en = {
|
|||||||
sign_yourself: 'Sign Yourself',
|
sign_yourself: 'Sign Yourself',
|
||||||
set_signing_date: 'Set signing date',
|
set_signing_date: 'Set signing date',
|
||||||
signing_date: 'Signing Date',
|
signing_date: 'Signing Date',
|
||||||
|
signing_date_and_time: 'Signing Date and Time',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
remove: 'Remove',
|
remove: 'Remove',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
@@ -199,7 +200,7 @@ const en = {
|
|||||||
learn_more: 'Learn more',
|
learn_more: 'Learn more',
|
||||||
and: 'and',
|
and: 'and',
|
||||||
or: 'or',
|
or: 'or',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
|
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
|
||||||
start_tour: 'Start Tour',
|
start_tour: 'Start Tour',
|
||||||
or_add_from: 'Or add from',
|
or_add_from: 'Or add from',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
@@ -214,7 +215,11 @@ const en = {
|
|||||||
align_bottom: 'Align Bottom',
|
align_bottom: 'Align Bottom',
|
||||||
fields_selected: '{count} Fields Selected',
|
fields_selected: '{count} Fields Selected',
|
||||||
field_added: '{count} Field Added',
|
field_added: '{count} Field Added',
|
||||||
fields_added: '{count} Fields Added'
|
fields_added: '{count} Fields Added',
|
||||||
|
revisions: 'Revisions',
|
||||||
|
apply: 'Apply',
|
||||||
|
no_revisions_yet: 'No revisions yet',
|
||||||
|
viewing_revision_from: 'Viewing revision from {date}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -238,7 +243,7 @@ const es = {
|
|||||||
min: 'Mín',
|
min: 'Mín',
|
||||||
max: 'Máx',
|
max: 'Máx',
|
||||||
date_signed: 'Fecha actual',
|
date_signed: 'Fecha actual',
|
||||||
fuente: 'Fuente',
|
font: 'Fuente',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
reorder_fields: 'Reordenar campos',
|
reorder_fields: 'Reordenar campos',
|
||||||
@@ -271,6 +276,7 @@ const es = {
|
|||||||
with_logo: 'Con logotipo',
|
with_logo: 'Con logotipo',
|
||||||
description: 'Descripción',
|
description: 'Descripción',
|
||||||
signing_date: 'Fecha de Firma',
|
signing_date: 'Fecha de Firma',
|
||||||
|
signing_date_and_time: 'Fecha y Hora de Firma',
|
||||||
display_title: 'Título de visualización',
|
display_title: 'Título de visualización',
|
||||||
unchecked: 'No marcado',
|
unchecked: 'No marcado',
|
||||||
price: 'Precio',
|
price: 'Precio',
|
||||||
@@ -296,7 +302,7 @@ const es = {
|
|||||||
set_signing_date: 'Establecer fecha de firma',
|
set_signing_date: 'Establecer fecha de firma',
|
||||||
are_you_sure_: '¿Estás seguro?',
|
are_you_sure_: '¿Estás seguro?',
|
||||||
sign_yourself: 'Firma tú mismo',
|
sign_yourself: 'Firma tú mismo',
|
||||||
checked: 'Seleccionado',
|
checked: 'Marcado',
|
||||||
current_date: 'Fecha actual',
|
current_date: 'Fecha actual',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
remove: 'Eliminar',
|
remove: 'Eliminar',
|
||||||
@@ -334,16 +340,16 @@ const es = {
|
|||||||
eighth_party: 'Octava Parte',
|
eighth_party: 'Octava Parte',
|
||||||
ninth_party: 'Novena Parte',
|
ninth_party: 'Novena Parte',
|
||||||
tenth_party: 'Décima Parte',
|
tenth_party: 'Décima Parte',
|
||||||
eleventh_party: 'Undécimo Partido',
|
eleventh_party: 'Undécima Parte',
|
||||||
twelfth_party: 'Duodécimo Partido',
|
twelfth_party: 'Duodécima Parte',
|
||||||
thirteenth_party: 'Decimotercer Partido',
|
thirteenth_party: 'Decimotercera Parte',
|
||||||
fourteenth_party: 'Catorceavo Partido',
|
fourteenth_party: 'Decimocuarta Parte',
|
||||||
fifteenth_party: 'Quinceavo Partido',
|
fifteenth_party: 'Decimoquinta Parte',
|
||||||
sixteenth_party: 'Dieciséisavo Partido',
|
sixteenth_party: 'Decimosexta Parte',
|
||||||
seventeenth_party: 'Diecisieteavo Partido',
|
seventeenth_party: 'Decimoséptima Parte',
|
||||||
eighteenth_party: 'Dieciochoavo Partido',
|
eighteenth_party: 'Decimoctava Parte',
|
||||||
nineteenth_party: 'Decimonovena Fiesta',
|
nineteenth_party: 'Decimonovena Parte',
|
||||||
twentieth_party: 'Vigésima Fiesta',
|
twentieth_party: 'Vigésima Parte',
|
||||||
draw: 'Dibujar',
|
draw: 'Dibujar',
|
||||||
add: 'Agregar',
|
add: 'Agregar',
|
||||||
text: 'Texto',
|
text: 'Texto',
|
||||||
@@ -418,7 +424,7 @@ const es = {
|
|||||||
learn_more: 'Aprende más',
|
learn_more: 'Aprende más',
|
||||||
and: 'y',
|
and: 'y',
|
||||||
or: 'o',
|
or: 'o',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
|
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
|
||||||
start_tour: 'Iniciar guía',
|
start_tour: 'Iniciar guía',
|
||||||
or_add_from: 'O agregar desde',
|
or_add_from: 'O agregar desde',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
@@ -433,7 +439,11 @@ const es = {
|
|||||||
align_bottom: 'Alinear abajo',
|
align_bottom: 'Alinear abajo',
|
||||||
fields_selected: '{count} Campos Seleccionados',
|
fields_selected: '{count} Campos Seleccionados',
|
||||||
field_added: '{count} Campo Añadido',
|
field_added: '{count} Campo Añadido',
|
||||||
fields_added: '{count} Campos Añadidos'
|
fields_added: '{count} Campos Añadidos',
|
||||||
|
revisions: 'Revisiones',
|
||||||
|
apply: 'Aplicar',
|
||||||
|
no_revisions_yet: 'Aún no hay revisiones',
|
||||||
|
viewing_revision_from: 'Viendo revisión del {date}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -456,15 +466,15 @@ const it = {
|
|||||||
length: 'Lunghezza',
|
length: 'Lunghezza',
|
||||||
min: 'Min',
|
min: 'Min',
|
||||||
max: 'Max',
|
max: 'Max',
|
||||||
date_signed: 'Data attuale',
|
date_signed: 'Data corrente',
|
||||||
font: 'Carattere',
|
font: 'Carattere',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Metodo',
|
method: 'Metodo',
|
||||||
reorder_fields: 'Riordina i campi',
|
reorder_fields: 'Riordina i campi',
|
||||||
verify_id: 'Verifica ID',
|
verify_id: 'Verifica ID',
|
||||||
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
|
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
|
||||||
ricorrente: 'Ricorrente',
|
recurrent: 'Ricorrente',
|
||||||
una_volta: 'Una volta',
|
one_off: 'Una tantum',
|
||||||
editable: 'Modificabile',
|
editable: 'Modificabile',
|
||||||
search_field: 'Campo di ricerca',
|
search_field: 'Campo di ricerca',
|
||||||
field_not_found: 'Campo non trovato',
|
field_not_found: 'Campo non trovato',
|
||||||
@@ -508,6 +518,7 @@ const it = {
|
|||||||
sign_yourself: 'Firma te stesso',
|
sign_yourself: 'Firma te stesso',
|
||||||
set_signing_date: 'Imposta data di firma',
|
set_signing_date: 'Imposta data di firma',
|
||||||
signing_date: 'Data di firma',
|
signing_date: 'Data di firma',
|
||||||
|
signing_date_and_time: 'Data e ora di firma',
|
||||||
send: 'Invia',
|
send: 'Invia',
|
||||||
remove: 'Rimuovi',
|
remove: 'Rimuovi',
|
||||||
edit: 'Modifica',
|
edit: 'Modifica',
|
||||||
@@ -522,7 +533,7 @@ const it = {
|
|||||||
drawn: 'Disegnato',
|
drawn: 'Disegnato',
|
||||||
drawn_or_typed: 'Disegnato o Digitato',
|
drawn_or_typed: 'Disegnato o Digitato',
|
||||||
drawn_or_upload: 'Disegnato o Caricato',
|
drawn_or_upload: 'Disegnato o Caricato',
|
||||||
upload: 'Caricare',
|
upload: 'Carica',
|
||||||
formula: 'Formula',
|
formula: 'Formula',
|
||||||
typed: 'Digitato',
|
typed: 'Digitato',
|
||||||
draw_field_on_the_document: 'Disegnare un campo sul documento',
|
draw_field_on_the_document: 'Disegnare un campo sul documento',
|
||||||
@@ -637,7 +648,7 @@ const it = {
|
|||||||
learn_more: 'Scopri di più',
|
learn_more: 'Scopri di più',
|
||||||
and: 'e',
|
and: 'e',
|
||||||
or: 'o',
|
or: 'o',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
|
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
|
||||||
start_tour: 'Inizia il tour',
|
start_tour: 'Inizia il tour',
|
||||||
or_add_from: 'O aggiungi da',
|
or_add_from: 'O aggiungi da',
|
||||||
sync: 'Sincronizza',
|
sync: 'Sincronizza',
|
||||||
@@ -652,7 +663,11 @@ const it = {
|
|||||||
align_bottom: 'Allinea in basso',
|
align_bottom: 'Allinea in basso',
|
||||||
fields_selected: '{count} Campi Selezionati',
|
fields_selected: '{count} Campi Selezionati',
|
||||||
field_added: '{count} Campo Aggiunto',
|
field_added: '{count} Campo Aggiunto',
|
||||||
fields_added: '{count} Campi Aggiunti'
|
fields_added: '{count} Campi Aggiunti',
|
||||||
|
revisions: 'Revisioni',
|
||||||
|
apply: 'Applica',
|
||||||
|
no_revisions_yet: 'Nessuna revisione ancora',
|
||||||
|
viewing_revision_from: 'Visualizzazione revisione del {date}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
@@ -676,13 +691,13 @@ const pt = {
|
|||||||
min: 'Mín',
|
min: 'Mín',
|
||||||
max: 'Máx',
|
max: 'Máx',
|
||||||
date_signed: 'Data atual',
|
date_signed: 'Data atual',
|
||||||
fonte: 'Fonte',
|
font: 'Fonte',
|
||||||
party: 'Parte',
|
party: 'Parte',
|
||||||
method: 'Método',
|
method: 'Método',
|
||||||
reorder_fields: 'Reorganizar campos',
|
reorder_fields: 'Reorganizar campos',
|
||||||
verify_id: 'Verificar ID',
|
verify_id: 'Verificar ID',
|
||||||
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
|
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
|
||||||
recurrent: 'Recurrente',
|
recurrent: 'Recorrente',
|
||||||
one_off: 'Único',
|
one_off: 'Único',
|
||||||
editable: 'Editável',
|
editable: 'Editável',
|
||||||
search_field: 'Campo de busca',
|
search_field: 'Campo de busca',
|
||||||
@@ -710,6 +725,7 @@ const pt = {
|
|||||||
description: 'Descrição',
|
description: 'Descrição',
|
||||||
display_title: 'Título de exibição',
|
display_title: 'Título de exibição',
|
||||||
signing_date: 'Data da Assinatura',
|
signing_date: 'Data da Assinatura',
|
||||||
|
signing_date_and_time: 'Data e Hora da Assinatura',
|
||||||
unchecked: 'Não marcado',
|
unchecked: 'Não marcado',
|
||||||
price: 'Preço',
|
price: 'Preço',
|
||||||
equal: 'Igual',
|
equal: 'Igual',
|
||||||
@@ -748,7 +764,7 @@ const pt = {
|
|||||||
processing_: 'Processando...',
|
processing_: 'Processando...',
|
||||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||||
add_documents_or_images: 'Adicionar documentos ou imagens',
|
add_documents_or_images: 'Adicionar documentos ou imagens',
|
||||||
add_a_new_document: 'Ajouter un nouveau document',
|
add_a_new_document: 'Adicionar um novo documento',
|
||||||
replace_existing_document: 'Substituir documento existente',
|
replace_existing_document: 'Substituir documento existente',
|
||||||
clone_and_replace_documents: 'Clonar e substituir documentos',
|
clone_and_replace_documents: 'Clonar e substituir documentos',
|
||||||
required: 'Obrigatório',
|
required: 'Obrigatório',
|
||||||
@@ -856,7 +872,7 @@ const pt = {
|
|||||||
learn_more: 'Saiba mais',
|
learn_more: 'Saiba mais',
|
||||||
and: 'e',
|
and: 'e',
|
||||||
or: 'ou',
|
or: 'ou',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
|
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
|
||||||
start_tour: 'Iniciar tour',
|
start_tour: 'Iniciar tour',
|
||||||
or_add_from: 'Ou adicionar de',
|
or_add_from: 'Ou adicionar de',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
@@ -871,7 +887,11 @@ const pt = {
|
|||||||
align_bottom: 'Alinhar à parte inferior',
|
align_bottom: 'Alinhar à parte inferior',
|
||||||
fields_selected: '{count} Campos Selecionados',
|
fields_selected: '{count} Campos Selecionados',
|
||||||
field_added: '{count} Campo Adicionado',
|
field_added: '{count} Campo Adicionado',
|
||||||
fields_added: '{count} Campos Adicionados'
|
fields_added: '{count} Campos Adicionados',
|
||||||
|
revisions: 'Revisões',
|
||||||
|
apply: 'Aplicar',
|
||||||
|
no_revisions_yet: 'Nenhuma revisão ainda',
|
||||||
|
viewing_revision_from: 'Visualizando revisão de {date}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -896,7 +916,7 @@ const fr = {
|
|||||||
max: 'Max',
|
max: 'Max',
|
||||||
font: 'Police',
|
font: 'Police',
|
||||||
party: 'Partie',
|
party: 'Partie',
|
||||||
date_signed: 'Date de signature',
|
date_signed: 'Date du jour',
|
||||||
method: 'Méthode',
|
method: 'Méthode',
|
||||||
reorder_fields: 'Réorganiser les champs',
|
reorder_fields: 'Réorganiser les champs',
|
||||||
verify_id: "Vérifier l'ID",
|
verify_id: "Vérifier l'ID",
|
||||||
@@ -946,6 +966,7 @@ const fr = {
|
|||||||
sign_yourself: 'Signer vous-même',
|
sign_yourself: 'Signer vous-même',
|
||||||
set_signing_date: 'Définir la date de signature',
|
set_signing_date: 'Définir la date de signature',
|
||||||
signing_date: 'Date de signature',
|
signing_date: 'Date de signature',
|
||||||
|
signing_date_and_time: 'Date et heure de signature',
|
||||||
send: 'Envoyer',
|
send: 'Envoyer',
|
||||||
remove: 'Supprimer',
|
remove: 'Supprimer',
|
||||||
edit: 'Modifier',
|
edit: 'Modifier',
|
||||||
@@ -1075,7 +1096,7 @@ const fr = {
|
|||||||
learn_more: 'En savoir plus',
|
learn_more: 'En savoir plus',
|
||||||
and: 'et',
|
and: 'et',
|
||||||
or: 'ou',
|
or: 'ou',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Commencez une visite rapide pour apprendre à créer et à envoyer votre premier document',
|
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Commencez une visite rapide pour apprendre à créer et à envoyer votre premier document',
|
||||||
start_tour: 'Démarrer',
|
start_tour: 'Démarrer',
|
||||||
or_add_from: 'Ou ajouter depuis',
|
or_add_from: 'Ou ajouter depuis',
|
||||||
sync: 'Synchroniser',
|
sync: 'Synchroniser',
|
||||||
@@ -1090,7 +1111,11 @@ const fr = {
|
|||||||
align_bottom: 'Aligner en bas',
|
align_bottom: 'Aligner en bas',
|
||||||
fields_selected: '{count} Champs Sélectionnés',
|
fields_selected: '{count} Champs Sélectionnés',
|
||||||
field_added: '{count} Champ Ajouté',
|
field_added: '{count} Champ Ajouté',
|
||||||
fields_added: '{count} Champs Ajoutés'
|
fields_added: '{count} Champs Ajoutés',
|
||||||
|
revisions: 'Révisions',
|
||||||
|
apply: 'Appliquer',
|
||||||
|
no_revisions_yet: 'Aucune révision pour le moment',
|
||||||
|
viewing_revision_from: 'Affichage de la révision du {date}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -1115,7 +1140,7 @@ const de = {
|
|||||||
max: 'Max.',
|
max: 'Max.',
|
||||||
font: 'Schriftart',
|
font: 'Schriftart',
|
||||||
party: 'Partei',
|
party: 'Partei',
|
||||||
date_signed: 'Unterzeichnungsdatum',
|
date_signed: 'Aktuelles Datum',
|
||||||
method: 'Methode',
|
method: 'Methode',
|
||||||
reorder_fields: 'Felder neu anordnen',
|
reorder_fields: 'Felder neu anordnen',
|
||||||
verify_id: 'ID überprüfen',
|
verify_id: 'ID überprüfen',
|
||||||
@@ -1165,6 +1190,7 @@ const de = {
|
|||||||
sign_yourself: 'Selbst unterschreiben',
|
sign_yourself: 'Selbst unterschreiben',
|
||||||
set_signing_date: 'Unterzeichnungsdatum festlegen',
|
set_signing_date: 'Unterzeichnungsdatum festlegen',
|
||||||
signing_date: 'Unterzeichnungsdatum',
|
signing_date: 'Unterzeichnungsdatum',
|
||||||
|
signing_date_and_time: 'Unterzeichnungsdatum und -uhrzeit',
|
||||||
send: 'Senden',
|
send: 'Senden',
|
||||||
remove: 'Entfernen',
|
remove: 'Entfernen',
|
||||||
edit: 'Bearbeiten',
|
edit: 'Bearbeiten',
|
||||||
@@ -1294,7 +1320,7 @@ const de = {
|
|||||||
learn_more: 'Mehr erfahren',
|
learn_more: 'Mehr erfahren',
|
||||||
and: 'und',
|
and: 'und',
|
||||||
or: 'oder',
|
or: 'oder',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Starten Sie eine Kurztour, um zu lernen, wie Sie Ihr erstes Dokument erstellen und versenden.',
|
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Starten Sie eine Kurztour, um zu lernen, wie Sie Ihr erstes Dokument erstellen und versenden.',
|
||||||
start_tour: 'Tour starten',
|
start_tour: 'Tour starten',
|
||||||
or_add_from: 'Oder hinzufügen aus',
|
or_add_from: 'Oder hinzufügen aus',
|
||||||
sync: 'Synchronisieren',
|
sync: 'Synchronisieren',
|
||||||
@@ -1309,7 +1335,11 @@ const de = {
|
|||||||
align_bottom: 'Unten ausrichten',
|
align_bottom: 'Unten ausrichten',
|
||||||
fields_selected: '{count} Felder Ausgewählt',
|
fields_selected: '{count} Felder Ausgewählt',
|
||||||
field_added: '{count} Feld Hinzugefügt',
|
field_added: '{count} Feld Hinzugefügt',
|
||||||
fields_added: '{count} Felder Hinzugefügt'
|
fields_added: '{count} Felder Hinzugefügt',
|
||||||
|
revisions: 'Revisionen',
|
||||||
|
apply: 'Anwenden',
|
||||||
|
no_revisions_yet: 'Noch keine Revisionen',
|
||||||
|
viewing_revision_from: 'Ansicht der Revision vom {date}'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
@@ -1334,7 +1364,7 @@ const nl = {
|
|||||||
max: 'Max',
|
max: 'Max',
|
||||||
font: 'Lettertype',
|
font: 'Lettertype',
|
||||||
party: 'Partij',
|
party: 'Partij',
|
||||||
date_signed: 'Datum ondertekend',
|
date_signed: 'Huidige datum',
|
||||||
method: 'Methode',
|
method: 'Methode',
|
||||||
reorder_fields: 'Velden herschikken',
|
reorder_fields: 'Velden herschikken',
|
||||||
verify_id: 'ID verifiëren',
|
verify_id: 'ID verifiëren',
|
||||||
@@ -1384,6 +1414,7 @@ const nl = {
|
|||||||
sign_yourself: 'Zelf ondertekenen',
|
sign_yourself: 'Zelf ondertekenen',
|
||||||
set_signing_date: 'Ondertekeningsdatum instellen',
|
set_signing_date: 'Ondertekeningsdatum instellen',
|
||||||
signing_date: 'Ondertekeningsdatum',
|
signing_date: 'Ondertekeningsdatum',
|
||||||
|
signing_date_and_time: 'Ondertekeningsdatum en -tijd',
|
||||||
send: 'Verzenden',
|
send: 'Verzenden',
|
||||||
remove: 'Verwijderen',
|
remove: 'Verwijderen',
|
||||||
edit: 'Bewerken',
|
edit: 'Bewerken',
|
||||||
@@ -1513,7 +1544,7 @@ const nl = {
|
|||||||
learn_more: 'Meer informatie',
|
learn_more: 'Meer informatie',
|
||||||
and: 'en',
|
and: 'en',
|
||||||
or: 'of',
|
or: 'of',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start een korte rondleiding om te leren hoe u uw eerste document maakt en verzendt',
|
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Start een korte rondleiding om te leren hoe u uw eerste document maakt en verzendt',
|
||||||
start_tour: 'Rondleiding starten',
|
start_tour: 'Rondleiding starten',
|
||||||
or_add_from: 'Of toevoegen van',
|
or_add_from: 'Of toevoegen van',
|
||||||
sync: 'Synchroniseren',
|
sync: 'Synchroniseren',
|
||||||
@@ -1528,7 +1559,11 @@ const nl = {
|
|||||||
align_bottom: 'Onder uitlijnen',
|
align_bottom: 'Onder uitlijnen',
|
||||||
fields_selected: '{count} Velden Geselecteerd',
|
fields_selected: '{count} Velden Geselecteerd',
|
||||||
field_added: '{count} Veld Toegevoegd',
|
field_added: '{count} Veld Toegevoegd',
|
||||||
fields_added: '{count} Velden Toegevoegd'
|
fields_added: '{count} Velden Toegevoegd',
|
||||||
|
revisions: 'Revisies',
|
||||||
|
apply: 'Toepassen',
|
||||||
|
no_revisions_yet: 'Nog geen revisies',
|
||||||
|
viewing_revision_from: 'Revisie van {date} bekijken'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de, nl }
|
export { en, es, it, pt, fr, de, nl }
|
||||||
@@ -37,6 +37,8 @@
|
|||||||
:ref="setAreaRefs"
|
:ref="setAreaRefs"
|
||||||
:area="item.area"
|
:area="item.area"
|
||||||
:input-mode="inputMode"
|
:input-mode="inputMode"
|
||||||
|
:conditional-field-index="conditionalFieldIndex"
|
||||||
|
:formula-values-index="formulaValuesIndex"
|
||||||
:page-width="width"
|
:page-width="width"
|
||||||
:page-height="height"
|
:page-height="height"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
@scroll-to="$emit('scroll-to', $event)"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
@add-custom-field="$emit('add-custom-field', $event)"
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||||
|
@click-title="closeContextMenu"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-for="(area, index) in newAreas"
|
v-for="(area, index) in newAreas"
|
||||||
@@ -180,6 +183,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
conditionalFieldIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
formulaValuesIndex: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
defaultFields: {
|
defaultFields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -143,8 +143,8 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="state"
|
name="oauth_data"
|
||||||
:value="oauthState"
|
:value="oauthData"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -334,7 +334,7 @@ export default {
|
|||||||
authenticityToken () {
|
authenticityToken () {
|
||||||
return document.querySelector('meta[name="csrf-token"]')?.content
|
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
},
|
},
|
||||||
oauthState () {
|
oauthData () {
|
||||||
const params = new URLSearchParams('')
|
const params = new URLSearchParams('')
|
||||||
|
|
||||||
params.set('redir', document.location.href)
|
params.set('redir', document.location.href)
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
/>
|
||||||
|
<div class="modal-box pt-4 pb-6 mt-20 w-full">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-3 font-medium">
|
||||||
|
<span>{{ t('revisions') }}</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-1.5">
|
||||||
|
<li
|
||||||
|
v-for="revision in revisions"
|
||||||
|
:key="revision.id"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full text-left rounded-lg p-3 border border-dashed border-base-200 transition-colors disabled:cursor-default hover:bg-base-200 hover:border-base-200"
|
||||||
|
:disabled="loadingId !== null"
|
||||||
|
@click="viewRevision(revision.id)"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center gap-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>{{ formatDate(revision.created_at) }}</span>
|
||||||
|
<span class="-ml-0.5 flex items-center space-x-1 text-xs text-base-content/60 mt-0.5">
|
||||||
|
<IconUser class="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
|
<span class="truncate">{{ revision.author.full_name || revision.author.email }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="btn btn-sm btn-neutral text-white pointer-events-none flex-shrink-0">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="loadingId === revision.id"
|
||||||
|
class="w-4 h-4 animate-spin"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ t('view') }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="!revisions.length"
|
||||||
|
class="py-4 text-center text-base-content/60"
|
||||||
|
>
|
||||||
|
{{ t('no_revisions_yet') }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconUser, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RevisionsModal',
|
||||||
|
components: { IconUser, IconInnerShadowTop },
|
||||||
|
inject: ['t', 'baseFetch'],
|
||||||
|
props: {
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
revisions: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'apply'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loadingId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
viewRevision (id) {
|
||||||
|
if (this.loadingId !== null) return
|
||||||
|
|
||||||
|
this.loadingId = id
|
||||||
|
|
||||||
|
this.baseFetch(`/templates/${this.template.id}/versions/${id}`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((revision) => { this.$emit('apply', revision) })
|
||||||
|
.finally(() => { this.loadingId = null })
|
||||||
|
},
|
||||||
|
formatDate (string) {
|
||||||
|
return new Date(string).toLocaleString(this.locale || undefined, {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -228,7 +228,7 @@ export default {
|
|||||||
'https://www.googleapis.com/auth/userinfo.email',
|
'https://www.googleapis.com/auth/userinfo.email',
|
||||||
'https://www.googleapis.com/auth/drive.file'
|
'https://www.googleapis.com/auth/drive.file'
|
||||||
].join(' '),
|
].join(' '),
|
||||||
state: new URLSearchParams({
|
oauth_data: new URLSearchParams({
|
||||||
redir: `/templates/${this.templateId}/edit?google_drive_open=1`
|
redir: `/templates/${this.templateId}/edit?google_drive_open=1`
|
||||||
}).toString()
|
}).toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class SendFormCompletedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendFormCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -39,4 +39,3 @@ class SendFormCompletedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -28,8 +28,8 @@ class SendFormDeclinedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
|
||||||
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -37,4 +37,3 @@ class SendFormDeclinedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -28,8 +28,8 @@ class SendFormStartedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
|
||||||
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -37,4 +37,3 @@ class SendFormStartedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -28,8 +28,8 @@ class SendFormViewedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter))
|
data: Submitters::SerializeForWebhook.call(submitter))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
|
||||||
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -37,4 +37,3 @@ class SendFormViewedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -26,8 +26,8 @@ class SendSubmissionArchivedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: submission.as_json(only: %i[id archived_at]))
|
data: submission.as_json(only: %i[id archived_at]))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -35,4 +35,3 @@ class SendSubmissionArchivedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -26,8 +26,8 @@ class SendSubmissionCompletedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Submissions::SerializeForApi.call(submission))
|
data: Submissions::SerializeForApi.call(submission))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
|
||||||
SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -35,4 +35,3 @@ class SendSubmissionCompletedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -26,8 +26,8 @@ class SendSubmissionCreatedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Submissions::SerializeForApi.call(submission))
|
data: Submissions::SerializeForApi.call(submission))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -35,4 +35,3 @@ class SendSubmissionCreatedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -26,8 +26,8 @@ class SendSubmissionExpiredWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Submissions::SerializeForApi.call(submission))
|
data: Submissions::SerializeForApi.call(submission))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
|
||||||
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -35,4 +35,3 @@ class SendSubmissionExpiredWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -26,8 +26,8 @@ class SendTemplateArchivedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: template.as_json(only: %i[id archived_at]))
|
data: template.as_json(only: %i[id archived_at]))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
|
||||||
SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -35,4 +35,3 @@ class SendTemplateArchivedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -26,8 +26,8 @@ class SendTemplateCreatedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Templates::SerializeForApi.call(template))
|
data: Templates::SerializeForApi.call(template))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -35,4 +35,3 @@ class SendTemplateCreatedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -26,8 +26,8 @@ class SendTemplateUpdatedWebhookRequestJob
|
|||||||
attempt:,
|
attempt:,
|
||||||
data: Templates::SerializeForApi.call(template))
|
data: Templates::SerializeForApi.call(template))
|
||||||
|
|
||||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||||
**params,
|
**params,
|
||||||
'attempt' => attempt + 1,
|
'attempt' => attempt + 1,
|
||||||
@@ -35,4 +35,3 @@ class SendTemplateUpdatedWebhookRequestJob
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -32,6 +32,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :submitters, dependent: :destroy
|
has_many :submitters, dependent: :destroy
|
||||||
has_many :account_linked_accounts, dependent: :destroy
|
has_many :account_linked_accounts, dependent: :destroy
|
||||||
has_many :email_events, dependent: :destroy
|
has_many :email_events, dependent: :destroy
|
||||||
|
has_many :document_metadata, class_name: 'DocumentMetadata', dependent: :destroy
|
||||||
has_many :webhook_urls, dependent: :destroy
|
has_many :webhook_urls, dependent: :destroy
|
||||||
has_many :webhook_events, dependent: nil
|
has_many :webhook_events, dependent: nil
|
||||||
has_many :account_accesses, dependent: :destroy
|
has_many :account_accesses, dependent: :destroy
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
|
DOWNLOAD_LINKS_EXPIRE_KEY = 'download_links_expire'
|
||||||
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'
|
||||||
|
ROTATE_INCREMENTAL_PDF_KEY = 'rotate_incremental_pdf'
|
||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
WITH_FILE_LINKS_KEY = 'with_file_links'
|
WITH_FILE_LINKS_KEY = 'with_file_links'
|
||||||
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
||||||
|
|||||||
@@ -101,13 +101,18 @@ class Submission < ApplicationRecord
|
|||||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||||
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
|
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
|
||||||
}
|
}
|
||||||
scope :expired, -> { pending.where(expire_at: ..Time.current) }
|
scope :expired, lambda {
|
||||||
|
where(expire_at: ..Time.current)
|
||||||
|
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||||
|
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
||||||
|
}
|
||||||
|
|
||||||
enum :source, {
|
enum :source, {
|
||||||
invite: 'invite',
|
invite: 'invite',
|
||||||
bulk: 'bulk',
|
bulk: 'bulk',
|
||||||
api: 'api',
|
api: 'api',
|
||||||
embed: 'embed',
|
embed: 'embed',
|
||||||
|
mcp: 'mcp',
|
||||||
link: 'link'
|
link: 'link'
|
||||||
}, scope: false, prefix: true
|
}, scope: false, prefix: true
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class Template < ApplicationRecord
|
|||||||
has_many :submissions, dependent: :destroy
|
has_many :submissions, dependent: :destroy
|
||||||
has_many :template_sharings, dependent: :destroy
|
has_many :template_sharings, dependent: :destroy
|
||||||
has_many :template_accesses, dependent: :destroy
|
has_many :template_accesses, dependent: :destroy
|
||||||
|
has_many :template_versions, dependent: :destroy
|
||||||
has_many :dynamic_documents, dependent: :destroy
|
has_many :dynamic_documents, dependent: :destroy
|
||||||
has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions
|
has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: template_versions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# data :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
# author_id :bigint not null
|
||||||
|
# template_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_template_versions_on_account_id (account_id)
|
||||||
|
# index_template_versions_on_author_id (author_id)
|
||||||
|
# index_template_versions_on_template_id_and_sha1 (template_id,sha1) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
# fk_rails_... (author_id => users.id)
|
||||||
|
# fk_rails_... (template_id => templates.id)
|
||||||
|
#
|
||||||
|
class TemplateVersion < ApplicationRecord
|
||||||
|
belongs_to :template
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :author, class_name: 'User'
|
||||||
|
|
||||||
|
attribute :data, :string, default: -> { {} }
|
||||||
|
|
||||||
|
serialize :data, coder: JSON
|
||||||
|
|
||||||
|
before_validation :set_account, on: :create
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
self.account ||= template.account
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# events :text not null
|
# events :text not null
|
||||||
|
# hmac_secret :text not null
|
||||||
# secret :text not null
|
# secret :text not null
|
||||||
# sha1 :string not null
|
# sha1 :string not null
|
||||||
# url :text not null
|
# url :text not null
|
||||||
@@ -47,10 +48,15 @@ class WebhookUrl < ApplicationRecord
|
|||||||
serialize :secret, coder: JSON
|
serialize :secret, coder: JSON
|
||||||
|
|
||||||
before_validation :set_sha1
|
before_validation :set_sha1
|
||||||
|
before_validation :set_hmac_secret
|
||||||
|
|
||||||
encrypts :url, :secret
|
encrypts :url, :secret, :hmac_secret
|
||||||
|
|
||||||
def set_sha1
|
def set_sha1
|
||||||
self.sha1 = Digest::SHA1.hexdigest(url)
|
self.sha1 = Digest::SHA1.hexdigest(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_hmac_secret
|
||||||
|
self.hmac_secret ||= WebhookUrls::Signatures.generate_secret
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Whitespace-only changes.
@@ -18,24 +18,7 @@
|
|||||||
<%= f.button button_title(title: t(:sign_in), disabled_with: t(:signing_in)), class: 'base-button' %>
|
<%= f.button button_title(title: t(:sign_in), disabled_with: t(:signing_in)), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if devise_mapping.omniauthable? %>
|
<%= render 'omniauthable' %>
|
||||||
<div class="space-y-4">
|
|
||||||
<% if User.omniauth_providers.include?(:google_oauth2) %>
|
|
||||||
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
|
|
||||||
<set-timezone data-input-id="state" data-params="true"></set-timezone>
|
|
||||||
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
|
|
||||||
<%= f.button button_title(title: t('sign_in_with_google'), icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if User.omniauth_providers.include?(:microsoft_office365) %>
|
|
||||||
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
|
|
||||||
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
|
|
||||||
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
|
|
||||||
<%= f.button button_title(title: t('sign_in_with_microsoft'), icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= render 'extra_links' %>
|
<%= render 'extra_links' %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<% if params[:modal].present? %>
|
<% if params[:modal].present? %>
|
||||||
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
|
<% modal_uri = Addressable::URI.parse(params[:modal]) %>
|
||||||
|
<% url_params = Rails.application.routes.recognize_path(modal_uri.path, method: :get) %>
|
||||||
<% if url_params[:action] == 'new' %>
|
<% if url_params[:action] == 'new' %>
|
||||||
<open-modal src="<%= url_for(url_params) %>"></open-modal>
|
<open-modal src="<%= url_for(**url_params, params: modal_uri.query_values) %>"></open-modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<turbo-frame id="modal"></turbo-frame>
|
<turbo-frame id="modal"></turbo-frame>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<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">
|
<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">
|
||||||
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
||||||
<%= svg_icon('start', class: 'h-3 w-3') %>
|
<%= svg_icon('start', class: 'h-3 w-3') %>
|
||||||
<span>11k</span>
|
<span>16k</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
|
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full py-1' } do |f| %>
|
||||||
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
|
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
|
||||||
<submit-form data-on="change" class="flex">
|
<submit-form data-on="change" class="flex">
|
||||||
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>
|
<%= f.check_box :testing_toggle, class: 'toggle', checked: current_account.testing?, style: 'height: 0.885rem; width: 1.35rem; --handleoffset: 0.395rem; margin-left: -2px; margin-right: 8px' %>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'shared/settings_nav_extra2' %>
|
<%= render 'shared/settings_nav_extra2' %>
|
||||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full' } do |f| %>
|
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full' } do |f| %>
|
||||||
<li>
|
<li>
|
||||||
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
|
<label class="flex items-center text-base hover:bg-base-300 w-full justify-between" for="testing_toggle">
|
||||||
<span class="mr-2 w-full">
|
<span class="mr-2 w-full">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %>
|
<% if can?(:manage, EncryptedConfig) || (current_user != true_user && current_account.testing?) %>
|
||||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'flex' } do |f| %>
|
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'flex' } do |f| %>
|
||||||
<label class="flex items-center justify-between" for="testing_toggle">
|
<label class="flex items-center justify-between" for="testing_toggle">
|
||||||
<span class="mr-2 text-lg">
|
<span class="mr-2 text-lg">
|
||||||
<%= t('test_mode') %>
|
<%= t('test_mode') %>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<% font = field.dig('preferences', 'font') %>
|
<% font = field.dig('preferences', 'font') %>
|
||||||
<% font_type = field.dig('preferences', 'font_type') %>
|
<% font_type = field.dig('preferences', 'font_type') %>
|
||||||
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
||||||
<field-value dir="auto" aria-hidden="true" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
<field-value dir="auto" aria-hidden="true" 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? && color.match?(Templates::COLOR_REGEXP) %><%= "background: #{bg_color}; " if bg_color.present? && bg_color.match?(Templates::COLOR_REGEXP) %>width: <%= area['w'].to_f * 100 %>%; height: <%= area['h'].to_f * 100 %>%; left: <%= area['x'].to_f * 100 %>%; top: <%= area['y'].to_f * 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'] * local_assigns[:page_width]).to_f / (area['h'] * local_assigns[:page_height])) > 4.5 %>
|
<% is_narrow = area['h'].to_f.positive? && ((area['w'].to_f * local_assigns[:page_width]) / (area['h'].to_f * local_assigns[:page_height])) > 4.5 %>
|
||||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
||||||
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
||||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
<% cell_width = area['cell_w'].to_f / area['w'] * 100 %>
|
||||||
<div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>">
|
<div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>">
|
||||||
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
<% (0..(area['w'].to_f / area['cell_w']).ceil).each do |index| %>
|
||||||
<% if value[index] %>
|
<% if value[index] %>
|
||||||
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
<div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
||||||
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
|
<% tz = local_assigns[:with_submitter_timezone] ? (submitter.timezone.presence || local_assigns[:timezone]) : local_assigns[:timezone] %>
|
||||||
<div class="w-full"><%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %></div>
|
<div class="w-full"><%= value == '{{date}}' ? TimeUtils.format_date_preview(field.dig('preferences', 'format'), local_assigns[:locale], tz) : TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale], timezone: tz) %></div>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'number' %>
|
<% elsif field['type'] == 'number' %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
@@ -83,14 +83,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'strikethrough' %>
|
<% elsif field['type'] == 'strikethrough' %>
|
||||||
<div class="w-full h-full flex items-center justify-center">
|
<div class="w-full h-full flex items-center justify-center">
|
||||||
<% if (((1000.0 / local_assigns[:page_width]) * local_assigns[:page_height]) * area['h']) < 40 %>
|
<% if (((1000.0 / local_assigns[:page_width]) * local_assigns[:page_height]) * area['h'].to_f) < 40 %>
|
||||||
<svg width="100%" height="100%">
|
<svg width="100%" height="100%">
|
||||||
<line x1="0" y1="50%" x2="100%" y2="50%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
<line x1="0" y1="50%" x2="100%" y2="50%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<% else %>
|
<% else %>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" style="overflow: visible; width: calc(100% - 6px); height: calc(100% - 6px); width: calc(100% - 0.6cqmin); height: calc(100% - 0.6cqmin)">
|
<svg xmlns="http://www.w3.org/2000/svg" style="overflow: visible; width: calc(100% - 6px); height: calc(100% - 6px); width: calc(100% - 0.6cqmin); height: calc(100% - 0.6cqmin)">
|
||||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
<line x1="0" y1="0" x2="100%" y2="100%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="<%= field.dig('preferences', 'color').presence || 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
<line x1="100%" y1="0" x2="0" y2="100%" stroke="<%= color.present? && color.match?(Templates::COLOR_REGEXP) ? color : 'red' %>" style="stroke-width: clamp(0px, 0.5vw, 6px); stroke-width: 0.6cqmin"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
<% 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 || Templates::ProcessDocument::US_LETTER_SIZE %>
|
<% 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_key, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}#{Templates::ProcessDocument::PREVIEW_FORMAT}")) %>
|
||||||
<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'] %>">
|
<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="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>" alt="<%= "#{item['name']} - #{t('page')} #{index + 1}" %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>" alt="<%= "#{item['name']} - #{t('page')} #{index + 1}" %>">
|
||||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
|
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
|
||||||
<% submitter_index = submitters_order_index[submitter.uuid] %>
|
<% submitter_index = submitters_order_index[submitter.uuid] %>
|
||||||
<% bg_class = bg_classes[submitter_index % bg_classes.size] %>
|
<% bg_class = bg_classes[submitter_index % bg_classes.size] %>
|
||||||
<div class="absolute overflow-visible" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%;">
|
<div class="absolute overflow-visible" style="width: <%= area['w'].to_f * 100 %>%; height: <%= area['h'].to_f * 100 %>%; left: <%= area['x'].to_f * 100 %>%; top: <%= area['y'].to_f * 100 %>%;">
|
||||||
<div class="flex h-full w-full bg-opacity-80 justify-center items-center <%= bg_class %>">
|
<div class="flex h-full w-full bg-opacity-80 justify-center items-center <%= bg_class %>">
|
||||||
<%= svg_icon(SubmissionsController::FIELD_ICONS.fetch(field['type'], 'text_size'), class: 'max-h-10 w-full h-full stroke-2 opacity-50') %>
|
<%= svg_icon(SubmissionsController::FIELD_ICONS.fetch(field['type'], 'text_size'), class: 'max-h-10 w-full h-full stroke-2 opacity-50') %>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
<% if field['type'] == 'number' %>
|
<% if field['type'] == 'number' %>
|
||||||
<% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
|
<% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %>
|
<% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale, timezone: (with_submitter_timezone ? (submitter.timezone.presence || @submission.account.timezone) : @submission.account.timezone)) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if (mask = field.dig('preferences', 'mask').presence) %>
|
<% if (mask = field.dig('preferences', 'mask').presence) %>
|
||||||
<% if signed_in? && can?(:read, @submission) %>
|
<% if signed_in? && can?(:read, @submission) %>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
<% 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 || Templates::ProcessDocument::US_LETTER_SIZE %>
|
<% 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_key, "#{index}.jpg")) %>
|
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_key, "#{index}#{Templates::ProcessDocument::PREVIEW_FORMAT}")) %>
|
||||||
<page-container class="block relative my-4 shadow-md" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
|
<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="<%= width %>" height="<%= height %>" alt="<%= "#{item['name']} - #{t('page')} #{index + 1}" %>">
|
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" height="<%= height %>" alt="<%= "#{item['name']} - #{t('page')} #{index + 1}" %>">
|
||||||
<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">
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
|
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<template-builder class="grid" data-template="<%= @template_data %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
|
<template-builder class="grid" data-template="<%= @template_data %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if @submissions.present? %>
|
<% if @submissions.present? %>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<%= render partial: 'submission', collection: @submissions, locals: { template: @template } %>
|
<%= render partial: 'submission', collection: @submissions, locals: { template: @template, archived: @template.archived_at? } %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="card bg-base-200">
|
<div class="card bg-base-200">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<%= t('welcome_to_docuseal') %>
|
<%= t('welcome_to_docuseal') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-2 text-center text-xs text-base-content/70">
|
<div class="my-2 text-center text-xs text-base-content/70">
|
||||||
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %>
|
<%= t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mt-3 w-full">
|
<div class="flex gap-2 mt-3 w-full">
|
||||||
<remove-on-event data-on="submit" data-selector-id="app_tour_manager" class="block w-full">
|
<remove-on-event data-on="submit" data-selector-id="app_tour_manager" class="block w-full">
|
||||||
|
|||||||
@@ -25,9 +25,11 @@
|
|||||||
<div class="flex gap-2 mt-3">
|
<div class="flex gap-2 mt-3">
|
||||||
<div class="relative flex-grow">
|
<div class="relative flex-grow">
|
||||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly>
|
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug, host: form_link_host) %>" class="base-input w-full pr-10" autocomplete="off" readonly>
|
||||||
<a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip tooltip-left text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>">
|
<% if @template.variables_schema.blank? && !@template.preferences&.dig('require_email_2fa') && !@template.preferences&.dig('require_phone_2fa') %>
|
||||||
|
<a href="<%= template_share_link_qr_path(@template) %>" target="_blank" rel="noopener" class="absolute top-1/2 -translate-y-1/2 right-2 flex items-center justify-center tooltip text-base-content/70 hover:text-base-content bg-white rounded px-1 py-0.5" data-tip="<%= t('qr_code') %>" aria-label="<%= t('qr_code') %>">
|
||||||
<%= svg_icon('qrcode', class: 'w-6 h-6') %>
|
<%= svg_icon('qrcode', class: 'w-6 h-6') %>
|
||||||
</a>
|
</a>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<check-on-click data-element-id="template_shared_link">
|
<check-on-click data-element-id="template_shared_link">
|
||||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
<% 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' %>
|
||||||
<% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %>
|
<% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %>
|
||||||
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 %>
|
<% next_attempt_at = last_attempt.created_at + (2**last_attempt.attempt).minutes %>
|
||||||
|
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 && next_attempt_at > 30.seconds.ago %>
|
||||||
<li class="ml-7">
|
<li class="ml-7">
|
||||||
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;">
|
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;">
|
||||||
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
|
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
|
||||||
</span>
|
</span>
|
||||||
<p class="leading-none text-base-content/90 pt-1">
|
<p class="leading-none text-base-content/90 pt-1">
|
||||||
<%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, last_attempt.created_at + (2**last_attempt.attempt).minutes)) %>
|
<%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, next_attempt_at)) %>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: t('webhook_security') do %>
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="inline-flex justify-center">
|
||||||
|
<%= link_to t('secret'), webhook_secret_path(@webhook_url), class: 'block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-l-3xl', data: { turbo_frame: 'modal' } %>
|
||||||
|
<%= link_to t('hmac'), webhook_hmac_path(@webhook_url), class: 'block bg-base-300 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-r-3xl', data: { turbo_frame: 'modal' } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="hmac_secret"><%= t('hmac_signing_secret') %></label>
|
||||||
|
<% token = @webhook_url.hmac_secret %>
|
||||||
|
<% obscured = "#{token[0, 10]}#{'*' * [token.length - 10, 0].max}" %>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<masked-input class="block w-full" data-token="<%= token %>">
|
||||||
|
<input id="hmac_secret" type="text" value="<%= obscured %>" class="base-input font-mono w-full" autocomplete="off" readonly>
|
||||||
|
</masked-input>
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm mt-2 opacity-70"><%= t('hmac_signature_header_hint_html', header: '<code>X-Docuseal-Signature</code>'.html_safe) %></p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
<%= render 'shared/turbo_modal', title: t('webhook_secret') do %>
|
<%= render 'shared/turbo_modal', title: t('webhook_security') do %>
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="inline-flex justify-center">
|
||||||
|
<%= link_to t('secret'), webhook_secret_path(@webhook_url), class: 'block bg-base-300 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-l-3xl', data: { turbo_frame: 'modal' } %>
|
||||||
|
<%= link_to t('hmac'), webhook_hmac_path(@webhook_url), class: 'block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-r-3xl', data: { turbo_frame: 'modal' } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
<%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
|
<%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
<%= render 'shared/settings_nav' %>
|
<%= render 'shared/settings_nav' %>
|
||||||
<div class="flex-grow min-w-0">
|
<div class="flex-grow min-w-0">
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
|
||||||
<h1 class="text-4xl font-bold">Webhooks</h1>
|
<h1 class="text-4xl font-bold">Webhooks</h1>
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||||
<%= render 'shared/test_mode_toggle' %>
|
<%= render 'shared/test_mode_toggle' %>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
<%= render 'shared/settings_nav' %>
|
<%= render 'shared/settings_nav' %>
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
|
||||||
<h1 class="text-4xl font-bold">Webhook</h1>
|
<h1 class="text-4xl font-bold">Webhook</h1>
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||||
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
|
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="flex items-center space-x-2 md:absolute md:right-0">
|
<div class="flex items-center space-x-2 md:absolute md:right-0">
|
||||||
<%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
|
<%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
|
||||||
<%= svg_icon('lock', class: 'w-4 h-4') %>
|
<%= svg_icon('lock', class: 'w-4 h-4') %>
|
||||||
<span><%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %></span>
|
<span><%= t('security') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
|
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
|
||||||
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
|
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||||
|
|||||||
+247
-199
File diff suppressed because it is too large.
Load diff
+3
-1
@@ -77,12 +77,13 @@ Rails.application.routes.draw do
|
|||||||
resources :console_redirect, only: %i[index]
|
resources :console_redirect, only: %i[index]
|
||||||
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
resources :upgrade, only: %i[index], controller: 'console_redirect'
|
||||||
resources :manage, only: %i[index], controller: 'console_redirect'
|
resources :manage, only: %i[index], controller: 'console_redirect'
|
||||||
resource :testing_account, only: %i[show destroy]
|
resource :testing_account, only: %i[create destroy]
|
||||||
resources :testing_api_settings, only: %i[index]
|
resources :testing_api_settings, only: %i[index]
|
||||||
resources :submitters_autocomplete, only: %i[index]
|
resources :submitters_autocomplete, only: %i[index]
|
||||||
resources :submitters_resubmit, only: %i[update]
|
resources :submitters_resubmit, only: %i[update]
|
||||||
resources :template_folders_autocomplete, only: %i[index]
|
resources :template_folders_autocomplete, only: %i[index]
|
||||||
resources :webhook_secret, only: %i[show update]
|
resources :webhook_secret, only: %i[show update]
|
||||||
|
resources :webhook_hmac, only: %i[show]
|
||||||
resources :webhook_preferences, only: %i[update]
|
resources :webhook_preferences, only: %i[update]
|
||||||
resource :templates_upload, only: %i[create]
|
resource :templates_upload, only: %i[create]
|
||||||
authenticated do
|
authenticated do
|
||||||
@@ -107,6 +108,7 @@ Rails.application.routes.draw do
|
|||||||
resource :form, only: %i[show], controller: 'templates_form_preview'
|
resource :form, only: %i[show], controller: 'templates_form_preview'
|
||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||||
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
|
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
|
||||||
|
resources :versions, only: %i[index show], controller: 'templates_versions'
|
||||||
resource :share_link, only: %i[show create], controller: 'templates_share_link'
|
resource :share_link, only: %i[show create], controller: 'templates_share_link'
|
||||||
resource :share_link_qr, only: %i[show], controller: 'templates_share_link_qr'
|
resource :share_link_qr, only: %i[show], controller: 'templates_share_link_qr'
|
||||||
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddHmacToWebhookUrls < ActiveRecord::Migration[8.1]
|
||||||
|
class MigrationWebhookUrl < ApplicationRecord
|
||||||
|
self.table_name = 'webhook_urls'
|
||||||
|
|
||||||
|
encrypts :hmac_secret
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column :webhook_urls, :hmac_secret, :text
|
||||||
|
|
||||||
|
MigrationWebhookUrl.find_each do |webhook_url|
|
||||||
|
webhook_url.update_columns(hmac_secret: WebhookUrls::Signatures.generate_secret)
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :webhook_urls, :hmac_secret, false
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :webhook_urls, :hmac_secret
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateTemplateVersions < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
create_table :template_versions do |t|
|
||||||
|
t.references :template, null: false, foreign_key: true, index: false
|
||||||
|
t.references :account, null: false, foreign_key: true, index: true
|
||||||
|
t.references :author, null: false, foreign_key: { to_table: :users }, index: true
|
||||||
|
t.text :data, null: false
|
||||||
|
t.string :sha1, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :template_versions, %i[template_id sha1], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
+18
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2026_04_16_100000) do
|
ActiveRecord::Schema[8.1].define(version: 2026_05_06_121640) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
@@ -444,6 +444,19 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_16_100000) do
|
|||||||
t.index ["template_id"], name: "index_template_sharings_on_template_id"
|
t.index ["template_id"], name: "index_template_sharings_on_template_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "template_versions", force: :cascade do |t|
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.bigint "author_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.text "data", null: false
|
||||||
|
t.string "sha1", null: false
|
||||||
|
t.bigint "template_id", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["account_id"], name: "index_template_versions_on_account_id"
|
||||||
|
t.index ["author_id"], name: "index_template_versions_on_author_id"
|
||||||
|
t.index ["template_id", "sha1"], name: "index_template_versions_on_template_id_and_sha1", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "templates", force: :cascade do |t|
|
create_table "templates", force: :cascade do |t|
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.datetime "archived_at"
|
t.datetime "archived_at"
|
||||||
@@ -545,6 +558,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_16_100000) do
|
|||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.text "events", null: false
|
t.text "events", null: false
|
||||||
|
t.text "hmac_secret", null: false
|
||||||
t.text "secret", null: false
|
t.text "secret", null: false
|
||||||
t.string "sha1", null: false
|
t.string "sha1", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
@@ -586,6 +600,9 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_16_100000) do
|
|||||||
add_foreign_key "template_folders", "template_folders", column: "parent_folder_id"
|
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 "template_versions", "accounts"
|
||||||
|
add_foreign_key "template_versions", "templates"
|
||||||
|
add_foreign_key "template_versions", "users", column: "author_id"
|
||||||
add_foreign_key "templates", "accounts"
|
add_foreign_key "templates", "accounts"
|
||||||
add_foreign_key "templates", "template_folders", column: "folder_id"
|
add_foreign_key "templates", "template_folders", column: "folder_id"
|
||||||
add_foreign_key "templates", "users", column: "author_id"
|
add_foreign_key "templates", "users", column: "author_id"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Docuseal
|
module Docuseal
|
||||||
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
|
|
||||||
PRODUCT_URL = 'https://www.docuseal.com'
|
PRODUCT_URL = 'https://www.docuseal.com'
|
||||||
PRODUCT_EMAIL_URL = ENV.fetch('PRODUCT_EMAIL_URL', PRODUCT_URL)
|
PRODUCT_EMAIL_URL = ENV.fetch('PRODUCT_EMAIL_URL', PRODUCT_URL)
|
||||||
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ module Mcp
|
|||||||
readOnlyHint: false,
|
readOnlyHint: false,
|
||||||
destructiveHint: false,
|
destructiveHint: false,
|
||||||
idempotentHint: false,
|
idempotentHint: false,
|
||||||
openWorldHint: false
|
openWorldHint: true
|
||||||
}
|
}
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
@@ -43,6 +43,7 @@ module Mcp
|
|||||||
account:,
|
account:,
|
||||||
author: current_user,
|
author: current_user,
|
||||||
folder: account.default_template_folder,
|
folder: account.default_template_folder,
|
||||||
|
source: :mcp,
|
||||||
name: arguments['name'].to_s.presence || 'New Template',
|
name: arguments['name'].to_s.presence || 'New Template',
|
||||||
fields: [],
|
fields: [],
|
||||||
schema: []
|
schema: []
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ module Mcp
|
|||||||
},
|
},
|
||||||
annotations: {
|
annotations: {
|
||||||
readOnlyHint: false,
|
readOnlyHint: false,
|
||||||
destructiveHint: false,
|
destructiveHint: true,
|
||||||
idempotentHint: false,
|
idempotentHint: false,
|
||||||
openWorldHint: true
|
openWorldHint: true
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ module Mcp
|
|||||||
submissions = Submissions.create_from_submitters(
|
submissions = Submissions.create_from_submitters(
|
||||||
template:,
|
template:,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :mcp,
|
||||||
submitters_order: 'random',
|
submitters_order: 'random',
|
||||||
submissions_attrs: { submitters: submitters },
|
submissions_attrs: { submitters: submitters },
|
||||||
params: { 'send_email' => true, 'submitters' => submitters }
|
params: { 'send_email' => true, 'submitters' => submitters }
|
||||||
|
|||||||
+203
@@ -192,6 +192,71 @@ class Pdfium
|
|||||||
attach_function :FPDFDOC_ExitFormFillEnvironment, [:FPDF_FORMHANDLE], :void
|
attach_function :FPDFDOC_ExitFormFillEnvironment, [:FPDF_FORMHANDLE], :void
|
||||||
attach_function :FPDF_FFLDraw, %i[FPDF_FORMHANDLE FPDF_BITMAP FPDF_PAGE int int int int int int], :void
|
attach_function :FPDF_FFLDraw, %i[FPDF_FORMHANDLE FPDF_BITMAP FPDF_PAGE int int int int int int], :void
|
||||||
|
|
||||||
|
attach_function :FPDFPage_Flatten, %i[FPDF_PAGE int], :int
|
||||||
|
|
||||||
|
FLAT_NORMALDISPLAY = 0
|
||||||
|
FLAT_PRINT = 1
|
||||||
|
|
||||||
|
FLATTEN_FAIL = 0
|
||||||
|
FLATTEN_SUCCESS = 1
|
||||||
|
FLATTEN_NOTHINGTODO = 2
|
||||||
|
|
||||||
|
# rubocop:disable Naming/ClassAndModuleCamelCase
|
||||||
|
class FS_MATRIX < FFI::Struct
|
||||||
|
layout :a, :float,
|
||||||
|
:b, :float,
|
||||||
|
:c, :float,
|
||||||
|
:d, :float,
|
||||||
|
:e, :float,
|
||||||
|
:f, :float
|
||||||
|
end
|
||||||
|
# rubocop:enable Naming/ClassAndModuleCamelCase
|
||||||
|
|
||||||
|
attach_function :FPDFPage_GetRotation, [:FPDF_PAGE], :int
|
||||||
|
attach_function :FPDFPage_SetRotation, %i[FPDF_PAGE int], :void
|
||||||
|
attach_function :FPDFPage_TransFormWithClip, %i[FPDF_PAGE pointer pointer], :int
|
||||||
|
attach_function :FPDFPage_TransformAnnots, %i[FPDF_PAGE double double double double double double], :void
|
||||||
|
attach_function :FPDFPage_GenerateContent, [:FPDF_PAGE], :int
|
||||||
|
attach_function :FPDFPage_GetMediaBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFPage_SetMediaBox, %i[FPDF_PAGE float float float float], :void
|
||||||
|
attach_function :FPDFPage_GetCropBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFPage_SetCropBox, %i[FPDF_PAGE float float float float], :void
|
||||||
|
attach_function :FPDFPage_GetBleedBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFPage_SetBleedBox, %i[FPDF_PAGE float float float float], :void
|
||||||
|
attach_function :FPDFPage_GetTrimBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFPage_SetTrimBox, %i[FPDF_PAGE float float float float], :void
|
||||||
|
attach_function :FPDFPage_GetArtBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFPage_SetArtBox, %i[FPDF_PAGE float float float float], :void
|
||||||
|
|
||||||
|
PAGE_BOX_ACCESSORS = [
|
||||||
|
%i[FPDFPage_GetMediaBox FPDFPage_SetMediaBox],
|
||||||
|
%i[FPDFPage_GetCropBox FPDFPage_SetCropBox],
|
||||||
|
%i[FPDFPage_GetBleedBox FPDFPage_SetBleedBox],
|
||||||
|
%i[FPDFPage_GetTrimBox FPDFPage_SetTrimBox],
|
||||||
|
%i[FPDFPage_GetArtBox FPDFPage_SetArtBox]
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
# rubocop:disable Naming/ClassAndModuleCamelCase
|
||||||
|
class FPDF_FILEWRITE < FFI::Struct
|
||||||
|
layout :version, :int,
|
||||||
|
:WriteBlock, :pointer
|
||||||
|
end
|
||||||
|
# rubocop:enable Naming/ClassAndModuleCamelCase
|
||||||
|
|
||||||
|
attach_function :FPDF_SaveAsCopy, %i[FPDF_DOCUMENT pointer ulong], :int
|
||||||
|
|
||||||
|
FPDF_INCREMENTAL = 1
|
||||||
|
FPDF_NO_INCREMENTAL = 2
|
||||||
|
FPDF_REMOVE_SECURITY = 3
|
||||||
|
|
||||||
|
attach_function :FPDF_CreateNewDocument, [], :FPDF_DOCUMENT
|
||||||
|
|
||||||
|
begin
|
||||||
|
attach_function :FPDF_ImportPages, %i[FPDF_DOCUMENT FPDF_DOCUMENT string int], :int
|
||||||
|
rescue FFI::NotFoundError
|
||||||
|
define_singleton_method(:FPDF_ImportPages) { |*| raise PdfiumError, 'FPDF_ImportPages is not available' } # rubocop:disable Naming/MethodName
|
||||||
|
end
|
||||||
|
|
||||||
FPDF_ERR_SUCCESS = 0
|
FPDF_ERR_SUCCESS = 0
|
||||||
FPDF_ERR_UNKNOWN = 1
|
FPDF_ERR_UNKNOWN = 1
|
||||||
FPDF_ERR_FILE = 2
|
FPDF_ERR_FILE = 2
|
||||||
@@ -257,6 +322,38 @@ class Pdfium
|
|||||||
@page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr)
|
@page_count ||= Pdfium.FPDF_GetPageCount(@document_ptr)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def import_pages(src_doc)
|
||||||
|
ensure_not_closed!
|
||||||
|
|
||||||
|
result = Pdfium.FPDF_ImportPages(@document_ptr, src_doc.document_ptr, nil, page_count)
|
||||||
|
|
||||||
|
raise PdfiumError, 'Failed to import pages' if result.zero?
|
||||||
|
|
||||||
|
@page_count = nil
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create
|
||||||
|
doc_ptr = Pdfium.FPDF_CreateNewDocument()
|
||||||
|
|
||||||
|
if doc_ptr.null?
|
||||||
|
Pdfium.check_last_error('Failed to create new document')
|
||||||
|
|
||||||
|
raise PdfiumError, 'Failed to create new document'
|
||||||
|
end
|
||||||
|
|
||||||
|
doc = new(doc_ptr)
|
||||||
|
|
||||||
|
return doc unless block_given?
|
||||||
|
|
||||||
|
begin
|
||||||
|
yield doc
|
||||||
|
ensure
|
||||||
|
doc.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.open_file(file_path, password = nil)
|
def self.open_file(file_path, password = nil)
|
||||||
doc_ptr = Pdfium.FPDF_LoadDocument(file_path, password)
|
doc_ptr = Pdfium.FPDF_LoadDocument(file_path, password)
|
||||||
|
|
||||||
@@ -318,6 +415,30 @@ class Pdfium
|
|||||||
@pages[page_index] ||= Page.new(self, page_index)
|
@pages[page_index] ||= Page.new(self, page_index)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def save(io, flags: Pdfium::FPDF_NO_INCREMENTAL)
|
||||||
|
ensure_not_closed!
|
||||||
|
|
||||||
|
file_write_mem = FFI::MemoryPointer.new(FPDF_FILEWRITE.size)
|
||||||
|
|
||||||
|
file_write_struct = FPDF_FILEWRITE.new(file_write_mem)
|
||||||
|
file_write_struct[:version] = 1
|
||||||
|
file_write_struct[:WriteBlock] = FFI::Function.new(:int, %i[pointer pointer ulong]) do |_, data, size|
|
||||||
|
io.write(data.read_bytes(size))
|
||||||
|
|
||||||
|
1
|
||||||
|
end
|
||||||
|
|
||||||
|
result = Pdfium.FPDF_SaveAsCopy(@document_ptr, file_write_mem, flags)
|
||||||
|
|
||||||
|
if result.zero?
|
||||||
|
Pdfium.check_last_error('Failed to save document')
|
||||||
|
|
||||||
|
raise PdfiumError, 'Failed to save document'
|
||||||
|
end
|
||||||
|
|
||||||
|
io
|
||||||
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
return if closed?
|
return if closed?
|
||||||
|
|
||||||
@@ -680,6 +801,88 @@ class Pdfium
|
|||||||
@line_nodes = @line_nodes.sort { |a, b| a.endy == b.endy ? a.x <=> b.x : a.endy <=> b.endy }
|
@line_nodes = @line_nodes.sort { |a, b| a.endy == b.endy ? a.x <=> b.x : a.endy <=> b.endy }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rotate
|
||||||
|
ensure_not_closed!
|
||||||
|
|
||||||
|
rotation = Pdfium.FPDFPage_GetRotation(page_ptr)
|
||||||
|
|
||||||
|
return false if rotation.zero?
|
||||||
|
|
||||||
|
l_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
b_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
r_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
t_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
|
||||||
|
has_crop = !Pdfium.FPDFPage_GetCropBox(page_ptr, l_ptr, b_ptr, r_ptr, t_ptr).zero?
|
||||||
|
Pdfium.FPDFPage_GetMediaBox(page_ptr, l_ptr, b_ptr, r_ptr, t_ptr) unless has_crop
|
||||||
|
|
||||||
|
pl = l_ptr.read_float
|
||||||
|
pb = b_ptr.read_float
|
||||||
|
pr = r_ptr.read_float
|
||||||
|
pt = t_ptr.read_float
|
||||||
|
|
||||||
|
a, b, c, d, e, f =
|
||||||
|
case rotation
|
||||||
|
when 1 then [0, -1, 1, 0, -pb, pr]
|
||||||
|
when 2 then [-1, 0, 0, -1, pr, pt]
|
||||||
|
when 3 then [0, 1, -1, 0, pt, -pl]
|
||||||
|
end
|
||||||
|
|
||||||
|
Pdfium::PAGE_BOX_ACCESSORS.each do |getter, setter|
|
||||||
|
next if Pdfium.public_send(getter, page_ptr, l_ptr, b_ptr, r_ptr, t_ptr).zero?
|
||||||
|
|
||||||
|
bl = l_ptr.read_float
|
||||||
|
bb = b_ptr.read_float
|
||||||
|
br = r_ptr.read_float
|
||||||
|
bt = t_ptr.read_float
|
||||||
|
|
||||||
|
c1x, c1y, c2x, c2y =
|
||||||
|
case rotation
|
||||||
|
when 1 then [br, bb, bl, bt]
|
||||||
|
when 2 then [br, bt, bl, bb]
|
||||||
|
when 3 then [bl, bt, br, bb]
|
||||||
|
end
|
||||||
|
|
||||||
|
new_llx = (a * c1x) + (c * c1y) + e
|
||||||
|
new_lly = (b * c1x) + (d * c1y) + f
|
||||||
|
new_urx = (a * c2x) + (c * c2y) + e
|
||||||
|
new_ury = (b * c2x) + (d * c2y) + f
|
||||||
|
|
||||||
|
Pdfium.public_send(setter, page_ptr, new_llx, new_lly, new_urx, new_ury)
|
||||||
|
end
|
||||||
|
|
||||||
|
Pdfium.FPDFPage_TransformAnnots(page_ptr, a, b, c, d, e, f)
|
||||||
|
|
||||||
|
matrix_ptr = FFI::MemoryPointer.new(FS_MATRIX.size)
|
||||||
|
matrix_struct = FS_MATRIX.new(matrix_ptr)
|
||||||
|
matrix_struct[:a] = a
|
||||||
|
matrix_struct[:b] = b
|
||||||
|
matrix_struct[:c] = c
|
||||||
|
matrix_struct[:d] = d
|
||||||
|
matrix_struct[:e] = e
|
||||||
|
matrix_struct[:f] = f
|
||||||
|
|
||||||
|
Pdfium.FPDFPage_TransFormWithClip(page_ptr, matrix_ptr, FFI::Pointer::NULL)
|
||||||
|
Pdfium.FPDFPage_SetRotation(page_ptr, 0)
|
||||||
|
Pdfium.FPDFPage_GenerateContent(page_ptr)
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def flatten(flag = Pdfium::FLAT_NORMALDISPLAY)
|
||||||
|
ensure_not_closed!
|
||||||
|
|
||||||
|
result = Pdfium.FPDFPage_Flatten(page_ptr, flag)
|
||||||
|
|
||||||
|
if result == Pdfium::FLATTEN_FAIL
|
||||||
|
Pdfium.check_last_error("Failed to flatten page #{page_index}")
|
||||||
|
|
||||||
|
raise PdfiumError, "Failed to flatten page #{page_index}"
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
return if closed?
|
return if closed?
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Puma::Plugin.create do
|
|||||||
Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty?
|
Dir.chdir(ENV.fetch('WORKDIR', nil)) unless ENV['WORKDIR'].to_s.empty?
|
||||||
|
|
||||||
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
|
exec('redis-server', '--requirepass', Digest::SHA1.hexdigest("redis#{ENV.fetch('SECRET_KEY_BASE', '')}"),
|
||||||
out: '/dev/null')
|
'--loglevel', 'warning')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ module SearchEntries
|
|||||||
end
|
end
|
||||||
|
|
||||||
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
|
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
|
||||||
elsif keyword.match?(/[^\p{L}\d&@.-]/) || keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.-]{2,}/)
|
elsif keyword.start_with?('@') || keyword.match?(/[^\p{L}\d&@.-]/) ||
|
||||||
|
keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.-]{2,}/)
|
||||||
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
|
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
|
||||||
else
|
else
|
||||||
keyword = TextUtils.transliterate(keyword.downcase).squish
|
keyword = TextUtils.transliterate(keyword.downcase).squish
|
||||||
|
|||||||
@@ -15,11 +15,7 @@ module SendWebhookRequest
|
|||||||
|
|
||||||
# rubocop:disable Metrics/AbcSize
|
# rubocop:disable Metrics/AbcSize
|
||||||
def call(webhook_url, event_uuid:, event_type:, record:, data:, attempt: 0)
|
def call(webhook_url, event_uuid:, event_type:, record:, data:, attempt: 0)
|
||||||
uri = begin
|
uri = parse_uri(webhook_url.url)
|
||||||
URI(webhook_url.url)
|
|
||||||
rescue URI::Error
|
|
||||||
Addressable::URI.parse(webhook_url.url).normalize
|
|
||||||
end
|
|
||||||
|
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || [443, nil].exclude?(uri.port)) &&
|
raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || [443, nil].exclude?(uri.port)) &&
|
||||||
@@ -43,6 +39,8 @@ module SendWebhookRequest
|
|||||||
data: data
|
data: data
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
|
req.headers['X-Docuseal-Signature'] = WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: req.body)
|
||||||
|
|
||||||
req.options.read_timeout = 15
|
req.options.read_timeout = 15
|
||||||
req.options.open_timeout = 8
|
req.options.open_timeout = 8
|
||||||
end
|
end
|
||||||
@@ -55,6 +53,12 @@ module SendWebhookRequest
|
|||||||
end
|
end
|
||||||
# rubocop:enable Metrics/AbcSize
|
# rubocop:enable Metrics/AbcSize
|
||||||
|
|
||||||
|
def parse_uri(url)
|
||||||
|
URI(url)
|
||||||
|
rescue URI::Error
|
||||||
|
Addressable::URI.parse(url).normalize
|
||||||
|
end
|
||||||
|
|
||||||
def create_webhook_event(webhook_url, event_uuid:, event_type:, record:)
|
def create_webhook_event(webhook_url, event_uuid:, event_type:, record:)
|
||||||
return if event_uuid.blank?
|
return if event_uuid.blank?
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -162,7 +162,7 @@ module Submissions
|
|||||||
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
|
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
|
||||||
|
|
||||||
return email.downcase if email.include?(',') ||
|
return email.downcase if email.include?(',') ||
|
||||||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie)\z/) ||
|
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) ||
|
||||||
email.exclude?('.')
|
email.exclude?('.')
|
||||||
|
|
||||||
fixed_email = EmailTypo.call(email.delete_prefix('<'))
|
fixed_email = EmailTypo.call(email.delete_prefix('<'))
|
||||||
|
|||||||
@@ -410,9 +410,7 @@ module Submissions
|
|||||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.values = submitter.values.transform_values do |v|
|
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
|
||||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.submission.account.timezone).to_date.to_s : v
|
|
||||||
end
|
|
||||||
|
|
||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -415,7 +415,8 @@ module Submissions
|
|||||||
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
||||||
else
|
else
|
||||||
if field['type'] == 'date'
|
if field['type'] == 'date'
|
||||||
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
|
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale,
|
||||||
|
timezone:)
|
||||||
end
|
end
|
||||||
|
|
||||||
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
|
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
|
||||||
|
|||||||
@@ -16,18 +16,21 @@ module Submissions
|
|||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||||
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
||||||
|
AccountConfig::ROTATE_INCREMENTAL_PDF_KEY,
|
||||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID_REASON_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
|
||||||
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
|
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.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
|
||||||
|
is_rotate_incremental = configs.find { |c| c.key == AccountConfig::ROTATE_INCREMENTAL_PDF_KEY }&.value == true
|
||||||
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
||||||
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
|
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
|
||||||
with_signature_id_reason =
|
with_signature_id_reason =
|
||||||
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
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,
|
||||||
|
incremental: is_rotate_incremental)
|
||||||
|
|
||||||
submitters = if submitter
|
submitters = if submitter
|
||||||
submission.submitters.where(id: submitter.id)
|
submission.submitters.where(id: submitter.id)
|
||||||
|
|||||||
@@ -141,18 +141,21 @@ module Submissions
|
|||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||||
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
||||||
|
AccountConfig::ROTATE_INCREMENTAL_PDF_KEY,
|
||||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID_REASON_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
|
||||||
|
is_rotate_incremental = configs.find { |c| c.key == AccountConfig::ROTATE_INCREMENTAL_PDF_KEY }&.value == true
|
||||||
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
|
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
|
||||||
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
||||||
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
|
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
|
||||||
with_signature_id_reason =
|
with_signature_id_reason =
|
||||||
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
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,
|
||||||
|
incremental: is_rotate_incremental)
|
||||||
|
|
||||||
if with_signature_id || submitter.account.testing?
|
if with_signature_id || submitter.account.testing?
|
||||||
pdfs_index.each_value do |pdf|
|
pdfs_index.each_value do |pdf|
|
||||||
@@ -649,7 +652,10 @@ module Submissions
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
if field['type'] == 'date'
|
if field['type'] == 'date'
|
||||||
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale)
|
timezone = submitter.account.timezone
|
||||||
|
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
|
||||||
|
|
||||||
|
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale, timezone:)
|
||||||
end
|
end
|
||||||
|
|
||||||
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
|
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
|
||||||
@@ -802,7 +808,7 @@ module Submissions
|
|||||||
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
|
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_pdfs_index(submission, submitter: nil, flatten: true)
|
def build_pdfs_index(submission, submitter: nil, flatten: true, incremental: false)
|
||||||
latest_submitter = find_last_submitter(submission, submitter:)
|
latest_submitter = find_last_submitter(submission, submitter:)
|
||||||
|
|
||||||
documents = Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
documents = Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||||
@@ -826,7 +832,7 @@ module Submissions
|
|||||||
HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
||||||
end
|
end
|
||||||
|
|
||||||
pdf = maybe_rotate_pdf(pdf)
|
pdf = maybe_rotate_pdf(pdf, incremental:)
|
||||||
|
|
||||||
maybe_flatten_pdf(pdf) if flatten
|
maybe_flatten_pdf(pdf) if flatten
|
||||||
|
|
||||||
@@ -845,7 +851,7 @@ module Submissions
|
|||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_rotate_pdf(pdf)
|
def maybe_rotate_pdf(pdf, incremental: false)
|
||||||
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
|
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
|
||||||
|
|
||||||
is_pages_rotated = pdf.pages.root[:Rotate].present? && pdf.pages.root[:Rotate] != 0
|
is_pages_rotated = pdf.pages.root[:Rotate].present? && pdf.pages.root[:Rotate] != 0
|
||||||
@@ -860,7 +866,7 @@ module Submissions
|
|||||||
|
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
pdf.write(io, incremental: false, validate: false)
|
pdf.write(io, incremental:, validate: false)
|
||||||
|
|
||||||
HexaPDF::Document.new(io:)
|
HexaPDF::Document.new(io:)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
@@ -869,6 +875,50 @@ module Submissions
|
|||||||
pdf
|
pdf
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_rotate_pdfium(io)
|
||||||
|
pdf = HexaPDF::Document.new(io:)
|
||||||
|
|
||||||
|
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
|
||||||
|
|
||||||
|
root_rotate = pdf.pages.root[:Rotate].to_i
|
||||||
|
|
||||||
|
rotated_indexes = pdf.pages.each_with_index.filter_map do |page, idx|
|
||||||
|
page_rotate = page[:Rotate]
|
||||||
|
|
||||||
|
effective = page_rotate.nil? ? root_rotate : page_rotate.to_i
|
||||||
|
|
||||||
|
idx if effective != 0
|
||||||
|
end
|
||||||
|
|
||||||
|
return pdf if rotated_indexes.blank?
|
||||||
|
|
||||||
|
has_widgets = pdf.acro_form && pdf.acro_form[:Fields].present?
|
||||||
|
|
||||||
|
io.rewind
|
||||||
|
out_io = StringIO.new
|
||||||
|
|
||||||
|
Pdfium::Document.open_bytes(io.string) do |doc|
|
||||||
|
rotated_indexes.each do |idx|
|
||||||
|
page = doc.get_page(idx)
|
||||||
|
page.flatten if has_widgets
|
||||||
|
page.rotate
|
||||||
|
end
|
||||||
|
|
||||||
|
doc.save(out_io)
|
||||||
|
end
|
||||||
|
|
||||||
|
pdf = HexaPDF::Document.new(io: out_io.tap(&:rewind))
|
||||||
|
pdf.pages.root[:Rotate] = 0
|
||||||
|
|
||||||
|
pdf
|
||||||
|
rescue StandardError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
io.rewind
|
||||||
|
|
||||||
|
HexaPDF::Document.new(io:)
|
||||||
|
end
|
||||||
|
|
||||||
def on_missing_glyph(character, font_wrapper)
|
def on_missing_glyph(character, font_wrapper)
|
||||||
Rails.logger.info("Missing glyph: #{character}") if character.present? && defined?(Rollbar)
|
Rails.logger.info("Missing glyph: #{character}") if character.present? && defined?(Rollbar)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ module Submitters
|
|||||||
UnableToSendCode = Class.new(StandardError)
|
UnableToSendCode = Class.new(StandardError)
|
||||||
InvalidOtp = Class.new(StandardError)
|
InvalidOtp = Class.new(StandardError)
|
||||||
MaliciousFileExtension = Class.new(StandardError)
|
MaliciousFileExtension = Class.new(StandardError)
|
||||||
ArgumentError = Class.new(StandardError)
|
ParamsError = Class.new(StandardError)
|
||||||
|
|
||||||
DANGEROUS_EXTENSIONS = Set.new(%w[
|
DANGEROUS_EXTENSIONS = Set.new(%w[
|
||||||
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
|
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
|
||||||
@@ -135,7 +135,7 @@ module Submitters
|
|||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
content_type: file.content_type)
|
content_type: file.content_type)
|
||||||
else
|
else
|
||||||
raise ArgumentError, 'file param is missing'
|
raise ParamsError, 'file param is missing'
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
|
|||||||
@@ -103,17 +103,43 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def normalize_date(field, value)
|
def normalize_date(field, value)
|
||||||
if value.is_a?(Integer)
|
format = field.dig('preferences', 'format')
|
||||||
|
|
||||||
|
if TimeUtils.format_with_time?(format)
|
||||||
|
normalize_date_time(value, format)
|
||||||
|
elsif TimeUtils.month_only_format?(format)
|
||||||
|
normalize_date_month(value, format)
|
||||||
|
elsif value.is_a?(Integer)
|
||||||
Time.zone.at(value.to_s.first(10).to_i).to_date.to_s
|
Time.zone.at(value.to_s.first(10).to_i).to_date.to_s
|
||||||
elsif value.gsub(/\w/, '0') == field.dig('preferences', 'format').to_s.gsub(/\w/, '0')
|
elsif value.gsub(/\w/, '0') == format.to_s.gsub(/\w/, '0')
|
||||||
TimeUtils.parse_date_string(value, field.dig('preferences', 'format')).to_s
|
TimeUtils.parse_date_string(value, format).to_s
|
||||||
else
|
else
|
||||||
Date.parse(value).to_s
|
Date.parse(value).to_s
|
||||||
end
|
end
|
||||||
rescue Date::Error
|
rescue ArgumentError
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_date_time(value, format)
|
||||||
|
if value.is_a?(Integer)
|
||||||
|
Time.zone.at(value.to_s.first(10).to_i).utc.iso8601
|
||||||
|
elsif value.to_s.match?(/T\d{2}:\d{2}/)
|
||||||
|
Time.iso8601(value).utc.iso8601
|
||||||
|
else
|
||||||
|
TimeUtils.parse_date_string(value, format).utc.iso8601
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_date_month(value, format)
|
||||||
|
if value.is_a?(Integer)
|
||||||
|
Time.zone.at(value.to_s.first(10).to_i).strftime('%Y-%m')
|
||||||
|
elsif value.to_s.match?(/\A\d{4}-\d{2}\z/)
|
||||||
|
value
|
||||||
|
else
|
||||||
|
TimeUtils.parse_date_string(value, format).strftime('%Y-%m')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
||||||
if submitter_name && !for_submitter
|
if submitter_name && !for_submitter
|
||||||
submitter =
|
submitter =
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ module Submitters
|
|||||||
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
|
submitter.values = maybe_remove_condition_values(submitter, required_field_uuids_acc:)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.values = submitter.values.transform_values do |v|
|
submitter.values = replace_current_date_placeholders(submitter)
|
||||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
|
||||||
end
|
|
||||||
|
|
||||||
required_field_uuids_acc.each do |uuid|
|
required_field_uuids_acc.each do |uuid|
|
||||||
next if submitter.values[uuid].present?
|
next if submitter.values[uuid].present?
|
||||||
@@ -194,7 +192,7 @@ module Submitters
|
|||||||
|
|
||||||
next if value.blank?
|
next if value.blank?
|
||||||
|
|
||||||
acc[field['uuid']] = template_default_value_for_submitter(value, submitter, with_time: true)
|
acc[field['uuid']] = template_default_value_for_submitter(value, submitter, field:, with_time: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
default_values.compact_blank.merge(submitter.values)
|
default_values.compact_blank.merge(submitter.values)
|
||||||
@@ -248,7 +246,20 @@ module Submitters
|
|||||||
0
|
0
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_default_value_for_submitter(value, submitter, with_time: false)
|
def replace_current_date_placeholders(submitter)
|
||||||
|
submitter.values.each_with_object({}) do |(uuid, v), acc|
|
||||||
|
acc[uuid] =
|
||||||
|
if v == '{{date}}'
|
||||||
|
field = submitter.submission.fields_uuid_index[uuid]
|
||||||
|
|
||||||
|
TimeUtils.current_date_value(field&.dig('preferences', 'format'), submitter.account.timezone)
|
||||||
|
else
|
||||||
|
v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_default_value_for_submitter(value, submitter, with_time: false, field: nil)
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
return if submitter.blank?
|
return if submitter.blank?
|
||||||
|
|
||||||
@@ -257,7 +268,8 @@ module Submitters
|
|||||||
replace_default_variables(value,
|
replace_default_variables(value,
|
||||||
submitter.attributes.merge('role' => role),
|
submitter.attributes.merge('role' => role),
|
||||||
submitter.submission,
|
submitter.submission,
|
||||||
with_time:)
|
with_time:,
|
||||||
|
field:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
|
def maybe_remove_condition_values(submitter, required_field_uuids_acc: nil)
|
||||||
@@ -394,7 +406,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def replace_default_variables(value, attrs, submission, with_time: false)
|
def replace_default_variables(value, attrs, submission, with_time: false, field: nil)
|
||||||
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
|
|
||||||
@@ -412,7 +424,7 @@ module Submitters
|
|||||||
when 'hour', 'minute', 'day', 'month', 'year'
|
when 'hour', 'minute', 'day', 'month', 'year'
|
||||||
with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e
|
with_time ? Time.current.in_time_zone(submission.account.timezone).strftime(STRFTIME_MAP[key]) : e
|
||||||
when 'date'
|
when 'date'
|
||||||
with_time ? Time.current.in_time_zone(submission.account.timezone).to_date.to_s : e
|
with_time ? TimeUtils.current_date_value(field&.dig('preferences', 'format'), submission.account.timezone) : e
|
||||||
when 'role', 'email', 'phone', 'name'
|
when 'role', 'email', 'phone', 'name'
|
||||||
attrs[key] || e
|
attrs[key] || e
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module TemplateVersions
|
||||||
|
SERIALIZE_PARAMS = {
|
||||||
|
only: %i[id created_at],
|
||||||
|
include: { author: { only: %i[email], methods: %i[full_name] } }
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
DATA_FIELDS = %i[name schema submitters variables_schema fields].freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def find_or_create_for(template, author:)
|
||||||
|
data = build_data(template)
|
||||||
|
sha1 = Digest::SHA1.hexdigest(data.to_json)
|
||||||
|
|
||||||
|
version = template.template_versions.find_by(sha1:)
|
||||||
|
version ||= template.template_versions.create!(data:, sha1:, author:)
|
||||||
|
|
||||||
|
version
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize(version)
|
||||||
|
data = version.data.dup
|
||||||
|
|
||||||
|
data['documents'] = serialize_documents(version.template, data['schema'].to_a)
|
||||||
|
data['dynamic_documents'] = serialize_dynamic_documents(version.template, data['dynamic_documents'].to_a)
|
||||||
|
|
||||||
|
version.as_json(SERIALIZE_PARAMS).merge('data' => data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_data(template)
|
||||||
|
dynamic_uuids = template.schema.select { |e| e['dynamic'] }.pluck('attachment_uuid')
|
||||||
|
|
||||||
|
dynamic_documents =
|
||||||
|
if dynamic_uuids.present?
|
||||||
|
template.dynamic_documents.where(uuid: dynamic_uuids).as_json(only: %i[uuid body])
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
template.as_json(only: DATA_FIELDS).merge('dynamic_documents' => dynamic_documents)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_documents(template, schema)
|
||||||
|
return [] if schema.blank?
|
||||||
|
|
||||||
|
template.documents_attachments
|
||||||
|
.where(uuid: schema.pluck('attachment_uuid'))
|
||||||
|
.preload(:blob, preview_images_attachments: :blob)
|
||||||
|
.as_json(
|
||||||
|
only: %i[id uuid],
|
||||||
|
methods: %i[metadata signed_key],
|
||||||
|
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_dynamic_documents(template, dynamic_docs)
|
||||||
|
return [] if dynamic_docs.blank?
|
||||||
|
|
||||||
|
dynamic_docs_index = template.dynamic_documents
|
||||||
|
.where(uuid: dynamic_docs.pluck('uuid'))
|
||||||
|
.preload(attachments_attachments: :blob)
|
||||||
|
.index_by(&:uuid)
|
||||||
|
|
||||||
|
dynamic_docs.map do |attrs|
|
||||||
|
document = dynamic_docs_index[attrs['uuid']]
|
||||||
|
|
||||||
|
attachments_data = document.attachments_attachments.as_json(only: %i[uuid], methods: %i[url metadata filename])
|
||||||
|
|
||||||
|
attrs.merge('head' => document.head, 'attachments' => attachments_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Templates
|
module Templates
|
||||||
|
COLOR_REGEXP = /\A(#(?:[0-9a-f]{3}|[0-9a-f]{6})|[a-z]+)\z/i
|
||||||
|
|
||||||
EXPIRATION_DURATIONS = {
|
EXPIRATION_DURATIONS = {
|
||||||
one_day: 1.day,
|
one_day: 1.day,
|
||||||
two_days: 2.days,
|
two_days: 2.days,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module Templates
|
|||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
|
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
|
||||||
template = original_template.account.templates.new
|
template = author.account.templates.new
|
||||||
|
|
||||||
template.external_id = external_id
|
template.external_id = external_id
|
||||||
template.shared_link = original_template.shared_link
|
template.shared_link = original_template.shared_link
|
||||||
@@ -16,8 +16,10 @@ module Templates
|
|||||||
|
|
||||||
if folder_name.present?
|
if folder_name.present?
|
||||||
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name)
|
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name)
|
||||||
else
|
elsif author.account_id == original_template.account_id
|
||||||
template.folder_id = original_template.folder_id
|
template.folder_id = original_template.folder_id
|
||||||
|
else
|
||||||
|
template.folder = author.account.default_template_folder
|
||||||
end
|
end
|
||||||
|
|
||||||
template.submitters, template.fields, template.schema, template.preferences =
|
template.submitters, template.fields, template.schema, template.preferences =
|
||||||
|
|||||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user