From e6ca6dfd859e786d09a4ecd5ea25e05a9f176f45 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Mon, 25 Nov 2024 18:59:37 +0200 Subject: [PATCH 01/10] fix missing file format --- app/controllers/templates_uploads_controller.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index 75b4e620a..4cd0072fa 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -55,11 +55,12 @@ def create_file_params_from_url tempfile.write(DownloadUtils.call(params[:url]).body) tempfile.rewind + filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present? + filename ||= File.basename(URI.decode_www_form_component(params[:url])) + file = ActionDispatch::Http::UploadedFile.new( tempfile:, - filename: File.basename( - URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*' - ), + filename:, type: Marcel::MimeType.for(tempfile) ) From 2552b24f21916008bde2aa7f0fc573d6c2d7ba46 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 27 Nov 2024 21:59:45 +0200 Subject: [PATCH 02/10] add documents copy attachment toggle --- .../templates_preferences_controller.rb | 1 + app/mailers/submitter_mailer.rb | 7 +++++-- .../_documents_copy_email_form.html.erb | 10 ++++++++-- app/views/templates_preferences/show.html.erb | 20 ++++++++++++------- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index 76af325b2..24aedbd4b 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -22,6 +22,7 @@ def template_params preferences: %i[bcc_completed request_email_subject request_email_body documents_copy_email_subject documents_copy_email_body documents_copy_email_enabled documents_copy_email_attach_audit + documents_copy_email_attach_documents completed_notification_email_attach_documents completed_redirect_url submitters_order diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index 2c5caa2ce..817732614 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -106,8 +106,11 @@ def documents_copy_email(submitter, to: nil, sig: false) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) - @documents = add_completed_email_attachments!( - submitter, with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false && + add_completed_email_attachments!( + submitter, + with_documents: @submitter.template.preferences['documents_copy_email_attach_documents'] != false && + (@email_config.nil? || @email_config.value['attach_documents'] != false), + with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false && (@email_config.nil? || @email_config.value['attach_audit_log'] != false) ) diff --git a/app/views/personalization_settings/_documents_copy_email_form.html.erb b/app/views/personalization_settings/_documents_copy_email_form.html.erb index 033100578..6b9e0fc35 100644 --- a/app/views/personalization_settings/_documents_copy_email_form.html.erb +++ b/app/views/personalization_settings/_documents_copy_email_form.html.erb @@ -8,7 +8,7 @@
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= f.hidden_field :key %> - <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log')) do |ff| %> + <%= f.fields_for :value, Struct.new(:subject, :body, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'attach_audit_log', 'attach_documents')) do |ff| %>
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> @@ -24,7 +24,13 @@ <%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
-
+
+ + <%= t('attach_documents') %> + + <%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %> +
+
<%= t('attach_audit_log_pdf') %> diff --git a/app/views/templates_preferences/show.html.erb b/app/views/templates_preferences/show.html.erb index 4473213cf..72437309b 100644 --- a/app/views/templates_preferences/show.html.erb +++ b/app/views/templates_preferences/show.html.erb @@ -117,7 +117,7 @@ <%= 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_DOCUMENTS_COPY_EMAIL_KEY).value %> - <%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false) do |ff| %> + <%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit, :documents_copy_email_attach_documents).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['documents_copy_email_attach_documents'] != false) do |ff| %>
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %> <%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %> @@ -133,6 +133,12 @@ <%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
+
+ + <%= t('attach_documents_to_the_email') %> + + <%= ff.check_box :documents_copy_email_attach_documents, { checked: ff.object.documents_copy_email_attach_documents != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_documents'] == false }, 'true', 'false' %> +
<%= t('attach_audit_log_pdf_to_the_email') %> @@ -182,21 +188,21 @@
- <%= t('send_emails_automatically_on_completion') %> + <%= t('attach_documents_to_the_email') %> - <%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %> + <%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
- <%= t('attach_documents_to_the_email') %> + <%= t('attach_audit_log_pdf_to_the_email') %> - <%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_documents'] == false }, 'true', 'false' %> + <%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
- <%= t('attach_audit_log_pdf_to_the_email') %> + <%= t('send_emails_automatically_on_completion') %> - <%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', onchange: 'this.form.requestSubmit()', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %> + <%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
<% end %>
From 36c64d1de44733869eab1f24bf10b045fb14c647 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 28 Nov 2024 14:57:20 +0200 Subject: [PATCH 03/10] fix counters --- app/views/templates/show.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/templates/show.html.erb b/app/views/templates/show.html.erb index 637292876..36ba1c42c 100644 --- a/app/views/templates/show.html.erb +++ b/app/views/templates/show.html.erb @@ -36,7 +36,7 @@ <%= t('all') %>
- <%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).count %> + <%= params[:status].blank? && filter_params.blank? ? @pagy.count : @base_submissions.unscope(:group, :order).select(:id).distinct.count %>
@@ -45,7 +45,7 @@ <%= t('pending') %>
- <%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).count %> + <%= params[:status] == 'pending' && filter_params.blank? ? @pagy.count : @base_submissions.pending.unscope(:group, :order).select(:id).distinct.count %>
@@ -54,7 +54,7 @@ <%= t('completed') %>
- <%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).count %> + <%= params[:status] == 'completed' && filter_params.blank? ? @pagy.count : @base_submissions.completed.unscope(:group, :order).select(:id).distinct.count %>
From 75683631f036098733adcd7d15247d124a555aff Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Wed, 27 Nov 2024 21:47:53 +0200 Subject: [PATCH 04/10] add logical 'or' to field conditions --- app/controllers/api/templates_controller.rb | 2 +- app/controllers/templates_controller.rb | 2 +- app/javascript/submission_form/form.vue | 61 +++--- .../template_builder/conditions_modal.vue | 15 +- app/javascript/template_builder/i18n.js | 24 ++- lib/submitters/submit_values.rb | 50 +++-- spec/system/signing_form_spec.rb | 193 ++++++++++++++++++ 7 files changed, 291 insertions(+), 56 deletions(-) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 80af5acf9..bf02f8305 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -105,7 +105,7 @@ def template_params :required, :readonly, :default_value, :title, :description, { preferences: {}, - conditions: [%i[field_uuid value action]], + conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], validation: %i[message pattern], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 04d63e1cc..306476b29 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -115,7 +115,7 @@ def template_params :required, :readonly, :default_value, :title, :description, { preferences: {}, - conditions: [%i[field_uuid value action]], + conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], validation: %i[message pattern], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index ac1da8a60..075f55db0 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -1027,35 +1027,46 @@ export default { }, checkFieldConditions (field) { if (field.conditions?.length) { - return field.conditions.reduce((acc, c) => { - const field = this.fieldsUuidIndex[c.field_uuid] - - if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) { - return false + const result = field.conditions.reduce((acc, cond) => { + if (cond.operation === 'or') { + acc.push(acc.pop() || this.checkFieldCondition(cond)) + } else { + acc.push(this.checkFieldCondition(cond)) } - if (['empty', 'unchecked'].includes(c.action)) { - return acc && isEmpty(this.values[c.field_uuid]) - } else if (['not_empty', 'checked'].includes(c.action)) { - return acc && !isEmpty(this.values[c.field_uuid]) - } else if (['equal', 'contains'].includes(c.action) && field) { - if (field.options) { - const option = field.options.find((o) => o.uuid === c.value) - const values = [this.values[c.field_uuid]].flat() + return acc + }, []) - return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) - } else { - return acc && [this.values[c.field_uuid]].flat().includes(c.value) - } - } else if (['not_equal', 'does_not_contain'].includes(c.action) && field) { - const option = field.options.find((o) => o.uuid === c.value) - const values = [this.values[c.field_uuid]].flat() + return !result.includes(false) + } else { + return true + } + }, + checkFieldCondition (condition) { + const field = this.fieldsUuidIndex[condition.field_uuid] - return acc && !values.includes(this.optionValue(option, field.options.indexOf(option))) - } else { - return acc - } - }, true) + if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) { + return false + } + + if (['empty', 'unchecked'].includes(condition.action)) { + return isEmpty(this.values[condition.field_uuid]) + } else if (['not_empty', 'checked'].includes(condition.action)) { + return !isEmpty(this.values[condition.field_uuid]) + } else if (['equal', 'contains'].includes(condition.action) && field) { + if (field.options) { + const option = field.options.find((o) => o.uuid === condition.value) + const values = [this.values[condition.field_uuid]].flat() + + return values.includes(this.optionValue(option, field.options.indexOf(option))) + } else { + return [this.values[condition.field_uuid]].flat().includes(condition.value) + } + } else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) { + const option = field.options.find((o) => o.uuid === condition.value) + const values = [this.values[condition.field_uuid]].flat() + + return !values.includes(this.optionValue(option, field.options.indexOf(option))) } else { return true } diff --git a/app/javascript/template_builder/conditions_modal.vue b/app/javascript/template_builder/conditions_modal.vue index 61ae8939e..3fbe13133 100644 --- a/app/javascript/template_builder/conditions_modal.vue +++ b/app/javascript/template_builder/conditions_modal.vue @@ -29,15 +29,26 @@ >{{ t('available_in_pro') }}
-
+
+
+ +
{{ t('condition') }} {{ cindex + 1 }} diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index 7070c9e5b..e20c20fb6 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -148,7 +148,9 @@ const en = { preferences: 'Preferences', available_in_pro: 'Available in Pro', some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.', - learn_more: 'Learn more' + learn_more: 'Learn more', + and: 'and', + or: 'or' } const es = { @@ -301,7 +303,9 @@ const es = { preferences: 'Preferencias', available_in_pro: 'Disponible en Pro', some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.', - learn_more: 'Aprende más' + learn_more: 'Aprende más', + and: 'y', + or: 'o' } const it = { @@ -454,7 +458,9 @@ const it = { preferences: 'Preferenze', available_in_pro: 'Disponibile in Pro', some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.', - learn_more: 'Scopri di più' + learn_more: 'Scopri di più', + and: 'e', + or: 'o' } const pt = { @@ -607,7 +613,9 @@ const pt = { preferences: 'Preferências', available_in_pro: 'Disponível no Pro', some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.', - learn_more: 'Saiba mais' + learn_more: 'Saiba mais', + and: 'e', + or: 'ou' } const fr = { @@ -760,7 +768,9 @@ const fr = { preferences: 'Préférences', available_in_pro: 'Disponible en version Pro', some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.', - learn_more: 'En savoir plus' + learn_more: 'En savoir plus', + and: 'et', + or: 'ou' } const de = { @@ -913,7 +923,9 @@ const de = { preferences: 'Einstellungen', available_in_pro: 'In Pro verfügbar', some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.', - learn_more: 'Erfahren Sie mehr' + learn_more: 'Erfahren Sie mehr', + and: 'und', + or: 'oder' } export { en, es, it, pt, fr, de } diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 65ecad757..a956b4259 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -175,38 +175,46 @@ def maybe_remove_condition_values(submitter) submitter.submission.template_fields.each do |field| next if field['submitter_uuid'] != submitter.uuid - submitter.values.delete(field['uuid']) unless check_field_condition(submitter, field, fields_uuid_index) + submitter.values.delete(field['uuid']) unless check_field_conditions(submitter, field, fields_uuid_index) end submitter.values end - def check_field_condition(submitter, field, fields_uuid_index) + def check_field_conditions(submitter, field, fields_uuid_index) return true if field['conditions'].blank? submitter_values = submitter.values - field['conditions'].reduce(true) do |acc, c| - case c['action'] - when 'empty', 'unchecked' - acc && submitter_values[c['field_uuid']].blank? - when 'not_empty', 'checked' - acc && submitter_values[c['field_uuid']].present? - when 'equal', 'contains' - field = fields_uuid_index[c['field_uuid']] - option = field['options'].find { |o| o['uuid'] == c['value'] } - values = Array.wrap(submitter_values[c['field_uuid']]) - - acc && values.include?(option['value'].presence || "Option #{field['options'].index(option)}") - when 'not_equal', 'does_not_contain' - field = fields_uuid_index[c['field_uuid']] - option = field['options'].find { |o| o['uuid'] == c['value'] } - values = Array.wrap(submitter_values[c['field_uuid']]) - - acc && values.exclude?(option['value'].presence || "Option #{field['options'].index(option)}") + field['conditions'].each_with_object([]) do |c, acc| + if c['operation'] == 'or' + acc.push(acc.pop || check_field_condition(c, submitter_values, fields_uuid_index)) else - acc + acc.push(check_field_condition(c, submitter_values, fields_uuid_index)) end + end.exclude?(false) + end + + def check_field_condition(condition, submitter_values, fields_uuid_index) + case condition['action'] + when 'empty', 'unchecked' + submitter_values[condition['field_uuid']].blank? + when 'not_empty', 'checked' + submitter_values[condition['field_uuid']].present? + when 'equal', 'contains' + field = fields_uuid_index[condition['field_uuid']] + option = field['options'].find { |o| o['uuid'] == condition['value'] } + values = Array.wrap(submitter_values[condition['field_uuid']]) + + values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option)}") + when 'not_equal', 'does_not_contain' + field = fields_uuid_index[condition['field_uuid']] + option = field['options'].find { |o| o['uuid'] == condition['value'] } + values = Array.wrap(submitter_values[condition['field_uuid']]) + + values.exclude?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option)}") + else + true end end diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb index 349e029bc..c6e512fe2 100644 --- a/spec/system/signing_form_spec.rb +++ b/spec/system/signing_form_spec.rb @@ -579,6 +579,199 @@ end end + context 'when the field with conditions' do + let(:template) { create(:template, account:, author:, only_field_types: ['text']) } + let(:submission) { create(:submission, :with_submitters, template:) } + let(:template_attachment) { template.schema.first } + let(:template_submitter) { submission.template_submitters.first } + let(:submitter) { submission.submitters.first } + let(:fields) do + [ + { + 'uuid' => 'da7e0d56-fdb0-441a-bbed-d0f6f2e10fd6', + 'submitter_uuid' => submitter.uuid, + 'name' => 'Full Name', + 'type' => 'text', + 'required' => false, + 'preferences' => {}, + 'conditions' => [], + 'areas' => [ + { + 'x' => 0.1117351575121163, + 'y' => 0.08950650415231329, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => template_attachment['attachment_uuid'], + 'page' => 0 + } + ] + }, + { + 'uuid' => 'd32ad52a-8f6b-4e32-b0d6-6258fb47440b', + 'submitter_uuid' => submitter.uuid, + 'name' => 'Email', + 'type' => 'text', + 'required' => false, + 'preferences' => {}, + 'conditions' => [], + 'validation' => { 'pattern' => '^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$' }, + 'areas' => [ + { + 'x' => 0.1097914983844911, + 'y' => 0.1417641720258019, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => template_attachment['attachment_uuid'], + 'page' => 0 + } + ] + }, + { + 'uuid' => 'c6e013ae-f9f6-4b3a-ad33-b7e772a0a49f', + 'submitter_uuid' => submitter.uuid, + 'name' => 'Phone', + 'type' => 'text', + 'required' => false, + 'preferences' => {}, + 'areas' => [ + { + 'x' => 0.1100060581583199, + 'y' => 0.2553160344676159, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => template_attachment['attachment_uuid'], + 'page' => 0 + } + ] + }, + { + 'uuid' => '64523936-22fd-41f8-b997-ede8fbe467cc', + 'submitter_uuid' => submitter.uuid, + 'name' => 'Comment', + 'type' => 'text', + 'required' => false, + 'preferences' => {}, + 'conditions' => [ + { 'field_uuid' => 'da7e0d56-fdb0-441a-bbed-d0f6f2e10fd6', 'action' => 'not_empty' }, + { 'field_uuid' => 'd32ad52a-8f6b-4e32-b0d6-6258fb47440b', 'action' => 'not_empty' }, + { 'field_uuid' => 'c6e013ae-f9f6-4b3a-ad33-b7e772a0a49f', 'action' => 'not_empty', 'operation' => 'or' } + ], + 'areas' => [ + { + 'x' => 0.1145875403877221, + 'y' => 0.1982961365432846, + 'w' => 0.2, + 'h' => 0.02857142857142857, + 'attachment_uuid' => template_attachment['attachment_uuid'], + 'page' => 0 + } + ] + } + ] + end + + before do + template.update(fields:) + submission.update(template_fields: fields) + end + + it 'completes the form and saves the conditional field when all required fields are filled' do + visit submit_form_path(slug: submitter.slug) + fill_in 'Full Name (optional)', with: 'John Doe' + click_button 'next' + + fill_in 'Email (optional)', with: 'john.due@example.com' + click_button 'next' + + fill_in 'Phone (optional)', with: '+1 (773) 229-8825' + click_button 'next' + + fill_in 'Comment', with: 'This is a comment' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Full Name')).to eq 'John Doe' + expect(field_value(submitter, 'Email')).to eq 'john.due@example.com' + expect(field_value(submitter, 'Phone')).to eq '+1 (773) 229-8825' + expect(field_value(submitter, 'Comment')).to eq 'This is a comment' + end + + it 'completes the form and saves the conditional field when minimum required fields are filled' do + visit submit_form_path(slug: submitter.slug) + fill_in 'Full Name (optional)', with: 'John Doe' + click_button 'next' + + fill_in 'Email (optional)', with: 'john.due@example.com' + click_button 'next' + + fill_in 'Phone (optional)', with: '' + click_button 'next' + + fill_in 'Comment', with: 'This is a comment' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Full Name')).to eq 'John Doe' + expect(field_value(submitter, 'Email')).to eq 'john.due@example.com' + expect(field_value(submitter, 'Phone')).to be_empty + expect(field_value(submitter, 'Comment')).to eq 'This is a comment' + end + + it 'completes the form without saving the conditional field when not enough fields are filled' do + visit submit_form_path(slug: submitter.slug) + + fill_in 'Full Name (optional)', with: 'Jane Doe' + click_button 'next' + + fill_in 'Email (optional)', with: '' + click_button 'next' + + fill_in 'Phone (optional)', with: '' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Full Name')).to eq 'Jane Doe' + expect(field_value(submitter, 'Email')).to be_empty + expect(field_value(submitter, 'Phone')).to be_empty + expect(field_value(submitter, 'Comment')).to be_nil + end + + it 'completes the form without saving the conditional field when only partial fields are filled' do + visit submit_form_path(slug: submitter.slug) + + fill_in 'Full Name (optional)', with: '' + click_button 'next' + + fill_in 'Email (optional)', with: 'john.due@example.com' + click_button 'next' + + fill_in 'Phone (optional)', with: '+1 (773) 229-8825' + click_button 'Complete' + + expect(page).to have_content('Form has been completed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Full Name')).to be_empty + expect(field_value(submitter, 'Email')).to eq 'john.due@example.com' + expect(field_value(submitter, 'Phone')).to eq '+1 (773) 229-8825' + expect(field_value(submitter, 'Comment')).to be_nil + end + end + it 'sends completed email' do template = create(:template, account:, author:, only_field_types: %w[text signature]) submission = create(:submission, template:) From eec266ee83b4f1e5d2cb30d448c453eadec91e76 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 28 Nov 2024 15:41:32 +0200 Subject: [PATCH 05/10] fix archived false api --- app/controllers/api/templates_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index bf02f8305..e922102a8 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -87,7 +87,7 @@ def destroy def filter_templates(templates, params) templates = Templates.search(templates, params[:q]) - templates = params[:archived] ? templates.archived : templates.active + templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present? From d25555b9b028623eb1eced6a016a63584ac3b8a7 Mon Sep 17 00:00:00 2001 From: Alex Turchyn Date: Thu, 28 Nov 2024 23:13:31 +0200 Subject: [PATCH 06/10] add policy links to personalization settings --- .../personalization_settings_controller.rb | 3 ++- app/models/account_config.rb | 1 + .../_form_policy_links_form.html.erb | 23 +++++++++++++++++++ .../personalization_settings/show.html.erb | 1 + app/views/submit_form/show.html.erb | 8 +++++++ config/locales/i18n.yml | 18 +++++++++++++++ 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 app/views/personalization_settings/_form_policy_links_form.html.erb diff --git a/app/controllers/personalization_settings_controller.rb b/app/controllers/personalization_settings_controller.rb index 21e1253c5..76d3f886f 100644 --- a/app/controllers/personalization_settings_controller.rb +++ b/app/controllers/personalization_settings_controller.rb @@ -6,7 +6,8 @@ class PersonalizationSettingsController < ApplicationController AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, - AccountConfig::FORM_COMPLETED_MESSAGE_KEY + AccountConfig::FORM_COMPLETED_MESSAGE_KEY, + *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY]) ].freeze InvalidKey = Class.new(StandardError) diff --git a/app/models/account_config.rb b/app/models/account_config.rb index 6ce4ef46c..adda80918 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -44,6 +44,7 @@ class AccountConfig < ApplicationRecord REUSE_SIGNATURE_KEY = 'reuse_signature' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format' + POLICY_LINKS_KEY = 'policy_links' DEFAULT_VALUES = { SUBMITTER_INVITATION_EMAIL_KEY => lambda { diff --git a/app/views/personalization_settings/_form_policy_links_form.html.erb b/app/views/personalization_settings/_form_policy_links_form.html.erb new file mode 100644 index 000000000..cabf1b83d --- /dev/null +++ b/app/views/personalization_settings/_form_policy_links_form.html.erb @@ -0,0 +1,23 @@ +<% unless Docuseal.multitenant? %> +
+ +
+
+ <%= t('policy_links') %> +
+
+
+ <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::POLICY_LINKS_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> + <%= f.hidden_field :key %> +
+ + <%= f.text_area :value, class: 'base-input w-full py-2 font-mono', placeholder: "#{t('markdown_content_e_g')}\n[#{t('privacy_policy')}](https://example.com/privacy)" %> + +
+
+ <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> +
+ <% end %> +
+
+<% end %> diff --git a/app/views/personalization_settings/show.html.erb b/app/views/personalization_settings/show.html.erb index 125c758f3..438da3114 100644 --- a/app/views/personalization_settings/show.html.erb +++ b/app/views/personalization_settings/show.html.erb @@ -19,6 +19,7 @@
<%= render 'form_completed_message_form' %> <%= render 'form_completed_button_form' %> + <%= render 'form_policy_links_form' %>
<%= render 'form_customization_settings' %>
diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 4b072b29e..a55944eb2 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -4,6 +4,7 @@ <% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %> <% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %> +<% policy_links_config = AccountConfigs.find_for_account(@submitter.account, AccountConfig::POLICY_LINKS_KEY) unless Docuseal.multitenant? %>
@@ -63,6 +64,13 @@ <% end %> <% end %> <%= render 'shared/attribution', link_path: '/start', account: @submitter.account, with_style: false %> + <% if policy_links_config %> +
+
+ <%= auto_link(MarkdownToHtml.call(policy_links_config.value)) %> +
+
+ <% end %>
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 588699256..d6daf17d9 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -646,6 +646,9 @@ en: &en this_month: This month last_month: Last month this_year: This year + policy_links: Policy Links + markdown_content_e_g: Markdown content, e.g. + privacy_policy: Privacy Policy submission_event_names: send_email_to_html: 'Email sent to %{submitter_name}' send_reminder_email_to_html: 'Reminder email sent to %{submitter_name}' @@ -1310,6 +1313,9 @@ es: &es this_month: Este Mes last_month: El Mes Pasado this_year: Este Año + policy_links: Enlaces de Políticas + markdown_content_e_g: Contenido Markdown, por ej. + privacy_policy: Política de Privacidad submission_event_names: send_email_to_html: 'Correo electrónico enviado a %{submitter_name}' send_reminder_email_to_html: 'Correo de recordatorio enviado a %{submitter_name}' @@ -1974,6 +1980,9 @@ it: &it this_month: Questo Mese last_month: Mese Scorso this_year: "Quest'Anno" + policy_links: Collegamenti alle Politiche + markdown_content_e_g: Contenuto Markdown, ad es. + privacy_policy: Politica sulla Privacy submission_event_names: send_email_to_html: 'E-mail inviato a %{submitter_name}' send_reminder_email_to_html: 'E-mail di promemoria inviato a %{submitter_name}' @@ -2639,6 +2648,9 @@ fr: &fr this_month: Ce Mois-ci last_month: Le Mois Dernier this_year: Cette Année + policy_links: Liens des Politiques + markdown_content_e_g: Contenu Markdown, par ex. + privacy_policy: Politique de Confidentialité submission_event_names: send_email_to_html: 'E-mail envoyé à %{submitter_name}' send_reminder_email_to_html: 'E-mail de rappel envoyé à %{submitter_name}' @@ -3303,6 +3315,9 @@ pt: &pt this_month: Este Mês last_month: Mês Passado this_year: Este Ano + policy_links: Links de Políticas + markdown_content_e_g: Conteúdo Markdown, ex. + privacy_policy: Política de Privacidade submission_event_names: send_email_to_html: 'E-mail enviado para %{submitter_name}' send_reminder_email_to_html: 'E-mail de lembrete enviado para %{submitter_name}' @@ -3967,6 +3982,9 @@ de: &de this_month: Dieser Monat last_month: Letzter Monat this_year: Dieses Jahr + policy_links: Richtlinien-Links + markdown_content_e_g: Markdown-Inhalt, z. B. + privacy_policy: Datenschutzrichtlinie submission_event_names: send_email_to_html: 'E-Mail gesendet an %{submitter_name}' send_reminder_email_to_html: 'Erinnerungs-E-Mail gesendet an %{submitter_name}' From 18b13b5b1e2b441552da2ef9125900e90e49b3c0 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 29 Nov 2024 19:49:44 +0200 Subject: [PATCH 07/10] adjust policy links --- app/views/submit_form/show.html.erb | 5 ++--- lib/submitters/form_configs.rb | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index a55944eb2..8385a30e9 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -4,7 +4,6 @@ <% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %> <% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %> -<% policy_links_config = AccountConfigs.find_for_account(@submitter.account, AccountConfig::POLICY_LINKS_KEY) unless Docuseal.multitenant? %>
@@ -64,10 +63,10 @@ <% end %> <% end %> <%= render 'shared/attribution', link_path: '/start', account: @submitter.account, with_style: false %> - <% if policy_links_config %> + <% if @form_configs[:policy_links].present? %>
- <%= auto_link(MarkdownToHtml.call(policy_links_config.value)) %> + <%= auto_link(MarkdownToHtml.call(@form_configs[:policy_links])) %>
<% end %> diff --git a/lib/submitters/form_configs.rb b/lib/submitters/form_configs.rb index 2d1ec0ee9..7b41afdff 100644 --- a/lib/submitters/form_configs.rb +++ b/lib/submitters/form_configs.rb @@ -10,7 +10,8 @@ module FormConfigs AccountConfig::ALLOW_TO_DECLINE_KEY, AccountConfig::REQUIRE_SIGNING_REASON_KEY, AccountConfig::REUSE_SIGNATURE_KEY, - AccountConfig::ALLOW_TYPED_SIGNATURE].freeze + AccountConfig::ALLOW_TYPED_SIGNATURE, + *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze module_function @@ -26,6 +27,7 @@ def call(submitter, keys = []) with_decline = find_safe_value(configs, AccountConfig::ALLOW_TO_DECLINE_KEY) != false with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true + policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY) attrs = { completed_button:, @@ -33,6 +35,7 @@ def call(submitter, keys = []) with_confetti:, reuse_signature:, with_decline:, + policy_links:, completed_message:, require_signing_reason:, prefill_signature:, From 05b7cc9fa658b83ddfb72d58adec36f08ce57c8e Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 29 Nov 2024 22:03:09 +0200 Subject: [PATCH 08/10] fix link --- app/javascript/submission_form/signature_step.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index 7755b038d..4a8fbe9c8 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -259,7 +259,7 @@ class="text-base-content/60 text-xs text-center w-full mt-1" > {{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} From 5724159b7bf5b9424f8dd427f0fc6d8921c343f4 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 1 Dec 2024 00:10:07 +0200 Subject: [PATCH 09/10] verification --- app/javascript/application.js | 1 + app/javascript/application.scss | 17 ++ app/javascript/submission_form/area.vue | 8 +- app/javascript/submission_form/form.vue | 37 +++- app/javascript/submission_form/i18n.js | 39 ++++ .../submission_form/payment_step.vue | 1 - .../submission_form/verification_step.vue | 169 ++++++++++++++ app/javascript/template_builder/builder.vue | 10 +- .../template_builder/field_type.vue | 15 +- app/javascript/template_builder/fields.vue | 32 ++- app/javascript/template_builder/i18n.js | 12 + .../template_builder/mobile_fields.vue | 4 +- app/models/submission_event.rb | 2 + app/models/submitter.rb | 1 + config/initializers/active_storage.rb | 1 + config/locales/i18n.yml | 24 ++ lib/submissions/generate_audit_trail.rb | 17 +- .../generate_preview_attachments.rb | 33 ++- .../generate_result_attachments.rb | 10 + lib/submitters/submit_values.rb | 11 + package.json | 1 + yarn.lock | 209 +++++++++++++++++- 22 files changed, 623 insertions(+), 31 deletions(-) create mode 100644 app/javascript/submission_form/verification_step.vue diff --git a/app/javascript/application.js b/app/javascript/application.js index 15f31a722..2239d6389 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -113,6 +113,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { backgroundColor: '#faf7f5', locale: this.dataset.locale, withPhone: this.dataset.withPhone === 'true', + withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null, withLogo: this.dataset.withLogo !== 'false', editable: this.dataset.editable !== 'false', authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, diff --git a/app/javascript/application.scss b/app/javascript/application.scss index 1a6b527da..61d3f645d 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -97,6 +97,23 @@ button[disabled] .enabled { bottom: auto; } +.tooltip-bottom-start:before { + transform: translateX(-31%); + top: var(--tooltip-offset); + left: 100%; + right: auto; + bottom: auto; +} + +.tooltip-bottom-start:after { + transform: translateX(-25%); + border-color: transparent transparent var(--tooltip-color) transparent; + top: var(--tooltip-tail-offset); + left: 50%; + right: auto; + bottom: auto; +} + .autocomplete { background: white; z-index: 1000; diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 750228fa8..2c3fc5d45 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -209,7 +209,7 @@ diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 04c6dcaf9..d8a9c3dc2 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -455,6 +455,7 @@ export default { fieldTypes: this.fieldTypes, backgroundColor: this.backgroundColor, withPhone: this.withPhone, + withVerification: this.withVerification, withPayment: this.withPayment, isPaymentConnected: this.isPaymentConnected, withFormula: this.withFormula, @@ -635,6 +636,11 @@ export default { required: false, default: false }, + withVerification: { + type: Boolean, + required: false, + default: null + }, withPayment: { type: Boolean, required: false, @@ -1075,7 +1081,7 @@ export default { } else if (type === 'image') { area.w = pageMask.clientWidth / 5 / pageMask.clientWidth area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) - } else if (type === 'signature' || type === 'stamp') { + } else if (type === 'signature' || type === 'stamp' || type === 'verification') { area.w = pageMask.clientWidth / 5 / pageMask.clientWidth area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2 } else if (type === 'initials') { @@ -1239,7 +1245,7 @@ export default { w: area.maskW / 5 / area.maskW, h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) } - } else if (field.type === 'signature' || field.type === 'stamp') { + } else if (field.type === 'signature' || field.type === 'stamp' || field.type === 'verification') { baseArea = { w: area.maskW / 5 / area.maskW, h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2 diff --git a/app/javascript/template_builder/field_type.vue b/app/javascript/template_builder/field_type.vue index 3034f5686..8f17d2564 100644 --- a/app/javascript/template_builder/field_type.vue +++ b/app/javascript/template_builder/field_type.vue @@ -30,7 +30,7 @@ v-for="(icon, type) in fieldIconsSorted" :key="type" > -
  • +