From 2c20fa2e27e307ad64cd318517ab03dcdf7ccb3d Mon Sep 17 00:00:00 2001 From: Bernardo Anderson Date: Wed, 29 Jul 2026 18:50:21 -0500 Subject: [PATCH] CP-14765 - Fix checkbox default values and deleted-option conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What Checkbox fields with a default value now save a true/false value instead of the text "true"/"false" when a form is completed. Conditional fields that depend on a checkbox now show and hide correctly, and conditions pointing at an option that was deleted from the template no longer crash form completion. Why Checkbox defaults were stored as plain text, so a checkbox defaulted to "false" was treated as checked by conditional logic — fields tied to it appeared or disappeared backwards. Found while reviewing CP-14012. How to test Create a template with a checkbox that has default value "false" plus a field shown only when that checkbox is checked. Complete a submission: the checkbox value should be stored as false and the conditional field should stay hidden. Then delete an option from a multiple-choice field that another field's condition references and complete again — no error. --- lib/submitters/submit_values.rb | 19 ++++-- spec/lib/submitters/submit_values_spec.rb | 71 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 2036eadd13..07069417db 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -173,10 +173,13 @@ def merge_default_values(submitter) next if value.blank? - acc[field['uuid']] = template_default_value_for_submitter(value, submitter, with_time: true) + value = template_default_value_for_submitter(value, submitter, with_time: true) + + acc[field['uuid']] = Submitters::NormalizeValues.normalize_value(field, value) end - default_values.compact_blank.merge(submitter.values) + # Reject blanks but keep explicit false (unchecked checkbox defaults), which compact_blank would drop. + default_values.reject { |_, v| v.blank? && v != false }.merge(submitter.values) end def build_formula_values(submitter) @@ -303,13 +306,21 @@ def check_field_condition(condition, submitter_values, fields_uuid_index) 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'] } + option = field['options']&.find { |o| o['uuid'] == condition['value'] } + + # Stale option uuid (template edited after submission): no value can match a deleted option. + return false if option.nil? + values = Array.wrap(submitter_values[condition['field_uuid']]) values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}") when 'not_equal', 'does_not_contain' field = fields_uuid_index[condition['field_uuid']] - option = field['options'].find { |o| o['uuid'] == condition['value'] } + option = field['options']&.find { |o| o['uuid'] == condition['value'] } + + # Stale option uuid (template edited after submission): every value differs from a deleted option. + return true if option.nil? + values = Array.wrap(submitter_values[condition['field_uuid']]) values.exclude?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}") diff --git a/spec/lib/submitters/submit_values_spec.rb b/spec/lib/submitters/submit_values_spec.rb index 652abbf167..36595fee2e 100644 --- a/spec/lib/submitters/submit_values_spec.rb +++ b/spec/lib/submitters/submit_values_spec.rb @@ -84,4 +84,75 @@ end end end + + describe '.merge_default_values' do + let(:checkbox_field) do + { 'uuid' => SecureRandom.uuid, 'name' => 'Agree', 'type' => 'checkbox', + 'submitter_uuid' => submitter.uuid, 'default_value' => default_value } + end + + before do + fields = [checkbox_field] + + template.update!(fields: fields) + submission.update!(template_fields: fields, + template_submitters: [{ 'uuid' => submitter.uuid, 'name' => 'Customer' }]) + submitter.update!(values: {}) + end + + context 'with checkbox default_value "true"' do + let(:default_value) { 'true' } + + it 'persists boolean true' do + expect(described_class.merge_default_values(submitter)[checkbox_field['uuid']]).to be(true) + end + end + + context 'with checkbox default_value "false"' do + let(:default_value) { 'false' } + + it 'persists boolean false' do + expect(described_class.merge_default_values(submitter)[checkbox_field['uuid']]).to be(false) + end + + it 'evaluates checked/unchecked conditions as unchecked' do + values = described_class.merge_default_values(submitter) + + checked = { 'action' => 'checked', 'field_uuid' => checkbox_field['uuid'] } + unchecked = { 'action' => 'unchecked', 'field_uuid' => checkbox_field['uuid'] } + + expect(described_class.check_field_condition(checked, values, {})).to be(false) + expect(described_class.check_field_condition(unchecked, values, {})).to be(true) + end + end + end + + describe '.check_field_condition' do + let(:options_field) do + { 'uuid' => 'field_with_options', 'type' => 'multiple', + 'options' => [{ 'uuid' => 'option_1', 'value' => 'One' }] } + end + let(:fields_uuid_index) { { options_field['uuid'] => options_field } } + let(:values) { { options_field['uuid'] => ['One'] } } + + context 'when the condition points at a deleted option uuid' do + it 'does not raise and evaluates equal/contains as false' do + %w[equal contains].each do |action| + condition = { 'action' => action, 'field_uuid' => options_field['uuid'], 'value' => 'deleted_option_uuid' } + + expect { described_class.check_field_condition(condition, values, fields_uuid_index) }.not_to raise_error + expect(described_class.check_field_condition(condition, values, fields_uuid_index)).to be(false) + end + end + + it 'does not raise and evaluates not_equal/does_not_contain as true' do + %w[not_equal does_not_contain].each do |action| + condition = { 'action' => action, 'field_uuid' => options_field['uuid'], 'value' => 'deleted_option_uuid' } + + expect { described_class.check_field_condition(condition, values, fields_uuid_index) }.not_to raise_error + expect(described_class.check_field_condition(condition, values, fields_uuid_index)).to be(true) + end + end + end + end end