Merge pull request #7767 from betagouv/condition_on_multiple_choice
feat(conditonal): ajoute l'operator d'inclusion dans une liste pour le conditionnel
This commit is contained in:
commit
0f2552d593
19 changed files with 211 additions and 35 deletions
|
@ -1,7 +1,7 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
.conditionnel {
|
||||
form.form > .conditionnel {
|
||||
|
||||
.condition-error {
|
||||
background: $background-red;
|
||||
|
@ -64,7 +64,7 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
input[type=text] {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -119,6 +119,10 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent
|
|||
[t('is', scope: 'logic'), Eq.name],
|
||||
[t('is_not', scope: 'logic'), NotEq.name]
|
||||
]
|
||||
when ChampValue::CHAMP_VALUE_TYPE.fetch(:enums)
|
||||
[
|
||||
[t(IncludeOperator.name, scope: 'logic.operators'), IncludeOperator.name]
|
||||
]
|
||||
when ChampValue::CHAMP_VALUE_TYPE.fetch(:number)
|
||||
[Eq, LessThan, GreaterThan, LessThanEq, GreaterThanEq]
|
||||
.map(&:name)
|
||||
|
@ -151,7 +155,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent
|
|||
options_for_select(empty_target_for_select),
|
||||
id: input_id_for('value', row_index)
|
||||
)
|
||||
when :enum
|
||||
when :enum, :enums
|
||||
enums_for_select = left.options
|
||||
|
||||
if right_invalid
|
||||
|
@ -165,7 +169,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent
|
|||
class: { alert: right_invalid }
|
||||
)
|
||||
when :number
|
||||
number_field_tag(
|
||||
text_field_tag(
|
||||
input_name_for('value'),
|
||||
right.value,
|
||||
required: true,
|
||||
|
@ -173,7 +177,7 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent
|
|||
class: { alert: right_invalid }
|
||||
)
|
||||
else
|
||||
number_field_tag(input_name_for('value'), '', id: input_id_for('value', row_index))
|
||||
text_field_tag(input_name_for('value'), '', id: input_id_for('value', row_index))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -7,28 +7,42 @@ class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent
|
|||
|
||||
def errors
|
||||
@conditions
|
||||
.filter { |condition| condition.errors(@upper_tdcs.map(&:stable_id)).present? }
|
||||
.map { |condition| row_error(Logic.split_condition(condition)) }
|
||||
.flat_map { |condition| condition.errors(@upper_tdcs.map(&:stable_id)) }
|
||||
.map { |error| humanize(error) }
|
||||
.uniq
|
||||
.map { |message| tag.li(message) }
|
||||
.then { |lis| tag.ul(lis.reduce(&:+)) }
|
||||
end
|
||||
|
||||
def row_error((left, operator_name, right))
|
||||
targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == left.stable_id }
|
||||
|
||||
if targeted_champ.nil?
|
||||
def humanize(error)
|
||||
case error
|
||||
in { type: :not_available }
|
||||
t('not_available', scope: '.errors')
|
||||
elsif left.type == :unmanaged
|
||||
t('unmanaged', scope: '.errors',
|
||||
in { type: :unmanaged, stable_id: stable_id }
|
||||
targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == stable_id }
|
||||
t('unmanaged',
|
||||
scope: '.errors',
|
||||
libelle: targeted_champ.libelle,
|
||||
type_champ: t(targeted_champ.type_champ, scope: 'activerecord.attributes.type_de_champ.type_champs')&.downcase)
|
||||
else
|
||||
in { type: :incompatible, stable_id: stable_id, right: right, operator_name: operator_name }
|
||||
targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == stable_id }
|
||||
t('incompatible', scope: '.errors',
|
||||
libelle: targeted_champ.libelle,
|
||||
type_champ: t(targeted_champ.type_champ, scope: 'activerecord.attributes.type_de_champ.type_champs')&.downcase,
|
||||
operator: t(operator_name, scope: 'logic.operators').downcase,
|
||||
right: right.to_s.downcase)
|
||||
in { type: :required_number, operator_name: operator_name }
|
||||
t('required_number', scope: '.errors',
|
||||
operator: t(operator_name, scope: 'logic.operators'))
|
||||
in { type: :not_included, stable_id: stable_id, right: right }
|
||||
targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == stable_id }
|
||||
t('not_included', scope: '.errors',
|
||||
libelle: targeted_champ.libelle,
|
||||
right: right.to_s.downcase)
|
||||
in { type: :required_list }
|
||||
t('required_list', scope: '.errors')
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
fr:
|
||||
errors:
|
||||
not_available: "A targeted field is not available."
|
||||
unmanaged: "The field « %{libelle} » is a « %{type_champ} » and cannot be used as conditional source."
|
||||
incompatible: "The field « %{libelle} » is a « %{type_champ} ». It cannot be %{operator} « %{right} »."
|
||||
required_number: "« %{operator} » applies only to number."
|
||||
required_list: "The « include » operator only applies to simple or multiple choice."
|
||||
not_included: "« %{right} » is not included in « %{libelle} »."
|
|
@ -3,4 +3,7 @@ fr:
|
|||
errors:
|
||||
not_available: "Un champ cible n'est plus disponible. Il est soit supprimé, soit déplacé en dessous de ce champ."
|
||||
unmanaged: "Le champ « %{libelle} » est de type « %{type_champ} » et ne peut pas être utilisé comme champ cible."
|
||||
incompatible: "Le champ « %{libelle} » est de type « %{type_champ} ». Il ne peut pas être %{operator} %{right}."
|
||||
incompatible: "Le champ « %{libelle} » est de type « %{type_champ} ». Il ne peut pas être %{operator} « %{right} »."
|
||||
required_number: "« %{operator} » ne s'applique qu'à des nombres."
|
||||
required_list: "Lʼopérateur « inclus » ne s'applique qu'au choix simple ou multiple."
|
||||
not_included: "« %{right} » ne fait pas partie de « %{libelle} »."
|
||||
|
|
|
@ -8,7 +8,7 @@ module Logic
|
|||
end
|
||||
|
||||
def self.class_from_name(name)
|
||||
[ChampValue, Constant, Empty, LessThan, LessThanEq, Eq, NotEq, GreaterThanEq, GreaterThan, EmptyOperator, And, Or]
|
||||
[ChampValue, Constant, Empty, LessThan, LessThanEq, Eq, NotEq, GreaterThanEq, GreaterThan, EmptyOperator, IncludeOperator, And, Or]
|
||||
.find { |c| c.name == name }
|
||||
end
|
||||
|
||||
|
@ -24,6 +24,8 @@ module Logic
|
|||
operator_class = EmptyOperator
|
||||
in [:enum, _]
|
||||
operator_class = Eq
|
||||
in [:enums, _]
|
||||
operator_class = IncludeOperator
|
||||
in [:number, EmptyOperator]
|
||||
operator_class = Eq
|
||||
in [:number, _]
|
||||
|
@ -35,7 +37,7 @@ module Logic
|
|||
Constant.new(true)
|
||||
when :empty
|
||||
Empty.new
|
||||
when :enum
|
||||
when :enum, :enums
|
||||
Constant.new(left.options.first.second)
|
||||
when :number
|
||||
Constant.new(0)
|
||||
|
@ -49,8 +51,8 @@ module Logic
|
|||
case [left.type, right.type]
|
||||
in [a, ^a] # syntax for same type
|
||||
true
|
||||
in [:enum, :string]
|
||||
left.options.map(&:second).include?(right.value)
|
||||
in [:enum, :string] | [:enums, :string]
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
|
@ -84,6 +86,8 @@ module Logic
|
|||
|
||||
def less_than_eq(left, right) = Logic::LessThanEq.new(left, right)
|
||||
|
||||
def ds_include(left, right) = Logic::IncludeOperator.new(left, right)
|
||||
|
||||
def constant(value) = Logic::Constant.new(value)
|
||||
|
||||
def champ_value(stable_id) = Logic::ChampValue.new(stable_id)
|
||||
|
|
|
@ -21,7 +21,7 @@ class Logic::BinaryOperator < Logic::Term
|
|||
errors = []
|
||||
|
||||
if @left.type != :number || @right.type != :number
|
||||
errors += ["les types sont incompatibles : #{self}"]
|
||||
errors << { type: :required_number, operator_name: self.class.name }
|
||||
end
|
||||
|
||||
errors + @left.errors(stable_ids) + @right.errors(stable_ids)
|
||||
|
|
|
@ -4,13 +4,15 @@ class Logic::ChampValue < Logic::Term
|
|||
:checkbox,
|
||||
:integer_number,
|
||||
:decimal_number,
|
||||
:drop_down_list
|
||||
:drop_down_list,
|
||||
:multiple_drop_down_list
|
||||
)
|
||||
|
||||
CHAMP_VALUE_TYPE = {
|
||||
boolean: :boolean,
|
||||
number: :number,
|
||||
enum: :enum,
|
||||
boolean: :boolean, # from yes_no or checkbox champ
|
||||
number: :number, # from integer or decimal number champ
|
||||
enum: :enum, # a choice from a dropdownlist
|
||||
enums: :enums, # multiple choice from a dropdownlist (multipledropdownlist)
|
||||
empty: :empty,
|
||||
unmanaged: :unmanaged
|
||||
}
|
||||
|
@ -35,6 +37,8 @@ class Logic::ChampValue < Logic::Term
|
|||
targeted_champ.for_api
|
||||
when MANAGED_TYPE_DE_CHAMP.fetch(:drop_down_list)
|
||||
targeted_champ.selected
|
||||
when MANAGED_TYPE_DE_CHAMP.fetch(:multiple_drop_down_list)
|
||||
targeted_champ.selected_options
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -49,6 +53,8 @@ class Logic::ChampValue < Logic::Term
|
|||
CHAMP_VALUE_TYPE.fetch(:number)
|
||||
when MANAGED_TYPE_DE_CHAMP.fetch(:drop_down_list)
|
||||
CHAMP_VALUE_TYPE.fetch(:enum)
|
||||
when MANAGED_TYPE_DE_CHAMP.fetch(:multiple_drop_down_list)
|
||||
CHAMP_VALUE_TYPE.fetch(:enums)
|
||||
else
|
||||
CHAMP_VALUE_TYPE.fetch(:unmanaged)
|
||||
end
|
||||
|
@ -56,7 +62,7 @@ class Logic::ChampValue < Logic::Term
|
|||
|
||||
def errors(stable_ids)
|
||||
if !stable_ids.include?(stable_id)
|
||||
["le type de champ stable_id=#{stable_id} n'est pas disponible"]
|
||||
[{ type: :not_available }]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
|
@ -2,10 +2,24 @@ class Logic::Eq < Logic::BinaryOperator
|
|||
def operation = :==
|
||||
|
||||
def errors(stable_ids = [])
|
||||
errors = []
|
||||
errors = [@left, @right]
|
||||
.filter { |term| term.type == :unmanaged }
|
||||
.map { |term| { type: :unmanaged, stable_id: term.stable_id } }
|
||||
|
||||
if !Logic.compatible_type?(@left, @right)
|
||||
errors += ["les types sont incompatibles : #{self}"]
|
||||
errors << {
|
||||
type: :incompatible,
|
||||
stable_id: @left.try(:stable_id),
|
||||
right: @right,
|
||||
operator_name: self.class.name
|
||||
}
|
||||
elsif @left.type == :enum &&
|
||||
!left.options.map(&:second).include?(right.value)
|
||||
errors << {
|
||||
type: :not_included,
|
||||
stable_id: @left.stable_id,
|
||||
right: @right
|
||||
}
|
||||
end
|
||||
|
||||
errors + @left.errors(stable_ids) + @right.errors(stable_ids)
|
||||
|
|
29
app/models/logic/include_operator.rb
Normal file
29
app/models/logic/include_operator.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
class Logic::IncludeOperator < Logic::BinaryOperator
|
||||
def operation = :include?
|
||||
|
||||
def errors(stable_ids = [])
|
||||
result = []
|
||||
|
||||
if left_not_a_list?
|
||||
result << { type: :required_list }
|
||||
elsif right_value_not_in_list?
|
||||
result << {
|
||||
type: :not_included,
|
||||
stable_id: @left.stable_id,
|
||||
right: @right
|
||||
}
|
||||
end
|
||||
|
||||
result + @left.errors(stable_ids) + @right.errors(stable_ids)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def left_not_a_list?
|
||||
@left.type != :enums
|
||||
end
|
||||
|
||||
def right_value_not_in_list?
|
||||
!@left.options.map(&:second).include?(@right.value)
|
||||
end
|
||||
end
|
|
@ -1,3 +1,11 @@
|
|||
= turbo_stream.replace 'breadcrumbs' , render(partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [['Démarches', admin_procedures_path],
|
||||
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
|
||||
['Configuration des champs']],
|
||||
preview: @procedure.draft_revision.valid? })
|
||||
|
||||
= turbo_stream.replace 'errors-summary', render(TypesDeChampEditor::ErrorsSummary.new(revision: @procedure.draft_revision))
|
||||
|
||||
- rendered = render @condition_component
|
||||
|
||||
- if rendered.present?
|
||||
|
|
|
@ -12,3 +12,4 @@ fr:
|
|||
'Logic::And': Et
|
||||
'Logic::Or': Ou
|
||||
'Logic::NotEq': N'est pas
|
||||
'Logic::IncludeOperator': Contient
|
||||
|
|
|
@ -28,7 +28,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do
|
|||
|
||||
it do
|
||||
expect(page).to have_css('.condition-error')
|
||||
expect(page).to have_content("ne peut pas être utilisé")
|
||||
expect(page).to have_content("Le champ « #{tdc.libelle} » est de type « adresse » et ne peut pas être utilisé comme champ cible.")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,7 +37,39 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do
|
|||
let(:upper_tdcs) { [tdc] }
|
||||
let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant('a'))] }
|
||||
|
||||
it { expect(page).to have_content("Il ne peut pas être") }
|
||||
it { expect(page).to have_content("Le champ « #{tdc.libelle} » est de type « nombre entier ». Il ne peut pas être égal à « a ».") }
|
||||
end
|
||||
|
||||
context 'when a number operator is applied on not a number' do
|
||||
let(:tdc) { create(:type_de_champ_multiple_drop_down_list) }
|
||||
let(:upper_tdcs) { [tdc] }
|
||||
let(:conditions) { [greater_than(champ_value(tdc.stable_id), constant('a text'))] }
|
||||
|
||||
it { expect(page).to have_content("« Supérieur à » ne s'applique qu'à des nombres.") }
|
||||
end
|
||||
|
||||
context 'when the include operator is applied on a list' do
|
||||
let(:tdc) { create(:type_de_champ_integer_number) }
|
||||
let(:upper_tdcs) { [tdc] }
|
||||
let(:conditions) { [ds_include(champ_value(tdc.stable_id), constant('a text'))] }
|
||||
|
||||
it { expect(page).to have_content("Lʼopérateur « inclus » ne s'applique qu'au choix simple ou multiple.") }
|
||||
end
|
||||
|
||||
context 'when a choice is not in a drop_down' do
|
||||
let(:tdc) { create(:type_de_champ_drop_down_list) }
|
||||
let(:upper_tdcs) { [tdc] }
|
||||
let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant('another choice'))] }
|
||||
|
||||
it { expect(page).to have_content("« another choice » ne fait pas partie de « #{tdc.libelle} ».") }
|
||||
end
|
||||
|
||||
context 'when a choice is not in a multiple_drop_down' do
|
||||
let(:tdc) { create(:type_de_champ_multiple_drop_down_list) }
|
||||
let(:upper_tdcs) { [tdc] }
|
||||
let(:conditions) { [ds_include(champ_value(tdc.stable_id), constant('another choice'))] }
|
||||
|
||||
it { expect(page).to have_content("« another choice » ne fait pas partie de « #{tdc.libelle} ».") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ describe Logic::BinaryOperator do
|
|||
end
|
||||
|
||||
describe '#errors' do
|
||||
it { expect(greater_than(constant(1), constant(true)).errors).to eq(['les types sont incompatibles : (1 > Oui)']) }
|
||||
it { expect(greater_than(constant(1), constant(true)).errors).to eq([{ operator_name: "Logic::GreaterThan", type: :required_number }]) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -80,6 +80,6 @@ describe Logic::ChampValue do
|
|||
let(:champ) { create(:champ) }
|
||||
|
||||
it { expect(champ_value(champ.stable_id).errors([champ.stable_id])).to be_empty }
|
||||
it { expect(champ_value(champ.stable_id).errors(['other stable ids'])).to eq(["le type de champ stable_id=#{champ.stable_id} n'est pas disponible"]) }
|
||||
it { expect(champ_value(champ.stable_id).errors(['other stable ids'])).to eq([{ type: :not_available }]) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,15 @@ describe Logic::Eq do
|
|||
|
||||
describe '#errors' do
|
||||
it { expect(ds_eq(constant(true), constant(true)).errors).to be_empty }
|
||||
it { expect(ds_eq(constant(true), constant(1)).errors).to eq(["les types sont incompatibles : (Oui == 1)"]) }
|
||||
it do
|
||||
expected = {
|
||||
operator_name: "Logic::Eq",
|
||||
right: constant(1),
|
||||
stable_id: nil,
|
||||
type: :incompatible
|
||||
}
|
||||
expect(ds_eq(constant(true), constant(1)).errors).to eq([expected])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#==' do
|
||||
|
|
29
spec/models/logic/include_operator_spec.rb
Normal file
29
spec/models/logic/include_operator_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
describe Logic::IncludeOperator do
|
||||
include Logic
|
||||
|
||||
let(:champ) { create(:champ_multiple_drop_down_list, value: '["val1", "val2"]') }
|
||||
|
||||
describe '#compute' do
|
||||
it { expect(ds_include(champ_value(champ.stable_id), constant('val1')).compute([champ])).to be(true) }
|
||||
it { expect(ds_include(champ_value(champ.stable_id), constant('something else')).compute([champ])).to be(false) }
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
it { expect(ds_include(champ_value(champ.stable_id), constant('val1')).errors([champ.stable_id])).to be_empty }
|
||||
it do
|
||||
expected = {
|
||||
right: constant('something else'),
|
||||
stable_id: champ.stable_id,
|
||||
type: :not_included
|
||||
}
|
||||
|
||||
expect(ds_include(champ_value(champ.stable_id), constant('something else')).errors([champ.stable_id])).to eq([expected])
|
||||
end
|
||||
|
||||
it { expect(ds_include(constant(1), constant('val1')).errors).to eq([{ type: :required_list }]) }
|
||||
end
|
||||
|
||||
describe '#==' do
|
||||
it { expect(ds_include(champ_value(champ.stable_id), constant('val1'))).to eq(ds_include(champ_value(champ.stable_id), constant('val1'))) }
|
||||
end
|
||||
end
|
|
@ -8,7 +8,15 @@ describe Logic::NotEq do
|
|||
|
||||
describe '#errors' do
|
||||
it { expect(ds_not_eq(constant(true), constant(true)).errors).to be_empty }
|
||||
it { expect(ds_not_eq(constant(true), constant(1)).errors).to eq(["les types sont incompatibles : (Oui != 1)"]) }
|
||||
it do
|
||||
expected = {
|
||||
operator_name: "Logic::NotEq",
|
||||
right: constant(1),
|
||||
stable_id: nil,
|
||||
type: :incompatible
|
||||
}
|
||||
expect(ds_not_eq(constant(true), constant(1)).errors).to eq([expected])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#==' do
|
||||
|
|
|
@ -49,6 +49,14 @@ describe Logic do
|
|||
|
||||
it { is_expected.to eq(ds_eq(champ_value(drop_down), constant(first_option))) }
|
||||
end
|
||||
|
||||
context 'when multiple dropdown empty operator true' do
|
||||
let(:multiple_drop_down) { create(:type_de_champ_multiple_drop_down_list) }
|
||||
let(:first_option) { multiple_drop_down.drop_down_list_enabled_non_empty_options.first }
|
||||
let(:condition) { empty_operator(champ_value(multiple_drop_down), constant(true)) }
|
||||
|
||||
it { is_expected.to eq(ds_include(champ_value(multiple_drop_down), constant(first_option))) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.compatible_type?' do
|
||||
|
@ -60,8 +68,7 @@ describe Logic do
|
|||
let(:first_option) { drop_down.drop_down_list_enabled_non_empty_options.first }
|
||||
|
||||
it do
|
||||
expect(Logic.compatible_type?(champ_value(drop_down.stable_id), constant(first_option))).to be true
|
||||
expect(Logic.compatible_type?(champ_value(drop_down.stable_id), constant('a'))).to be false
|
||||
expect(Logic.compatible_type?(champ_value(drop_down.stable_id), constant('a'))).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue