mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13fa87c449 | |||
| 246ccfb049 | |||
| cbffb1e5b8 | |||
| c4b007411d | |||
| 04264fe937 | |||
| 0c37ad12b7 | |||
| 8c5298ed2e | |||
| 262118e047 | |||
| e5b63ea2b0 | |||
| 746757d402 | |||
| e6e640328b | |||
| 845782a69c | |||
| fc6baa1b3b | |||
| 47822ecc15 | |||
| 3d0c7f1118 | |||
| e9c8e4d325 | |||
| 39cc82ce0c | |||
| 451f83421a | |||
| 5073f2eefb | |||
| 231fff5508 | |||
| f1d146eca3 | |||
| b6635fcc4f | |||
| fb5e13ee4c | |||
| ba84741a64 | |||
| 65c275ac17 | |||
| 825322d489 | |||
| 739e2abdf8 | |||
| 1d2394e31e | |||
| 1b41af798d | |||
| 118f4a231b | |||
| e48652f425 | |||
| 848f01edf8 | |||
| 43fbc42770 | |||
| 2c736a0eed | |||
| 33ca930055 | |||
| 3b396f8421 | |||
| aa77e3a8d3 | |||
| 75316d8d87 | |||
| a999109a5c | |||
| bdd33c7d6b | |||
| 61c5ee22a0 | |||
| 12c5b909e0 | |||
| 871ef6dda6 | |||
| 40052a2d7c | |||
| 347be0137d | |||
| fe6baba8bf | |||
| ed8c313bd4 | |||
| ca0acb34d6 | |||
| 34ea639c25 | |||
| 680ab9dbed | |||
| 9377766e52 | |||
| 7a2c37454e | |||
| 8f6f418f54 | |||
| 9b7745c565 |
+8
-1
@@ -84,7 +84,7 @@ RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 5
|
||||
Max: 6
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
@@ -106,3 +106,10 @@ Rails/StrongParametersExpect:
|
||||
|
||||
Rails/RedirectBackOrTo:
|
||||
Enabled: false
|
||||
|
||||
Rails/UnknownEnv:
|
||||
Environments:
|
||||
- development
|
||||
- test
|
||||
- production
|
||||
- local
|
||||
|
||||
@@ -48,6 +48,7 @@ ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
ENV VIPS_MAX_COORD=10000
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ gem 'pretender'
|
||||
gem 'puma', require: false
|
||||
gem 'rack'
|
||||
gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rotp'
|
||||
gem 'rouge', require: false
|
||||
|
||||
+6
-15
@@ -219,7 +219,6 @@ GEM
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (~> 0.7)
|
||||
ffi (1.17.3)
|
||||
ffi (1.17.3-aarch64-linux-gnu)
|
||||
ffi (1.17.3-aarch64-linux-musl)
|
||||
ffi (1.17.3-arm64-darwin)
|
||||
@@ -338,22 +337,20 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||
nokogiri (1.19.1-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-aarch64-linux-musl)
|
||||
nokogiri (1.19.1-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm64-darwin)
|
||||
nokogiri (1.19.1-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
nokogiri (1.19.1-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
nokogiri (1.19.1-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
numo-narray-alt (0.9.13)
|
||||
oj (3.16.13)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
onnxruntime (0.10.1)
|
||||
ffi
|
||||
onnxruntime (0.10.1-aarch64-linux)
|
||||
ffi
|
||||
onnxruntime (0.10.1-arm64-darwin)
|
||||
@@ -372,7 +369,6 @@ GEM
|
||||
parser (3.3.10.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.6.3)
|
||||
pg (1.6.3-aarch64-linux)
|
||||
pg (1.6.3-aarch64-linux-musl)
|
||||
pg (1.6.3-arm64-darwin)
|
||||
@@ -405,7 +401,7 @@ GEM
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack (3.2.5)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.1.1)
|
||||
@@ -439,10 +435,6 @@ GEM
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
@@ -662,7 +654,6 @@ DEPENDENCIES
|
||||
rack
|
||||
rails
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rotp
|
||||
rouge
|
||||
rqrcode
|
||||
|
||||
@@ -172,7 +172,10 @@ module Api
|
||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||
|
||||
submitters.each do |submitter|
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
||||
if submitter.completed_at?
|
||||
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
|
||||
end
|
||||
end
|
||||
|
||||
submissions
|
||||
|
||||
@@ -34,6 +34,7 @@ module Api
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def update
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||
@@ -60,7 +61,10 @@ module Api
|
||||
|
||||
@submitter.submission.save!
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
||||
if @submitter.completed_at?
|
||||
Submitters::SubmitValues.maybe_invite_via_field(@submitter, request)
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request)
|
||||
end
|
||||
end
|
||||
|
||||
if @submitter.completed_at?
|
||||
@@ -78,6 +82,7 @@ module Api
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def submitter_params
|
||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||
|
||||
@@ -107,7 +107,8 @@ module Api
|
||||
:external_id,
|
||||
:shared_link,
|
||||
{
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid
|
||||
optional_invite_by_uuid linked_to_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
|
||||
@@ -42,10 +42,6 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
if request.domain == 'docuseal.com'
|
||||
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
||||
end
|
||||
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
|
||||
|
||||
@@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
return head :not_found
|
||||
if last_submitter.completed_at < TTL.ago
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
url = build_combined_url(@submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
respond_with_combined(last_submitter)
|
||||
else
|
||||
render json: build_urls(last_submitter)
|
||||
end
|
||||
@@ -48,8 +46,18 @@ class SubmissionsDownloadController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def respond_with_combined(submitter)
|
||||
url = build_combined_url(submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def current_user_submitter?(submitter)
|
||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
||||
current_user && current_ability.can?(:read, submitter)
|
||||
end
|
||||
|
||||
def build_urls(submitter)
|
||||
@@ -57,7 +65,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(
|
||||
ActiveStorage::Blob.proxy_path(
|
||||
attachment.blob,
|
||||
expires_at: FILES_TTL.from_now.to_i,
|
||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||
@@ -75,7 +83,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
ActiveStorage::Blob.proxy_url(
|
||||
ActiveStorage::Blob.proxy_path(
|
||||
attachment.blob,
|
||||
expires_at: FILES_TTL.from_now.to_i,
|
||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||
|
||||
@@ -9,15 +9,15 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
before_action :load_submitter, only: %i[show update completed]
|
||||
before_action :maybe_render_locked_page, only: :show
|
||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
||||
before_action :maybe_require_link_2fa, only: %i[show]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
submission = @submitter.submission
|
||||
|
||||
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :email_2fa if require_email_2fa?(@submitter)
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
|
||||
@@ -48,7 +48,7 @@ class SubmitFormController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
if require_email_2fa?(@submitter)
|
||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
@@ -84,7 +84,9 @@ class SubmitFormController < ApplicationController
|
||||
def completed
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def success; end
|
||||
@@ -92,10 +94,7 @@ class SubmitFormController < ApplicationController
|
||||
private
|
||||
|
||||
def maybe_require_link_2fa
|
||||
return if @submitter.submission.source != 'link'
|
||||
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
||||
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
||||
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
|
||||
|
||||
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||
end
|
||||
@@ -117,12 +116,4 @@ class SubmitFormController < ApplicationController
|
||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
end
|
||||
|
||||
def require_email_2fa?(submitter)
|
||||
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||
submitter.preferences['require_email_2fa'] != true
|
||||
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,9 @@ class SubmitFormDeclineController < ApplicationController
|
||||
submitter.completed_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template&.archived_at?
|
||||
submitter.submission.template&.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(submitter, current_user,
|
||||
request)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
submitter.update!(declined_at: Time.current)
|
||||
|
||||
@@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
value: false)
|
||||
value: false) ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
last_completed_submitter = @submitter.submission.submitters
|
||||
.where.not(id: @submitter.id)
|
||||
@@ -32,7 +33,7 @@ class SubmitFormDownloadController < ApplicationController
|
||||
end
|
||||
|
||||
urls = attachments.map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
end
|
||||
|
||||
render json: urls
|
||||
|
||||
@@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ class SubmitFormInviteController < ApplicationController
|
||||
next unless attrs
|
||||
next if attrs[:email].blank?
|
||||
|
||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
||||
email = Submissions.normalize_email(attrs[:email])
|
||||
|
||||
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 })
|
||||
end
|
||||
@@ -45,7 +47,8 @@ class SubmitFormInviteController < ApplicationController
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
!submitter.submission.template&.archived_at? &&
|
||||
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
end
|
||||
|
||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||
|
||||
@@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||
return render json: {} if submitter.submission.template&.archived_at? ||
|
||||
return render json: {} if submitter.completed_at? ||
|
||||
submitter.declined_at? ||
|
||||
submitter.submission.template&.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired?
|
||||
submitter.submission.expired? ||
|
||||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
|
||||
value = submitter.values[params['field_uuid']]
|
||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
|
||||
@@ -6,7 +6,7 @@ class TemplateDocumentsController < ApplicationController
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||
end
|
||||
|
||||
def create
|
||||
|
||||
@@ -97,7 +97,8 @@ class TemplatesController < ApplicationController
|
||||
:name,
|
||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
||||
{ conditions: [%i[field_uuid value action operation]] }]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
|
||||
invite_by_uuid optional_invite_by_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
|
||||
@@ -22,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
|
||||
def submitters_params
|
||||
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
||||
invite_by_uuid linked_to_uuid email option order]] }
|
||||
invite_by_uuid invite_via_field_uuid linked_to_uuid email option order]] }
|
||||
|
||||
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
||||
next if s[:uuid].blank?
|
||||
@@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
s[:order] = s[:order].to_i if s[:order].present?
|
||||
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
||||
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
||||
s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank?
|
||||
|
||||
normalize_option_value(s)
|
||||
end
|
||||
@@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
attrs.delete(:email)
|
||||
attrs.delete(:linked_to_uuid)
|
||||
attrs.delete(:invite_by_uuid)
|
||||
attrs.delete(:invite_via_field_uuid)
|
||||
attrs.delete(:optional_invite_by_uuid)
|
||||
when /\Alinked_to_(.*)\z/
|
||||
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
|
||||
|
||||
@@ -56,7 +56,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
def create_file_params_from_url
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
||||
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
|
||||
tempfile.rewind
|
||||
|
||||
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
||||
|
||||
@@ -40,6 +40,7 @@ import DashboardDropzone from './elements/dashboard_dropzone'
|
||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||
import PageContainer from './elements/page_container'
|
||||
import EmailEditor from './elements/email_editor'
|
||||
import MarkdownEditor from './elements/markdown_editor'
|
||||
import MountOnClick from './elements/mount_on_click'
|
||||
import RemoveOnEvent from './elements/remove_on_event'
|
||||
import ScrollTo from './elements/scroll_to'
|
||||
@@ -131,6 +132,7 @@ safeRegisterElement('check-on-click', CheckOnClick)
|
||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('email-editor', EmailEditor)
|
||||
safeRegisterElement('markdown-editor', MarkdownEditor)
|
||||
safeRegisterElement('mount-on-click', MountOnClick)
|
||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||
safeRegisterElement('scroll-to', ScrollTo)
|
||||
|
||||
@@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled {
|
||||
.font-courier {
|
||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||
}
|
||||
|
||||
markdown-editor [contenteditable] p {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
import { actionable } from '@github/catalyst/lib/actionable'
|
||||
|
||||
function loadTiptap () {
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
|
||||
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
|
||||
]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({
|
||||
Editor: core.Editor,
|
||||
Extension: core.Extension,
|
||||
Bold: bold.default || bold,
|
||||
Italic: italic.default || italic,
|
||||
Paragraph: paragraph.default || paragraph,
|
||||
Text: text.default || text,
|
||||
HardBreak: hardBreak.default || hardBreak,
|
||||
Document: document.default || document,
|
||||
Link: link.default || link,
|
||||
Underline: underline.default || underline,
|
||||
UndoRedo: extensions.UndoRedo,
|
||||
Markdown: markdown.Markdown,
|
||||
Plugin: pmState.Plugin,
|
||||
Decoration: pmView.Decoration,
|
||||
DecorationSet: pmView.DecorationSet
|
||||
}))
|
||||
}
|
||||
|
||||
class LinkTooltip {
|
||||
constructor (container, editor) {
|
||||
this.container = container
|
||||
this.editor = editor
|
||||
|
||||
const template = document.createElement('template')
|
||||
|
||||
template.innerHTML = container.dataset.linkTooltipHtml
|
||||
|
||||
this.tooltip = template.content.firstElementChild
|
||||
|
||||
this.input = this.tooltip.querySelector('input')
|
||||
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
|
||||
this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]')
|
||||
|
||||
container.style.position = 'relative'
|
||||
container.appendChild(this.tooltip)
|
||||
}
|
||||
|
||||
isVisible () {
|
||||
return !this.tooltip.classList.contains('hidden')
|
||||
}
|
||||
|
||||
normalizeUrl (url) {
|
||||
if (!url) return url
|
||||
if (/^{/i.test(url)) return url
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
if (/^mailto:/i.test(url)) return url
|
||||
|
||||
return `https://${url}`
|
||||
}
|
||||
|
||||
show (url, pos, { focus = false } = {}) {
|
||||
this.input.value = url || ''
|
||||
this.removeButton.classList.toggle('hidden', !url)
|
||||
|
||||
this.tooltip.classList.remove('hidden')
|
||||
|
||||
const coords = this.editor.view.coordsAtPos(pos)
|
||||
const containerRect = this.container.getBoundingClientRect()
|
||||
|
||||
this.tooltip.style.left = `${coords.left - containerRect.left}px`
|
||||
this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px`
|
||||
|
||||
if (focus) this.input.focus()
|
||||
|
||||
this.saveHandler = () => {
|
||||
const inputUrl = this.input.value.trim()
|
||||
|
||||
if (inputUrl) {
|
||||
this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run()
|
||||
}
|
||||
|
||||
this.hide()
|
||||
}
|
||||
|
||||
this.removeHandler = () => {
|
||||
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
this.hide()
|
||||
}
|
||||
|
||||
this.keyHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
this.saveHandler()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
this.hide()
|
||||
}
|
||||
}
|
||||
|
||||
this.saveButton.addEventListener('click', this.saveHandler, { once: true })
|
||||
this.removeButton.addEventListener('click', this.removeHandler, { once: true })
|
||||
this.input.addEventListener('keydown', this.keyHandler)
|
||||
}
|
||||
|
||||
hide () {
|
||||
if (this.saveHandler) {
|
||||
this.saveButton.removeEventListener('click', this.saveHandler)
|
||||
this.saveHandler = null
|
||||
}
|
||||
|
||||
if (this.removeHandler) {
|
||||
this.removeButton.removeEventListener('click', this.removeHandler)
|
||||
this.removeHandler = null
|
||||
}
|
||||
|
||||
if (this.keyHandler) {
|
||||
this.input.removeEventListener('keydown', this.keyHandler)
|
||||
this.keyHandler = null
|
||||
}
|
||||
|
||||
this.tooltip.classList.add('hidden')
|
||||
this.currentMark = null
|
||||
}
|
||||
}
|
||||
|
||||
export default actionable(targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'textarea',
|
||||
'editorElement',
|
||||
'boldButton',
|
||||
'italicButton',
|
||||
'underlineButton',
|
||||
'linkButton'
|
||||
]
|
||||
|
||||
async connectedCallback () {
|
||||
if (!this.textarea || !this.editorElement) return
|
||||
|
||||
this.textarea.style.display = 'none'
|
||||
this.adjustShortcutsForPlatform()
|
||||
|
||||
const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()
|
||||
|
||||
const buildDecorations = (doc) => {
|
||||
const decorations = []
|
||||
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (!node.isText) return
|
||||
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(node.text)) !== null) {
|
||||
decorations.push(
|
||||
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
|
||||
class: 'bg-amber-100 py-0.5 px-1 rounded'
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
const VariableHighlight = Extension.create({
|
||||
name: 'variableHighlight',
|
||||
addProseMirrorPlugins () {
|
||||
return [new Plugin({
|
||||
state: {
|
||||
init (_, { doc }) {
|
||||
return buildDecorations(doc)
|
||||
},
|
||||
apply (tr, oldSet) {
|
||||
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return this.getState(state)
|
||||
}
|
||||
}
|
||||
})]
|
||||
}
|
||||
})
|
||||
|
||||
this.editor = new Editor({
|
||||
element: this.editorElement,
|
||||
extensions: [
|
||||
Markdown,
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Bold,
|
||||
Italic,
|
||||
HardBreak.extend({
|
||||
addKeyboardShortcuts () {
|
||||
return {
|
||||
Enter: () => this.editor.commands.setHardBreak()
|
||||
}
|
||||
}
|
||||
}),
|
||||
UndoRedo,
|
||||
Link.extend({
|
||||
inclusive: true,
|
||||
addKeyboardShortcuts: () => ({
|
||||
'Mod-k': () => {
|
||||
this.toggleLink()
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
}).configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: {
|
||||
class: 'link',
|
||||
'data-turbo': 'false',
|
||||
style: 'color: #2563eb; text-decoration: underline; cursor: text;'
|
||||
}
|
||||
}),
|
||||
Underline,
|
||||
VariableHighlight
|
||||
],
|
||||
content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'),
|
||||
contentType: 'markdown',
|
||||
editorProps: {
|
||||
attributes: {
|
||||
style: 'min-height: 220px',
|
||||
dir: 'auto',
|
||||
class: 'p-3 outline-none focus:outline-none'
|
||||
}
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
this.textarea.value = editor.getMarkdown()
|
||||
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
},
|
||||
onSelectionUpdate: ({ editor }) => {
|
||||
this.updateToolbarState()
|
||||
this.handleLinkTooltip(editor)
|
||||
},
|
||||
onBlur: () => {
|
||||
setTimeout(() => {
|
||||
if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
|
||||
this.linkTooltip.hide()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
|
||||
this.linkTooltip = new LinkTooltip(this, this.editor)
|
||||
}
|
||||
|
||||
adjustShortcutsForPlatform () {
|
||||
if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) {
|
||||
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
|
||||
const tip = tooltip.getAttribute('data-tip')
|
||||
|
||||
if (tip && tip.includes('Ctrl')) {
|
||||
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bold (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().toggleBold().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
italic (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().toggleItalic().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
underline (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().toggleUnderline().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
linkSelection (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.toggleLink()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
undo (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().undo().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
redo (e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.editor.chain().focus().redo().run()
|
||||
this.updateToolbarState()
|
||||
}
|
||||
|
||||
updateToolbarState () {
|
||||
this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
|
||||
this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
|
||||
this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
|
||||
this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
|
||||
}
|
||||
|
||||
handleLinkTooltip (editor) {
|
||||
const { from } = editor.state.selection
|
||||
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
|
||||
|
||||
if (!mark) {
|
||||
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
|
||||
|
||||
let linkStart = from
|
||||
const start = editor.state.doc.resolve(from).start()
|
||||
|
||||
for (let i = from - 1; i >= start; i--) {
|
||||
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
|
||||
linkStart = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.linkTooltip.hide()
|
||||
this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
|
||||
this.linkTooltip.currentMark = mark
|
||||
}
|
||||
|
||||
toggleLink () {
|
||||
if (this.editor.isActive('link')) {
|
||||
this.linkTooltip.hide()
|
||||
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
this.updateToolbarState()
|
||||
} else {
|
||||
const { from } = this.editor.state.selection
|
||||
|
||||
this.linkTooltip.hide()
|
||||
this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true })
|
||||
}
|
||||
}
|
||||
|
||||
insertVariable (e) {
|
||||
const variable = e.target.closest('[data-variable]')?.dataset.variable
|
||||
|
||||
if (variable) {
|
||||
const { from, to } = this.editor.state.selection
|
||||
|
||||
if (variable.includes('link') && from !== to) {
|
||||
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
|
||||
} else {
|
||||
this.editor.chain().focus().insertContent(`{${variable}}`).run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.linkTooltip.hide()
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.destroy()
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -1,12 +1,18 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.input.addEventListener('change', (event) => {
|
||||
if (!this.target) return
|
||||
|
||||
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||
const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true
|
||||
|
||||
if (this.dataset.attribute) {
|
||||
this.target[this.dataset.attribute] = event.target.checked
|
||||
this.target[this.dataset.attribute] = value === dataValue
|
||||
}
|
||||
|
||||
if (this.dataset.className) {
|
||||
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
|
||||
this.target.classList.toggle(this.dataset.className, value !== dataValue)
|
||||
|
||||
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
|
||||
this.target.disabled = event.target.value !== this.dataset.value
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const button = this.querySelector('a, button')
|
||||
const button = this.querySelector('a, button, label')
|
||||
|
||||
const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.dataset.classes.split(' ').forEach((cls) => {
|
||||
button.classList.toggle(cls)
|
||||
if (this.dataset.action === 'remove') {
|
||||
target.classList.remove(cls)
|
||||
} else if (this.dataset.action === 'add') {
|
||||
target.classList.add(cls)
|
||||
} else {
|
||||
target.classList.toggle(cls)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -161,6 +161,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
completedButton: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -214,7 +219,10 @@ export default {
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
|
||||
method: 'GET',
|
||||
...this.fetchOptions
|
||||
}).then(async (response) => {
|
||||
if (response.ok) {
|
||||
const urls = await response.json()
|
||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||
|
||||
@@ -530,6 +530,7 @@
|
||||
v-else-if="isInvite"
|
||||
:submitters="inviteSubmitters"
|
||||
:optional-submitters="optionalInviteSubmitters"
|
||||
:fetch-options="fetchOptions"
|
||||
:submitter-slug="submitterSlug"
|
||||
:authenticity-token="authenticityToken"
|
||||
:url="baseUrl + submitPath + '/invite'"
|
||||
@@ -543,6 +544,7 @@
|
||||
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
|
||||
:has-multiple-documents="hasMultipleDocuments"
|
||||
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||
:fetch-options="fetchOptions"
|
||||
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
||||
@@ -678,6 +680,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
optionalInviteSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -1467,7 +1474,8 @@ export default {
|
||||
} else {
|
||||
return fetch(this.baseUrl + this.submitPath, {
|
||||
method: 'POST',
|
||||
body: formData || new FormData(this.$refs.form)
|
||||
body: formData || new FormData(this.$refs.form),
|
||||
...this.fetchOptions
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
currentFieldUuids.forEach((fieldUuid) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const en = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Please upload an image file',
|
||||
must_be_characters_length: 'Must be {number} characters length',
|
||||
must_be_characters_length: 'Must be {number} characters long',
|
||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||
verify_id: 'Verify ID',
|
||||
identity_verification: 'Identity verification',
|
||||
@@ -97,6 +97,7 @@ const en = {
|
||||
upload: 'Upload',
|
||||
files: 'Files',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
||||
browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.',
|
||||
wait_countdown_seconds: 'Wait {countdown} seconds'
|
||||
}
|
||||
|
||||
@@ -199,6 +200,7 @@ const es = {
|
||||
upload: 'Subir',
|
||||
files: 'Archivos',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
||||
browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.',
|
||||
wait_countdown_seconds: 'Espera {countdown} segundos'
|
||||
}
|
||||
|
||||
@@ -301,6 +303,7 @@ const it = {
|
||||
upload: 'Carica',
|
||||
files: 'File',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
||||
browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.',
|
||||
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
||||
}
|
||||
|
||||
@@ -403,6 +406,7 @@ const de = {
|
||||
upload: 'Hochladen',
|
||||
files: 'Dateien',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
|
||||
browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.',
|
||||
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
|
||||
}
|
||||
|
||||
@@ -505,6 +509,7 @@ const fr = {
|
||||
upload: 'Téléverser',
|
||||
files: 'Fichiers',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||
browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.',
|
||||
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
|
||||
}
|
||||
|
||||
@@ -607,6 +612,7 @@ const pl = {
|
||||
upload: 'Przesyłanie',
|
||||
files: 'Pliki',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
||||
browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.',
|
||||
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
||||
}
|
||||
|
||||
@@ -709,6 +715,7 @@ const uk = {
|
||||
upload: 'Завантажити',
|
||||
files: 'Файли',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
||||
browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.',
|
||||
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
||||
}
|
||||
|
||||
@@ -811,6 +818,7 @@ const cs = {
|
||||
upload: 'Nahrát',
|
||||
files: 'Soubory',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
||||
browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.',
|
||||
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
||||
}
|
||||
|
||||
@@ -913,6 +921,7 @@ const pt = {
|
||||
upload: 'Carregar',
|
||||
files: 'Arquivos',
|
||||
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||
browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.',
|
||||
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
||||
}
|
||||
|
||||
@@ -1015,6 +1024,7 @@ const he = {
|
||||
upload: 'העלאה',
|
||||
files: 'קבצים',
|
||||
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
||||
browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.',
|
||||
wait_countdown_seconds: 'המתן {countdown} שניות'
|
||||
}
|
||||
|
||||
@@ -1117,6 +1127,7 @@ const nl = {
|
||||
upload: 'Uploaden',
|
||||
files: 'Bestanden',
|
||||
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
||||
browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.',
|
||||
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
||||
}
|
||||
|
||||
@@ -1219,6 +1230,7 @@ const ar = {
|
||||
upload: 'تحميل',
|
||||
files: 'الملفات',
|
||||
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
||||
browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.',
|
||||
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
||||
}
|
||||
|
||||
@@ -1321,6 +1333,7 @@ const ko = {
|
||||
upload: '업로드',
|
||||
files: '파일',
|
||||
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
||||
browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.',
|
||||
wait_countdown_seconds: '{countdown}초 기다리세요'
|
||||
}
|
||||
|
||||
@@ -1423,6 +1436,7 @@ const ja = {
|
||||
upload: 'アップロード',
|
||||
files: 'ファイル',
|
||||
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
||||
browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。',
|
||||
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
|
||||
<script>
|
||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||
import { isCanvasBlocked } from './validate_signature'
|
||||
import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
import SignaturePad from 'signature_pad'
|
||||
import AppearsOn from './appears_on'
|
||||
@@ -419,7 +420,15 @@ export default {
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (this.field.required === true) {
|
||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||
if (isCanvasBlocked()) {
|
||||
alert(this.t('browser_privacy_settings_block_canvas'))
|
||||
|
||||
if (window.Rollbar) {
|
||||
window.Rollbar.info('Canvas blocked')
|
||||
}
|
||||
} else {
|
||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||
}
|
||||
|
||||
return reject(error)
|
||||
} else {
|
||||
|
||||
@@ -78,6 +78,11 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
optionalSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -108,7 +113,8 @@ export default {
|
||||
|
||||
return fetch(this.url, {
|
||||
method: 'POST',
|
||||
body: new FormData(this.$refs.form)
|
||||
body: new FormData(this.$refs.form),
|
||||
...this.fetchOptions
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.$emit('success')
|
||||
|
||||
@@ -309,7 +309,7 @@
|
||||
<script>
|
||||
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||
import { isValidSignatureCanvas } from './validate_signature'
|
||||
import { isValidSignatureCanvas, isCanvasBlocked } from './validate_signature'
|
||||
import SignaturePad from 'signature_pad'
|
||||
import AppearsOn from './appears_on'
|
||||
import FileDropzone from './dropzone'
|
||||
@@ -790,7 +790,15 @@ export default {
|
||||
|
||||
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
|
||||
if (this.field.required === true || this.pad.toData().length > 0) {
|
||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||
if (isCanvasBlocked()) {
|
||||
alert(this.t('browser_privacy_settings_block_canvas'))
|
||||
|
||||
if (window.Rollbar) {
|
||||
window.Rollbar.info('Canvas blocked')
|
||||
}
|
||||
} else {
|
||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Image too small or simple'))
|
||||
} else {
|
||||
@@ -846,7 +854,15 @@ export default {
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (this.field.required === true) {
|
||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||
if (isCanvasBlocked()) {
|
||||
alert(this.t('browser_privacy_settings_block_canvas'))
|
||||
|
||||
if (window.Rollbar) {
|
||||
window.Rollbar.info('Canvas blocked')
|
||||
}
|
||||
} else {
|
||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||
}
|
||||
|
||||
return reject(error)
|
||||
} else {
|
||||
|
||||
@@ -35,4 +35,24 @@ function isValidSignatureCanvas (data) {
|
||||
return validStrokes.length > 0
|
||||
}
|
||||
|
||||
export { isValidSignatureCanvas }
|
||||
function isCanvasBlocked () {
|
||||
try {
|
||||
const testCanvas = document.createElement('canvas')
|
||||
|
||||
testCanvas.width = 2
|
||||
testCanvas.height = 2
|
||||
|
||||
const ctx = testCanvas.getContext('2d')
|
||||
|
||||
ctx.fillStyle = 'rgb(255, 0, 0)'
|
||||
ctx.fillRect(0, 0, 2, 2)
|
||||
|
||||
const pixel = ctx.getImageData(0, 0, 1, 1).data
|
||||
|
||||
return pixel[0] !== 255 || pixel[1] !== 0 || pixel[2] !== 0 || pixel[3] !== 255
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export { isValidSignatureCanvas, isCanvasBlocked }
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
/>
|
||||
<template v-else>
|
||||
<form
|
||||
v-if="withSignYourselfButton && template.submitters.length < 2"
|
||||
v-if="withSignYourselfButton && undefinedSubmitters.length < 2"
|
||||
target="_blank"
|
||||
data-turbo="false"
|
||||
class="inline"
|
||||
@@ -938,6 +938,16 @@ export default {
|
||||
language () {
|
||||
return this.locale.split('-')[0].toLowerCase()
|
||||
},
|
||||
undefinedSubmitters () {
|
||||
return this.template.submitters.filter((submitter) => {
|
||||
return !submitter.invite_by_uuid &&
|
||||
!submitter.optional_invite_by_uuid &&
|
||||
!submitter.invite_via_field_uuid &&
|
||||
!submitter.linked_to_uuid &&
|
||||
!submitter.is_requester &&
|
||||
!submitter.email
|
||||
})
|
||||
},
|
||||
withPrefillable () {
|
||||
if (this.template.fields) {
|
||||
return this.template.fields.some((f) => f.prefillable)
|
||||
|
||||
@@ -161,7 +161,7 @@ export default {
|
||||
computed: {
|
||||
fields () {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
|
||||
if (f !== this.field && this.isNumberField(f) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
@@ -176,9 +176,12 @@ export default {
|
||||
this.formula = this.humanizeFormula(this.field.preferences.formula || '')
|
||||
},
|
||||
methods: {
|
||||
isNumberField (field) {
|
||||
return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
|
||||
},
|
||||
humanizeFormula (text) {
|
||||
return text.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||
const foundField = this.fields.find((f) => f.uuid === uuid)
|
||||
const foundField = this.template.fields.find((f) => f.uuid === uuid)
|
||||
|
||||
if (foundField) {
|
||||
return `{{${foundField.name || this.buildDefaultName(foundField)}}}`
|
||||
@@ -189,7 +192,7 @@ export default {
|
||||
},
|
||||
normalizeFormula (text) {
|
||||
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
||||
const foundField = this.fields.find((f) => {
|
||||
const foundField = this.template.fields.find((f) => {
|
||||
return (f.name || this.buildDefaultName(f)).trim() === name.trim()
|
||||
})
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ const en = {
|
||||
learn_more: 'Learn more',
|
||||
and: 'and',
|
||||
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 an send your first document',
|
||||
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_tour: 'Start Tour',
|
||||
or_add_from: 'Or add from',
|
||||
sync: 'Sync',
|
||||
|
||||
@@ -261,7 +261,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isSelectMode () {
|
||||
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField
|
||||
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField && !this.drawCustomField
|
||||
},
|
||||
pageSelectedAreas () {
|
||||
if (!this.selectedAreasRef.value) return []
|
||||
|
||||
@@ -26,7 +26,7 @@ class SendTestWebhookRequestJob
|
||||
Addressable::URI.parse(webhook_url.url).normalize
|
||||
end
|
||||
|
||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https'
|
||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || [443, nil].exclude?(uri.port)
|
||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
||||
end
|
||||
|
||||
|
||||
@@ -14,12 +14,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
|
||||
end
|
||||
|
||||
template_submitters_index =
|
||||
if @email_message.blank?
|
||||
build_submitter_preferences_index(@submitter)
|
||||
else
|
||||
{}
|
||||
end
|
||||
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
|
||||
|
||||
@body = @email_message&.body.presence ||
|
||||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
|
||||
@@ -36,6 +31,8 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
reply_to = build_submitter_reply_to(@submitter)
|
||||
|
||||
maybe_set_custom_domain(@submitter)
|
||||
|
||||
I18n.with_locale(@current_account.locale) do
|
||||
subject = build_invite_subject(@subject, @email_config, submitter)
|
||||
|
||||
@@ -133,6 +130,8 @@ class SubmitterMailer < ApplicationMailer
|
||||
assign_message_metadata('submitter_documents_copy', @submitter)
|
||||
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
|
||||
|
||||
maybe_set_custom_domain(@submitter)
|
||||
|
||||
I18n.with_locale(@current_account.locale) do
|
||||
subject =
|
||||
@subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy)
|
||||
@@ -262,4 +261,10 @@ class SubmitterMailer < ApplicationMailer
|
||||
def fetch_config_email_body(email_config, _submitter = nil)
|
||||
email_config ? email_config.value['body'].presence : nil
|
||||
end
|
||||
|
||||
def maybe_set_custom_domain(submitter)
|
||||
if Docuseal.multitenant? && (config = AccountConfig.find_by(account_id: submitter.account_id, key: :custom_domain))
|
||||
@custom_domain = config.value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,6 +49,7 @@ class AccountConfig < ApplicationRecord
|
||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||
WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
|
||||
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
|
||||
WITH_TIMESTAMP_SECONDS_KEY = 'with_timestamp_seconds'
|
||||
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
|
||||
REUSE_SIGNATURE_KEY = 'reuse_signature'
|
||||
WITH_FIELD_LABELS_KEY = 'with_field_labels'
|
||||
@@ -57,6 +58,13 @@ class AccountConfig < ApplicationRecord
|
||||
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
||||
POLICY_LINKS_KEY = 'policy_links'
|
||||
|
||||
EMAIL_VARIABLES = {
|
||||
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
||||
SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze,
|
||||
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze
|
||||
}.freeze
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
SUBMITTER_INVITATION_EMAIL_KEY => lambda {
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :password, 'Password', class: 'label' %>
|
||||
<%= ff.password_field :password, value: value['password'], class: 'base-input' %>
|
||||
<%= ff.password_field :password, class: 'base-input', required: value['password'].present?, placeholder: value['password'].present? ? '*************' : '' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 14l-4 -4l4 -4" />
|
||||
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
@@ -0,0 +1,5 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M15 14l4 -4l-4 -4" />
|
||||
<path d="M19 10h-11a4 4 0 1 0 0 8h1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1,5 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6l0 -7" />
|
||||
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
@@ -0,0 +1,6 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11 5l6 0" />
|
||||
<path d="M7 19l6 0" />
|
||||
<path d="M14 5l-4 14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -0,0 +1,5 @@
|
||||
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 5v5a5 5 0 0 0 10 0v-5" />
|
||||
<path d="M5 19h14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -22,7 +22,7 @@
|
||||
<% if params[:modal].present? %>
|
||||
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
|
||||
<% if url_params[:action] == 'new' %>
|
||||
<open-modal src="<%= params[:modal] %>"></open-modal>
|
||||
<open-modal src="<%= url_for(url_params) %>"></open-modal>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<turbo-frame id="modal"></turbo-frame>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :body, t('body'), class: 'label' %>
|
||||
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full !rounded-2xl py-2', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<%= ff.label :body, t('body'), class: 'label' %>
|
||||
<% variables = AccountConfig::EMAIL_VARIABLES[local_assigns[:config].key] %>
|
||||
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:config].value['body'], variables: variables %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<% link_tooltip_html = capture do %>
|
||||
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
|
||||
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
|
||||
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
|
||||
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
|
||||
</button>
|
||||
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
|
||||
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<markdown-editor data-link-tooltip-html="<%= link_tooltip_html.squish %>">
|
||||
<div class="border border-base-content/20 rounded-2xl bg-white">
|
||||
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
|
||||
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||
<%= svg_icon('bold', class: 'w-4 h-4') %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
|
||||
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||
<%= svg_icon('italic', class: 'w-4 h-4') %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
|
||||
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||
<%= svg_icon('underline', class: 'w-4 h-4') %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
|
||||
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||
<%= svg_icon('link', class: 'w-4 h-4') %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-2 h-5 border-l border-base-content/20"></div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
|
||||
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
|
||||
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% if local_assigns[:variables]&.any? %>
|
||||
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
|
||||
<div class="dropdown dropdown-end ml-auto">
|
||||
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
|
||||
<%= t('add_variable') %>
|
||||
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
|
||||
</label>
|
||||
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
|
||||
<% local_assigns[:variables]&.each do |variable| %>
|
||||
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
|
||||
<%= variable_labels.fetch(variable, "{#{variable}}") %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div data-target="markdown-editor.editorElement"></div>
|
||||
</div>
|
||||
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
|
||||
</markdown-editor>
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].call['subject'].scan(/{.*?}/).join(', ') %>">
|
||||
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"id": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.svg",
|
||||
"src": "/favicon.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "any"
|
||||
},
|
||||
|
||||
@@ -27,4 +27,5 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="theme-color" content="#faf7f5">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<% end %>
|
||||
<% if params[:q].present? %>
|
||||
<div class="relative">
|
||||
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||
<a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||
×
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %>
|
||||
<div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2">
|
||||
<%= auto_link(MarkdownToHtml.call(configs.value)) %>
|
||||
<%= MarkdownToHtml.call(configs.value) %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= fff.label :secret_access_key, class: 'label' %>
|
||||
<%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true, class: 'base-input' %>
|
||||
<%= fff.password_field :secret_access_key, required: true, class: 'base-input', placeholder: configs['secret_access_key'].present? ? '*************' : '' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= fff.label :storage_access_key, 'Storage Access Key', class: 'label' %>
|
||||
<%= fff.password_field :storage_access_key, value: configs['storage_access_key'], required: true, class: 'base-input' %>
|
||||
<%= fff.password_field :storage_access_key, required: true, class: 'base-input', placeholder: configs['storage_access_key'].present? ? '*************' : '' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= fff.label :credentials, 'Credentials (JSON key content)', class: 'label' %>
|
||||
<%= fff.text_area :credentials, value: configs['credentials'], required: true, class: 'base-textarea w-full font-mono', rows: 4 %>
|
||||
<%= fff.text_area :credentials, required: true, class: 'base-textarea w-full font-mono', rows: 4, placeholder: configs['credentials'].present? ? "{\n**REDACTED**\n}" : '' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<% has_phone_field = false %>
|
||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %>
|
||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||
<dynamic-list class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %>
|
||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||
<% if submitters.size == 1 %>
|
||||
<submitter-item class="form-control">
|
||||
<emails-textarea data-bulk-enabled="<%= Docuseal.demo? || !Docuseal.multitenant? || can?(:manage, :bulk_send) %>" data-limit="<%= Docuseal.multitenant? ? (can?(:manage, :bulk_send) ? 40 : 1) : nil %>">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %>
|
||||
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
|
||||
<dynamic-list class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="card card-compact bg-base-300/40" data-targets="dynamic-list.items">
|
||||
|
||||
@@ -55,15 +55,9 @@
|
||||
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= f.label :message, t('body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<%= f.label :message, t('body'), class: 'label' %>
|
||||
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
||||
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
||||
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||
@@ -97,15 +91,8 @@
|
||||
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :message, t('body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<%= ff.label :message, t('body'), class: 'label' %>
|
||||
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
<% end %>
|
||||
<div>
|
||||
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
|
||||
<%= l(attachment.created_at.in_time_zone(timezone), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
|
||||
<% time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %>
|
||||
<%= l(attachment.created_at.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<%= render 'submissions/preview_tags' %>
|
||||
<% end %>
|
||||
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
|
||||
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) %>
|
||||
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY]) %>
|
||||
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
|
||||
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %>
|
||||
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
||||
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
|
||||
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
||||
<div style="max-width: 1600px" class="mx-auto pl-4">
|
||||
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
||||
@@ -20,7 +21,7 @@
|
||||
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
|
||||
<% end %>
|
||||
<% if @submission.audit_trail.present? %>
|
||||
<a href="<%= ActiveStorage::Blob.proxy_url(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank">
|
||||
<a href="<%= ActiveStorage::Blob.proxy_path(@submission.audit_trail.blob, expires_at: 4.hours.from_now) %>" class="white-button" target="_blank">
|
||||
<%= svg_icon('external_link', class: 'w-6 h-6') %>
|
||||
<span class="hidden md:inline"><%= t('audit_log') %></span>
|
||||
</a>
|
||||
@@ -68,7 +69,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %>
|
||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? %>
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -125,7 +126,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<% else %>
|
||||
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_signature_id_reason: %>
|
||||
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_timestamp_seconds:, with_signature_id_reason: %>
|
||||
<% end %>
|
||||
<% elsif field['readonly'] != true && submitter && !submitter.completed_at? %>
|
||||
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
|
||||
@@ -159,7 +160,7 @@
|
||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||
</span>
|
||||
</div>
|
||||
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||
@@ -225,15 +226,15 @@
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<div class="mt-2 mb-1">
|
||||
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<%= render 'submissions/send_sms_button', submitter: %>
|
||||
<% end %>
|
||||
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||
<div class="mt-2 mb-1">
|
||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||
<%= t('sign_in_person') %>
|
||||
|
||||
@@ -35,19 +35,19 @@
|
||||
<% if is_show_tabs %>
|
||||
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
||||
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('all') %></span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'pending')) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('pending') %></span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'completed')) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('completed') %></span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= t(params[:status]) %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:status)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= params[:folder] %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:folder)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
||||
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:completed_at_from, :completed_at_to)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to url_for(params.to_unsafe_h.except(:created_at_to, :created_at_from)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<% if params[:with_remove] %>
|
||||
<div class="text-center w-full mt-4">
|
||||
<%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||
<%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
|
||||
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
|
||||
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
|
||||
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_timestamp_seconds: @form_configs[:with_timestamp_seconds], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
|
||||
<% end %>
|
||||
</div>
|
||||
</page-container>
|
||||
@@ -91,7 +91,7 @@
|
||||
<% if @form_configs[:policy_links].present? %>
|
||||
<div class="text-center md:text-neutral-500 md:pr-3 md:pb-3 md:text-sm md:text-left mt-2 md:mt-0 md:fixed md:bottom-0 md:right-0">
|
||||
<div class="md:max-w-[8rem] flex flex-wrap md:flex-col justify-center md:justify-start md:items-start space-x-2 md:space-x-0">
|
||||
<%= auto_link(MarkdownToHtml.call(@form_configs[:policy_links])) %>
|
||||
<%= MarkdownToHtml.call(@form_configs[:policy_links]) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<%= auto_link(simple_format(MarkdownToHtml.call(h(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig]))))) %>
|
||||
<%= MarkdownToHtml.call(ReplaceEmailVariables.call(local_assigns[:content], submitter: local_assigns[:submitter], sig: local_assigns[:sig])) %>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<p><%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %>
|
||||
<p><%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %></p>
|
||||
<p>
|
||||
<%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %>
|
||||
<%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig, host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) }.compact) %>
|
||||
</p>
|
||||
<p>
|
||||
<%= t('thanks') %>,<br><%= @current_account.name %>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<% if @body.present? %>
|
||||
<%= render 'custom_content', content: @body, submitter: @submitter %>
|
||||
<% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
|
||||
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p><%= t('hi_there') %>,</p>
|
||||
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p>
|
||||
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
|
||||
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
|
||||
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
|
||||
<p>
|
||||
<%= t('thanks') %>,<br><%= @current_account.name %>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<% elsif !submission.archived_at? && !template&.archived_at? && !submission.expired? && !submitter.declined_at? %>
|
||||
<% if current_user.email == submitter.email %>
|
||||
<div class="flex-1 md:flex-none md:w-36 flex">
|
||||
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]">
|
||||
<a href="<%= submit_form_path(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]">
|
||||
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
||||
<% if t('sign_now').length < 12 %>
|
||||
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||
@@ -167,7 +167,7 @@
|
||||
<% elsif !template&.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
|
||||
<div class="relative flex items-center space-x-3">
|
||||
<% if current_user.email == submitter.email %>
|
||||
<a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]">
|
||||
<a href="<%= submit_form_path(slug: submitter.slug) %>" data-turbo="false" target="_blank" id="sign_yourself_button" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]">
|
||||
<span class="flex items-center justify-center space-x-1 md:space-x-2">
|
||||
<% if t('sign_now').length < 12 %>
|
||||
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<% if is_show_tabs %>
|
||||
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
||||
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('all') %></span>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'pending')) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('pending') %></span>
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</a>
|
||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'completed')) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
||||
<span class="font-normal"><%= t('completed') %></span>
|
||||
@@ -94,7 +94,7 @@
|
||||
<span class="mr-1"><%= t('send_to_recipients') %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @template.submitters.size == 1 %>
|
||||
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 %>
|
||||
<%= button_to start_form_path(@template.slug), params: { selfsign: true }, method: :put, class: 'white-button w-full', form: { style: 'display: inline', target: '_blank', data: { turbo: false } } do %>
|
||||
<%= svg_icon('writing', class: 'w-6 h-6') %>
|
||||
<span class="mr-1"><%= t('sign_it_yourself') %></span>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<div class="space-y-3 divide-y">
|
||||
<% template.submitters.each_with_index do |submitter, index| %>
|
||||
<div class="<%= 'pt-3' if index.positive? %>">
|
||||
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %>
|
||||
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
|
||||
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :invite_via_field_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'invite_via_field_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %>
|
||||
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %>
|
||||
<%= ff.hidden_field :uuid %>
|
||||
<div class="form-control">
|
||||
<div class="flex justify-between">
|
||||
@@ -26,35 +26,71 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% if template.submitters.size == 2 %>
|
||||
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
|
||||
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, multiple: true, autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid = SecureRandom.uuid, disabled: is_email_disabled = ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present?, class: "base-input w-full #{'hidden' if ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present?}" %>
|
||||
<% if index == 1 %>
|
||||
<% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %>
|
||||
<disable-hidden id="<%= invite_by_selector_id = SecureRandom.uuid %>" class="<%= 'hidden' if ff.object.invite_by_uuid.blank? && ff.object.optional_invite_by_uuid.blank? && ff.object.invite_via_field_uuid.blank? %>">
|
||||
<toggle-attribute data-target-id="<%= invite_via_field_selector_id = SecureRandom.uuid %>" data-class-name="hidden" data-value="invite_via_field">
|
||||
<%= ff.select :option, [*(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, (invite_fields.present? ? [t('invite_via_form_field'), 'invite_via_field'] : nil)].compact, {}, class: 'base-select' %>
|
||||
</toggle-attribute>
|
||||
<% if invite_fields.present? %>
|
||||
<disable-hidden id="<%= invite_via_field_selector_id %>" class="mt-2 <%= 'hidden' if ff.object.invite_via_field_uuid.blank? %>">
|
||||
<%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, ff.object.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select mt-3' %>
|
||||
</disable-hidden>
|
||||
<% end %>
|
||||
</disable-hidden>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %>
|
||||
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
|
||||
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %>
|
||||
<toggle-attribute data-target-id="<%= invite_field_wrapper_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="invite_via_field">
|
||||
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }, (invite_fields.present? ? [t('invite_via_form_field'), 'invite_via_field'] : nil)].compact, {}, class: 'base-select mb-3' %>
|
||||
</toggle-attribute>
|
||||
</toggle-attribute>
|
||||
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
|
||||
<% if invite_fields.present? %>
|
||||
<disable-hidden id="<%= invite_field_wrapper_uuid %>" class="<%= 'hidden' if item.option != 'invite_via_field' %>">
|
||||
<%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, item.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select', required: true %>
|
||||
</disable-hidden>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if template.submitters.size == 2 %>
|
||||
<checkbox-group class="mt-3 flex items-center space-x-4">
|
||||
<% if local_assigns[:with_submission_requester] != false %>
|
||||
<% if index == 0 && local_assigns[:with_submission_requester] != false %>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
|
||||
<toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-attribute="disabled">
|
||||
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
|
||||
</toggle-attribute>
|
||||
<span class="select-none">
|
||||
<%= t('submission_requester') %>
|
||||
</span>
|
||||
</label>
|
||||
<% end %>
|
||||
<% if index == 1 %>
|
||||
<% elsif index == 1 %>
|
||||
<% if local_assigns[:with_submission_requester] != false %>
|
||||
<toggle-classes data-target-id="<%= invite_by_selector_id %>" class="flex" data-classes="hidden" data-action="add">
|
||||
<toggle-classes data-target-id="<%= email_field_uuid %>" class="flex" data-classes="hidden" data-action="remove">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-attribute="disabled">
|
||||
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
|
||||
</toggle-attribute>
|
||||
<span class="select-none">
|
||||
<%= t('submission_requester') %>
|
||||
</span>
|
||||
</label>
|
||||
</toggle-classes>
|
||||
</toggle-classes>
|
||||
<% end %>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
|
||||
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex">
|
||||
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, template.submitters.first['uuid'], '' %>
|
||||
</indeterminate-checkbox>
|
||||
<toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-class-name="hidden" data-value="false">
|
||||
<toggle-attribute data-target-id="<%= invite_by_selector_id %>" class="flex" data-class-name="hidden">
|
||||
<toggle-attribute data-target-id="<%= email_field_uuid %>" class="flex" data-attribute="disabled">
|
||||
<%= ff.check_box :invite_checbox, { class: 'base-checkbox', checked: ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present? }, true, '' %>
|
||||
</toggle-attribute>
|
||||
</toggle-attribute>
|
||||
</toggle-attribute>
|
||||
<span class="select-none">
|
||||
<%= t('invite_by_name', name: template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span>
|
||||
<%= t('invite_by_name', name: template.submitters.first['name']) %>
|
||||
</span>
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
@@ -20,15 +20,8 @@
|
||||
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
|
||||
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:completed_notification_email_body), value: ff.object.completed_notification_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -20,15 +20,8 @@
|
||||
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
|
||||
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:documents_copy_email_body), value: ff.object.documents_copy_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY] %>
|
||||
</div>
|
||||
<% if can?(:manage, :reply_to) %>
|
||||
<div class="form-control">
|
||||
|
||||
@@ -27,15 +27,8 @@
|
||||
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:request_email_body), value: ff.object.request_email_body, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -52,38 +45,28 @@
|
||||
<% end %>
|
||||
</ul>
|
||||
</toggle-visible>
|
||||
<%= f.fields_for :preferences do |ff| %>
|
||||
<% @template.submitters.each_with_index do |submitter, index| %>
|
||||
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
|
||||
<% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %>
|
||||
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || template_email_preferences_values.presence || default_template_email_preferences_values)), index: nil do |fff| %>
|
||||
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
|
||||
<div class="form-control">
|
||||
<div class="flex justify-between">
|
||||
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
|
||||
<% if submitter_email_preferences_values.present? %>
|
||||
<label for="submitter_invitation_email_reset_link" class="label underline">
|
||||
<%= t('reset_default') %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<div class="flex items-center">
|
||||
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
|
||||
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
|
||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||
</span>
|
||||
</div>
|
||||
<autoresize-textarea>
|
||||
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<% @template.submitters.each_with_index do |submitter, index| %>
|
||||
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
|
||||
<% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %>
|
||||
<% submitter_email_values = submitter_email_preferences_values || template_email_preferences_values.presence || default_template_email_preferences_values %>
|
||||
<%= hidden_field_tag 'template[preferences][submitters][][uuid]', submitter['uuid'] %>
|
||||
<div class="form-control">
|
||||
<div class="flex justify-between">
|
||||
<label class="label"><%= t('email_subject') %></label>
|
||||
<% if submitter_email_preferences_values.present? %>
|
||||
<label for="submitter_invitation_email_reset_link" class="label underline">
|
||||
<%= t('reset_default') %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= text_field_tag 'template[preferences][submitters][][request_email_subject]', submitter_email_values.first, required: true, class: 'base-input', dir: 'auto', autocomplete: 'off' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control">
|
||||
<label class="label"><%= t('email_body') %></label>
|
||||
<%= render 'personalization_settings/markdown_editor', name: 'template[preferences][submitters][][request_email_body]', value: submitter_email_values.last, variables: AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %>
|
||||
<div class="form-control">
|
||||
<%= f.hidden_field :prefillable, value: 'true' %>
|
||||
<%= f.label :field_uuid, t(:invite_form_fields), class: 'label' %>
|
||||
<%= f.label :field_uuid, t(:sender_form_fields), class: 'label' %>
|
||||
<div class="join w-full">
|
||||
<%= select_tag :field_uuid, options_for_select(select_fields), prompt: t(:select_field), class: 'base-select w-full join-item', dir: 'auto', required: true %>
|
||||
<%= f.button button_title(title: t('add'), disabled_with: t('add')), class: 'base-button join-item !px-6' %>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
</label>
|
||||
<% end %>
|
||||
<div class="flex gap-2 mt-3">
|
||||
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" 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" autocomplete="off" readonly>
|
||||
<check-on-click data-element-id="template_shared_link">
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), 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') %>
|
||||
</check-on-click>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -85,9 +85,9 @@
|
||||
<div class="mt-6">
|
||||
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2>
|
||||
<div class="tabs border-b mt-4">
|
||||
<%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
<%= link_to t('succeeded'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
<%= link_to t('failed'), url_for(params.to_unsafe_h.merge(status: 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
<%= link_to t('all'), url_for(params: request.query_parameters.except('status')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
<%= link_to t('succeeded'), url_for(params: request.query_parameters.merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
<%= link_to t('failed'), url_for(params: request.query_parameters.merge('status' => 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||
</div>
|
||||
<% if @webhook_events.present? %>
|
||||
<div class="divide-y divide-base-300 rounded-lg">
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ if ENV['RAILS_ENV'] == 'production'
|
||||
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
|
||||
end
|
||||
|
||||
unless Process.euid == 2000
|
||||
if Process.uid.zero?
|
||||
begin
|
||||
test_file = "#{ENV.fetch('WORKDIR', '.')}/test"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ ActiveSupport.on_load(:active_storage_attachment) do
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/BlockLength
|
||||
ActiveSupport.on_load(:active_storage_blob) do
|
||||
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||
attribute :io_data, :string, default: ''
|
||||
@@ -22,6 +23,12 @@ ActiveSupport.on_load(:active_storage_blob) do
|
||||
)
|
||||
end
|
||||
|
||||
def self.proxy_path(blob, expires_at: nil, filename: nil)
|
||||
Rails.application.routes.url_helpers.blobs_proxy_path(
|
||||
signed_uuid: blob.signed_uuid(expires_at:), filename: filename || blob.filename
|
||||
)
|
||||
end
|
||||
|
||||
def uuid
|
||||
super || begin
|
||||
new_uuid = SecureRandom.uuid
|
||||
@@ -40,6 +47,7 @@ ActiveSupport.on_load(:active_storage_blob) do
|
||||
service.delete(key)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
||||
|
||||
ActiveStorage::LogSubscriber.detach_from(:active_storage) if Rails.env.production?
|
||||
|
||||
|
||||
+149
-17
@@ -44,7 +44,7 @@ en: &en
|
||||
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.'
|
||||
edit_order: Edit Order
|
||||
expirable_file_download_links: Expirable file download links
|
||||
invite_form_fields: Invite form fields
|
||||
sender_form_fields: Sender form fields
|
||||
default_parties: Default parties
|
||||
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
|
||||
stripe_integration: Stripe Integration
|
||||
@@ -218,7 +218,7 @@ en: &en
|
||||
copy: Copy
|
||||
copied: Copied
|
||||
rotate: Rotate
|
||||
remove_existing_api_token_and_generated_a_new_one_are_you_sure_: Remove existing API token and generated a new one. Are you sure?
|
||||
remove_existing_api_token_and_generated_a_new_one_are_you_sure_: Remove existing API token and generate a new one. Are you sure?
|
||||
request_signature_multiple_submitters_with_default_values: Request signature, multiple submitters with default values
|
||||
request_signature_single_submitter: Request signature, single submitter
|
||||
template_details: Template details
|
||||
@@ -330,8 +330,8 @@ en: &en
|
||||
initials: Initials
|
||||
update_initials: Update Initials
|
||||
unable_to_save_initials: Unable to save initials.
|
||||
initials_has_been_saved: Initials has been saved.
|
||||
initials_has_been_removed: Initials has been removed.
|
||||
initials_has_been_saved: Initials have been saved.
|
||||
initials_has_been_removed: Initials have been removed.
|
||||
change_password: Change Password
|
||||
two_factor_authentication: Two-Factor Authentication
|
||||
2fa_is_not_configured: 2FA is not configured
|
||||
@@ -388,7 +388,7 @@ en: &en
|
||||
from: From
|
||||
account_sid: Account SID
|
||||
send_sms_via_webhook: Send SMS via webhook
|
||||
webhook_integration_allows_to_send_sms_using_any_provider: Webhook integration allows to send SMS using any provider
|
||||
webhook_integration_allows_to_send_sms_using_any_provider: Webhook integration allows you to send SMS using any provider
|
||||
test: Test
|
||||
single_sign_on_with_saml_2_0: Single Sign On with SAML 2.0
|
||||
force_sso_disable_login_with_email_and_password: Force SSO (disable login with email and password)
|
||||
@@ -420,7 +420,7 @@ en: &en
|
||||
send_signature_request_emails_without_limits_with_docuseal_pro: Send signature request emails without limits with DocuSeal Pro
|
||||
count_emails_used: '%{count} emails used'
|
||||
has_been_connected: has been connected
|
||||
sms_not_configured: SMS not Configure
|
||||
sms_not_configured: SMS not Configured
|
||||
configure_sms_settings_in_order_to_send_text_messages_: 'Configure SMS settings in order to send text messages:'
|
||||
go_to_sms_settings: Go to SMS settings
|
||||
back_to_active: Back to Active
|
||||
@@ -500,6 +500,7 @@ en: &en
|
||||
submission_requester: Submission requester
|
||||
specified_email: Specified email
|
||||
invite_by_name: 'Invite by %{name}'
|
||||
invite_via_form_field: Invite via Form Field
|
||||
same_as_name: 'Same as %{name}'
|
||||
default_email: Default Email
|
||||
processing: Processing
|
||||
@@ -596,7 +597,7 @@ en: &en
|
||||
upload_file: Upload file
|
||||
upgrade_your_plan_to_invite_more_users_contact_email: 'Upgrade your plan to invite more users (contact %{email}).'
|
||||
contact_your_admin_email_to_invite_more_users: 'Contact your admin %{email} to invite more users.'
|
||||
contact_your_administrator_to_add_new_users: Contact your administrator to add new user
|
||||
contact_your_administrator_to_add_new_users: Contact your administrator to add new users
|
||||
one_hour: 1 hour
|
||||
two_hours: 2 hours
|
||||
four_hours: 4 hours
|
||||
@@ -625,7 +626,7 @@ en: &en
|
||||
personalize_email_content: Personalize email content
|
||||
automated_reminders: Automated reminders
|
||||
bulk_send_from_spreadsheet: Bulk send from spreadsheet
|
||||
identify_verification_via_sms: Identify verification via SMS
|
||||
identify_verification_via_sms: Identity verification via SMS
|
||||
start_with_pro: Start with Pro
|
||||
user_month: user / month
|
||||
developer_sandbox: Developer Sandbox.
|
||||
@@ -737,7 +738,7 @@ en: &en
|
||||
find_suitable_zapier_templates_to_automate_your_workflow: Find suitable Zapier templates to automate your workflow.
|
||||
get_started: Get started
|
||||
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://www.docuseal.com/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Click here</a> to learn more about user roles and permissions.'
|
||||
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 signature request emails sent this month. Upgrade to Pro to send unlimited signature request email.'
|
||||
count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 signature request emails sent this month. Upgrade to Pro to send unlimited signature request emails.'
|
||||
test_mode_emails_limit_will_be_reset_within_24_hours: Test mode emails limit will be reset within 24 hours.
|
||||
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: On a scale of 1 to 10, how satisfied are you with the DocuSeal product?
|
||||
tell_us_more_about_your_experience: Tell us more about your experience
|
||||
@@ -755,7 +756,7 @@ en: &en
|
||||
manage_plan: Manage plan
|
||||
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: This submission has multiple signers, which prevents the use of a sharing link as it's unclear which signer is responsible for specific fields. To resolve this, follow this <a href="https://www.docuseal.com/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> to define the default signer details.
|
||||
welcome_to_docuseal: Welcome to DocuSeal
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create an send your first document
|
||||
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_tour: Start Tour
|
||||
name_a_z: Name A-Z
|
||||
recently_used: Recently used
|
||||
@@ -815,7 +816,7 @@ en: &en
|
||||
connect_gmail_or_outlook: Connect Gmail or Outlook
|
||||
connect_your_email_to_bulk_send: Connect your email to bulk send
|
||||
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connect your Gmail or Outlook account or add SMTP settings to bulk send.
|
||||
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails it requires to connect Gmail or Outlook.
|
||||
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails you need to connect Gmail or Outlook.
|
||||
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
|
||||
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
|
||||
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
|
||||
@@ -886,6 +887,13 @@ en: &en
|
||||
if_you_didnt_request_this_you_can_ignore_this_email: "If you didn't request this, please ignore this email."
|
||||
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Your password won't change until you open the link above and set a new one."
|
||||
too_many_requests_try_again_later: Too many requests, try again later.
|
||||
bold: Bold
|
||||
italic: Italic
|
||||
underline: Underline
|
||||
undo: Undo
|
||||
redo: Redo
|
||||
add_variable: Add variable
|
||||
enter_a_url_or_variable_name: Enter a URL or variable name
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Your email address has been successfully confirmed.
|
||||
@@ -1002,6 +1010,16 @@ en: &en
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} of %{count} events"
|
||||
range_without_total: "%{from}-%{to} events"
|
||||
variables:
|
||||
account_name: Account name
|
||||
submitter_link: Submitter link
|
||||
template_name: Template name
|
||||
submission_submitters: Submitters list
|
||||
submission_link: Submission link
|
||||
documents_link: Documents link
|
||||
time:
|
||||
formats:
|
||||
detailed: "%B %d, %Y %H:%M:%S"
|
||||
|
||||
es: &es
|
||||
knowledge_based_authentication: Autenticación basada en el conocimiento
|
||||
@@ -1022,7 +1040,7 @@ es: &es
|
||||
party: Parte
|
||||
edit_order: Edita Pedido
|
||||
select: Seleccionar
|
||||
invite_form_fields: Invitar campos del formulario
|
||||
sender_form_fields: Campos del formulario del remitente
|
||||
pro: Pro
|
||||
default_parties: Partes predeterminadas
|
||||
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
|
||||
@@ -1487,6 +1505,7 @@ es: &es
|
||||
submission_requester: Solicitante del envío
|
||||
specified_email: Correo electrónico especificado
|
||||
invite_by_name: 'Invitar por %{name}'
|
||||
invite_via_form_field: Invitar a través de campo del formulario
|
||||
same_as_name: 'Igual que %{name}'
|
||||
default_email: Correo electrónico predeterminado
|
||||
processing: Procesando
|
||||
@@ -1870,6 +1889,13 @@ es: &es
|
||||
if_you_didnt_request_this_you_can_ignore_this_email: "Si no solicitaste esto, puedes ignorar este correo electrónico."
|
||||
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Tu contraseña no cambiará hasta que abras el enlace anterior y establezcas una nueva."
|
||||
too_many_requests_try_again_later: Demasiadas solicitudes. Intenta de nuevo más tarde.
|
||||
bold: Negrita
|
||||
italic: Cursiva
|
||||
underline: Subrayado
|
||||
undo: Deshacer
|
||||
redo: Rehacer
|
||||
add_variable: Agregar variable
|
||||
enter_a_url_or_variable_name: Ingrese una URL o nombre de variable
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
|
||||
@@ -1986,6 +2012,16 @@ es: &es
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} de %{count} eventos"
|
||||
range_without_total: "%{from}-%{to} eventos"
|
||||
variables:
|
||||
account_name: Nombre de la cuenta
|
||||
submitter_link: Enlace del firmante
|
||||
template_name: Nombre de la plantilla
|
||||
submission_submitters: Lista de firmantes
|
||||
submission_link: Enlace del envío
|
||||
documents_link: Enlace de los documentos
|
||||
time:
|
||||
formats:
|
||||
detailed: "%-d de %B de %Y %H:%M:%S"
|
||||
|
||||
it: &it
|
||||
knowledge_based_authentication: Autenticazione basata sulla conoscenza
|
||||
@@ -2006,7 +2042,7 @@ it: &it
|
||||
party: Parte
|
||||
edit_order: Modifica Ordine
|
||||
select: Seleziona
|
||||
invite_form_fields: Invita campi modulo
|
||||
sender_form_fields: Campi del modulo del mittente
|
||||
pro: Pro
|
||||
default_parties: Parti predefiniti
|
||||
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
|
||||
@@ -2471,6 +2507,7 @@ it: &it
|
||||
submission_requester: "Richiedente dell'invio"
|
||||
specified_email: Email specificata
|
||||
invite_by_name: 'Invito da %{name}'
|
||||
invite_via_form_field: Invito tramite campo del modulo
|
||||
same_as_name: 'Uguale a %{name}'
|
||||
default_email: Email predefinita
|
||||
processing: Elaborazione in corso
|
||||
@@ -2855,6 +2892,13 @@ it: &it
|
||||
if_you_didnt_request_this_you_can_ignore_this_email: "Se non hai richiesto questo, puoi ignorare questa email."
|
||||
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "La tua password non cambierà finché non apri il link sopra e ne imposti una nuova."
|
||||
too_many_requests_try_again_later: Troppe richieste. Riprova più tardi.
|
||||
bold: Grassetto
|
||||
italic: Corsivo
|
||||
underline: Sottolineato
|
||||
undo: Annulla
|
||||
redo: Ripeti
|
||||
add_variable: Aggiungi variabile
|
||||
enter_a_url_or_variable_name: Inserisci un URL o nome variabile
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Il tuo indirizzo email è stato confermato con successo.
|
||||
@@ -2971,6 +3015,16 @@ it: &it
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} di %{count} eventi"
|
||||
range_without_total: "%{from}-%{to} eventi"
|
||||
variables:
|
||||
account_name: Nome dell'account
|
||||
submitter_link: Link del firmatario
|
||||
template_name: Nome del modello
|
||||
submission_submitters: Lista dei firmatari
|
||||
submission_link: Link dell'invio
|
||||
documents_link: Link dei documenti
|
||||
time:
|
||||
formats:
|
||||
detailed: "%d %B %Y %H:%M:%S"
|
||||
|
||||
fr: &fr
|
||||
knowledge_based_authentication: Authentification basée sur la connaissance
|
||||
@@ -2991,7 +3045,7 @@ fr: &fr
|
||||
party: Partie
|
||||
edit_order: Modifier l’ordre
|
||||
select: Sélectionner
|
||||
invite_form_fields: Champs du formulaire d’invitation
|
||||
sender_form_fields: Champs du formulaire de l’expéditeur
|
||||
pro: Pro
|
||||
default_parties: Parties par défaut
|
||||
authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton
|
||||
@@ -3456,6 +3510,7 @@ fr: &fr
|
||||
submission_requester: Demandeur de soumission
|
||||
specified_email: E‑mail spécifié
|
||||
invite_by_name: Inviter par %{name}
|
||||
invite_via_form_field: Inviter via champ du formulaire
|
||||
same_as_name: Identique à %{name}
|
||||
default_email: E‑mail par défaut
|
||||
processing: Traitement en cours
|
||||
@@ -3836,6 +3891,13 @@ fr: &fr
|
||||
if_you_didnt_request_this_you_can_ignore_this_email: "Si vous n'avez pas fait cette demande, veuillez ignorer cet e-mail."
|
||||
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Votre mot de passe ne changera pas tant que vous n’aurez pas ouvert le lien ci-dessus et défini un nouveau mot de passe."
|
||||
too_many_requests_try_again_later: Trop de demandes. Réessayez plus tard.
|
||||
bold: Gras
|
||||
italic: Italique
|
||||
underline: Souligné
|
||||
undo: Annuler
|
||||
redo: Rétablir
|
||||
add_variable: Ajouter une variable
|
||||
enter_a_url_or_variable_name: Entrez une URL ou un nom de variable
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Votre adresse e-mail a été confirmée avec succès.
|
||||
@@ -3952,6 +4014,16 @@ fr: &fr
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} sur %{count} événements"
|
||||
range_without_total: "%{from}-%{to} événements"
|
||||
variables:
|
||||
account_name: Nom du compte
|
||||
submitter_link: Lien du signataire
|
||||
template_name: Nom du modèle
|
||||
submission_submitters: Liste des signataires
|
||||
submission_link: Lien de la soumission
|
||||
documents_link: Lien des documents
|
||||
time:
|
||||
formats:
|
||||
detailed: "%A %d %B %Y %Hh%Mm%Ss"
|
||||
|
||||
pt: &pt
|
||||
knowledge_based_authentication: Autenticação baseada em conhecimento
|
||||
@@ -3972,7 +4044,7 @@ pt: &pt
|
||||
party: Parte
|
||||
edit_order: Edita Pedido
|
||||
select: Selecionar
|
||||
invite_form_fields: Convidar campos do formulário
|
||||
sender_form_fields: Campos do formulário do remetente
|
||||
pro: Pro
|
||||
default_parties: Partes padrão
|
||||
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
|
||||
@@ -4437,6 +4509,7 @@ pt: &pt
|
||||
submission_requester: Solicitante de submissão
|
||||
specified_email: E-mail especificado
|
||||
invite_by_name: 'Convidado por %{name}'
|
||||
invite_via_form_field: Convidar via campo do formulário
|
||||
same_as_name: 'Igual a %{name}'
|
||||
default_email: E-mail padrão
|
||||
processing: Processando
|
||||
@@ -4820,6 +4893,13 @@ pt: &pt
|
||||
if_you_didnt_request_this_you_can_ignore_this_email: "Se você não solicitou isso, pode ignorar este e-mail."
|
||||
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Sua senha não será alterada até que você abra o link acima e defina uma nova."
|
||||
too_many_requests_try_again_later: Muitas solicitações. Tente novamente mais tarde.
|
||||
bold: Negrito
|
||||
italic: Itálico
|
||||
underline: Sublinhado
|
||||
undo: Desfazer
|
||||
redo: Refazer
|
||||
add_variable: Adicionar variável
|
||||
enter_a_url_or_variable_name: Digite uma URL ou nome de variável
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
|
||||
@@ -4936,6 +5016,16 @@ pt: &pt
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} de %{count} eventos"
|
||||
range_without_total: "%{from}-%{to} eventos"
|
||||
variables:
|
||||
account_name: Nome da conta
|
||||
submitter_link: Link do signatário
|
||||
template_name: Nome do modelo
|
||||
submission_submitters: Lista de signatários
|
||||
submission_link: Link da submissão
|
||||
documents_link: Link dos documentos
|
||||
time:
|
||||
formats:
|
||||
detailed: "%A, %d de %B de %Y, %H:%M:%Sh"
|
||||
|
||||
de: &de
|
||||
knowledge_based_authentication: Wissensbasierte Authentifizierung
|
||||
@@ -4965,7 +5055,7 @@ de: &de
|
||||
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Klicken Sie hier</label>, um eine E-Mail zum Zurücksetzen des Passworts zu senden.'
|
||||
edit_order: Bestellung bearbeiten
|
||||
expirable_file_download_links: Ablaufbare Datei-Download-Links
|
||||
invite_form_fields: Einladungsformular-Felder
|
||||
sender_form_fields: Absenderformular-Felder
|
||||
default_parties: Standardparteien
|
||||
authenticate_embedded_form_preview_with_token: Eingebettete Formularvorschau mit Token authentifizieren
|
||||
stripe_integration: Stripe-Integration
|
||||
@@ -5421,6 +5511,7 @@ de: &de
|
||||
submission_requester: Anfragende Person
|
||||
specified_email: Angegebene E-Mail
|
||||
invite_by_name: 'Einladung von %{name}'
|
||||
invite_via_form_field: Einladung über Formularfeld
|
||||
same_as_name: 'Gleich wie %{name}'
|
||||
default_email: Standard-E-Mail
|
||||
processing: Verarbeitung
|
||||
@@ -5804,6 +5895,13 @@ de: &de
|
||||
if_you_didnt_request_this_you_can_ignore_this_email: "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
|
||||
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Ihr Passwort wird erst geändert, wenn Sie den obigen Link öffnen und ein neues festlegen."
|
||||
too_many_requests_try_again_later: Zu viele Anfragen. Versuchen Sie es später erneut.
|
||||
bold: Fett
|
||||
italic: Kursiv
|
||||
underline: Unterstrichen
|
||||
undo: Rückgängig
|
||||
redo: Wiederholen
|
||||
add_variable: Variable hinzufügen
|
||||
enter_a_url_or_variable_name: Geben Sie eine URL oder einen Variablennamen ein
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
|
||||
@@ -5920,6 +6018,16 @@ de: &de
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} von %{count} Ereignissen"
|
||||
range_without_total: "%{from}-%{to} Ereignisse"
|
||||
variables:
|
||||
account_name: Kontoname
|
||||
submitter_link: Link des Unterzeichners
|
||||
template_name: Vorlagenname
|
||||
submission_submitters: Liste der Unterzeichner
|
||||
submission_link: Link der Einreichung
|
||||
documents_link: Link der Dokumente
|
||||
time:
|
||||
formats:
|
||||
detailed: "%A, %d. %B %Y, %H:%M:%S Uhr"
|
||||
|
||||
pl:
|
||||
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
|
||||
@@ -6337,7 +6445,7 @@ nl: &nl
|
||||
click_here_to_send_a_reset_password_email_html: <label class="link" for="resend_password_button">Klik hier</label> om een e-mail voor wachtwoordherstel te verzenden.
|
||||
edit_order: Volgorde bewerken
|
||||
expirable_file_download_links: Verlopende downloadlinks voor bestanden
|
||||
invite_form_fields: Velden van uitnodigingsformulier
|
||||
sender_form_fields: Velden van afzenderformulier
|
||||
default_parties: Standaard partijen
|
||||
authenticate_embedded_form_preview_with_token: Preview van ingesloten formulier authenticeren met token
|
||||
stripe_integration: Stripe-integratie
|
||||
@@ -6794,6 +6902,7 @@ nl: &nl
|
||||
submission_requester: Aanvrager van inzending
|
||||
specified_email: Opgegeven e-mail
|
||||
invite_by_name: Uitnodigen door %{name}
|
||||
invite_via_form_field: Uitnodigen via formulierveld
|
||||
same_as_name: Zelfde als %{name}
|
||||
default_email: Standaard e-mail
|
||||
processing: Verwerken
|
||||
@@ -7173,6 +7282,13 @@ nl: &nl
|
||||
if_you_didnt_request_this_you_can_ignore_this_email: "Als je dit niet hebt aangevraagd, kun je deze e-mail negeren."
|
||||
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Je wachtwoord wordt niet gewijzigd totdat je de bovenstaande link opent en een nieuw wachtwoord instelt."
|
||||
too_many_requests_try_again_later: Te veel verzoeken. Probeer het later opnieuw.
|
||||
bold: Vet
|
||||
italic: Cursief
|
||||
underline: Onderstreept
|
||||
undo: Ongedaan maken
|
||||
redo: Opnieuw
|
||||
add_variable: Variabele toevoegen
|
||||
enter_a_url_or_variable_name: Voer een URL of variabelenaam in
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Je e-mailadres is succesvol bevestigd.
|
||||
@@ -7289,6 +7405,16 @@ nl: &nl
|
||||
events:
|
||||
range_with_total: "%{from}-%{to} van %{count} gebeurtenissen"
|
||||
range_without_total: "%{from}-%{to} gebeurtenissen"
|
||||
variables:
|
||||
account_name: Accountnaam
|
||||
submitter_link: Link van de ondertekenaar
|
||||
template_name: Sjabloonnaam
|
||||
submission_submitters: Lijst van ondertekenaars
|
||||
submission_link: Link van de inzending
|
||||
documents_link: Link van de documenten
|
||||
time:
|
||||
formats:
|
||||
detailed: "%d %B %Y %H:%M:%S"
|
||||
|
||||
ar:
|
||||
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
|
||||
@@ -7586,12 +7712,18 @@ en-US:
|
||||
date:
|
||||
formats:
|
||||
default: "%m/%d/%Y"
|
||||
time:
|
||||
formats:
|
||||
detailed: "%B %d, %Y %I:%M:%S %p"
|
||||
|
||||
en-GB:
|
||||
<<: *en
|
||||
date:
|
||||
formats:
|
||||
default: "%d/%m/%Y"
|
||||
time:
|
||||
formats:
|
||||
detailed: "%d %B, %Y %H:%M:%S"
|
||||
|
||||
es-ES:
|
||||
<<: *es
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ Rails.application.routes.draw do
|
||||
resources :account_custom_fields, only: %i[create]
|
||||
resources :user_configs, only: %i[create]
|
||||
resources :encrypted_user_configs, only: %i[destroy]
|
||||
resources :timestamp_server, only: %i[create]
|
||||
resources :timestamp_server, only: %i[create] unless Docuseal.multitenant?
|
||||
resources :dashboard, only: %i[index]
|
||||
resources :setup, only: %i[index create]
|
||||
resource :newsletter, only: %i[show update]
|
||||
|
||||
@@ -35,16 +35,16 @@ module DownloadUtils
|
||||
|
||||
module_function
|
||||
|
||||
def call(url)
|
||||
def call(url, validate: Docuseal.multitenant?)
|
||||
uri = begin
|
||||
URI(url)
|
||||
rescue URI::Error
|
||||
Addressable::URI.parse(url).normalize
|
||||
end
|
||||
|
||||
validate_uri!(uri) if Docuseal.multitenant?
|
||||
validate_uri!(uri) if validate
|
||||
|
||||
resp = conn.get(uri)
|
||||
resp = conn(validate:).get(uri)
|
||||
|
||||
raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400
|
||||
|
||||
@@ -52,14 +52,15 @@ module DownloadUtils
|
||||
end
|
||||
|
||||
def validate_uri!(uri)
|
||||
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https'
|
||||
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' ||
|
||||
[443, nil].exclude?(uri.port)
|
||||
raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS)
|
||||
end
|
||||
|
||||
def conn
|
||||
def conn(validate: Docuseal.multitenant?)
|
||||
Faraday.new do |faraday|
|
||||
faraday.response :follow_redirects, callback: lambda { |_, new_env|
|
||||
validate_uri!(new_env[:url]) if Docuseal.multitenant?
|
||||
validate_uri!(new_env[:url]) if validate
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,7 +29,12 @@ module LoadActiveStorageConfigs
|
||||
|
||||
service_configurations = ActiveSupport::ConfigurationFile.parse(STORAGE_YML_PATH)
|
||||
service_configurations[service].merge!(configs) if configs.present?
|
||||
service_configurations[service][:force_path_style] = true if configs&.dig('endpoint').present?
|
||||
if configs&.dig('endpoint').present?
|
||||
service_configurations[service][:force_path_style] = true
|
||||
if configs['endpoint'].include?('cloudflarestorage.com')
|
||||
service_configurations[service][:request_checksum_calculation] = 'when_required'
|
||||
end
|
||||
end
|
||||
|
||||
if service == 'google'
|
||||
service_configurations[service][:credentials] = JSON.parse(configs.fetch('credentials', '{}'))
|
||||
|
||||
+124
-5
@@ -1,13 +1,132 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MarkdownToHtml
|
||||
LINK_REGEXP = %r{\[([^\]]+)\]\((https?://[^)]+)\)}
|
||||
TAGS = {
|
||||
'' => %w[<em> </em>],
|
||||
'*' => %w[<strong> </strong>],
|
||||
'~' => %w[<s> </s>]
|
||||
}.freeze
|
||||
|
||||
INLINE_TOKENIZER = /(\[)|(\]\(([^)]+?)\))|(?:`([^`].*?)`)|(\*\*\*|\*\*|\*|~~)/
|
||||
|
||||
ALLOWED_TAGS = %w[p br strong b em i u a].freeze
|
||||
ALLOWED_ATTRIBUTES = %w[href].freeze
|
||||
|
||||
module_function
|
||||
|
||||
def call(text)
|
||||
text.gsub(LINK_REGEXP) do
|
||||
ApplicationController.helpers.link_to(Regexp.last_match(1), Regexp.last_match(2))
|
||||
end
|
||||
def call(markdown)
|
||||
return '' if markdown.blank?
|
||||
|
||||
text = auto_link_urls(markdown)
|
||||
html = render_markdown(text)
|
||||
|
||||
ActionController::Base.helpers.sanitize(html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES)
|
||||
end
|
||||
|
||||
BRACKETS = { ')' => '(', ']' => '[', '}' => '{' }.freeze
|
||||
|
||||
def auto_link_urls(text)
|
||||
link_parts = text.split(%r{((?:https?://|www\.)[^\s<>\u00A0"]+)})
|
||||
|
||||
link_parts.map.with_index do |part, index|
|
||||
if part.match?(%r{\A(?:https?://|www\.)}) && !(index > 0 && link_parts[index - 1]&.match?(/\]\(\s*\z/))
|
||||
url_part = part.dup
|
||||
punctuation = []
|
||||
|
||||
while url_part.sub!(%r{[^\p{Word}/\-=;]\z}, '')
|
||||
punctuation.push(::Regexp.last_match(0))
|
||||
|
||||
opening = BRACKETS[punctuation.last]
|
||||
|
||||
next unless opening && url_part.count(opening) > url_part.count(punctuation.last)
|
||||
|
||||
url_part << punctuation.pop
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
trail = punctuation.reverse.join
|
||||
url = url_part.start_with?('www.') ? "https://#{url_part}" : url_part
|
||||
|
||||
"[#{url_part}](#{url})#{trail}"
|
||||
else
|
||||
part
|
||||
end
|
||||
end.join
|
||||
end
|
||||
|
||||
def render_markdown(text)
|
||||
text = text.gsub(/\+\+([^+]+)\+\+/, '<u>\1</u>')
|
||||
|
||||
paragraphs = text.split(/\n{2,}/)
|
||||
|
||||
html = paragraphs.filter_map do |para|
|
||||
content = para.strip
|
||||
|
||||
next if content.empty?
|
||||
|
||||
next '<p><br></p>' if [' ', '&nbsp;'].include?(content)
|
||||
|
||||
content = content.gsub(/ *\n/, '<br>')
|
||||
|
||||
"<p>#{parse_inline(content)}</p>"
|
||||
end.join
|
||||
|
||||
html.presence || '<p></p>'
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def parse_inline(text)
|
||||
context = []
|
||||
out = ''
|
||||
last = 0
|
||||
|
||||
tag = lambda do |t|
|
||||
desc = TAGS[t[1] || '']
|
||||
|
||||
return t unless desc
|
||||
|
||||
is_end = context.last == t
|
||||
is_end ? context.pop : context.push(t)
|
||||
desc[is_end ? 1 : 0]
|
||||
end
|
||||
|
||||
flush = lambda do
|
||||
str = ''
|
||||
str += tag.call(context.last) while context.any?
|
||||
str
|
||||
end
|
||||
|
||||
while last <= text.length && (m = INLINE_TOKENIZER.match(text, last))
|
||||
prev = text[last...m.begin(0)]
|
||||
last = m.end(0)
|
||||
chunk = m[0]
|
||||
|
||||
if m[4]
|
||||
chunk = "<code>#{ERB::Util.html_escape(m[4])}</code>"
|
||||
elsif m[2]
|
||||
out = out.sub(/\A(.*)<a>/m, "\\1<a href=\"#{ERB::Util.html_escape(m[3])}\">")
|
||||
out = out.gsub('<a>', '[')
|
||||
chunk = "#{flush.call}</a>"
|
||||
elsif m[1]
|
||||
chunk = '<a>'
|
||||
elsif m[5]
|
||||
chunk =
|
||||
if m[5] == '***'
|
||||
if context.include?('*') && context.include?('**')
|
||||
tag.call('*') + tag.call('**')
|
||||
else
|
||||
tag.call('**') + tag.call('*')
|
||||
end
|
||||
else
|
||||
tag.call(m[5])
|
||||
end
|
||||
end
|
||||
|
||||
out += prev.to_s + chunk
|
||||
end
|
||||
|
||||
(out + text[last..].to_s + flush.call).gsub('<a>', '[')
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
|
||||
@@ -89,8 +89,10 @@ module ReplaceEmailVariables
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def build_documents_links_text(submitter, sig = nil)
|
||||
url_options = build_url_options_for(submitter)
|
||||
|
||||
Rails.application.routes.url_helpers.submissions_preview_url(
|
||||
submitter.submission.slug, { sig:, **Docuseal.default_url_options }.compact
|
||||
submitter.submission.slug, { sig:, **url_options }.compact
|
||||
)
|
||||
end
|
||||
|
||||
@@ -139,14 +141,9 @@ module ReplaceEmailVariables
|
||||
end
|
||||
|
||||
def build_submitter_link(submitter, tracking_event_type)
|
||||
if tracking_event_type == 'click_email'
|
||||
url_options =
|
||||
if EMAIL_HOST.present?
|
||||
{ host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
||||
else
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
url_options = build_url_options_for(submitter, is_email: tracking_event_type == 'click_email')
|
||||
|
||||
if tracking_event_type == 'click_email'
|
||||
Rails.application.routes.url_helpers.submit_form_url(
|
||||
slug: submitter.slug,
|
||||
t: SubmissionEvents.build_tracking_param(submitter, 'click_email'),
|
||||
@@ -156,11 +153,22 @@ module ReplaceEmailVariables
|
||||
Rails.application.routes.url_helpers.submit_form_url(
|
||||
slug: submitter.slug,
|
||||
c: SubmissionEvents.build_tracking_param(submitter, 'click_sms'),
|
||||
**Docuseal.default_url_options
|
||||
**url_options
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def build_url_options_for(submitter, is_email: true)
|
||||
if Docuseal.multitenant? &&
|
||||
(config = AccountConfig.find_by(account_id: submitter.account_id, key: :custom_domain))
|
||||
{ host: config.value, protocol: 'https' }
|
||||
elsif is_email && EMAIL_HOST.present?
|
||||
{ host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
||||
else
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
end
|
||||
|
||||
def build_submission_link(submission)
|
||||
Rails.application.routes.url_helpers.submission_url(submission, **Docuseal.default_url_options)
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ module SendWebhookRequest
|
||||
end
|
||||
|
||||
if Docuseal.multitenant?
|
||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' &&
|
||||
raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || [443, nil].exclude?(uri.port)) &&
|
||||
!AccountConfig.exists?(key: :allow_http,
|
||||
account_id: webhook_url.account_id)
|
||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)
|
||||
|
||||
+3
-2
@@ -18,7 +18,8 @@ module Submissions
|
||||
def plain_search(submissions, keyword, search_values: false, search_template: false)
|
||||
return submissions if keyword.blank?
|
||||
|
||||
term = "%#{keyword.downcase}%"
|
||||
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
|
||||
term = "%#{sanitized}%"
|
||||
|
||||
arel_table = Submitter.arel_table
|
||||
|
||||
@@ -31,7 +32,7 @@ module Submissions
|
||||
if search_template
|
||||
submissions = submissions.left_joins(:template)
|
||||
|
||||
arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
||||
arel = arel.or(Template.arel_table[:name].lower.matches("%#{sanitized}%"))
|
||||
end
|
||||
|
||||
submissions.joins(:submitters).where(arel).group(:id)
|
||||
|
||||
@@ -56,7 +56,9 @@ module Submissions
|
||||
template_submitter = template_submitters.find { |e| e['uuid'] == uuid }
|
||||
end
|
||||
|
||||
template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid')
|
||||
template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid',
|
||||
'invite_via_field_uuid')
|
||||
|
||||
template_submitter['order'] = submitter_attrs['order'] if submitter_attrs['order'].present?
|
||||
|
||||
submission.template_submitters << template_submitter
|
||||
@@ -113,7 +115,10 @@ module Submissions
|
||||
item = item.merge('invite_by_uuid' => invite_by_uuid) if invite_by_uuid
|
||||
end
|
||||
|
||||
next if item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank?
|
||||
next if item['invite_by_uuid'].blank? &&
|
||||
item['optional_invite_by_uuid'].blank? &&
|
||||
item['invite_via_field_uuid'].blank?
|
||||
|
||||
next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] }
|
||||
|
||||
item = item.merge('order' => submitter_attr['order']) if submitter_attr && submitter_attr['order'].present?
|
||||
|
||||
@@ -116,6 +116,7 @@ module Submissions
|
||||
configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
||||
AccountConfig::WITH_AUDIT_SENDER_KEY,
|
||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY])
|
||||
|
||||
@@ -126,6 +127,7 @@ module Submissions
|
||||
with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false
|
||||
with_audit_sender = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_SENDER_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
|
||||
|
||||
timezone = account.timezone
|
||||
timezone = last_submitter.timezone || account.timezone if with_submitter_timezone
|
||||
@@ -489,8 +491,10 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
time_format = with_timestamp_seconds ? :detailed : :long
|
||||
|
||||
[
|
||||
"#{I18n.l(event.event_timestamp.in_time_zone(timezone), format: :long, locale: account.locale)} " \
|
||||
"#{I18n.l(event.event_timestamp.in_time_zone(timezone), format: time_format, locale: account.locale)} " \
|
||||
"#{TimeUtils.timezone_abbr(timezone, event.event_timestamp)}",
|
||||
composer.document.layout.formatted_text_box(text_box)
|
||||
]
|
||||
|
||||
@@ -15,6 +15,7 @@ module Submissions
|
||||
configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
|
||||
|
||||
@@ -22,6 +23,7 @@ module Submissions
|
||||
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
|
||||
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_signature_id_reason =
|
||||
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
||||
|
||||
@@ -37,7 +39,7 @@ module Submissions
|
||||
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
|
||||
with_signature_id:, is_flatten:, with_headings: index.zero?,
|
||||
with_submitter_timezone:, with_file_links:,
|
||||
with_signature_id_reason:)
|
||||
with_signature_id_reason:, with_timestamp_seconds:)
|
||||
end
|
||||
|
||||
template = submission.template
|
||||
|
||||
@@ -140,11 +140,13 @@ module Submissions
|
||||
configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
|
||||
|
||||
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
|
||||
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
|
||||
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_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
|
||||
with_signature_id_reason =
|
||||
@@ -162,7 +164,7 @@ module Submissions
|
||||
|
||||
pdf.trailer.info[:DocumentID] = document_id
|
||||
pdf.pages.each do |page|
|
||||
font_size = (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i
|
||||
font_size = [(([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i, 4].max
|
||||
cnv = page.canvas(type: :overlay)
|
||||
|
||||
text =
|
||||
@@ -195,11 +197,13 @@ module Submissions
|
||||
fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:,
|
||||
with_submitter_timezone:,
|
||||
with_file_links:,
|
||||
with_timestamp_seconds:,
|
||||
with_signature_id_reason:)
|
||||
end
|
||||
|
||||
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
||||
with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil)
|
||||
with_submitter_timezone: false, with_signature_id_reason: true,
|
||||
with_timestamp_seconds: false, with_file_links: nil)
|
||||
cell_layouters = Hash.new do |hash, valign|
|
||||
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
|
||||
end
|
||||
@@ -320,13 +324,15 @@ module Submissions
|
||||
timezone = submitter.account.timezone
|
||||
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
|
||||
|
||||
time_format = with_timestamp_seconds ? :detailed : :long
|
||||
|
||||
if with_signature_id_reason || field.dig('preferences', 'reasons').present?
|
||||
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
|
||||
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \
|
||||
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
|
||||
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
|
||||
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
|
||||
else
|
||||
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
|
||||
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
|
||||
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
|
||||
end
|
||||
end
|
||||
@@ -569,7 +575,11 @@ module Submissions
|
||||
fill_color:,
|
||||
font_size:)
|
||||
|
||||
line_height = layouter.fit([text], cell_width, height).lines.first.height
|
||||
line = layouter.fit([text], width, height).lines.first
|
||||
|
||||
line_height = line.height
|
||||
|
||||
cell_width = [line.width, cell_width].max
|
||||
|
||||
if preferences_font_size.blank? && line_height > (area['h'] * height)
|
||||
text = HexaPDF::Layout::TextFragment.create(char,
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ module Submitters
|
||||
UnableToSendCode = Class.new(StandardError)
|
||||
InvalidOtp = Class.new(StandardError)
|
||||
MaliciousFileExtension = Class.new(StandardError)
|
||||
ArgumentError = Class.new(StandardError)
|
||||
|
||||
DANGEROUS_EXTENSIONS = Set.new(%w[
|
||||
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
|
||||
@@ -133,7 +134,7 @@ module Submitters
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
else
|
||||
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
|
||||
raise ArgumentError, 'file param is missing'
|
||||
end
|
||||
|
||||
ActiveStorage::Attachment.create!(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Submitters
|
||||
module AuthorizedForForm
|
||||
Unauthorized = Class.new(StandardError)
|
||||
|
||||
module_function
|
||||
|
||||
def call(submitter, current_user, request)
|
||||
pass_email_2fa?(submitter, request) && pass_link_2fa?(submitter, current_user, request)
|
||||
end
|
||||
|
||||
def pass_email_2fa?(submitter, request)
|
||||
return false unless submitter
|
||||
|
||||
return true if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||
submitter.preferences['require_email_2fa'] != true
|
||||
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
|
||||
|
||||
token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence
|
||||
|
||||
return true if token.present? &&
|
||||
Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == submitter.slug
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def pass_link_2fa?(submitter, current_user, request)
|
||||
return false unless submitter
|
||||
|
||||
return true if submitter.submission.source != 'link'
|
||||
return true unless submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
|
||||
return true if submitter.email == current_user&.email && current_user&.account_id == submitter.account_id
|
||||
|
||||
if (token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence)
|
||||
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
|
||||
|
||||
return true if Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == link_2fa_key
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -15,6 +15,7 @@ module Submitters
|
||||
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
|
||||
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
|
||||
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
|
||||
|
||||
@@ -35,6 +36,7 @@ module Submitters
|
||||
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
|
||||
enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true
|
||||
with_submitter_timezone = find_safe_value(configs, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY) == true
|
||||
with_timestamp_seconds = find_safe_value(configs, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY) == true
|
||||
with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false
|
||||
with_field_labels = find_safe_value(configs, AccountConfig::WITH_FIELD_LABELS_KEY) != false
|
||||
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
|
||||
@@ -43,7 +45,7 @@ module Submitters
|
||||
reuse_signature:, with_decline:, with_partial_download:,
|
||||
policy_links:, enforce_signing_order:, completed_message:,
|
||||
require_signing_reason:, prefill_signature:, with_submitter_timezone:,
|
||||
with_signature_id_reason:, with_signature_id:, with_field_labels: }
|
||||
with_signature_id_reason:, with_signature_id:, with_field_labels:, with_timestamp_seconds: }
|
||||
|
||||
keys.each do |key|
|
||||
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value
|
||||
|
||||
@@ -236,7 +236,7 @@ module Submitters
|
||||
|
||||
return blob if blob
|
||||
|
||||
data = DownloadUtils.call(url).body
|
||||
data = DownloadUtils.call(url, validate: true).body
|
||||
|
||||
checksum = Digest::MD5.base64digest(data)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ module Submitters
|
||||
RequiredFieldError = Class.new(StandardError)
|
||||
|
||||
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
|
||||
PHONE_REGEXP = /[+\d()\s-]+/
|
||||
NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze
|
||||
|
||||
STRFTIME_MAP = {
|
||||
@@ -45,14 +46,18 @@ module Submitters
|
||||
assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true'
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
maybe_set_signature_reason!(values, submitter, params)
|
||||
validate_values!(values, submitter, params, request)
|
||||
reason_field = maybe_set_signature_reason!(values, submitter, params)
|
||||
validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request)
|
||||
|
||||
if (touch_attachment_uuid = params[:touch_attachment_uuid].presence)
|
||||
ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at)
|
||||
end
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true'
|
||||
if params[:completed] == 'true'
|
||||
maybe_invite_via_field(submitter, request)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request)
|
||||
end
|
||||
|
||||
submitter.save!
|
||||
end
|
||||
@@ -107,7 +112,9 @@ module Submitters
|
||||
signature_field['preferences'] ||= {}
|
||||
signature_field['preferences']['reason_field_uuid'] = reason_field_uuid
|
||||
|
||||
unless submitter.submission.template_fields.find { |e| e['uuid'] == reason_field_uuid }
|
||||
reason_field = submitter.submission.template_fields.find { |e| e['uuid'] == reason_field_uuid }
|
||||
|
||||
unless reason_field
|
||||
reason_field = { 'type' => 'text',
|
||||
'uuid' => reason_field_uuid,
|
||||
'name' => I18n.t(:reason),
|
||||
@@ -119,6 +126,8 @@ module Submitters
|
||||
end
|
||||
|
||||
submitter.submission.save!
|
||||
|
||||
reason_field
|
||||
end
|
||||
|
||||
def normalized_values(params)
|
||||
@@ -403,7 +412,54 @@ module Submitters
|
||||
end
|
||||
end
|
||||
|
||||
def validate_value!(_value, _field, _params, _submitter, _request)
|
||||
def maybe_invite_via_field(submitter, request)
|
||||
submission = submitter.submission
|
||||
|
||||
is_invited = false
|
||||
|
||||
submission.template_submitters.each do |s|
|
||||
field_uuid = s['invite_via_field_uuid']
|
||||
|
||||
next if field_uuid.blank?
|
||||
|
||||
field = submission.template_fields.find { |e| e['uuid'] == field_uuid }
|
||||
|
||||
next unless field
|
||||
next unless field['submitter_uuid'] == submitter.uuid
|
||||
|
||||
next if submission.submitters.exists?(uuid: s['uuid'])
|
||||
|
||||
value = submitter.values[field_uuid]
|
||||
|
||||
next if value.blank?
|
||||
|
||||
if value.include?('@')
|
||||
email = Submissions.normalize_email(value)
|
||||
elsif value.match?(PHONE_REGEXP)
|
||||
phone = value.gsub(/[^+\d]/, '')
|
||||
end
|
||||
|
||||
next if email.blank? && phone.blank?
|
||||
|
||||
submission.submitters.create!(uuid: s['uuid'], email:, phone:, account_id: submitter.account_id)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
||||
|
||||
is_invited = true
|
||||
end
|
||||
|
||||
submission.update!(submitters_order: :preserved) if is_invited
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def validate_value!(_value, field, _params, submitter, _request)
|
||||
if field['readonly'] == true
|
||||
Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar)
|
||||
|
||||
raise ValidationError, 'Read-only field'
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,7 +20,9 @@ module TemplateFolders
|
||||
def search(folders, keyword)
|
||||
return folders if keyword.blank?
|
||||
|
||||
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
||||
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
|
||||
|
||||
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{sanitized}%"))
|
||||
end
|
||||
|
||||
def filter_active_folders(template_folders, templates)
|
||||
|
||||
+4
-1
@@ -52,7 +52,9 @@ module Templates
|
||||
def plain_search(templates, keyword)
|
||||
return templates if keyword.blank?
|
||||
|
||||
templates.where(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
||||
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
|
||||
|
||||
templates.where(Template.arel_table[:name].lower.matches("%#{sanitized}%"))
|
||||
end
|
||||
|
||||
def fulltext_search(current_user, templates, keyword)
|
||||
@@ -70,6 +72,7 @@ module Templates
|
||||
def filter_undefined_submitters(template_submitters)
|
||||
template_submitters.to_a.select do |item|
|
||||
item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? &&
|
||||
item['invite_via_field_uuid'].blank? &&
|
||||
item['linked_to_uuid'].blank? && item['is_requester'].blank? && item['email'].blank?
|
||||
end
|
||||
end
|
||||
|
||||
+20
-16
@@ -4,7 +4,7 @@ module Templates
|
||||
module Clone
|
||||
module_function
|
||||
|
||||
# rubocop:disable Metrics, Style/CombinableLoops
|
||||
# rubocop:disable Metrics
|
||||
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
|
||||
template = original_template.account.templates.new
|
||||
|
||||
@@ -49,20 +49,6 @@ module Templates
|
||||
submitter['uuid'] = new_submitter_uuid
|
||||
end
|
||||
|
||||
cloned_submitters.each do |submitter|
|
||||
if submitter['optional_invite_by_uuid'].present?
|
||||
submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']]
|
||||
end
|
||||
|
||||
if submitter['invite_by_uuid'].present?
|
||||
submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']]
|
||||
end
|
||||
|
||||
if submitter['linked_to_uuid'].present?
|
||||
submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']]
|
||||
end
|
||||
end
|
||||
|
||||
cloned_preferences['submitters'].to_a.each do |submitter|
|
||||
submitter['uuid'] = submitter_uuids_replacements[submitter['uuid']]
|
||||
end
|
||||
@@ -97,8 +83,26 @@ module Templates
|
||||
end
|
||||
end
|
||||
|
||||
cloned_submitters.each do |submitter|
|
||||
if submitter['optional_invite_by_uuid'].present?
|
||||
submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']]
|
||||
end
|
||||
|
||||
if submitter['invite_by_uuid'].present?
|
||||
submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']]
|
||||
end
|
||||
|
||||
if submitter['linked_to_uuid'].present?
|
||||
submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']]
|
||||
end
|
||||
|
||||
if submitter['invite_via_field_uuid'].present?
|
||||
submitter['invite_via_field_uuid'] = field_uuids_replacements[submitter['invite_via_field_uuid']]
|
||||
end
|
||||
end
|
||||
|
||||
[cloned_submitters, cloned_fields, cloned_schema, cloned_preferences]
|
||||
end
|
||||
# rubocop:enable Metrics, Style/CombinableLoops
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,7 @@ module Templates
|
||||
].freeze
|
||||
|
||||
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
|
||||
MAX_ZIP_SIZE = 100.megabytes
|
||||
InvalidFileType = Class.new(StandardError)
|
||||
PdfEncrypted = Class.new(StandardError)
|
||||
|
||||
@@ -72,9 +73,15 @@ module Templates
|
||||
|
||||
Array.wrap(files).each do |file|
|
||||
if file.content_type == ZIP_CONTENT_TYPE || file.content_type == X_ZIP_CONTENT_TYPE
|
||||
total_size = 0
|
||||
|
||||
Zip::File.open(file.tempfile).each do |entry|
|
||||
next if entry.directory?
|
||||
|
||||
total_size += entry.size
|
||||
|
||||
raise InvalidFileType, 'zip_too_large' if total_size > MAX_ZIP_SIZE
|
||||
|
||||
tempfile = Tempfile.new(entry.name)
|
||||
tempfile.binmode
|
||||
entry.get_input_stream { |in_stream| IO.copy_stream(in_stream, tempfile) }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user