diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 80af5acf..bf02f830 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -105,7 +105,7 @@ module Api :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 04d63e1c..306476b2 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -115,7 +115,7 @@ class TemplatesController < ApplicationController :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 ac1da8a6..075f55db 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 61ae8939..3fbe1313 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 7070c9e5..e20c20fb 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 65ecad75..a956b425 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -175,38 +175,46 @@ module Submitters 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 349e029b..c6e512fe 100644 --- a/spec/system/signing_form_spec.rb +++ b/spec/system/signing_form_spec.rb @@ -579,6 +579,199 @@ RSpec.describe 'Signing Form', type: :system do 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:)