Merge pull request #7544 from betagouv/types_de_champ_controller_manage_condition
feat: rajoute les conditions dans l'éditeur
This commit is contained in:
commit
b9945696f3
26 changed files with 419 additions and 138 deletions
|
@ -1,3 +1,3 @@
|
||||||
%ul.types-de-champ-block{ id: block_id, data: sortable_options }
|
%ul.types-de-champ-block{ id: block_id, data: sortable_options }
|
||||||
- @coordinates.each do |coordinate|
|
- @coordinates.each.with_index do |coordinate, i|
|
||||||
= render TypesDeChampEditor::ChampComponent.new(coordinate: coordinate)
|
= render TypesDeChampEditor::ChampComponent.new(coordinate: coordinate, upper_coordinates: @coordinates.take(i))
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
||||||
def initialize(coordinate:, focused: false)
|
attr_reader :coordinate, :upper_coordinates
|
||||||
|
|
||||||
|
def initialize(coordinate:, upper_coordinates:, focused: false)
|
||||||
@coordinate = coordinate
|
@coordinate = coordinate
|
||||||
@focused = focused
|
@focused = focused
|
||||||
|
@upper_coordinates = upper_coordinates
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :coordinate
|
|
||||||
delegate :type_de_champ, :revision, :procedure, to: :coordinate
|
delegate :type_de_champ, :revision, :procedure, to: :coordinate
|
||||||
|
|
||||||
def can_be_mandatory?
|
def can_be_mandatory?
|
||||||
|
|
|
@ -83,3 +83,5 @@
|
||||||
.editor-block.flex-grow.cell
|
.editor-block.flex-grow.cell
|
||||||
= render TypesDeChampEditor::BlockComponent.new(block: coordinate, coordinates: coordinate.revision_types_de_champ)
|
= render TypesDeChampEditor::BlockComponent.new(block: coordinate, coordinates: coordinate.revision_types_de_champ)
|
||||||
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate, is_annotation: coordinate.private?)
|
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate, is_annotation: coordinate.private?)
|
||||||
|
|
||||||
|
= render(TypesDeChampEditor::ConditionsComponent.new(tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ), procedure_id: procedure.id))
|
||||||
|
|
|
@ -201,7 +201,11 @@ class TypesDeChampEditor::ConditionsComponent < ApplicationComponent
|
||||||
end
|
end
|
||||||
|
|
||||||
def render?
|
def render?
|
||||||
@condition.present? || available_targets_for_select.any?
|
if Flipper.enabled?(:conditional, controller.current_user)
|
||||||
|
@condition.present? || available_targets_for_select.any?
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def input_name_for(name)
|
def input_name_for(name)
|
||||||
|
|
|
@ -22,11 +22,11 @@ class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent
|
||||||
elsif left.type == :unmanaged
|
elsif left.type == :unmanaged
|
||||||
t('unmanaged', scope: '.errors',
|
t('unmanaged', scope: '.errors',
|
||||||
libelle: targeted_champ.libelle,
|
libelle: targeted_champ.libelle,
|
||||||
type_champ: t(targeted_champ.type_champ, scope: '.type'))
|
type_champ: t(targeted_champ.type_champ, scope: 'activerecord.attributes.type_de_champ.type_champs')&.downcase)
|
||||||
else
|
else
|
||||||
t('incompatible', scope: '.errors',
|
t('incompatible', scope: '.errors',
|
||||||
libelle: targeted_champ.libelle,
|
libelle: targeted_champ.libelle,
|
||||||
type_champ: t(targeted_champ.type_champ, scope: '.type'),
|
type_champ: t(targeted_champ.type_champ, scope: 'activerecord.attributes.type_de_champ.type_champs')&.downcase,
|
||||||
operator: t(operator_name, scope: 'logic.operators').downcase,
|
operator: t(operator_name, scope: 'logic.operators').downcase,
|
||||||
right: right.to_s.downcase)
|
right: right.to_s.downcase)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
---
|
---
|
||||||
fr:
|
fr:
|
||||||
type:
|
|
||||||
number: un nombre
|
|
||||||
string: un texte
|
|
||||||
boolean: soit oui, soit non
|
|
||||||
yes_no: soit oui, soit non
|
|
||||||
address: une adresse
|
|
||||||
errors:
|
errors:
|
||||||
not_available: "Un champ cible n'est plus disponible. Il est soit supprimé, soit déplacé en dessous de ce champ."
|
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 %{type_champ} et ne peut pas être utilisé comme champ cible."
|
unmanaged: "Le champ « %{libelle} » est de type « %{type_champ} » et ne peut pas être utilisé comme champ cible."
|
||||||
incompatible: "Le champ « %{libelle} » est %{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}."
|
||||||
|
|
|
@ -6,54 +6,58 @@ module Administrateurs
|
||||||
|
|
||||||
def update
|
def update
|
||||||
condition = condition_form.to_condition
|
condition = condition_form.to_condition
|
||||||
tdc.update!(condition: condition)
|
@tdc.update!(condition: condition)
|
||||||
|
|
||||||
render 'administrateurs/types_de_champ/update.turbo_stream.haml'
|
@condition_component = build_condition_component
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_row
|
def add_row
|
||||||
condition = Logic.add_empty_condition_to(tdc.condition)
|
condition = Logic.add_empty_condition_to(@tdc.condition)
|
||||||
tdc.update!(condition: condition)
|
@tdc.update!(condition: condition)
|
||||||
|
|
||||||
render 'administrateurs/types_de_champ/update.turbo_stream.haml'
|
@condition_component = build_condition_component
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_row
|
def delete_row
|
||||||
condition = condition_form.delete_row(row_index).to_condition
|
condition = condition_form.delete_row(row_index).to_condition
|
||||||
tdc.update!(condition: condition)
|
@tdc.update!(condition: condition)
|
||||||
|
|
||||||
render 'administrateurs/types_de_champ/update.turbo_stream.haml'
|
@condition_component = build_condition_component
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
tdc.update!(condition: nil)
|
@tdc.update!(condition: nil)
|
||||||
|
|
||||||
render 'administrateurs/types_de_champ/update.turbo_stream.haml'
|
@condition_component = build_condition_component
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_targeted_champ
|
def change_targeted_champ
|
||||||
condition = condition_form.change_champ(row_index).to_condition
|
condition = condition_form.change_champ(row_index).to_condition
|
||||||
tdc.update!(condition: condition)
|
@tdc.update!(condition: condition)
|
||||||
|
|
||||||
render 'administrateurs/types_de_champ/update.turbo_stream.haml'
|
@condition_component = build_condition_component
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_condition_component
|
||||||
|
TypesDeChampEditor::ConditionsComponent.new(
|
||||||
|
tdc: @tdc,
|
||||||
|
upper_tdcs: @upper_tdcs,
|
||||||
|
procedure_id: @procedure.id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def condition_form
|
def condition_form
|
||||||
ConditionForm.new(condition_params)
|
ConditionForm.new(condition_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def retrieve_coordinate_and_uppers
|
def retrieve_coordinate_and_uppers
|
||||||
@coordinate = draft_revision.coordinate_for(tdc)
|
@tdc = draft_revision.find_and_ensure_exclusive_use(params[:stable_id])
|
||||||
@upper_coordinates = draft_revision
|
@coordinate = draft_revision.coordinate_for(@tdc)
|
||||||
.revision_types_de_champ_public
|
@upper_tdcs = draft_revision
|
||||||
.includes(:type_de_champ)
|
.upper_coordinates(@coordinate.position)
|
||||||
.take_while { |c| c != @coordinate }
|
.map(&:type_de_champ)
|
||||||
end
|
|
||||||
|
|
||||||
def tdc
|
|
||||||
@tdc ||= draft_revision.find_and_ensure_exclusive_use(params[:stable_id])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def draft_revision
|
def draft_revision
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
module Administrateurs
|
module Administrateurs
|
||||||
class TypesDeChampController < AdministrateurController
|
class TypesDeChampController < AdministrateurController
|
||||||
before_action :retrieve_procedure, only: [:create, :update, :move, :move_up, :move_down, :destroy]
|
before_action :retrieve_procedure, :procedure_revisable?
|
||||||
before_action :procedure_revisable?, only: [:create, :update, :move, :move_up, :move_down, :destroy]
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
type_de_champ = @procedure.draft_revision.add_type_de_champ(type_de_champ_create_params)
|
type_de_champ = draft.add_type_de_champ(type_de_champ_create_params)
|
||||||
|
|
||||||
if type_de_champ.valid?
|
if type_de_champ.valid?
|
||||||
@coordinate = @procedure.draft_revision.coordinate_for(type_de_champ)
|
@coordinate = draft.coordinate_for(type_de_champ)
|
||||||
|
@created = champ_component_from(@coordinate, focused: true)
|
||||||
|
@morphed = champ_components_starting_at(@coordinate.position + 1)
|
||||||
|
|
||||||
reset_procedure
|
reset_procedure
|
||||||
flash.notice = "Formulaire enregistré"
|
flash.notice = "Formulaire enregistré"
|
||||||
else
|
else
|
||||||
|
@ -16,12 +18,12 @@ module Administrateurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
type_de_champ = @procedure.draft_revision.find_and_ensure_exclusive_use(params[:stable_id])
|
type_de_champ = draft.find_and_ensure_exclusive_use(params[:stable_id])
|
||||||
|
|
||||||
if type_de_champ.update(type_de_champ_update_params)
|
if type_de_champ.update(type_de_champ_update_params)
|
||||||
if params[:should_render]
|
@coordinate = draft.coordinate_for(type_de_champ)
|
||||||
@coordinate = @procedure.draft_revision.coordinate_for(type_de_champ)
|
@morphed = champ_components_starting_at(@coordinate.position)
|
||||||
end
|
|
||||||
reset_procedure
|
reset_procedure
|
||||||
flash.notice = "Formulaire enregistré"
|
flash.notice = "Formulaire enregistré"
|
||||||
else
|
else
|
||||||
|
@ -31,27 +33,53 @@ module Administrateurs
|
||||||
|
|
||||||
def move
|
def move
|
||||||
flash.notice = "Formulaire enregistré"
|
flash.notice = "Formulaire enregistré"
|
||||||
@procedure.draft_revision.move_type_de_champ(params[:stable_id], params[:position].to_i)
|
draft.move_type_de_champ(params[:stable_id], params[:position].to_i)
|
||||||
end
|
end
|
||||||
|
|
||||||
def move_up
|
def move_up
|
||||||
flash.notice = "Formulaire enregistré"
|
flash.notice = "Formulaire enregistré"
|
||||||
@coordinate = @procedure.draft_revision.move_up_type_de_champ(params[:stable_id])
|
@coordinate = draft.move_up_type_de_champ(params[:stable_id])
|
||||||
|
@destroyed = @coordinate
|
||||||
|
@created = champ_component_from(@coordinate)
|
||||||
|
# update the one component below
|
||||||
|
@morphed = champ_components_starting_at(@coordinate.position + 1).take(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def move_down
|
def move_down
|
||||||
flash.notice = "Formulaire enregistré"
|
flash.notice = "Formulaire enregistré"
|
||||||
@coordinate = @procedure.draft_revision.move_down_type_de_champ(params[:stable_id])
|
@coordinate = draft.move_down_type_de_champ(params[:stable_id])
|
||||||
|
@destroyed = @coordinate
|
||||||
|
@created = champ_component_from(@coordinate)
|
||||||
|
# update the one component above
|
||||||
|
@morphed = champ_components_starting_at(@coordinate.position - 1).take(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@coordinate = @procedure.draft_revision.remove_type_de_champ(params[:stable_id])
|
@coordinate = draft.remove_type_de_champ(params[:stable_id])
|
||||||
reset_procedure
|
reset_procedure
|
||||||
flash.notice = "Formulaire enregistré"
|
flash.notice = "Formulaire enregistré"
|
||||||
|
|
||||||
|
@destroyed = @coordinate
|
||||||
|
@morphed = champ_components_starting_at(@coordinate.position)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def champ_components_starting_at(position)
|
||||||
|
draft
|
||||||
|
.coordinates_starting_at(position)
|
||||||
|
.lazy
|
||||||
|
.map { |c| champ_component_from(c) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def champ_component_from(coordinate, focused: false)
|
||||||
|
TypesDeChampEditor::ChampComponent.new(
|
||||||
|
coordinate: coordinate,
|
||||||
|
upper_coordinates: draft.upper_coordinates(coordinate.position),
|
||||||
|
focused: focused
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def type_de_champ_create_params
|
def type_de_champ_create_params
|
||||||
params
|
params
|
||||||
.required(:type_de_champ)
|
.required(:type_de_champ)
|
||||||
|
@ -82,5 +110,9 @@ module Administrateurs
|
||||||
:znieff
|
:znieff
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def draft
|
||||||
|
@procedure.draft_revision
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,10 +33,10 @@ class Logic::ChampValue < Logic::Term
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_s = "#{type_de_champ.libelle} Nº#{stable_id}"
|
def to_s = "#{type_de_champ&.libelle} Nº#{stable_id}" # TODO: gerer le cas ou un tdc est supprimé
|
||||||
|
|
||||||
def type
|
def type
|
||||||
case type_de_champ.type_champ
|
case type_de_champ&.type_champ # TODO: gerer le cas ou un tdc est supprimé
|
||||||
when MANAGED_TYPE_DE_CHAMP.fetch(:yes_no),
|
when MANAGED_TYPE_DE_CHAMP.fetch(:yes_no),
|
||||||
MANAGED_TYPE_DE_CHAMP.fetch(:checkbox)
|
MANAGED_TYPE_DE_CHAMP.fetch(:checkbox)
|
||||||
CHAMP_VALUE_TYPE.fetch(:boolean)
|
CHAMP_VALUE_TYPE.fetch(:boolean)
|
||||||
|
|
|
@ -59,7 +59,7 @@ class ProcedureRevision < ApplicationRecord
|
||||||
h = { type_de_champ: tdc, parent_id: parent_id, position: position }
|
h = { type_de_champ: tdc, parent_id: parent_id, position: position }
|
||||||
coordinate = revision_types_de_champ.create!(h)
|
coordinate = revision_types_de_champ.create!(h)
|
||||||
|
|
||||||
reorder(coordinate.reload.siblings)
|
renumber(coordinate.reload.siblings)
|
||||||
end
|
end
|
||||||
|
|
||||||
# they are not aware of the addition
|
# they are not aware of the addition
|
||||||
|
@ -88,7 +88,7 @@ class ProcedureRevision < ApplicationRecord
|
||||||
|
|
||||||
siblings.insert(position, siblings.delete_at(siblings.index(coordinate)))
|
siblings.insert(position, siblings.delete_at(siblings.index(coordinate)))
|
||||||
|
|
||||||
reorder(siblings)
|
renumber(siblings)
|
||||||
coordinate.reload
|
coordinate.reload
|
||||||
|
|
||||||
coordinate
|
coordinate
|
||||||
|
@ -107,7 +107,7 @@ class ProcedureRevision < ApplicationRecord
|
||||||
types_de_champ_public.reset
|
types_de_champ_public.reset
|
||||||
types_de_champ_private.reset
|
types_de_champ_private.reset
|
||||||
|
|
||||||
reorder(coordinate.siblings)
|
renumber(coordinate.siblings)
|
||||||
|
|
||||||
coordinate
|
coordinate
|
||||||
end
|
end
|
||||||
|
@ -197,6 +197,14 @@ class ProcedureRevision < ApplicationRecord
|
||||||
revision_types_de_champ.find_by!(type_de_champ: tdc)
|
revision_types_de_champ.find_by!(type_de_champ: tdc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def upper_coordinates(position)
|
||||||
|
revision_types_de_champ_public.filter { |c| c.position < position }
|
||||||
|
end
|
||||||
|
|
||||||
|
def coordinates_starting_at(position)
|
||||||
|
revision_types_de_champ_public.reload.filter { |c| position <= c.position }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def compute_estimated_fill_duration
|
def compute_estimated_fill_duration
|
||||||
|
@ -221,10 +229,10 @@ class ProcedureRevision < ApplicationRecord
|
||||||
.joins(:type_de_champ)
|
.joins(:type_de_champ)
|
||||||
.find_by(type_de_champ: { stable_id: stable_id })
|
.find_by(type_de_champ: { stable_id: stable_id })
|
||||||
|
|
||||||
[coordinate, coordinate.type_de_champ]
|
[coordinate, coordinate&.type_de_champ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def reorder(siblings)
|
def renumber(siblings)
|
||||||
siblings.to_a.compact.each.with_index do |sibling, position|
|
siblings.to_a.compact.each.with_index do |sibling, position|
|
||||||
sibling.update_column(:position, position)
|
sibling.update_column(:position, position)
|
||||||
end
|
end
|
||||||
|
@ -482,7 +490,8 @@ class ProcedureRevision < ApplicationRecord
|
||||||
types_de_champ_public
|
types_de_champ_public
|
||||||
.map.with_index
|
.map.with_index
|
||||||
.filter_map { |tdc, i| tdc.condition.present? ? [tdc, i] : nil }
|
.filter_map { |tdc, i| tdc.condition.present? ? [tdc, i] : nil }
|
||||||
.flat_map { |tdc, i| tdc.condition.errors(stable_ids.take(i)) }
|
.map { |tdc, i| [tdc, tdc.condition.errors(stable_ids.take(i))] }
|
||||||
.each { |message| errors.add(:condition, message) }
|
.filter { |_tdc, errors| errors.present? }
|
||||||
|
.each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
- rendered = render @condition_component
|
||||||
|
|
||||||
|
- if rendered.present?
|
||||||
|
= turbo_stream.morph dom_id(@tdc, :conditions) do
|
||||||
|
- rendered
|
||||||
|
end
|
||||||
|
- else
|
||||||
|
= turbo_stream.remove dom_id(@tdc, :conditions)
|
|
@ -0,0 +1 @@
|
||||||
|
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
||||||
|
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
||||||
|
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
||||||
|
= render partial: 'update'
|
|
@ -0,0 +1 @@
|
||||||
|
= render partial: 'update'
|
|
@ -1,9 +1,19 @@
|
||||||
- sibling = coordinate.previous_sibling
|
- if @destroyed.present?
|
||||||
|
= turbo_stream.remove dom_id(@destroyed, :type_de_champ_editor)
|
||||||
|
|
||||||
|
- if @created.present?
|
||||||
|
- if @created.coordinate.previous_sibling.present?
|
||||||
|
= turbo_stream.after dom_id(@created.coordinate.previous_sibling, :type_de_champ_editor) do
|
||||||
|
= render @created
|
||||||
|
- else
|
||||||
|
= turbo_stream.prepend dom_id(@created.coordinate.block, :types_de_champ_editor_block) do
|
||||||
|
= render @created
|
||||||
|
|
||||||
|
- @morphed&.each do |champ_component|
|
||||||
|
= turbo_stream.morph dom_id(champ_component.coordinate, :type_de_champ_editor) do
|
||||||
|
= render champ_component
|
||||||
|
|
||||||
|
= turbo_stream.morph dom_id(@coordinate.revision, :estimated_fill_duration) do
|
||||||
|
= render TypesDeChampEditor::EstimatedFillDurationComponent.new(revision: @coordinate.revision, is_annotation: @coordinate.private?)
|
||||||
|
|
||||||
- if sibling.present?
|
|
||||||
= turbo_stream.after dom_id(sibling, :type_de_champ_editor) do
|
|
||||||
= render TypesDeChampEditor::ChampComponent.new(coordinate: coordinate, focused: true)
|
|
||||||
- else
|
|
||||||
= turbo_stream.prepend dom_id(coordinate.block, :types_de_champ_editor_block) do
|
|
||||||
= render TypesDeChampEditor::ChampComponent.new(coordinate: coordinate, focused: true)
|
|
||||||
= turbo_stream.dispatch 'sortable:sort'
|
= turbo_stream.dispatch 'sortable:sort'
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
- if @coordinate&.type_de_champ&.valid?
|
= render partial: 'insert'
|
||||||
= render partial: 'insert', locals: { coordinate: @coordinate }
|
|
||||||
|
|
|
@ -1,4 +1 @@
|
||||||
= turbo_stream.remove dom_id(@coordinate, :type_de_champ_editor)
|
= render partial: 'insert'
|
||||||
= turbo_stream.dispatch 'sortable:sort'
|
|
||||||
= turbo_stream.morph dom_id(@coordinate.revision, :estimated_fill_duration) do
|
|
||||||
= render TypesDeChampEditor::EstimatedFillDurationComponent.new(revision: @coordinate.revision, is_annotation: @coordinate.private?)
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
= turbo_stream.remove dom_id(@coordinate, :type_de_champ_editor)
|
= render partial: 'insert'
|
||||||
= render partial: 'insert', locals: { coordinate: @coordinate }
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
= turbo_stream.remove dom_id(@coordinate, :type_de_champ_editor)
|
= render partial: 'insert'
|
||||||
= render partial: 'insert', locals: { coordinate: @coordinate }
|
|
||||||
|
|
|
@ -1,5 +1 @@
|
||||||
- if @coordinate&.type_de_champ&.valid?
|
= render partial: 'insert'
|
||||||
= turbo_stream.morph dom_id(@coordinate, :type_de_champ_editor) do
|
|
||||||
= render TypesDeChampEditor::ChampComponent.new(coordinate: @coordinate)
|
|
||||||
= turbo_stream.morph dom_id(@coordinate.revision, :estimated_fill_duration) do
|
|
||||||
= render TypesDeChampEditor::EstimatedFillDurationComponent.new(revision: @coordinate.revision, is_annotation: @coordinate.private?)
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
describe TypesDeChampEditor::ConditionsComponent, type: :component do
|
describe TypesDeChampEditor::ConditionsComponent, type: :component do
|
||||||
include Logic
|
include Logic
|
||||||
|
|
||||||
|
# TODO: remove along with feature flipping
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(nil)
|
||||||
|
allow(Flipper).to receive(:enabled?).with(:conditional, anything).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'render' do
|
describe 'render' do
|
||||||
let(:tdc) { create(:type_de_champ, condition: condition) }
|
let(:tdc) { create(:type_de_champ, condition: condition) }
|
||||||
let(:condition) { nil }
|
let(:condition) { nil }
|
||||||
|
|
|
@ -5,9 +5,7 @@ describe Administrateurs::ConditionsController, type: :controller do
|
||||||
let(:first_coordinate) { procedure.draft_revision.revision_types_de_champ.first }
|
let(:first_coordinate) { procedure.draft_revision.revision_types_de_champ.first }
|
||||||
let(:second_tdc) { procedure.draft_revision.types_de_champ.second }
|
let(:second_tdc) { procedure.draft_revision.types_de_champ.second }
|
||||||
|
|
||||||
before do
|
before { sign_in(procedure.administrateurs.first.user) }
|
||||||
sign_in(procedure.administrateurs.first.user)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:default_params) do
|
let(:default_params) do
|
||||||
{
|
{
|
||||||
|
@ -17,9 +15,7 @@ describe Administrateurs::ConditionsController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#update' do
|
describe '#update' do
|
||||||
before do
|
before { post :update, params: params, format: :turbo_stream }
|
||||||
post :update, params: params
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:params) { default_params.merge(type_de_champ: { condition_form: condition_form }) }
|
let(:params) { default_params.merge(type_de_champ: { condition_form: condition_form }) }
|
||||||
|
|
||||||
|
@ -38,26 +34,22 @@ describe Administrateurs::ConditionsController, type: :controller do
|
||||||
it do
|
it do
|
||||||
expect(second_tdc.reload.condition).to eq(ds_eq(champ_value(1), constant(2)))
|
expect(second_tdc.reload.condition).to eq(ds_eq(champ_value(1), constant(2)))
|
||||||
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
||||||
expect(assigns(:upper_coordinates)).to eq([first_coordinate])
|
expect(assigns(:upper_tdcs)).to eq([first_coordinate.type_de_champ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#add_row' do
|
describe '#add_row' do
|
||||||
before do
|
before { post :add_row, params: default_params, format: :turbo_stream }
|
||||||
post :add_row, params: default_params
|
|
||||||
end
|
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect(second_tdc.reload.condition).to eq(empty_operator(empty, empty))
|
expect(second_tdc.reload.condition).to eq(empty_operator(empty, empty))
|
||||||
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
||||||
expect(assigns(:upper_coordinates)).to eq([first_coordinate])
|
expect(assigns(:upper_tdcs)).to eq([first_coordinate.type_de_champ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#delete_row' do
|
describe '#delete_row' do
|
||||||
before do
|
before { delete :delete_row, params: params.merge(row_index: 0), format: :turbo_stream }
|
||||||
delete :delete_row, params: params.merge(row_index: 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:params) { default_params.merge(type_de_champ: { condition_form: condition_form }) }
|
let(:params) { default_params.merge(type_de_champ: { condition_form: condition_form }) }
|
||||||
|
|
||||||
|
@ -76,20 +68,20 @@ describe Administrateurs::ConditionsController, type: :controller do
|
||||||
it do
|
it do
|
||||||
expect(second_tdc.reload.condition).to eq(nil)
|
expect(second_tdc.reload.condition).to eq(nil)
|
||||||
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
||||||
expect(assigns(:upper_coordinates)).to eq([first_coordinate])
|
expect(assigns(:upper_tdcs)).to eq([first_coordinate.type_de_champ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#destroy' do
|
describe '#destroy' do
|
||||||
before do
|
before do
|
||||||
second_tdc.update(condition: empty_operator(empty, empty))
|
second_tdc.update(condition: empty_operator(empty, empty))
|
||||||
delete :destroy, params: default_params
|
delete :destroy, params: default_params, format: :turbo_stream
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect(second_tdc.reload.condition).to eq(nil)
|
expect(second_tdc.reload.condition).to eq(nil)
|
||||||
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
||||||
expect(assigns(:upper_coordinates)).to eq([first_coordinate])
|
expect(assigns(:upper_tdcs)).to eq([first_coordinate.type_de_champ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -98,7 +90,7 @@ describe Administrateurs::ConditionsController, type: :controller do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
second_tdc.update(condition: empty_operator(empty, empty))
|
second_tdc.update(condition: empty_operator(empty, empty))
|
||||||
patch :change_targeted_champ, params: params
|
patch :change_targeted_champ, params: params, format: :turbo_stream
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:params) { default_params.merge(type_de_champ: { condition_form: condition_form }) }
|
let(:params) { default_params.merge(type_de_champ: { condition_form: condition_form }) }
|
||||||
|
@ -118,7 +110,7 @@ describe Administrateurs::ConditionsController, type: :controller do
|
||||||
it do
|
it do
|
||||||
expect(second_tdc.reload.condition).to eq(ds_eq(champ_value(number_tdc.stable_id), constant(0)))
|
expect(second_tdc.reload.condition).to eq(ds_eq(champ_value(number_tdc.stable_id), constant(0)))
|
||||||
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
expect(assigns(:coordinate)).to eq(procedure.draft_revision.coordinate_for(second_tdc))
|
||||||
expect(assigns(:upper_coordinates)).to eq([first_coordinate])
|
expect(assigns(:upper_tdcs)).to eq([first_coordinate.type_de_champ])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,65 +1,159 @@
|
||||||
describe Administrateurs::TypesDeChampController, type: :controller do
|
describe Administrateurs::TypesDeChampController, type: :controller do
|
||||||
let(:admin) { create(:administrateur) }
|
let(:procedure) do
|
||||||
|
create(:procedure).tap do |p|
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'l1')
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'l2')
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'l3')
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :yes_no, libelle: 'bon dossier', private: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#types_de_champs editor' do
|
def first_coordinate = procedure.draft_revision.revision_types_de_champ_public.first
|
||||||
let(:procedure) { create(:procedure) }
|
def second_coordinate = procedure.draft_revision.revision_types_de_champ_public.reload.second
|
||||||
|
def third_coordinate = procedure.draft_revision.revision_types_de_champ_public.reload.third
|
||||||
|
|
||||||
before do
|
def extract_libelle(champ_component) = [champ_component.coordinate.libelle, champ_component.upper_coordinates.map(&:libelle)]
|
||||||
admin.procedures << procedure
|
|
||||||
sign_in(admin.user)
|
def morpheds
|
||||||
|
assigns(:morphed)
|
||||||
|
.map { |component| extract_libelle(component) }.to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
before { sign_in(procedure.administrateurs.first.user) }
|
||||||
|
|
||||||
|
describe '#create' do
|
||||||
|
let(:params) { default_params }
|
||||||
|
|
||||||
|
let(:default_params) do
|
||||||
|
{
|
||||||
|
procedure_id: procedure.id,
|
||||||
|
type_de_champ: {
|
||||||
|
type_champ: type_champ,
|
||||||
|
libelle: 'l1.5',
|
||||||
|
placeholder: "custom placeholder",
|
||||||
|
after_stable_id: first_coordinate.stable_id
|
||||||
|
}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:type_champ) { TypeDeChamp.type_champs.fetch(:text) }
|
subject { post :create, params: params, format: :turbo_stream }
|
||||||
|
|
||||||
context "create type_de_champ text" do
|
context "create type_de_champ text" do
|
||||||
before do
|
let(:type_champ) { TypeDeChamp.type_champs.fetch(:text) }
|
||||||
post :create, params: {
|
|
||||||
procedure_id: procedure.id,
|
|
||||||
type_de_champ: {
|
|
||||||
type_champ: type_champ,
|
|
||||||
libelle: 'Nouveau champ',
|
|
||||||
private: false,
|
|
||||||
placeholder: "custom placeholder"
|
|
||||||
}
|
|
||||||
}, format: :turbo_stream
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(response).to have_http_status(:ok) }
|
# l1, l2, l3 => l1, l1.5, l2, l3
|
||||||
|
# created: (l1.5, [l1]), morphed: (l2, [l1, l1.5]), (l3, [l1, l1.5, l2])
|
||||||
|
it do
|
||||||
|
is_expected.to have_http_status(:ok)
|
||||||
|
expect(flash.alert).to eq(nil)
|
||||||
|
expect(assigns(:coordinate)).to eq(second_coordinate)
|
||||||
|
expect(extract_libelle(assigns(:created))).to eq(['l1.5', ['l1']])
|
||||||
|
expect(morpheds).to eq([['l2', ['l1', 'l1.5']], ['l3', ['l1', 'l1.5', 'l2']]])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "validate type_de_champ linked_drop_down_list" do
|
context "validate type_de_champ linked_drop_down_list" do
|
||||||
let(:type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) }
|
let(:type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) }
|
||||||
|
|
||||||
before do
|
it do
|
||||||
post :create, params: {
|
is_expected.to have_http_status(:ok)
|
||||||
procedure_id: procedure.id,
|
expect(flash.alert).to eq(nil)
|
||||||
type_de_champ: {
|
|
||||||
type_champ: type_champ,
|
|
||||||
libelle: 'Nouveau champ',
|
|
||||||
private: false
|
|
||||||
}
|
|
||||||
}, format: :turbo_stream
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(response).to have_http_status(:ok) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "create type_de_champ linked_drop_down_list" do
|
context "create type_de_champ linked_drop_down_list" do
|
||||||
let(:type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) }
|
let(:type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) }
|
||||||
|
let(:params) { default_params.deep_merge(type_de_champ: { drop_down_list_value: '--value--' }) }
|
||||||
|
|
||||||
before do
|
it do
|
||||||
post :create, params: {
|
is_expected.to have_http_status(:ok)
|
||||||
procedure_id: procedure.id,
|
expect(flash.alert).to eq(nil)
|
||||||
type_de_champ: {
|
|
||||||
type_champ: type_champ,
|
|
||||||
libelle: 'Nouveau champ',
|
|
||||||
drop_down_list_value: '--value--',
|
|
||||||
private: false
|
|
||||||
}
|
|
||||||
}, format: :turbo_stream
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it { expect(response).to have_http_status(:ok) }
|
describe '#update' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
procedure_id: procedure.id,
|
||||||
|
stable_id: second_coordinate.stable_id,
|
||||||
|
type_de_champ: {
|
||||||
|
libelle: 'updated'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { post :update, params: params, format: :turbo_stream }
|
||||||
|
|
||||||
|
# l1, l2, l3 => l1, updated, l3
|
||||||
|
# morphed: (updated, [l1]), (l3, [l1, updated])
|
||||||
|
it do
|
||||||
|
is_expected.to have_http_status(:ok)
|
||||||
|
expect(flash.alert).to eq(nil)
|
||||||
|
expect(second_coordinate.libelle).to eq('updated')
|
||||||
|
|
||||||
|
expect(assigns(:coordinate)).to eq(second_coordinate)
|
||||||
|
expect(morpheds).to eq([['updated', ['l1']], ['l3', ['l1', 'updated']]])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# l1, l2, l3 => l1, l3, l2
|
||||||
|
# destroyed: l3, created: (l3, [l1]), morphed: (l2, [l1, l3])
|
||||||
|
describe '#move_up' do
|
||||||
|
let(:params) do
|
||||||
|
{ procedure_id: procedure.id, stable_id: third_coordinate.stable_id }
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { patch :move_up, params: params, format: :turbo_stream }
|
||||||
|
|
||||||
|
it do
|
||||||
|
is_expected.to have_http_status(:ok)
|
||||||
|
expect(flash.alert).to eq(nil)
|
||||||
|
expect(second_coordinate.libelle).to eq('l3')
|
||||||
|
expect(assigns(:coordinate)).to eq(second_coordinate)
|
||||||
|
expect(assigns(:destroyed).libelle).to eq('l3')
|
||||||
|
expect(extract_libelle(assigns(:created))).to eq(['l3', ['l1']])
|
||||||
|
expect(morpheds).to eq([['l2', ['l1', 'l3']]])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# l1, l2, l3 => l2, l1, l3
|
||||||
|
# destroyed: l1, created: (l1, [l2]), morphed: (l2, [])
|
||||||
|
describe '#move_down' do
|
||||||
|
let(:params) do
|
||||||
|
{ procedure_id: procedure.id, stable_id: first_coordinate.stable_id }
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { patch :move_down, params: params, format: :turbo_stream }
|
||||||
|
|
||||||
|
it do
|
||||||
|
is_expected.to have_http_status(:ok)
|
||||||
|
expect(flash.alert).to eq(nil)
|
||||||
|
|
||||||
|
expect(assigns(:coordinate)).to eq(second_coordinate)
|
||||||
|
expect(assigns(:destroyed).libelle).to eq('l1')
|
||||||
|
expect(extract_libelle(assigns(:created))).to eq(['l1', ['l2']])
|
||||||
|
expect(morpheds).to eq([['l2', []]])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# l1, l2, l3 => l1, l3
|
||||||
|
# destroyed: l2, morphed: (l3, [l1])
|
||||||
|
describe '#destroy' do
|
||||||
|
let(:params) do
|
||||||
|
{ procedure_id: procedure.id, stable_id: second_coordinate.stable_id }
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { delete :destroy, params: params, format: :turbo_stream }
|
||||||
|
|
||||||
|
it do
|
||||||
|
used_to_be_second_coordinate = second_coordinate
|
||||||
|
|
||||||
|
is_expected.to have_http_status(:ok)
|
||||||
|
expect(flash.alert).to eq(nil)
|
||||||
|
expect(assigns(:coordinate)).to eq(used_to_be_second_coordinate)
|
||||||
|
expect(assigns(:destroyed).libelle).to eq('l2')
|
||||||
|
expect(morpheds).to eq([['l3', ['l1']]])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
129
spec/system/administrateurs/condition_spec.rb
Normal file
129
spec/system/administrateurs/condition_spec.rb
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
describe 'As an administrateur I can edit types de champ condition', js: true do
|
||||||
|
include Logic
|
||||||
|
|
||||||
|
let(:administrateur) { procedure.administrateurs.first }
|
||||||
|
let(:procedure) do
|
||||||
|
create(:procedure).tap do |p|
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :integer_number, libelle: 'age')
|
||||||
|
# private
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :boolean, libelle: 'bon dossier', private: true)
|
||||||
|
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :text, libelle: 'nom du parent')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:first_tdc) { procedure.draft_revision.types_de_champ.first }
|
||||||
|
let(:second_tdc) { procedure.draft_revision.types_de_champ.second }
|
||||||
|
|
||||||
|
before do
|
||||||
|
login_as administrateur.user, scope: :user
|
||||||
|
# TODO: remove along with feature flipping
|
||||||
|
allow(Flipper).to receive(:enabled?).with(:conditional, anything).and_return(true)
|
||||||
|
visit champs_admin_procedure_path(procedure)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "adding a new condition" do
|
||||||
|
within '.type-de-champ:nth-child(2)' do
|
||||||
|
click_on 'cliquer pour activer'
|
||||||
|
|
||||||
|
within '.condition-table tbody tr:nth-child(1)' do
|
||||||
|
expect(page).to have_select('type_de_champ[condition_form][rows][][targeted_champ]', options: ['Sélectionner', 'age'])
|
||||||
|
|
||||||
|
within('.target') { select('age') }
|
||||||
|
within('.operator') { select('Supérieur ou égal à') }
|
||||||
|
within('.value') { fill_in with: 18 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expected_condition = greater_than_eq(champ_value(first_tdc.stable_id), constant(18))
|
||||||
|
wait_until { second_tdc.reload.condition == expected_condition }
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "the first type de champ is removed" do
|
||||||
|
within '.type-de-champ:nth-child(1)' do
|
||||||
|
click_on 'Supprimer'
|
||||||
|
page.accept_alert
|
||||||
|
end
|
||||||
|
|
||||||
|
# the condition table is deleted
|
||||||
|
expect(page).to have_no_content('Logique conditionnelle')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a preexisting condition' do
|
||||||
|
before do
|
||||||
|
second_tdc.update(condition: greater_than_eq(champ_value(first_tdc.stable_id), constant(18)))
|
||||||
|
|
||||||
|
page.refresh
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "removing all conditions" do
|
||||||
|
within '.type-de-champ:nth-child(2)' do
|
||||||
|
click_on 'cliquer pour désactiver'
|
||||||
|
page.accept_alert
|
||||||
|
|
||||||
|
# the condition table is deleted
|
||||||
|
expect(page).to have_no_table
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "removing a condition" do
|
||||||
|
within '.type-de-champ:nth-child(2)' do
|
||||||
|
within '.condition-table tbody tr:nth-child(1)' do
|
||||||
|
within('.delete-column') { click_on 'Supprimer la ligne' }
|
||||||
|
end
|
||||||
|
|
||||||
|
# the condition table is deleted
|
||||||
|
expect(page).to have_no_table
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "adding a second row" do
|
||||||
|
within '.type-de-champ:nth-child(2)' do
|
||||||
|
click_on 'Ajouter une condition'
|
||||||
|
|
||||||
|
# the condition table has 2 rows
|
||||||
|
within '.condition-table tbody' do
|
||||||
|
expect(page).to have_selector('tr', count: 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "changing target champ to a not managed type" do
|
||||||
|
expect(page).to have_no_selector('.condition-error')
|
||||||
|
|
||||||
|
within '.type-de-champ:nth-child(1)' do
|
||||||
|
select('Départements', from: 'Type de champ')
|
||||||
|
end
|
||||||
|
|
||||||
|
within '.type-de-champ:nth-child(2)' do
|
||||||
|
expect(page).to have_selector('.condition-error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "moving a target champ below the condition" do
|
||||||
|
expect(page).to have_no_selector('.condition-error')
|
||||||
|
|
||||||
|
within '.type-de-champ:nth-child(1)' do
|
||||||
|
click_on 'Déplacer le champ vers le bas'
|
||||||
|
end
|
||||||
|
|
||||||
|
# the now first champ has an error
|
||||||
|
within '.type-de-champ:nth-child(1)' do
|
||||||
|
expect(page).to have_selector('.condition-error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "moving the condition champ above the target" do
|
||||||
|
expect(page).to have_no_selector('.condition-error')
|
||||||
|
|
||||||
|
within '.type-de-champ:nth-child(2)' do
|
||||||
|
click_on 'Déplacer le champ vers le haut'
|
||||||
|
end
|
||||||
|
|
||||||
|
# the now first champ has an error
|
||||||
|
within '.type-de-champ:nth-child(1)' do
|
||||||
|
expect(page).to have_selector('.condition-error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue