diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 0ccc5b39..88a606f4 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -109,7 +109,7 @@ module Api submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, - :title, :description, + :title, :description, :prefillable, { preferences: {}, conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 9d4de051..4f31322c 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -32,10 +32,7 @@ class SubmissionsController < ApplicationController def create save_template_message(@template, params) if params[:save_message] == '1' - if params[:is_custom_message] != '1' - params.delete(:subject) - params.delete(:body) - end + [params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1' submissions = if params[:emails].present? @@ -46,11 +43,16 @@ class SubmissionsController < ApplicationController emails: params[:emails], params: params.merge('send_completed_email' => true)) else + submissions_attrs = submissions_params[:submission].to_h.values + + submissions_attrs, = + Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template) + Submissions.create_from_submitters(template: @template, user: current_user, source: :invite, submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', - submissions_attrs: submissions_params[:submission].to_h.values, + submissions_attrs:, params: params.merge('send_completed_email' => true)) end @@ -62,9 +64,8 @@ class SubmissionsController < ApplicationController redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added') rescue Submissions::CreateFromSubmitters::BaseError => e - render turbo_stream: turbo_stream.replace(:submitters_error, - partial: 'submissions/error', - locals: { error: e.message }), + render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error', + locals: { error: e.message }), status: :unprocessable_entity end @@ -95,7 +96,7 @@ class SubmissionsController < ApplicationController end def submissions_params - params.permit(submission: { submitters: [%i[uuid email phone name]] }) + params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] }) end def load_template diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index fba51b90..73e68e85 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -120,7 +120,7 @@ class TemplatesController < ApplicationController submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, - :title, :description, + :title, :description, :prefillable, { preferences: {}, conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index 31ac7190..cbee0993 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -27,7 +27,7 @@ class TemplatesPreferencesController < ApplicationController completed_redirect_url validate_unique_submitters require_all_submitters submitters_order require_phone_2fa default_expire_at_duration shared_link_2fa - default_expire_at + default_expire_at request_email_enabled completed_notification_email_subject completed_notification_email_body completed_notification_email_enabled completed_notification_email_attach_audit] + [completed_message: %i[title body], diff --git a/app/controllers/templates_prefillable_fields_controller.rb b/app/controllers/templates_prefillable_fields_controller.rb new file mode 100644 index 00000000..6202ca5c --- /dev/null +++ b/app/controllers/templates_prefillable_fields_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class TemplatesPrefillableFieldsController < ApplicationController + PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze + + load_and_authorize_resource :template + + def create + authorize!(:update, @template) + + field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] } + + if params[:prefillable] == 'false' + field.delete('prefillable') + field.delete('readonly') + elsif params[:prefillable] == 'true' + field['prefillable'] = true + field['readonly'] = true + end + + @template.save! + + render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list', + locals: { template: @template }) + end +end diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index 9622b503..ad8ab7d5 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -141,6 +141,7 @@ :with-required="false" :with-areas="false" :with-signature-id="withSignatureId" + :with-prefillable="withPrefillable" @click-formula="isShowFormulaModal = true" @click-font="isShowFontModal = true" @click-description="isShowDescriptionModal = true" @@ -353,6 +354,11 @@ export default { required: false, default: null }, + withPrefillable: { + type: Boolean, + required: false, + default: false + }, defaultSubmitters: { type: Array, required: false, diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index ef0748f6..cd2339f4 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -331,6 +331,7 @@ :default-fields="[...defaultRequiredFields, ...defaultFields]" :allow-draw="!onlyDefinedFields || drawField" :with-signature-id="withSignatureId" + :with-prefillable="withPrefillable" :data-document-uuid="document.uuid" :default-submitters="defaultSubmitters" :drag-field-placeholder="fieldsDragFieldRef.value || dragField" @@ -438,6 +439,7 @@ :field-types="fieldTypes" :with-sticky-submitters="withStickySubmitters" :with-signature-id="withSignatureId" + :with-prefillable="withPrefillable" :only-defined-fields="onlyDefinedFields" :editable="editable" :show-tour-start-form="showTourStartForm" @@ -543,7 +545,6 @@ export default { withPayment: this.withPayment, isPaymentConnected: this.isPaymentConnected, withFormula: this.withFormula, - withSignatureId: this.withSignatureId, withConditions: this.withConditions, isInlineSize: this.isInlineSize, defaultDrawFieldType: this.defaultDrawFieldType, @@ -802,6 +803,13 @@ export default { language () { return this.locale.split('-')[0].toLowerCase() }, + withPrefillable () { + if (this.template.fields) { + return this.template.fields.some((f) => f.prefillable) + } else { + return false + } + }, isInlineSize () { return CSS.supports('container-type: size') }, diff --git a/app/javascript/template_builder/document.vue b/app/javascript/template_builder/document.vue index abba5052..e512fb2c 100644 --- a/app/javascript/template_builder/document.vue +++ b/app/javascript/template_builder/document.vue @@ -11,6 +11,7 @@ :areas="areasIndex[index]" :allow-draw="allowDraw" :with-signature-id="withSignatureId" + :with-prefillable="withPrefillable" :is-drag="isDrag" :with-field-placeholder="withFieldPlaceholder" :default-fields="defaultFields" @@ -72,6 +73,11 @@ export default { required: false, default: null }, + withPrefillable: { + type: Boolean, + required: false, + default: false + }, drawFieldType: { type: String, required: false, diff --git a/app/javascript/template_builder/field.vue b/app/javascript/template_builder/field.vue index f9e1c0e9..b12a33d9 100644 --- a/app/javascript/template_builder/field.vue +++ b/app/javascript/template_builder/field.vue @@ -126,6 +126,7 @@ :default-field="defaultField" :editable="editable" :with-signature-id="withSignatureId" + :with-prefillable="withPrefillable" :background-color="dropdownBgColor" @click-formula="isShowFormulaModal = true" @click-font="isShowFontModal = true" @@ -308,6 +309,11 @@ export default { required: false, default: null }, + withPrefillable: { + type: Boolean, + required: false, + default: false + }, withOptions: { type: Boolean, required: false, diff --git a/app/javascript/template_builder/field_settings.vue b/app/javascript/template_builder/field_settings.vue index b78c6b4e..22821f4c 100644 --- a/app/javascript/template_builder/field_settings.vue +++ b/app/javascript/template_builder/field_settings.vue @@ -406,6 +406,21 @@ {{ t('read_only') }} +
  • + +

  • <%= 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? } %> @@ -11,6 +12,7 @@
    <% submitters.each_with_index do |item, index| %> + <% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] } %> <% if submitters.size > 1 %> <% end %> - - "> - <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %> - - -
    - - "> - " value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>"> + <% if prefillable_fields.blank? %> + + "> + <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %> - - "> - <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", 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 %> +
    + + "> + " value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>"> + + + <% has_phone_field = true %> + + "> + <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", 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 %> + + +
    + <% end %> + <% if prefillable_fields.present? %> + + "> + -
    + <% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %> + <% has_phone_field = true %> + + "> + <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %> + + + <% end %> + <% prefillable_fields.each do |field| %> + <% if field['type'] == 'checkbox' %> + + <% 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'] %> + <% 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'] %> + <% 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'] %> + <% end %> + <% end %> + <% end %>
    <% end %>
    - <% if params[:selfsign].blank? %> + <% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? %> <%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %> <%= t('add_new') %> @@ -51,7 +85,9 @@
    <%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %> <%= render 'send_email', f:, template: %> - <%= render 'send_sms', f: %> + <% if has_phone_field %> + <%= render 'send_sms', f: %> + <% end %>
    <%= f.button button_title(title: t('add_recipients')), class: 'base-button' %> diff --git a/app/views/submissions/_send_email.html.erb b/app/views/submissions/_send_email.html.erb index 785eac90..d7f357c0 100644 --- a/app/views/submissions/_send_email.html.erb +++ b/app/views/submissions/_send_email.html.erb @@ -4,7 +4,7 @@ <% can_send_emails = Accounts.can_send_emails?(current_account) %>
    <%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %> - <%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] %> + <%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %> <%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %> <% end %>
    diff --git a/app/views/submissions/new.html.erb b/app/views/submissions/new.html.erb index b9874d75..ccea6b4b 100644 --- a/app/views/submissions/new.html.erb +++ b/app/views/submissions/new.html.erb @@ -1,11 +1,13 @@ <% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %> +<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %> +<% only_detailed = require_phone_2fa || prefillable_fields.present? %> <%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %> - <% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? 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 %>
    <% options.each_with_index do |(label, value), index| %>
    - <%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> + <%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> @@ -14,7 +16,7 @@
    - <% unless require_phone_2fa %> + <% unless only_detailed %>
    <%= render 'email_form', template: @template %>
    @@ -22,8 +24,8 @@ <%= render 'phone_form', template: @template %>
    <% end %> -
    - <%= render 'detailed_form', template: @template, require_phone_2fa: %> +
    + <%= render 'detailed_form', template: @template, require_phone_2fa:, prefillable_fields: %>
    + <%= render 'templates_prefillable_fields/form', template: @template %> <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %> <% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %> @@ -68,7 +69,7 @@ <%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
    <%= ff.fields_for :completed_message, ff.object.completed_message do |fff| %> -
    +
    <%= fff.label :body, t('completion_message'), class: 'label' %> <%= fff.text_area :body, required: false, class: 'base-input w-full py-2', dir: 'auto' %> @@ -162,6 +163,14 @@ <% end %>
    <% end %> + <%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %> +
    + + <%= 'Send signature request email' %> + + <%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %> +
    + <% end %>
    <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
    diff --git a/app/views/templates_prefillable_fields/_form.html.erb b/app/views/templates_prefillable_fields/_form.html.erb new file mode 100644 index 00000000..bdfd92f7 --- /dev/null +++ b/app/views/templates_prefillable_fields/_form.html.erb @@ -0,0 +1,16 @@ +<% select_fields = template.fields.filter_map { |f| [f['name'], f['uuid']] if f['name'].present? && f['type'].in?(TemplatesPrefillableFieldsController::PREFILLABLE_FIELD_TYPES) } %> +<% if select_fields.present? %> +
    + <%= 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' %> +
    + <%= 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' %> +
    +
    + <% end %> + <%= render partial: 'templates_prefillable_fields/list', locals: { template: } %> +
    +<% end %> diff --git a/app/views/templates_prefillable_fields/_list.html.erb b/app/views/templates_prefillable_fields/_list.html.erb new file mode 100644 index 00000000..416619c6 --- /dev/null +++ b/app/views/templates_prefillable_fields/_list.html.erb @@ -0,0 +1,6 @@ +
    + <% template.fields.each do |f| %> + <% next unless f['prefillable'] %> + <%= button_to button_title(title: f['name'].presence || f['type'].capitalize, disabled_with: f['name'].presence || f['type'].capitalize, icon: svg_icon('x', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), template_prefillable_fields_path(template), params: { field_uuid: f['uuid'], prefillable: 'false' }, class: 'badge badge-lg badge-primary space-x-1 pr-3 pl-2', form: { data: { close_on_submit: false } } %> + <% end %> +
    diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index c56f70d3..9c263909 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -23,6 +23,8 @@ en: &en pro: Pro thanks: Thanks private: Private + select: Select + invite_form_fields: Invite form fields default_parties: Default parties authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token stripe_integration: Stripe Integration @@ -896,6 +898,8 @@ en: &en range_without_total: "%{from}-%{to} events" es: &es + select: Seleccionar + invite_form_fields: Invitar campos del formulario pro: Pro default_parties: Partes predeterminadas authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token @@ -1772,6 +1776,8 @@ es: &es range_without_total: "%{from}-%{to} eventos" it: &it + select: Seleziona + invite_form_fields: Invita campi modulo pro: Pro default_parties: Parti predefiniti authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token" @@ -2648,6 +2654,8 @@ it: &it range_without_total: "%{from}-%{to} eventi" fr: &fr + select: Sélectionner + invite_form_fields: Inviter des champs de formulaire 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 @@ -3527,6 +3535,8 @@ fr: &fr range_without_total: "%{from} à %{to} événements" pt: &pt + select: Selecionar + invite_form_fields: Convidar campos do formulário pro: Pro default_parties: Partes padrão authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token @@ -4404,6 +4414,8 @@ pt: &pt range_without_total: "%{from}-%{to} eventos" de: &de + select: Auswählen + invite_form_fields: Formularfelder einladen pro: Pro default_parties: Standardparteien authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token diff --git a/config/routes.rb b/config/routes.rb index 20015262..76ed7af9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -108,6 +108,7 @@ Rails.application.routes.draw do resource :preferences, only: %i[show create], controller: 'templates_preferences' resource :share_link, only: %i[show create], controller: 'templates_share_link' resources :recipients, only: %i[create], controller: 'templates_recipients' + resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields' resources :submissions_export, only: %i[index new] end resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'