recipient fields config

pull/555/head
Pete Matsyburka 2 months ago
parent 964b5d4e74
commit d525e0597a

@ -53,14 +53,15 @@ class SubmissionsController < ApplicationController
else else
submissions_attrs = submissions_params[:submission].to_h.values submissions_attrs = submissions_params[:submission].to_h.values
submissions_attrs, = submissions_attrs, _, new_fields =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template) Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
Submissions.create_from_submitters(template: @template, Submissions.create_from_submitters(template: @template,
user: current_user, user: current_user,
source: :invite, source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
submissions_attrs:, submissions_attrs:,
new_fields:,
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
end end

@ -45,6 +45,7 @@ class AccountConfig < ApplicationRecord
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'
WITH_FILE_LINKS_KEY = 'with_file_links' WITH_FILE_LINKS_KEY = 'with_file_links'
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason' WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone' WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'

@ -12,7 +12,8 @@
</div> </div>
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4"> <div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
<% submitters.each_with_index do |item, index| %> <% submitters.each_with_index do |item, index| %>
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] } %> <% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] }.presence %>
<% prefillable_fields ||= local_assigns[:recipient_form_fields].presence %>
<submitter-item class="form-control"> <submitter-item class="form-control">
<% if submitters.size > 1 %> <% if submitters.size > 1 %>
<label class="label pt-0 pb-1"> <label class="label pt-0 pb-1">
@ -29,14 +30,14 @@
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>"> <div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %> <%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
<% has_phone_field = true %> <% has_phone_field = true %>
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>"> <custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
</custom-validation> </custom-validation>
@ -60,16 +61,16 @@
<% end %> <% end %>
<% prefillable_fields.each do |field| %> <% prefillable_fields.each do |field| %>
<% if field['type'] == 'checkbox' %> <% if field['type'] == 'checkbox' %>
<label for="detailed_field_<%= field['uuid'] %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white"> <label for="detailed_field_<%= index %>_<%= field['uuid'] || field['name'].parameterize %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span> <span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid']}]", id: "detailed_field_#{field['uuid']}", class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %> <%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", id: "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}", class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
</label> </label>
<% elsif field['type'] == 'select' || field['type'] == 'radio' %> <% elsif field['type'] == 'select' || field['type'] == 'radio' %>
<%= select_tag "submission[1][submitters][][values][#{field['uuid']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: "detailed_field_#{field['uuid']}", class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %> <%= select_tag "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}", class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
<% elsif field['type'] == 'date' %> <% elsif field['type'] == 'date' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %> <%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}", required: field['required'] %>
<% elsif field['type'] != 'phone' %> <% elsif field['type'] != 'phone' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %> <%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid'] || field['name']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}", required: field['required'] %>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
@ -79,7 +80,7 @@
</div> </div>
</div> </div>
</div> </div>
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? %> <% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? && local_assigns[:recipient_form_fields].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem"> <a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span> <span><%= t('add_new') %></span>

@ -1,7 +1,8 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %> <% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %> <% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %> <% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? %> <% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %> <%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %> <% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block"> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
@ -26,7 +27,7 @@
</div> </div>
<% end %> <% end %>
<div id="detailed" class="<%= 'hidden' unless only_detailed %>"> <div id="detailed" class="<%= 'hidden' unless only_detailed %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields: %> <%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields: %>
</div> </div>
<div id="list" class="hidden"> <div id="list" class="hidden">
<%= render 'list_form', template: @template %> <%= render 'list_form', template: @template %>

@ -101,6 +101,10 @@ module Accounts
new_template new_template
end end
def load_recipient_form_fields(_account)
[]
end
def load_signing_pkcs(account) def load_signing_pkcs(account)
cert_data = cert_data =
if Docuseal.multitenant? if Docuseal.multitenant?

