add condition_component

This commit is contained in:
simon lehericey 2022-07-06 09:55:08 +02:00
parent 61839ef1ac
commit e2a236b73d
6 changed files with 408 additions and 1 deletions

View file

@ -0,0 +1,62 @@
@import "colors";
@import "constants";
.conditionnel {
.condition-table {
table-layout: fixed;
.far-left {
width: 100px;
}
.target {
width: 350px;
select {
width: 100%;
}
}
.operator {
width: 250px;
select {
width: 100%;
}
}
.value {
width: 200px;
}
.delete-column {
width: 50px;
}
}
th {
text-align: left;
padding: $default-spacer;
}
td {
padding: $default-spacer;
input,
select {
margin-bottom: 0;
}
input[type=number] {
display: inline-block;
margin-bottom: 0;
}
input.alert,
select.alert {
border-color: $dark-red;
}
}
}

View file

@ -93,7 +93,7 @@
.flex {
&.section {
padding: 10px 10px 0 10px;
padding: $default-spacer $default-spacer 0;
margin-bottom: 8px;
}

View file

@ -0,0 +1,151 @@
class TypesDeChamp::ConditionsComponent < ApplicationComponent
include Logic
def initialize(tdc:, upper_tdcs:, procedure_id:)
@tdc, @condition, @upper_tdcs = tdc, tdc.condition, upper_tdcs
@procedure_id = procedure_id
end
private
def rows
condition_per_row.map { |c| Logic.split_condition(c) }
end
def condition_per_row
if [And, Or].include?(@condition.class)
@condition.operands
else
[@condition].compact
end
end
def logic_conditionnel_button
if @condition.nil?
submit_tag('cliquer pour activer', formaction: add_row_admin_procedure_condition_path(@procedure_id, @tdc.id))
else
submit_tag(
'cliquer pour désactiver',
formmethod: 'delete',
formnovalidate: true,
data: { confirm: "La logique conditionnelle appliquée à ce champ sera désactivé.\nVoulez-vous continuer ?" }
)
end
end
def far_left_tag(row_number)
if row_number == 0
'Afficher si'
elsif row_number == 1
select_tag(
"#{input_prefix}[top_operator_name]",
options_for_select([['Et', And.name], ['Ou', Or.name]], @condition.class.name)
)
end
end
def left_operand_tag(targeted_champ, row_index)
select_tag(
input_name_for('targeted_champ'),
options_for_select(available_targets, targeted_champ.to_json),
onchange: "this.form.action = this.form.action + '/change_targeted_champ?row_index=#{row_index}'",
id: input_id_for('targeted_champ', row_index)
)
end
def available_targets
targets = @upper_tdcs
.filter { |tdc| ChampValue::MANAGED_TYPE_DE_CHAMP.values.include?(tdc.type_champ) }
.map do |tdc|
[tdc.libelle, champ_value(tdc.stable_id).to_json]
end
if targets.present?
targets.unshift(['Sélectionner', empty.to_json])
end
targets
end
def operator_tag(operator_name, targeted_champ, row_index)
select_tag(
input_name_for('operator_name'),
options_for_select(available_operators(targeted_champ), operator_name),
id: input_id_for('operator_name', row_index)
)
end
def available_operators(left)
case left.type
when ChampValue::CHAMP_VALUE_TYPE.fetch(:boolean)
[
['Est', Eq.name]
]
when ChampValue::CHAMP_VALUE_TYPE.fetch(:empty)
[
['Est', Eq.name]
]
when ChampValue::CHAMP_VALUE_TYPE.fetch(:enum)
[
['Est', Eq.name]
]
when ChampValue::CHAMP_VALUE_TYPE.fetch(:number)
[Eq, LessThan, GreaterThan, LessThanEq, GreaterThanEq]
.map(&:name)
.map { |name| [t(name, scope: 'logic.operators'), name] }
else
[]
end
end
def right_operand_tag(left, right, row_index)
case left.type
when :boolean
select_tag(
input_name_for('value'),
options_for_select([['Oui', constant(true).to_json], ['Non', constant(false).to_json]], right.to_json),
id: input_id_for('value', row_index)
)
when :empty
select_tag(
input_name_for('value'),
options_for_select([['Sélectionner', empty.to_json]]),
id: input_id_for('value', row_index)
)
when :enum
select_tag(
input_name_for('value'),
options_for_select(left.options, right.value),
id: input_id_for('value', row_index)
)
when :number
number_field_tag(input_name_for('value'), right.value, required: true, id: input_id_for('value', row_index))
else
number_field_tag(input_name_for('value'), '', id: input_id_for('value', row_index))
end
end
def add_condition_tag
submit_tag('Ajouter une condition', formaction: add_row_condition_path(@tdc.id))
end
def delete_condition_tag(row_index)
submit_tag('X', formaction: delete_row_condition_path(@tdc.id, row_index: row_index))
end
def render?
@condition.present? || available_targets.any?
end
def input_name_for(name)
"#{input_prefix}[rows][][#{name}]"
end
def input_id_for(name, row_index)
"#{@tdc.id}-#{name}-#{row_index}"
end
def input_prefix
'type_de_champ[condition_form]'
end
end

View file

@ -0,0 +1,26 @@
.flex.justify-start.section{ id: dom_id(@tdc, :conditions) }
= form_with url: admin_procedure_condition_path(@procedure_id, @tdc), method: :patch, class: 'form width-100' do |f|
.conditionnel.mt-2.width-100
.flex
%p.mr-2 Logique conditionnelle
= logic_conditionnel_button
- if @condition.present?
%table.condition-table.mt-2.width-100
%thead
%tr
%th.far-left
%th.target Champ Cible
%th.operator Opérateur
%th.value Valeur
%th.delete-column
%tbody
- rows.each.with_index do |(targeted_champ, operator_name, value), row_index|
%tr
%td.far-left= far_left_tag(row_index)
%td.target= left_operand_tag(targeted_champ, row_index)
%td.operator= operator_tag(operator_name, targeted_champ, row_index)
%td.value= right_operand_tag(targeted_champ, value, row_index)
%td.delete-column= delete_condition_tag(row_index)
.flex.justify-end.mt-2= add_condition_tag

View file

@ -1,3 +1,9 @@
fr:
logic:
empty: un membre vide
operators:
'Logic::LessThan': Inférieur à
'Logic::LessThanEq': Inférieur ou égal à
'Logic::Eq': Égal à
'Logic::GreaterThan': Supérieur à
'Logic::GreaterThanEq': Supérieur ou égal à

View file

@ -0,0 +1,162 @@
describe TypesDeChamp::ConditionsComponent, type: :component do
include Logic
describe 'render' do
let(:tdc) { create(:type_de_champ, condition: condition) }
let(:condition) { nil }
let(:upper_tdcs) { [] }
before { render_inline(described_class.new(tdc: tdc, upper_tdcs: upper_tdcs, procedure_id: 123)) }
context 'when there are no upper tdc' do
it { expect(page).not_to have_text('Logique conditionnelle') }
end
context 'when there are upper tdcs but not managed' do
let(:upper_tdcs) { [build(:type_de_champ_address)] }
it { expect(page).not_to have_text('Logique conditionnelle') }
end
context 'when there are upper tdc but no condition to display' do
let(:upper_tdcs) { [build(:type_de_champ_integer_number)] }
it do
expect(page).to have_text('Logique conditionnelle')
expect(page).to have_button('cliquer pour activer')
expect(page).not_to have_selector('table')
end
end
context 'when there are upper tdc and a condition' do
let(:upper_tdc) { create(:type_de_champ_number) }
let(:upper_tdcs) { [upper_tdc] }
context 'and one condition' do
let(:condition) { ds_eq(champ_value(upper_tdc.stable_id), constant(1)) }
it do
expect(page).to have_button('cliquer pour désactiver')
expect(page).to have_selector('table')
expect(page).to have_selector('tbody > tr', count: 1)
end
end
context 'focus one row' do
context 'empty' do
let(:condition) { empty_operator(empty, empty) }
it do
expect(page).to have_select('type_de_champ[condition_form][rows][][operator_name]', options: ['Est'])
expect(page).to have_select('type_de_champ[condition_form][rows][][value]', options: ['Sélectionner'])
end
end
context 'number' do
let(:condition) { empty_operator(constant(1), constant(0)) }
it do
expect(page).to have_select('type_de_champ[condition_form][rows][][operator_name]', with_options: ['Égal à'])
expect(page).to have_selector('input[name="type_de_champ[condition_form][rows][][value]"][value=0]')
end
end
context 'boolean' do
let(:condition) { empty_operator(constant(true), constant(true)) }
it do
expect(page).to have_select('type_de_champ[condition_form][rows][][operator_name]', with_options: ['Est'])
expect(page).to have_select('type_de_champ[condition_form][rows][][value]', options: ['Oui', 'Non'])
end
end
context 'enum' do
let(:drop_down) { create(:type_de_champ_drop_down_list) }
let(:upper_tdcs) { [drop_down] }
let(:condition) { empty_operator(champ_value(drop_down.stable_id), constant(true)) }
it do
expect(page).to have_select('type_de_champ[condition_form][rows][][operator_name]', with_options: ['Est'])
expect(page).to have_select('type_de_champ[condition_form][rows][][value]', options: ['val1', 'val2', 'val3'])
end
end
end
context 'and 2 conditions' do
let(:condition) { ds_and([empty_operator(empty, empty), empty_operator(empty, empty)]) }
it do
expect(page).to have_selector('tbody > tr', count: 2)
expect(page).to have_select("type_de_champ_condition_form_top_operator_name", selected: "Et", options: ['Et', 'Ou'])
end
end
context 'when there are 3 conditions' do
let(:upper_tdc) { create(:type_de_champ_number) }
let(:upper_tdcs) { [upper_tdc] }
let(:condition) do
ds_or([
ds_eq(champ_value(upper_tdc.stable_id), constant(1)),
ds_eq(champ_value(upper_tdc.stable_id), empty),
greater_than(champ_value(upper_tdc.stable_id), constant(3))
])
end
it do
expect(page).to have_selector('tbody > tr', count: 3)
expect(page).to have_select("type_de_champ_condition_form_top_operator_name", selected: "Ou", options: ['Et', 'Ou'])
end
end
end
end
describe '.rows' do
let(:tdc) { build(:type_de_champ, condition: condition) }
let(:condition) { nil }
subject { described_class.new(tdc: tdc, upper_tdcs: [], procedure_id: 123).send(:rows) }
context 'when there is one condition' do
let(:condition) { ds_eq(empty, constant(1)) }
it { is_expected.to eq([[empty, Logic::Eq.name, constant(1)]]) }
end
context 'when there are 2 conditions' do
let(:condition) { ds_and([ds_eq(empty, constant(1)), ds_eq(empty, empty)]) }
let(:expected) do
[
[empty, Logic::Eq.name, constant(1)],
[empty, Logic::Eq.name, empty]
]
end
it { is_expected.to eq(expected) }
end
context 'when there are 3 conditions' do
let(:upper_tdc) { create(:type_de_champ_number) }
let(:upper_tdcs) { [upper_tdc] }
let(:condition) do
ds_or([
ds_eq(champ_value(upper_tdc.stable_id), constant(1)),
ds_eq(champ_value(upper_tdc.stable_id), empty),
greater_than(champ_value(upper_tdc.stable_id), constant(3))
])
end
let(:expected) do
[
[champ_value(upper_tdc.stable_id), Logic::Eq.name, constant(1)],
[champ_value(upper_tdc.stable_id), Logic::Eq.name, empty],
[champ_value(upper_tdc.stable_id), Logic::GreaterThan.name, constant(3)]
]
end
it { is_expected.to eq(expected) }
end
end
end