add logical 'or' to field conditions

pull/342/head
Alex Turchyn 11 months ago committed by Pete Matsyburka
parent 36c64d1de4
commit 75683631f0

@ -105,7 +105,7 @@ module Api
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]

@ -115,7 +115,7 @@ class TemplatesController < ApplicationController
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }

@ -1027,35 +1027,46 @@ export default {
}, },
checkFieldConditions (field) { checkFieldConditions (field) {
if (field.conditions?.length) { if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => { const result = field.conditions.reduce((acc, cond) => {
const field = this.fieldsUuidIndex[c.field_uuid] if (cond.operation === 'or') {
acc.push(acc.pop() || this.checkFieldCondition(cond))
} else {
acc.push(this.checkFieldCondition(cond))
}
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) { return acc
}, [])
return !result.includes(false)
} else {
return true
}
},
checkFieldCondition (condition) {
const field = this.fieldsUuidIndex[condition.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
return false return false
} }
if (['empty', 'unchecked'].includes(c.action)) { if (['empty', 'unchecked'].includes(condition.action)) {
return acc && isEmpty(this.values[c.field_uuid]) return isEmpty(this.values[condition.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) { } else if (['not_empty', 'checked'].includes(condition.action)) {
return acc && !isEmpty(this.values[c.field_uuid]) return !isEmpty(this.values[condition.field_uuid])
} else if (['equal', 'contains'].includes(c.action) && field) { } else if (['equal', 'contains'].includes(condition.action) && field) {
if (field.options) { if (field.options) {
const option = field.options.find((o) => o.uuid === c.value) const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[c.field_uuid]].flat() const values = [this.values[condition.field_uuid]].flat()
return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } else {
return acc && [this.values[c.field_uuid]].flat().includes(c.value) return [this.values[condition.field_uuid]].flat().includes(condition.value)
} }
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) { } else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
const option = field.options.find((o) => o.uuid === c.value) const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[c.field_uuid]].flat() const values = [this.values[condition.field_uuid]].flat()
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option))) return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return acc
}
}, true)
} else { } else {
return true return true
} }

@ -29,15 +29,26 @@
>{{ t('available_in_pro') }}</a> >{{ t('available_in_pro') }}</a>
</div> </div>
<form @submit.prevent="validateSaveAndClose"> <form @submit.prevent="validateSaveAndClose">
<div class="my-4 space-y-5"> <div class="my-4">
<div <div
v-for="(condition, cindex) in conditions" v-for="(condition, cindex) in conditions"
:key="cindex" :key="cindex"
class="space-y-4 relative" class="space-y-4 relative"
> >
<div
v-if="cindex > 0"
class="divider -mb-2 mx-1"
>
<button
class="btn btn-xs btn-primary w-24"
@click.prevent="condition.operation === 'or' ? delete condition.operation : condition.operation = 'or'"
>
{{ condition.operation === 'or' ? t('or') : t('and') }}
</button>
</div>
<div <div
v-if="conditions.length > 1" v-if="conditions.length > 1"
class="flex justify-between border-b mx-1 -mb-1 pb-1" class="flex justify-between mx-1"
> >
<span class="text-sm"> <span class="text-sm">
{{ t('condition') }} {{ cindex + 1 }} {{ t('condition') }} {{ cindex + 1 }}

@ -148,7 +148,9 @@ const en = {
preferences: 'Preferences', preferences: 'Preferences',
available_in_pro: 'Available in Pro', available_in_pro: 'Available in Pro',
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.', 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 = { const es = {
@ -301,7 +303,9 @@ const es = {
preferences: 'Preferencias', preferences: 'Preferencias',
available_in_pro: 'Disponible en Pro', available_in_pro: 'Disponible en Pro',
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.', 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 = { const it = {
@ -454,7 +458,9 @@ const it = {
preferences: 'Preferenze', preferences: 'Preferenze',
available_in_pro: 'Disponibile in Pro', available_in_pro: 'Disponibile in Pro',
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.', 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 = { const pt = {
@ -607,7 +613,9 @@ const pt = {
preferences: 'Preferências', preferences: 'Preferências',
available_in_pro: 'Disponível no Pro', available_in_pro: 'Disponível no Pro',
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.', 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 = { const fr = {
@ -760,7 +768,9 @@ const fr = {
preferences: 'Préférences', preferences: 'Préférences',
available_in_pro: 'Disponible en version Pro', available_in_pro: 'Disponible en version Pro',
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.', 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 = { const de = {
@ -913,7 +923,9 @@ const de = {
preferences: 'Einstellungen', preferences: 'Einstellungen',
available_in_pro: 'In Pro verfügbar', available_in_pro: 'In Pro verfügbar',
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.', 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 } export { en, es, it, pt, fr, de }

@ -175,38 +175,46 @@ module Submitters
submitter.submission.template_fields.each do |field| submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid 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 end
submitter.values submitter.values
end 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? return true if field['conditions'].blank?
submitter_values = submitter.values submitter_values = submitter.values
field['conditions'].reduce(true) do |acc, c| field['conditions'].each_with_object([]) do |c, acc|
case c['action'] if c['operation'] == 'or'
acc.push(acc.pop || check_field_condition(c, submitter_values, fields_uuid_index))
else
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' when 'empty', 'unchecked'
acc && submitter_values[c['field_uuid']].blank? submitter_values[condition['field_uuid']].blank?
when 'not_empty', 'checked' when 'not_empty', 'checked'
acc && submitter_values[c['field_uuid']].present? submitter_values[condition['field_uuid']].present?
when 'equal', 'contains' when 'equal', 'contains'
field = fields_uuid_index[c['field_uuid']] field = fields_uuid_index[condition['field_uuid']]
option = field['options'].find { |o| o['uuid'] == c['value'] } option = field['options'].find { |o| o['uuid'] == condition['value'] }
values = Array.wrap(submitter_values[c['field_uuid']]) values = Array.wrap(submitter_values[condition['field_uuid']])
acc && values.include?(option['value'].presence || "Option #{field['options'].index(option)}") values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option)}")
when 'not_equal', 'does_not_contain' when 'not_equal', 'does_not_contain'
field = fields_uuid_index[c['field_uuid']] field = fields_uuid_index[condition['field_uuid']]
option = field['options'].find { |o| o['uuid'] == c['value'] } option = field['options'].find { |o| o['uuid'] == condition['value'] }
values = Array.wrap(submitter_values[c['field_uuid']]) values = Array.wrap(submitter_values[condition['field_uuid']])
acc && values.exclude?(option['value'].presence || "Option #{field['options'].index(option)}") values.exclude?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option)}")
else else
acc true
end
end end
end end

@ -579,6 +579,199 @@ RSpec.describe 'Signing Form', type: :system do
end end
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 it 'sends completed email' do
template = create(:template, account:, author:, only_field_types: %w[text signature]) template = create(:template, account:, author:, only_field_types: %w[text signature])
submission = create(:submission, template:) submission = create(:submission, template:)

Loading…
Cancel
Save