@ -133,9 +133,9 @@ module Submissions
end end
def create_from_submitters(template:, user:, submissions_attrs:, source:, with_template: true, def create_from_submitters(template:, user:, submissions_attrs:, source:, with_template: true,
submitters_order: DEFAULT_SUBMITTERS_ORDER, params: {}) submitters_order: DEFAULT_SUBMITTERS_ORDER, params: {}, new_fields: nil)
Submissions::CreateFromSubmitters.call( Submissions::CreateFromSubmitters.call(
template:, user:, submissions_attrs:, source:, submitters_order:, params:, with_template: template:, user:, submissions_attrs:, source:, submitters_order:, params:, with_template:, new_fields:
) )
end end

@ -7,7 +7,8 @@ module Submissions
module_function module_function
# rubocop:disable Metrics # rubocop:disable Metrics
def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {}, with_template: true) def call(template:, user:, submissions_attrs:, source:, submitters_order:, params: {}, with_template: true,
new_fields: nil)
preferences = Submitters.normalize_preferences(user.account, user, params) preferences = Submitters.normalize_preferences(user.account, user, params)
submissions = Array.wrap(submissions_attrs).filter_map do |attrs| submissions = Array.wrap(submissions_attrs).filter_map do |attrs|
@ -67,7 +68,7 @@ module Submissions
preferences: preferences.merge(submission_preferences)) preferences: preferences.merge(submission_preferences))
end end
maybe_set_template_fields(submission, attrs[:submitters], with_template:) maybe_set_template_fields(submission, attrs[:submitters], with_template:, new_fields:)
if submission.submitters.size > template.submitters.size if submission.submitters.size > template.submitters.size
raise BaseError, 'Defined more signing parties than in template' raise BaseError, 'Defined more signing parties than in template'
@ -92,7 +93,6 @@ module Submissions
submissions submissions
end end
# rubocop:enable Metrics
def maybe_enqueue_expire_at(submissions) def maybe_enqueue_expire_at(submissions)
submissions.each do |submission| submissions.each do |submission|
@ -135,7 +135,8 @@ module Submissions
}.compact_blank }.compact_blank
end end
def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil, with_template: true) def maybe_set_template_fields(submission, submitters_attrs, default_submitter_uuid: nil, with_template: true,
new_fields: nil)
template_fields = (submission.template_fields || submission.template.fields).deep_dup template_fields = (submission.template_fields || submission.template.fields).deep_dup
submitters = submission.template_submitters || submission.template.submitters submitters = submission.template_submitters || submission.template.submitters
@ -149,9 +150,9 @@ module Submissions
process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid) process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid)
end end
if template_fields != (submission.template_fields || submission.template.fields) || if template_fields != (submission.template_fields || submission.template.fields) || new_fields.present? ||
submitters_attrs.any? { |e| e[:completed].present? } || !with_template || submission.variables.present? submitters_attrs.any? { |e| e[:completed].present? } || !with_template || submission.variables.present?
submission.template_fields = template_fields submission.template_fields = new_fields ? new_fields + template_fields : template_fields
submission.template_schema = submission.template.schema if submission.template_schema.blank? submission.template_schema = submission.template.schema if submission.template_schema.blank?
submission.variables_schema = submission.template.variables_schema if submission.template && submission.variables_schema = submission.template.variables_schema if submission.template &&
submission.variables_schema.blank? submission.variables_schema.blank?
@ -159,6 +160,7 @@ module Submissions
submission submission
end end
# rubocop:enable Metrics
def merge_submitters_and_fields(submitter_attrs, template_submitters, template_fields) def merge_submitters_and_fields(submitter_attrs, template_submitters, template_fields)
selected_submitters = submitter_attrs[:roles].map do |role| selected_submitters = submitter_attrs[:roles].map do |role|

