From 2c736a0eedb0bc59590459a6f62be17c39e81cbb Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 08:42:21 +0200 Subject: [PATCH 01/33] validate readonly field --- lib/submitters/submit_values.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index dc370791..4ecb2b92 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -403,7 +403,13 @@ module Submitters end end - def validate_value!(_value, _field, _params, _submitter, _request) + 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 From 43fbc427708671b0e43ae5357391614e47f67c58 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 10:04:11 +0200 Subject: [PATCH 02/33] fix reason field --- lib/submitters/submit_values.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 4ecb2b92..4b7f4dcb 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -45,8 +45,8 @@ 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) @@ -119,6 +119,8 @@ module Submitters end submitter.submission.save! + + reason_field end def normalized_values(params) From 848f01edf81f0926be877bc996c8e8af47a74fbf Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 10:19:29 +0200 Subject: [PATCH 03/33] add spec --- spec/system/signing_form_spec.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb index 073ef79f..7af76c2f 100644 --- a/spec/system/signing_form_spec.rb +++ b/spec/system/signing_form_spec.rb @@ -626,6 +626,34 @@ RSpec.describe 'Signing Form' do end end + context 'when the signature step with signing reason' do + let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + before do + create(:account_config, account:, key: AccountConfig::REQUIRE_SIGNING_REASON_KEY, value: true) + end + + it 'completes the form with signing reason selected' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + draw_canvas + select 'Approved' + click_button 'Sign and Complete' + + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Signature')).to be_present + end + end + context 'when the number step' do let(:template) { create(:template, account:, author:, only_field_types: %w[number]) } let(:submission) { create(:submission, template:) } From e48652f425616e1809792bfd7bce2225af987f27 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 10:29:00 +0200 Subject: [PATCH 04/33] fix rubocop --- .rubocop.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index bda7960e..019c504b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -106,3 +106,10 @@ Rails/StrongParametersExpect: Rails/RedirectBackOrTo: Enabled: false + +Rails/UnknownEnv: + Environments: + - development + - test + - production + - local From 118f4a231be1799f124e3e4ec24fc225d3f06f85 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 17:45:46 +0200 Subject: [PATCH 05/33] detailed time format --- app/models/account_config.rb | 1 + app/views/submissions/_value.html.erb | 3 ++- app/views/submissions/show.html.erb | 5 ++-- app/views/submit_form/show.html.erb | 2 +- config/locales/i18n.yml | 27 +++++++++++++++++++ lib/submissions/generate_audit_trail.rb | 6 ++++- .../generate_preview_attachments.rb | 4 ++- .../generate_result_attachments.rb | 12 ++++++--- lib/submitters/form_configs.rb | 4 ++- 9 files changed, 54 insertions(+), 10 deletions(-) diff --git a/app/models/account_config.rb b/app/models/account_config.rb index 276194da..083aea07 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -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' diff --git a/app/views/submissions/_value.html.erb b/app/views/submissions/_value.html.erb index a0b3205d..5654080c 100644 --- a/app/views/submissions/_value.html.erb +++ b/app/views/submissions/_value.html.erb @@ -28,7 +28,8 @@ <% end %>
<% 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) %>
<% end %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 5d19f351..7940f337 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -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 %>
@@ -125,7 +126,7 @@ <% 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] } %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 9e73f00d..f2bfedd3 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -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 %>
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index c95a793a..7fc42386 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -1002,6 +1002,9 @@ en: &en events: range_with_total: "%{from}-%{to} of %{count} events" range_without_total: "%{from}-%{to} events" + time: + formats: + detailed: "%B %d, %Y %H:%M:%S" es: &es knowledge_based_authentication: Autenticación basada en el conocimiento @@ -1986,6 +1989,9 @@ es: &es events: range_with_total: "%{from}-%{to} de %{count} eventos" range_without_total: "%{from}-%{to} eventos" + time: + formats: + detailed: "%-d de %B de %Y %H:%M:%S" it: &it knowledge_based_authentication: Autenticazione basata sulla conoscenza @@ -2971,6 +2977,9 @@ it: &it events: range_with_total: "%{from}-%{to} di %{count} eventi" range_without_total: "%{from}-%{to} eventi" + time: + formats: + detailed: "%d %B %Y %H:%M:%S" fr: &fr knowledge_based_authentication: Authentification basée sur la connaissance @@ -3952,6 +3961,9 @@ fr: &fr events: range_with_total: "%{from}-%{to} sur %{count} événements" range_without_total: "%{from}-%{to} événements" + time: + formats: + detailed: "%A %d %B %Y %Hh%Mm%Ss" pt: &pt knowledge_based_authentication: Autenticação baseada em conhecimento @@ -4936,6 +4948,9 @@ pt: &pt events: range_with_total: "%{from}-%{to} de %{count} eventos" range_without_total: "%{from}-%{to} eventos" + time: + formats: + detailed: "%A, %d de %B de %Y, %H:%M:%Sh" de: &de knowledge_based_authentication: Wissensbasierte Authentifizierung @@ -5920,6 +5935,9 @@ de: &de events: range_with_total: "%{from}-%{to} von %{count} Ereignissen" range_without_total: "%{from}-%{to} Ereignisse" + time: + formats: + detailed: "%A, %d. %B %Y, %H:%M:%S Uhr" pl: require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia @@ -7289,6 +7307,9 @@ nl: &nl events: range_with_total: "%{from}-%{to} van %{count} gebeurtenissen" range_without_total: "%{from}-%{to} gebeurtenissen" + time: + formats: + detailed: "%d %B %Y %H:%M:%S" ar: require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" @@ -7586,12 +7607,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 diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 92fa783c..ed80a86d 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -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) ] diff --git a/lib/submissions/generate_preview_attachments.rb b/lib/submissions/generate_preview_attachments.rb index ff5a85a8..a5e22632 100644 --- a/lib/submissions/generate_preview_attachments.rb +++ b/lib/submissions/generate_preview_attachments.rb @@ -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 diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 580b9be1..1f0eb4ee 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -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 = @@ -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 diff --git a/lib/submitters/form_configs.rb b/lib/submitters/form_configs.rb index 38d18993..4db19b32 100644 --- a/lib/submitters/form_configs.rb +++ b/lib/submitters/form_configs.rb @@ -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 From 1b41af798da48d6af30031316667ce08c238922f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 19:42:47 +0200 Subject: [PATCH 06/33] update --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 962c2364..5e359ad0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -405,7 +405,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) From 1d2394e31e0e7d96f9eb35fea47d9bb30bbf39d8 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 19 Feb 2026 14:08:27 +0200 Subject: [PATCH 07/33] size limit --- lib/templates/create_attachments.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/templates/create_attachments.rb b/lib/templates/create_attachments.rb index 10b08ac7..b6297a76 100644 --- a/lib/templates/create_attachments.rb +++ b/lib/templates/create_attachments.rb @@ -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) } From 739e2abdf80b4591ce5dc504a23a54944a2db6c4 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 19 Feb 2026 14:24:02 +0200 Subject: [PATCH 08/33] vips size limit --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index f6412c78..b1341fcf 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 From 825322d489d9f9f3e39b62ee96063f717741bf2c Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 19 Feb 2026 15:54:15 +0200 Subject: [PATCH 09/33] adjust for custom domain --- app/controllers/application_controller.rb | 4 --- .../submissions_download_controller.rb | 4 +-- .../submit_form_download_controller.rb | 2 +- .../template_documents_controller.rb | 2 +- app/mailers/submitter_mailer.rb | 17 +++++++----- app/views/submissions/show.html.erb | 2 +- .../documents_copy_email.html.erb | 2 +- .../invitation_email.html.erb | 4 +-- app/views/templates/_submission.html.erb | 4 +-- app/views/templates_share_link/show.html.erb | 4 +-- config/initializers/active_storage.rb | 8 ++++++ lib/replace_email_variables.rb | 26 ++++++++++++------- 12 files changed, 48 insertions(+), 31 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 50823ca7..592c006d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index eb216bc5..39dee165 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -65,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) @@ -83,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) diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index 3ebdc5e2..af9cbeb4 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -33,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 diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb index b29a18f6..51fc4111 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/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index 18d2f570..976ddc78 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -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 diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 7940f337..acbaedad 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -21,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? %> - + <%= svg_icon('external_link', class: 'w-6 h-6') %> diff --git a/app/views/submitter_mailer/documents_copy_email.html.erb b/app/views/submitter_mailer/documents_copy_email.html.erb index 3117481d..9e230cf2 100644 --- a/app/views/submitter_mailer/documents_copy_email.html.erb +++ b/app/views/submitter_mailer/documents_copy_email.html.erb @@ -5,7 +5,7 @@

