diff --git a/.rubocop.yml b/.rubocop.yml
index bda7960e8..9be296e58 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -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
diff --git a/Dockerfile b/Dockerfile
index f6412c784..b1341fcf3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/Gemfile b/Gemfile
index 416dca88d..15ad01df3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -35,7 +35,6 @@ gem 'pretender'
gem 'puma', require: false
gem 'rack'
gem 'rails'
-gem 'rails_autolink'
gem 'rails-i18n'
gem 'rotp'
gem 'rouge', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 4f9e416bc..5104f648e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,6 +1,6 @@
GIT
remote: https://github.com/trilogy-libraries/trilogy.git
- revision: 3963d490459df7a2b5bedb42424c3285f25eab22
+ revision: 3fb9d2a06bfc5ac8c2ac6af918f0b7dbd2d9f630
glob: contrib/ruby/*.gemspec
specs:
trilogy (2.10.0)
@@ -83,16 +83,16 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
- addressable (2.8.8)
+ addressable (2.8.9)
public_suffix (>= 2.0.2, < 8.0)
- annotaterb (4.20.0)
+ annotaterb (4.22.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
arabic-letter-connector (0.1.1)
ast (2.4.3)
aws-eventstream (1.4.0)
- aws-partitions (1.1209.0)
- aws-sdk-core (3.241.4)
+ aws-partitions (1.1220.0)
+ aws-sdk-core (3.242.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -100,10 +100,10 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
- aws-sdk-kms (1.121.0)
+ aws-sdk-kms (1.122.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.212.0)
+ aws-sdk-s3 (1.213.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -126,9 +126,9 @@ GEM
smart_properties
bigdecimal (4.0.1)
bindex (0.8.1)
- bootsnap (1.21.1)
+ bootsnap (1.23.0)
msgpack (~> 1.2)
- brakeman (7.1.2)
+ brakeman (8.0.4)
racc
builder (3.3.0)
bullet (8.1.0)
@@ -158,7 +158,7 @@ GEM
bigdecimal
rexml
crass (1.0.6)
- css_parser (1.21.1)
+ css_parser (2.0.0)
addressable
csv (3.3.5)
csv-safe (3.3.1)
@@ -171,16 +171,16 @@ GEM
irb (~> 1.10)
reline (>= 0.3.8)
declarative (0.0.20)
- devise (4.9.4)
+ devise (5.0.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
- railties (>= 4.1.0)
+ railties (>= 7.0)
responders
warden (~> 1.2.3)
- devise-two-factor (6.3.1)
- activesupport (>= 7.0, < 8.2)
- devise (>= 4.0, < 5.0)
- railties (>= 7.0, < 8.2)
+ devise-two-factor (6.4.0)
+ activesupport (>= 7.2, < 8.2)
+ devise (>= 4.0, < 6.0)
+ railties (>= 7.2, < 8.2)
rotp (~> 6.0)
diff-lcs (1.6.2)
digest-crc (0.7.0)
@@ -189,7 +189,7 @@ GEM
dotenv (3.2.0)
drb (2.2.3)
email_typo (0.2.3)
- erb (6.0.1)
+ erb (6.0.2)
erb_lint (0.9.0)
activesupport
better_html (>= 2.0.1)
@@ -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)
@@ -240,7 +239,7 @@ GEM
retriable (~> 3.1)
google-apis-iamcredentials_v1 (0.26.0)
google-apis-core (>= 0.15.0, < 2.a)
- google-apis-storage_v1 (0.59.0)
+ google-apis-storage_v1 (0.61.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
@@ -259,7 +258,7 @@ GEM
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.2.0)
- googleauth (1.16.1)
+ googleauth (1.16.2)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
@@ -270,7 +269,7 @@ GEM
hashdiff (1.2.1)
hashie (5.1.0)
logger
- hexapdf (1.5.0)
+ hexapdf (1.6.0)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
@@ -282,12 +281,16 @@ GEM
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2)
- irb (1.16.0)
+ irb (1.17.0)
pp (>= 0.6.0)
+ prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.18.1)
+ json-schema (6.1.0)
+ addressable (~> 2.8)
+ bigdecimal (>= 3.1, < 5)
jwt (3.1.2)
base64
language_server-protocol (3.17.0.5)
@@ -320,11 +323,14 @@ GEM
net-smtp
marcel (1.1.0)
matrix (0.4.3)
+ mcp (0.7.1)
+ json-schema (>= 4.1)
method_source (1.1.0)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
- minitest (6.0.1)
+ minitest (6.0.2)
+ drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
multi_json (1.19.1)
@@ -332,7 +338,7 @@ GEM
bigdecimal (>= 3.1, < 5)
net-http (0.9.1)
uri (>= 0.11.1)
- net-imap (0.6.2)
+ net-imap (0.6.3)
date
net-protocol
net-pop (0.1.2)
@@ -342,17 +348,17 @@ 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)
+ numo-narray-alt (0.10.3)
oauth2 (2.0.18)
faraday (>= 0.17.3, < 4.0)
jwt (>= 1.0, < 4.0)
@@ -361,7 +367,7 @@ GEM
rack (>= 1.2, < 4)
snaky_hash (~> 2.0, >= 2.0.3)
version_gem (~> 1.1, >= 1.1.9)
- oj (3.16.13)
+ oj (3.16.15)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.4)
@@ -369,7 +375,7 @@ GEM
logger
rack (>= 2.2.3)
rack-protection
- omniauth-google-oauth2 (1.2.1)
+ omniauth-google-oauth2 (1.2.2)
jwt (>= 2.9.2)
oauth2 (~> 2.0)
omniauth (~> 2.0)
@@ -380,27 +386,25 @@ GEM
omniauth-rails_csrf_protection (2.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
- onnxruntime (0.10.1)
- ffi
- onnxruntime (0.10.1-aarch64-linux)
+ onnxruntime (0.11.0-aarch64-linux)
ffi
- onnxruntime (0.10.1-arm64-darwin)
+ onnxruntime (0.11.0-arm64-darwin)
ffi
- onnxruntime (0.10.1-x86_64-linux)
+ onnxruntime (0.11.0-x86_64-linux)
ffi
- openssl (4.0.0)
+ openssl (4.0.1)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.3)
package_json (0.2.0)
- pagy (43.2.8)
+ pagy (43.3.1)
json
+ uri
yaml
parallel (1.27.0)
- parser (3.3.10.1)
+ parser (3.3.10.2)
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)
@@ -408,7 +412,7 @@ GEM
pg (1.6.3-x86_64-linux-musl)
pp (0.6.3)
prettyprint
- premailer (1.27.0)
+ premailer (1.28.0)
addressable
css_parser (>= 1.19.0)
htmlentities (>= 4.0.0)
@@ -419,7 +423,7 @@ GEM
pretender (0.6.0)
actionpack (>= 7.1)
prettyprint (0.2.0)
- prism (1.8.0)
+ prism (1.9.0)
pry (0.16.0)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -433,7 +437,7 @@ GEM
puma (7.2.0)
nio4r (~> 2.0)
racc (1.8.1)
- rack (3.2.4)
+ rack (3.2.5)
rack-protection (4.2.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
@@ -465,16 +469,12 @@ GEM
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
- rails-html-sanitizer (1.6.2)
- loofah (~> 2.21)
+ rails-html-sanitizer (1.7.0)
+ loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
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)
@@ -486,7 +486,7 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
- rdoc (7.1.0)
+ rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
@@ -504,7 +504,7 @@ GEM
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
- retriable (3.1.2)
+ retriable (3.2.1)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.7.0)
@@ -517,10 +517,10 @@ GEM
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-mocks (3.13.7)
+ rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-rails (8.0.2)
+ rspec-rails (8.0.3)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
@@ -528,16 +528,17 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
- rspec-support (3.13.6)
- rubocop (1.82.1)
+ rspec-support (3.13.7)
+ rubocop (1.85.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
+ mcp (~> 0.6)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
- rubocop-ast (>= 1.48.0, < 2.0)
+ rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
@@ -565,14 +566,14 @@ GEM
rubyzip (>= 3.2.2)
rubyzip (3.2.2)
securerandom (0.4.1)
- semantic_range (3.1.0)
+ semantic_range (3.1.1)
shakapacker (9.5.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
- sidekiq (8.1.0)
+ sidekiq (8.1.1)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
@@ -606,7 +607,7 @@ GEM
timeout (0.6.0)
trailblazer-option (0.1.2)
tsort (0.2.0)
- turbo-rails (2.0.21)
+ turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
twitter_cldr (6.14.0)
@@ -628,11 +629,10 @@ GEM
version_gem (1.1.9)
warden (1.2.9)
rack (>= 2.0.9)
- web-console (4.2.1)
- actionview (>= 6.0.0)
- activemodel (>= 6.0.0)
+ web-console (4.3.0)
+ actionview (>= 8.0.0)
bindex (>= 0.4.0)
- railties (>= 6.0.0)
+ railties (>= 8.0.0)
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
@@ -645,7 +645,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yaml (0.4.0)
- zeitwerk (2.7.4)
+ zeitwerk (2.7.5)
PLATFORMS
aarch64-linux
@@ -700,7 +700,6 @@ DEPENDENCIES
rack
rails
rails-i18n
- rails_autolink
rotp
rouge
rqrcode
diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb
index 4258154d6..986bbb87d 100644
--- a/app/controllers/api/submissions_controller.rb
+++ b/app/controllers/api/submissions_controller.rb
@@ -172,7 +172,10 @@ def create_submissions(template, params)
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
diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb
index cded880f3..1071549e6 100644
--- a/app/controllers/api/submitters_controller.rb
+++ b/app/controllers/api/submitters_controller.rb
@@ -34,6 +34,7 @@ def show
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 @@ def update
@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 @@ def update
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
diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb
index c8211b7fa..c3f1dd421 100644
--- a/app/controllers/api/templates_controller.rb
+++ b/app/controllers/api/templates_controller.rb
@@ -107,7 +107,8 @@ def template_params
: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,
diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb
index aac293e4d..7b82a43dd 100644
--- a/app/controllers/submissions_download_controller.rb
+++ b/app/controllers/submissions_download_controller.rb
@@ -27,20 +27,18 @@ def index
Submissions::EnsureResultGenerated.call(last_submitter)
- if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
- Rails.logger.info("TTL: #{last_submitter.id}")
+ 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
+ Rails.logger.info("TTL: #{last_submitter.id}")
+
+ 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 @@ def index
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 @@ def build_urls(submitter)
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 @@ def build_combined_url(submitter)
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)
diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb
index 1c10932a4..efbdf3946 100644
--- a/app/controllers/submit_form_controller.rb
+++ b/app/controllers/submit_form_controller.rb
@@ -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 @@ def show
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 @@ def update
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 @@ def success; end
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 @@ def build_attachments_index(submission)
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
diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb
index 918903fe7..a8f969c33 100644
--- a/app/controllers/submit_form_decline_controller.rb
+++ b/app/controllers/submit_form_decline_controller.rb
@@ -11,7 +11,9 @@ def create
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)
diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb
index d6e0b6921..af9cbeb43 100644
--- a/app/controllers/submit_form_download_controller.rb
+++ b/app/controllers/submit_form_download_controller.rb
@@ -17,7 +17,8 @@ def index
@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 @@ def index
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
diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb
index 773eb9e71..5ba141c1a 100644
--- a/app/controllers/submit_form_draw_signature_controller.rb
+++ b/app/controllers/submit_form_draw_signature_controller.rb
@@ -12,7 +12,8 @@ def show
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
diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb
index ab1f26c34..413e2b9a1 100644
--- a/app/controllers/submit_form_invite_controller.rb
+++ b/app/controllers/submit_form_invite_controller.rb
@@ -19,7 +19,9 @@ def create
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 @@ def can_invite?(submitter)
!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')
diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb
index e1a6b9ab7..affd37ba4 100644
--- a/app/controllers/submit_form_values_controller.rb
+++ b/app/controllers/submit_form_values_controller.rb
@@ -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?
diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb
index b29a18f60..51fc41118 100644
--- a/app/controllers/template_documents_controller.rb
+++ b/app/controllers/template_documents_controller.rb
@@ -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
diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb
index 39b450445..0d6db6f7d 100644
--- a/app/controllers/templates_controller.rb
+++ b/app/controllers/templates_controller.rb
@@ -97,7 +97,8 @@ def template_params
: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,
diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb
index 17a4bbb85..64fa58b25 100644
--- a/app/controllers/templates_recipients_controller.rb
+++ b/app/controllers/templates_recipients_controller.rb
@@ -22,7 +22,7 @@ def create
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 @@ def submitters_params
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 @@ def normalize_option_value(attrs)
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)
diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb
index 92311dfed..cf4e38ea6 100644
--- a/app/controllers/templates_uploads_controller.rb
+++ b/app/controllers/templates_uploads_controller.rb
@@ -56,7 +56,7 @@ def save_template!(template, url_params)
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?
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 49cd95163..8889609fe 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -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)
diff --git a/app/javascript/application.scss b/app/javascript/application.scss
index 47eeb4f62..fd02cecf2 100644
--- a/app/javascript/application.scss
+++ b/app/javascript/application.scss
@@ -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;
+}
diff --git a/app/javascript/elements/markdown_editor.js b/app/javascript/elements/markdown_editor.js
new file mode 100644
index 000000000..174312771
--- /dev/null
+++ b/app/javascript/elements/markdown_editor.js
@@ -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, '
'),
+ 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()
+ }
+ }
+}))
diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js
index e9ee30754..5ff6b7c3b 100644
--- a/app/javascript/elements/toggle_attribute.js
+++ b/app/javascript/elements/toggle_attribute.js
@@ -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
}
diff --git a/app/javascript/elements/toggle_classes.js b/app/javascript/elements/toggle_classes.js
index ab96f293d..332ac84ff 100644
--- a/app/javascript/elements/toggle_classes.js
+++ b/app/javascript/elements/toggle_classes.js
@@ -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)
+ }
})
})
}
diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue
index 94246a8e0..b33ef36d9 100644
--- a/app/javascript/submission_form/completed.vue
+++ b/app/javascript/submission_form/completed.vue
@@ -129,6 +129,11 @@ export default {
required: false,
default: false
},
+ fetchOptions: {
+ type: Object,
+ required: false,
+ default: () => ({})
+ },
completedButton: {
type: Object,
required: false,
@@ -182,7 +187,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)
diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue
index e3a3faddf..e2bdcf428 100644
--- a/app/javascript/submission_form/form.vue
+++ b/app/javascript/submission_form/form.vue
@@ -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) => {
diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js
index 705679920..b9da958de 100644
--- a/app/javascript/submission_form/i18n.js
+++ b/app/javascript/submission_form/i18n.js
@@ -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} 秒お待ちください'
}
diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue
index 8e1cc1b50..814b9c122 100644
--- a/app/javascript/submission_form/initials_step.vue
+++ b/app/javascript/submission_form/initials_step.vue
@@ -150,6 +150,7 @@