@ -4,21 +4,23 @@ module Submissions
module NormalizeParamUtils module NormalizeParamUtils
module_function module_function
def normalize_submissions_params!(submissions_params, template) def normalize_submissions_params!(submissions_params, template, add_fields: false)
attachments = [] attachments = []
fields = []
Array.wrap(submissions_params).each do |submission| Array.wrap(submissions_params).each do |submission|
submission[:submitters].each_with_index do |submitter, index| submission[:submitters].each_with_index do |submitter, index|
_, new_attachments = normalize_submitter_params!(submitter, template, index) _, new_attachments, new_fields = normalize_submitter_params!(submitter, template, index, add_fields:)
attachments.push(*new_attachments) attachments.push(*new_attachments)
fields.push(*new_fields)
end end
end end
[submissions_params, attachments] [submissions_params, attachments, fields]
end end
def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil) def normalize_submitter_params!(submitter_params, template, index = nil, for_submitter: nil, add_fields: false)
with_values = submitter_params[:values].present? with_values = submitter_params[:values].present?
default_values = with_values ? submitter_params[:values] : {} default_values = with_values ? submitter_params[:values] : {}
@ -30,18 +32,19 @@ module Submissions
return submitter_params if default_values.blank? return submitter_params if default_values.blank?
values, new_attachments = values, new_attachments, new_fields =
Submitters::NormalizeValues.call(template, Submitters::NormalizeValues.call(template,
default_values, default_values,
submitter_name: submitter_params[:role] || submitter_name: submitter_params[:role] ||
template.submitters.dig(index, 'name'), template.submitters.dig(index, 'name'),
role_names: submitter_params[:roles], role_names: submitter_params[:roles],
for_submitter:, for_submitter:,
add_fields:,
throw_errors: !with_values) throw_errors: !with_values)
submitter_params[:values] = values submitter_params[:values] = values
[submitter_params, new_attachments] [submitter_params, new_attachments, new_fields]
end end
def save_default_value_attachments!(attachments, submitters) def save_default_value_attachments!(attachments, submitters)

@ -17,7 +17,9 @@ module Submitters
module_function module_function
def call(template, values, submitter_name: nil, role_names: nil, for_submitter: nil, throw_errors: false) # rubocop:disable Metrics
def call(template, values, submitter_name: nil, role_names: nil, for_submitter: nil, throw_errors: false,
add_fields: false)
fields = fields =
if role_names.present? if role_names.present?
fetch_roles_fields(template, roles: role_names) fetch_roles_fields(template, roles: role_names)
@ -29,6 +31,8 @@ module Submitters
fields_name_index = build_fields_index(fields) fields_name_index = build_fields_index(fields)
attachments = [] attachments = []
new_fields = []
recipient_form_fields = nil
normalized_values = values.to_h.each_with_object({}) do |(key, value), acc| normalized_values = values.to_h.each_with_object({}) do |(key, value), acc|
next if key.blank? next if key.blank?
@ -40,7 +44,22 @@ module Submitters
if value_fields.blank? if value_fields.blank?
value_fields = fields_name_index[key].presence || fields_name_index[key.to_s.downcase] value_fields = fields_name_index[key].presence || fields_name_index[key.to_s.downcase]
raise(UnknownFieldName, "Unknown field: #{key}") if value_fields.blank? && throw_errors if value_fields.blank?
if add_fields && (recipient_form_fields ||= Accounts.load_recipient_form_fields(template.account))
new_field = recipient_form_fields.to_a.find { |e| e['name'] == key }.deep_dup
if new_field && fields.present?
new_field = new_field.merge('uuid' => SecureRandom.uuid,
'readonly' => true,
'submitter_uuid' => fields.first['submitter_uuid'])
new_fields.push(new_field)
value_fields = [new_field]
end
elsif throw_errors
raise(UnknownFieldName, "Unknown field: #{key}")
end
end
end end
next if value_fields.blank? next if value_fields.blank?
@ -59,8 +78,9 @@ module Submitters
end end
end end
[normalized_values, attachments] [normalized_values, attachments, new_fields]
end end
# rubocop:enable Metrics
def normalize_value(field, value) def normalize_value(field, value)
if field['type'] == 'checkbox' if field['type'] == 'checkbox'

Loading…
Cancel
Save