<%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %>

<%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %>

- <%= 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) %>

<%= t('thanks') %>,
<%= @current_account.name %> diff --git a/app/views/submitter_mailer/invitation_email.html.erb b/app/views/submitter_mailer/invitation_email.html.erb index 06e38f30..408016df 100644 --- a/app/views/submitter_mailer/invitation_email.html.erb +++ b/app/views/submitter_mailer/invitation_email.html.erb @@ -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]) %> -

<%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %>

+

<%= 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])) %>

<% end %> <% else %>

<%= t('hi_there') %>,

<%= 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) %>

-

<%= 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])) %>

+

<%= 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])) %>

<%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %>

<%= t('thanks') %>,
<%= @current_account.name %> diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb index 3c3348ec..bcfcd66c 100644 --- a/app/views/templates/_submission.html.erb +++ b/app/views/templates/_submission.html.erb @@ -81,7 +81,7 @@ <% elsif !submission.archived_at? && !template&.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if current_user.email == submitter.email %>

- + <% 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? %>
<% if current_user.email == submitter.email %> - + <% if t('sign_now').length < 12 %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> diff --git a/app/views/templates_share_link/show.html.erb b/app/views/templates_share_link/show.html.erb index 1511b4c2..43149a29 100644 --- a/app/views/templates_share_link/show.html.erb +++ b/app/views/templates_share_link/show.html.erb @@ -17,9 +17,9 @@ <% end %>
- + - <%= 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') %>
<% end %> diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index 78637ddb..2bcdc57b 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -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? diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index 4222e676..392c585d 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -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 From 65c275ac175789ac101de23485ef460bd1fb8402 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 10:16:06 +0200 Subject: [PATCH 10/33] rename --- .../templates_prefillable_fields/_form.html.erb | 2 +- config/locales/i18n.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/templates_prefillable_fields/_form.html.erb b/app/views/templates_prefillable_fields/_form.html.erb index bdfd92f7..a9eda336 100644 --- a/app/views/templates_prefillable_fields/_form.html.erb +++ b/app/views/templates_prefillable_fields/_form.html.erb @@ -4,7 +4,7 @@ <%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %>
<%= 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' %>
<%= 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' %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 7fc42386..4b3f7777 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -44,7 +44,7 @@ en: &en click_here_to_send_a_reset_password_email_html: ' 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 @@ -1025,7 +1025,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 @@ -2012,7 +2012,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" @@ -3000,7 +3000,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 @@ -3984,7 +3984,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 @@ -4980,7 +4980,7 @@ de: &de click_here_to_send_a_reset_password_email_html: ', 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 @@ -6355,7 +6355,7 @@ nl: &nl click_here_to_send_a_reset_password_email_html: 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 From ba84741a64aa19434798aebed58503c7e4185532 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 12:27:39 +0200 Subject: [PATCH 11/33] invite party via field --- app/controllers/api/submissions_controller.rb | 5 +- app/controllers/api/submitters_controller.rb | 7 ++- app/controllers/api/templates_controller.rb | 3 +- .../submit_form_invite_controller.rb | 4 +- app/controllers/templates_controller.rb | 3 +- .../templates_recipients_controller.rb | 4 +- app/views/submissions/_detailed_form.html.erb | 2 +- app/views/submissions/_email_form.html.erb | 2 +- app/views/submissions/_phone_form.html.erb | 2 +- .../_recipients.html.erb | 12 +++-- config/locales/i18n.yml | 7 +++ lib/submissions/create_from_submitters.rb | 9 +++- lib/submitters/submit_values.rb | 48 ++++++++++++++++++- lib/templates.rb | 1 + lib/templates/clone.rb | 36 +++++++------- 15 files changed, 114 insertions(+), 31 deletions(-) diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 22af24d9..77b4e615 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -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 diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index e56eb8b8..f28bf5e1 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -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 diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index c8211b7f..c3f1dd42 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -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, diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index bcc848ae..413e2b9a 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -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 diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 39b45044..0d6db6f7 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -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, diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb index 17a4bbb8..64fa58b2 100644 --- a/app/controllers/templates_recipients_controller.rb +++ b/app/controllers/templates_recipients_controller.rb @@ -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) diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index ac4c2607..7e38fecb 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -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? } %>
diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index a88e3a0b..2fee6706 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -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 %> diff --git a/app/views/submissions/_phone_form.html.erb b/app/views/submissions/_phone_form.html.erb index 68bf6d45..02900a53 100644 --- a/app/views/submissions/_phone_form.html.erb +++ b/app/views/submissions/_phone_form.html.erb @@ -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? } %>
diff --git a/app/views/templates_preferences/_recipients.html.erb b/app/views/templates_preferences/_recipients.html.erb index a8547669..4d45e38f 100644 --- a/app/views/templates_preferences/_recipients.html.erb +++ b/app/views/templates_preferences/_recipients.html.erb @@ -7,8 +7,8 @@
<% template.submitters.each_with_index do |submitter, index| %>
- <%= 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 %>
@@ -28,10 +28,16 @@ <% 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 %> <% else %> + <% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %> - <%= 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' %> + + <%= 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' %> + <%= 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 %> + + <%= 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 %> + <% end %>
<% if template.submitters.size == 2 %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 4b3f7777..1a5312c6 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -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 @@ -1490,6 +1491,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 @@ -2477,6 +2479,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 @@ -3465,6 +3468,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 @@ -4449,6 +4453,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 @@ -5436,6 +5441,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 @@ -6812,6 +6818,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 diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index b1e318cf..b367403c 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -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? diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 4b7f4dcb..a2992957 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -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 = { @@ -52,7 +53,11 @@ module Submitters 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 @@ -405,6 +410,47 @@ module Submitters end end + 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) diff --git a/lib/templates.rb b/lib/templates.rb index 2abd93dd..ad63302b 100644 --- a/lib/templates.rb +++ b/lib/templates.rb @@ -72,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 diff --git a/lib/templates/clone.rb b/lib/templates/clone.rb index e4b5fc60..1d213152 100644 --- a/lib/templates/clone.rb +++ b/lib/templates/clone.rb @@ -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 From fb5e13ee4ca1bc6e9d63c39aa2e6571ea523c04f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 18:01:12 +0200 Subject: [PATCH 12/33] adjust recipients form --- .rubocop.yml | 2 +- app/javascript/elements/toggle_attribute.js | 10 +++- app/javascript/elements/toggle_classes.js | 12 +++- .../_recipients.html.erb | 56 ++++++++++++++----- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 019c504b..9be296e5 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 diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js index e9ee3075..5ff6b7c3 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 ab96f293..332ac84f 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/views/templates_preferences/_recipients.html.erb b/app/views/templates_preferences/_recipients.html.erb index 4d45e38f..4e5eb61b 100644 --- a/app/views/templates_preferences/_recipients.html.erb +++ b/app/views/templates_preferences/_recipients.html.erb @@ -26,7 +26,20 @@ <% end %>
<% 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'] } %> + + + <%= 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' %> + + <% if invite_fields.present? %> + + <%= 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' %> + + <% end %> + + <% end %> <% else %> <% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %> @@ -35,32 +48,49 @@ <%= 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 %> - - <%= 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 %> - + <% if invite_fields.present? %> + + <%= 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 %> + + <% end %> <% end %>
<% if template.submitters.size == 2 %> - <% if local_assigns[:with_submission_requester] != false %> + <% if index == 0 && local_assigns[:with_submission_requester] != false %> - <% end %> - <% if index == 1 %> + <% elsif index == 1 %> + <% if local_assigns[:with_submission_requester] != false %> + + + + + + <% end %> <% end %> From b6635fcc4f078e7de69a6676430c39efe4f7b760 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 22:25:12 +0200 Subject: [PATCH 13/33] change user only when root --- config/dotenv.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/dotenv.rb b/config/dotenv.rb index b17d9aac..cb0f8e7c 100644 --- a/config/dotenv.rb +++ b/config/dotenv.rb @@ -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" From f1d146eca3faad1193aafe3c6acf052241b757a3 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 22:29:39 +0200 Subject: [PATCH 14/33] fix typos --- app/javascript/submission_form/i18n.js | 2 +- app/javascript/template_builder/i18n.js | 2 +- config/locales/i18n.yml | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 70567992..687215ff 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', diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index aea33d4b..51b45f78 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -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', diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 1a5312c6..b50ae270 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -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 @@ -597,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 @@ -626,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. @@ -738,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: '
Click here 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 @@ -756,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 guide 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 @@ -816,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: '"{template.name}" has been completed by {submission.submitters}' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "{template.name}" in the email attachments.' you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "{template.name}".' From 231fff5508f1bd8460c87256aae9dc9f6bf7bc33 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 21 Feb 2026 09:53:03 +0200 Subject: [PATCH 15/33] canvas blocked message --- app/javascript/submission_form/i18n.js | 14 ++++++++++++ .../submission_form/initials_step.vue | 11 +++++++++- .../submission_form/signature_step.vue | 22 ++++++++++++++++--- .../submission_form/validate_signature.js | 22 ++++++++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 687215ff..b9da958d 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -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 8e1cc1b5..814b9c12 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -150,6 +150,7 @@