diff --git a/Gemfile.lock b/Gemfile.lock index 984b23c4..bf6f2456 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -559,7 +559,7 @@ GEM unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uniform_notifier (1.16.0) - uri (1.0.2) + uri (1.0.3) useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 0a453397..9066684e 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -3,7 +3,7 @@ class="field-area flex absolute lg:text-base -outline-offset-1" dir="auto" :style="computedStyle" - :class="{ 'font-mono': field.preferences?.font === 'Courier', 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" + :class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" >
+ + + + + + -
+ - - - -
-
+ {{ formatNumber(123456789.567, format) }} + + +
-
+
+
+ - - - -
-
+ {{ method }} + + +
-
+
+
+ - -
-
+ {{ t(value) }} + + +
-
+
+
+ - -
-
+ {{ t('none') }} + + + +
-
+
+
+ - - -
-
  • - -
  • -
  • +
  • +
    + - {{ t('with_logo') }} - - -
  • + {{ t('none') }} + + + + +
  • -
  • +
  • +
    + - - -
  • - -
  • -
  • +
  • +
    + - {{ t('read_only') }} - - -
    + {{ formatDate(new Date(), format) }} + + +
    +
    + + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
    +
  • +
  • +
  • + -
  • -
  • +
  • +
  • +
  • -
  • - +
  • +
  • + + +
  • +
    + +
  • + + + {{ t('copy_to_all_pages') }} + +
  • \ No newline at end of file diff --git a/app/javascript/template_builder/font_modal.vue b/app/javascript/template_builder/font_modal.vue new file mode 100644 index 00000000..7b2dbac4 --- /dev/null +++ b/app/javascript/template_builder/font_modal.vue @@ -0,0 +1,290 @@ + + + diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index 62166bc7..390ba550 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -1,4 +1,5 @@ const en = { + font: 'Font', party: 'Party', method: 'Method', reorder_fields: 'Reorder fields', @@ -162,6 +163,7 @@ const en = { } const es = { + fuente: 'Fuente', party: 'Parte', method: 'Método', reorder_fields: 'Reordenar campos', @@ -325,6 +327,7 @@ const es = { } const it = { + font: 'Carattere', party: 'Parte', method: 'Metodo', reorder_fields: 'Riordina i campi', @@ -488,6 +491,7 @@ const it = { } const pt = { + fonte: 'Fonte', party: 'Parte', method: 'Método', reorder_fields: 'Reorganizar campos', @@ -651,6 +655,7 @@ const pt = { } const fr = { + font: 'Police', party: 'Partie', method: 'Méthode', reorder_fields: 'Réorganiser les champs', @@ -814,6 +819,7 @@ const fr = { } const de = { + font: 'Schriftart', party: 'Partei', method: 'Verfahren', reorder_fields: 'Felder neu anordnen', diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index d0f63ce7..b8e945d5 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -63,7 +63,7 @@ <%= t('add_signature_id_to_the_documents') %> - <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %> + <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %>
    <% end %> <% end %> @@ -75,7 +75,7 @@ <%= t('require_signing_reason') %> - <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %> + <%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()', disabled: can?(:manage, :cfr) %> <% end %> <% end %> diff --git a/app/views/submissions/_value.html.erb b/app/views/submissions/_value.html.erb index 5d898310..248b410d 100644 --- a/app/views/submissions/_value.html.erb +++ b/app/views/submissions/_value.html.erb @@ -1,7 +1,8 @@ <% align = field.dig('preferences', 'align') %> <% color = field.dig('preferences', 'color') %> <% font = field.dig('preferences', 'font') %> -width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> +<% font_type = field.dig('preferences', 'font_type') %> +width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>"> <% if field['type'] == 'signature' %>
    diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 3256ad36..b027c0c8 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -19,7 +19,7 @@ <%= svg_icon('external_link', class: 'w-6 h-6') %> - <% else %> + <% elsif signed_in? %> <%= link_to submission_events_path(@submission), class: 'white-button', data: { turbo_frame: :modal } do %> <%= svg_icon('logs', class: 'w-6 h-6') %> @@ -105,7 +105,18 @@ <% value = values[field['uuid']] %> <% value ||= field['default_value'] if field['type'] == 'heading' %> <% next if value.blank? %> - <%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> + <% if (mask = field.dig('preferences', 'mask').presence) && signed_in? && can?(:read, @submission) %> + + + + <%= render 'submissions/value', area:, field:, attachments_index:, value: Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', '), locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> + + + <% else %> + <%= render 'submissions/value', area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: %> + <% end %> <% end %>
    @@ -232,12 +243,21 @@ <% elsif field['type'] == 'checkbox' %> <%= svg_icon('check', class: 'w-6 h-6') %> - <% elsif field['type'] == 'number' %> - <%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %> - <% elsif field['type'] == 'date' %> - <%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %> <% else %> -
    <%= Array.wrap(value).join(', ') %>
    + <% if field['type'] == 'number' %> + <% value = NumberUtils.format_number(value, field.dig('preferences', 'format')) %> + <% elsif field['type'] == 'date' %> + <% value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), @submission.account.locale) %> + <% end %> + <% if (mask = field.dig('preferences', 'mask').presence) %> + <% if signed_in? && can?(:read, @submission) %> +
    + <% else %> +
    <%= Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') %>
    + <% end %> + <% else %> +
    <%= Array.wrap(value).join(', ') %>
    + <% end %> <% end %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 02967492..b4774423 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -58,7 +58,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', area:, field:, attachments_index: @attachments_index, value:, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id] %> + <%= render 'submissions/value', 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] %> <% end %> diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 147e5113..154deb25 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -187,7 +187,7 @@ Devise.setup do |config| # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. - config.remember_for = 2.years + config.remember_for = ENV.fetch('SESSION_REMEMBER_DAYS', '730').to_i.days # Invalidates all the remember me tokens when the user signs out. config.expire_all_remember_me_on_sign_out = true diff --git a/config/puma.rb b/config/puma.rb index 964a3298..ed99f0c9 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -33,8 +33,14 @@ environment ENV.fetch('RAILS_ENV', 'development') # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). -# -workers ENV.fetch('WEB_CONCURRENCY', 0) + +if ENV['WEB_CONCURRENCY_AUTO'] == 'true' + require 'etc' + + workers Etc.nprocessors +else + workers ENV.fetch('WEB_CONCURRENCY', 0) +end # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code diff --git a/config/routes.rb b/config/routes.rb index b5cedae1..57e02300 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -122,6 +122,8 @@ Rails.application.routes.draw do get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service put '/disk/:encoded_token' => 'active_storage/disk#update', as: :update_rails_disk_service post '/direct_uploads' => 'active_storage/direct_uploads#create', as: :rails_direct_uploads + + ActiveSupport.run_load_hooks(:multitenant_routes, self) end resources :start_form, only: %i[show update], path: 'd', param: 'slug' do diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index f787c5fb..3bc461a6 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -359,13 +359,14 @@ module Submissions value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) end - if field['type'] == 'number' - value = NumberUtils.format_number(value, - field.dig('preferences', 'format')) - end + value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number' value = value.join(', ') if value.is_a?(Array) + if (mask = field.dig('preferences', 'mask').presence) + value = TextUtils.mask_value(value, mask) + end + composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }], text_align: value.to_s.match?(RTL_REGEXP) ? :right : :left, padding: [0, 0, 10, 0]) diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 3039ac19..2fe766c0 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -4,12 +4,29 @@ module Submissions module GenerateResultAttachments FONT_SIZE = 11 FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf' + FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf' FONT_NAME = if File.exist?(FONT_PATH) FONT_PATH else 'Helvetica' end + FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH) + FONT_BOLD_PATH + else + 'Helvetica' + end + + FONT_ITALIC_NAME = 'Helvetica' + FONT_BOLD_ITALIC_NAME = 'Helvetica' + + FONT_VARIANS = { + none: FONT_NAME, + bold: FONT_BOLD_NAME, + italic: FONT_ITALIC_NAME, + bold_italic: FONT_BOLD_ITALIC_NAME + }.freeze + SIGN_REASON = 'Signed by %s with DocuSeal.com' RTL_REGEXP = TextUtils::RTL_REGEXP @@ -18,12 +35,15 @@ module Submissions TEXT_TOP_MARGIN = 1 MAX_PAGE_ROTATE = 20 - COURIER_FONT = 'Courier' - A4_SIZE = [595, 842].freeze TESTING_FOOTER = 'Testing Document - NOT LEGALLY BINDING' DEFAULT_FONTS = %w[Times Helvetica Courier].freeze + FONTS_LINE_HEIGHT = { + 'Times' => 1.4, + 'Helvetica' => 1.4, + 'Courier' => 1.6 + }.freeze MISSING_GLYPH_REPLACE = { '▪' => '-', @@ -192,8 +212,16 @@ module Submissions fill_color = field.dig('preferences', 'color').presence font_name = field.dig('preferences', 'font') + font_variant = (field.dig('preferences', 'font_type').presence || 'none').to_sym + font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS) - font = pdf.fonts.add(font_name) + + if font_variant != :none && font_name == FONT_NAME + font_name = FONT_VARIANS[font_variant] + font_variant = nil unless font_name.in?(DEFAULT_FONTS) + end + + font = pdf.fonts.add(font_name, variant: font_variant) value = submitter.values[field['uuid']] value = field['default_value'] if field['type'] == 'heading' @@ -391,6 +419,10 @@ module Submissions when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? } cell_width = area['cell_w'] * width + if (mask = field.dig('preferences', 'mask').presence) + value = TextUtils.mask_value(value, mask) + end + chars = TextUtils.maybe_rtl_reverse(value).chars chars = chars.reverse if field.dig('preferences', 'align') == 'right' @@ -440,8 +472,12 @@ module Submissions value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', ')) + if (mask = field.dig('preferences', 'mask').presence) + value = TextUtils.mask_value(value, mask) + end + text_params = { font:, fill_color:, font_size: } - text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT + text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1) text = HexaPDF::Layout::TextFragment.create(value, **text_params) @@ -450,7 +486,7 @@ module Submissions if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 text_params[:font_size] = (font_size / 1.4).to_i - text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT + text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1) text = HexaPDF::Layout::TextFragment.create(value, **text_params) @@ -461,7 +497,7 @@ module Submissions if preferences_font_size.blank? && box_height > (area['h'] * height) + 1 text_params[:font_size] = (font_size / 1.9).to_i - text_params[:line_height] = text_params[:font_size] * 1.6 if font_name == COURIER_FONT + text_params[:line_height] = text_params[:font_size] * (FONTS_LINE_HEIGHT[font_name] || 1) text = HexaPDF::Layout::TextFragment.create(value, **text_params) diff --git a/lib/text_utils.rb b/lib/text_utils.rb index 8d33f3ef..f286311f 100644 --- a/lib/text_utils.rb +++ b/lib/text_utils.rb @@ -2,6 +2,8 @@ module TextUtils RTL_REGEXP = /[\p{Hebrew}\p{Arabic}]/ + MASK_REGEXP = /[^\s\-_\[\]\(\)\+\?\.\,]/ + MASK_SYMBOL = 'X' module_function @@ -13,6 +15,24 @@ module TextUtils false end + def mask_value(text, unmask_size = 0) + if unmask_size.is_a?(Numeric) && !unmask_size.zero? && unmask_size.abs < text.length + if unmask_size.negative? + [ + text.first(text.length + unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL), + text.last(-unmask_size) + ].join + elsif unmask_size.positive? + [ + text.first(unmask_size), + text.last(text.length - unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL) + ].join + end + else + text.to_s.gsub(MASK_REGEXP, MASK_SYMBOL) + end + end + def maybe_rtl_reverse(text) if text.match?(RTL_REGEXP) TwitterCldr::Shared::Bidi diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb index c6e512fe..be486c65 100644 --- a/spec/system/signing_form_spec.rb +++ b/spec/system/signing_form_spec.rb @@ -772,6 +772,30 @@ RSpec.describe 'Signing Form', type: :system do end end + context 'when the masked field' do + let(:template) { create(:template, submitter_count: 2, account:, author:, only_field_types: %w[text]) } + let(:submission) { create(:submission, template: template) } + let!(:first_submitter) { create(:submitter, submission:, uuid: template.submitters[0]['uuid'], account:) } + let!(:second_submitter) { create(:submitter, submission:, uuid: template.submitters[1]['uuid'], account:) } + + it 'shows the masked value instead of the real value' do + field = submission.template_fields.find do |f| + f['name'] == 'First Name' && f['submitter_uuid'] == first_submitter.uuid + end + field['preferences']['mask'] = true + submission.save! + + visit submit_form_path(slug: first_submitter.slug) + + fill_in 'First Name', with: 'Jahn' + click_button 'Complete' + + visit submit_form_path(slug: second_submitter.slug) + + expect(page).to have_content('XXXX') + end + end + it 'sends completed email' do template = create(:template, account:, author:, only_field_types: %w[text signature]) submission = create(:submission, template:)