Merge pull request #8695 from mfo/US/form-section
amelioration(a11y): regroupe les champs précédés d'un titre de section dans un fieldset
This commit is contained in:
commit
9a29e459cf
45 changed files with 874 additions and 80 deletions
|
@ -1,17 +1,7 @@
|
||||||
@import "colors";
|
@import "colors";
|
||||||
@import "constants";
|
@import "constants";
|
||||||
|
|
||||||
.conditionnel {
|
form.form > .conditionnel {
|
||||||
|
|
||||||
.condition-error {
|
|
||||||
background: $background-red;
|
|
||||||
margin: ($default-spacer) (-$default-spacer);
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding: $default-spacer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.condition-table {
|
.condition-table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
|
||||||
|
|
12
app/assets/stylesheets/errors_summary.scss
Normal file
12
app/assets/stylesheets/errors_summary.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@import "colors";
|
||||||
|
@import "constants";
|
||||||
|
|
||||||
|
.errors-summary {
|
||||||
|
background: $background-red;
|
||||||
|
margin: ($default-spacer) (-$default-spacer);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: $default-spacer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -438,14 +438,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section {
|
|
||||||
display: inline-block;
|
|
||||||
color: $blue-france-500;
|
|
||||||
font-size: 30px;
|
|
||||||
margin-bottom: 3 * $default-padding;
|
|
||||||
border-bottom: 3px solid $blue-france-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-subsection {
|
.header-subsection {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
color: $blue-france-500;
|
color: $blue-france-500;
|
||||||
|
|
|
@ -1,11 +1,63 @@
|
||||||
.counter-start-header-section {
|
.counter-start-header-section {
|
||||||
counter-reset: headerSectionCounter;
|
counter-reset: h1 h2 h3 h4 h5 h6;
|
||||||
}
|
|
||||||
|
|
||||||
.header-section {
|
.reset-h1 {
|
||||||
counter-increment: headerSectionCounter;
|
counter-reset: h2;
|
||||||
|
}
|
||||||
|
|
||||||
&.header-section-counter::before {
|
.reset-h2 {
|
||||||
content: counter(headerSectionCounter) ". ";
|
counter-reset: h3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-h3 {
|
||||||
|
counter-reset: h4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-h4 {
|
||||||
|
counter-reset: h5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-h5 {
|
||||||
|
counter-reset: h6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.header-section.fr-h1::before {
|
||||||
|
counter-increment: h1;
|
||||||
|
content: counter(h1) ". ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.fr-h2::before {
|
||||||
|
counter-increment: h2;
|
||||||
|
content: counter(h1) "."counter(h2) ". ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.fr-h3::before {
|
||||||
|
counter-increment: h3;
|
||||||
|
content: counter(h1) "."counter(h2) "." counter(h3) ". ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.fr-h4::before {
|
||||||
|
counter-increment: h4;
|
||||||
|
content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) ". ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.fr-h5::before {
|
||||||
|
counter-increment: h5;
|
||||||
|
content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section.fr-h6::before {
|
||||||
|
counter-increment: h6;
|
||||||
|
content: counter(h1) "."counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.repetition {
|
||||||
|
counter-reset: repetition;
|
||||||
|
|
||||||
|
.block-id::after {
|
||||||
|
counter-increment: repetition;
|
||||||
|
content: counter(repetition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,11 @@ class EditableChamp::EditableChampComponent < ApplicationComponent
|
||||||
private
|
private
|
||||||
|
|
||||||
def has_label?(champ)
|
def has_label?(champ)
|
||||||
types_without_label = [TypeDeChamp.type_champs.fetch(:header_section), TypeDeChamp.type_champs.fetch(:explication)]
|
types_without_label = [
|
||||||
|
TypeDeChamp.type_champs.fetch(:header_section),
|
||||||
|
TypeDeChamp.type_champs.fetch(:explication),
|
||||||
|
TypeDeChamp.type_champs.fetch(:repetition)
|
||||||
|
]
|
||||||
!types_without_label.include?(@champ.type_champ)
|
!types_without_label.include?(@champ.type_champ)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
.editable-champ{ html_options }
|
.editable-champ{ html_options }
|
||||||
- if @champ.block?
|
- if has_label?(@champ)
|
||||||
%h3.header-subsection= @champ.libelle
|
|
||||||
- if @champ.description.present?
|
|
||||||
.notice= render SimpleFormatComponent.new(@champ.description, allow_a: true)
|
|
||||||
|
|
||||||
- elsif has_label?(@champ)
|
|
||||||
= render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at
|
= render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at
|
||||||
- if @champ.titre_identite?
|
- if @champ.titre_identite?
|
||||||
%p.notice= t('.titre_identite_notice')
|
%p.notice= t('.titre_identite_notice')
|
||||||
|
|
|
@ -1,2 +1,24 @@
|
||||||
class EditableChamp::HeaderSectionComponent < EditableChamp::EditableChampBaseComponent
|
class EditableChamp::HeaderSectionComponent < ApplicationComponent
|
||||||
|
def initialize(form: nil, champ:, seen_at: nil)
|
||||||
|
@champ = champ
|
||||||
|
end
|
||||||
|
|
||||||
|
def level
|
||||||
|
@champ.level
|
||||||
|
end
|
||||||
|
|
||||||
|
def libelle
|
||||||
|
@champ.libelle
|
||||||
|
end
|
||||||
|
|
||||||
|
def header_section_classnames
|
||||||
|
class_names = ["fr-h#{level}"]
|
||||||
|
|
||||||
|
class_names << 'header-section' if @champ.dossier.auto_numbering_section_headers_for?(@champ)
|
||||||
|
class_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_for_depth
|
||||||
|
"h#{level + 1}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
%h2.header-section{ class: @champ.dossier.auto_numbering_section_headers_for?(@champ) ? "header-section-counter" : nil }
|
= tag.send(tag_for_depth, class: header_section_classnames) do
|
||||||
= @champ.libelle
|
= libelle
|
||||||
|
|
|
@ -1,2 +1,9 @@
|
||||||
class EditableChamp::RepetitionComponent < EditableChamp::EditableChampBaseComponent
|
class EditableChamp::RepetitionComponent < EditableChamp::EditableChampBaseComponent
|
||||||
|
def legend_params
|
||||||
|
@champ.description.present? ? { describedby: dom_id(@champ, :repetition) } : {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def notice_params
|
||||||
|
@champ.description.present? ? { id: dom_id(@champ, :repetition) } : {}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
.repetition{ id: dom_id(@champ, :rows) }
|
%fieldset
|
||||||
- @champ.rows.each do |champs|
|
%legend.header-subsection{ legend_params }= @champ.libelle
|
||||||
= render EditableChamp::RepetitionRowComponent.new(form: @form, champ: @champ, row: champs, seen_at: @seen_at)
|
- if @champ.description.present?
|
||||||
|
.notice{ notice_params }= render SimpleFormatComponent.new(@champ.description, allow_a: true)
|
||||||
|
|
||||||
.actions{ 'data-turbo': 'true' }
|
|
||||||
= render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id), http_method: :create, opt: { class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-circle-line fr-mb-3w", title: t(".add_title", libelle: @champ.libelle), id: dom_id(@champ, :create_repetition)}) do
|
.repetition{ id: dom_id(@champ, :rows) }
|
||||||
= t(".add", libelle: @champ.libelle)
|
- @champ.rows.each do |champs|
|
||||||
|
= render EditableChamp::RepetitionRowComponent.new(form: @form, champ: @champ, row: champs, seen_at: @seen_at)
|
||||||
|
|
||||||
|
.actions{ 'data-turbo': 'true' }
|
||||||
|
= render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id), http_method: :create, opt: { class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-circle-line fr-mb-3w", title: t(".add_title", libelle: @champ.libelle), id: dom_id(@champ, :create_repetition)}) do
|
||||||
|
= t(".add", libelle: @champ.libelle)
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
- row_id = "safe-row-selector-#{@row.first.row_id}"
|
- row_id = "safe-row-selector-#{@row.first.row_id}"
|
||||||
.row{ id: row_id }
|
.row{ id: row_id }
|
||||||
- @row.each do |champ|
|
- if @row.size > 1
|
||||||
= fields_for champ.input_name, champ do |form|
|
%fieldset
|
||||||
= render EditableChamp::EditableChampComponent.new form: form, champ: champ, seen_at: @seen_at
|
%legend.block-id= "#{@champ.libelle} "
|
||||||
|
= render EditableChamp::SectionComponent.new(champs: @row)
|
||||||
|
- else
|
||||||
|
= render EditableChamp::SectionComponent.new(champs: @row)
|
||||||
|
|
||||||
.flex.row-reverse{ 'data-turbo': 'true' }
|
.flex.row-reverse{ 'data-turbo': 'true' }
|
||||||
= render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id, row_id: @row.first.row_id), http_method: :delete, opt: { class: "fr-btn fr-btn--sm fr-btn--tertiary fr-text-action-high--red-marianne", title: t(".delete_title", row_number: @champ.rows.find_index(@row))}) do
|
= render NestedForms::OwnedButtonComponent.new(formaction: champs_repetition_path(@champ.id, row_id: @row.first.row_id), http_method: :delete, opt: { class: "fr-btn fr-btn--sm fr-btn--tertiary fr-text-action-high--red-marianne", title: t(".delete_title", row_number: @champ.rows.find_index(@row))}) do
|
||||||
|
|
64
app/components/editable_champ/section_component.rb
Normal file
64
app/components/editable_champ/section_component.rb
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
class EditableChamp::SectionComponent < ApplicationComponent
|
||||||
|
include ApplicationHelper
|
||||||
|
include TreeableConcern
|
||||||
|
|
||||||
|
def initialize(nodes: nil, champs: nil)
|
||||||
|
if (nodes.nil?)
|
||||||
|
nodes = to_tree(champs:)
|
||||||
|
end
|
||||||
|
@nodes = to_fieldset(nodes:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_within_fieldset?
|
||||||
|
first_champ_is_an_header_section? && any_champ_fillable?
|
||||||
|
end
|
||||||
|
|
||||||
|
def header_section
|
||||||
|
return @nodes.first if @nodes.first.is_a?(Champs::HeaderSectionChamp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def splitted_tail
|
||||||
|
tail.map { split_section_champ(_1) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def tail
|
||||||
|
return @nodes if !first_champ_is_an_header_section?
|
||||||
|
_, *rest_of_champ = @nodes
|
||||||
|
|
||||||
|
rest_of_champ
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_for_depth
|
||||||
|
"h#{header_section.level + 1}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# if two headers follows each others [h1, [h2, c]]
|
||||||
|
# the first one must not be contained in fieldset
|
||||||
|
# so we make the tree not fillable
|
||||||
|
def fillable?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def split_section_champ(node)
|
||||||
|
case node
|
||||||
|
when EditableChamp::SectionComponent
|
||||||
|
[node, nil]
|
||||||
|
else
|
||||||
|
[nil, node]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def to_fieldset(nodes:)
|
||||||
|
nodes.map { _1.is_a?(Array) ? EditableChamp::SectionComponent.new(nodes: _1) : _1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_champ_is_an_header_section?
|
||||||
|
header_section.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def any_champ_fillable?
|
||||||
|
tail.any? { _1&.fillable? }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
- if render_within_fieldset?
|
||||||
|
= tag.fieldset(class: "reset-#{tag_for_depth}") do
|
||||||
|
= tag.legend do
|
||||||
|
= render EditableChamp::HeaderSectionComponent.new(champ: header_section)
|
||||||
|
- splitted_tail.each do |section, champ|
|
||||||
|
- if section.present?
|
||||||
|
= render section
|
||||||
|
- else
|
||||||
|
= fields_for champ.input_name, champ do |form|
|
||||||
|
= render EditableChamp::EditableChampComponent.new form: ,champ:
|
||||||
|
- else
|
||||||
|
- if header_section
|
||||||
|
= render EditableChamp::HeaderSectionComponent.new(champ: header_section)
|
||||||
|
- splitted_tail.each do |section, champ|
|
||||||
|
- if section.present?
|
||||||
|
= render section
|
||||||
|
- else
|
||||||
|
= fields_for champ.input_name, champ do |form|
|
||||||
|
= render EditableChamp::EditableChampComponent.new form: ,champ:
|
|
@ -48,6 +48,11 @@
|
||||||
.cell.mt-1
|
.cell.mt-1
|
||||||
= form.label :description, "Description du champ (optionnel)", for: dom_id(type_de_champ, :description)
|
= form.label :description, "Description du champ (optionnel)", for: dom_id(type_de_champ, :description)
|
||||||
= form.text_area :description, class: 'small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :description)
|
= form.text_area :description, class: 'small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :description)
|
||||||
|
- if type_de_champ.header_section?
|
||||||
|
.cell.mt-1
|
||||||
|
= render TypesDeChampEditor::HeaderSectionComponent.new(form: form, tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.flex.justify-start.mt-1
|
.flex.justify-start.mt-1
|
||||||
- if type_de_champ.drop_down_list?
|
- if type_de_champ.drop_down_list?
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.flex.justify-start.section{ id: dom_id(@tdc.stable_self, :conditions) }
|
.flex.justify-start.section{ id: dom_id(@tdc.stable_self, :condition) }
|
||||||
= form_tag admin_procedure_condition_path(@procedure_id, @tdc.stable_id), method: :patch, class: 'form width-100' do
|
= form_tag admin_procedure_condition_path(@procedure_id, @tdc.stable_id), method: :patch, class: 'form width-100' do
|
||||||
.conditionnel.mt-2.width-100
|
.conditionnel.mt-2.width-100
|
||||||
.flex
|
.flex
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
.condition-error
|
.errors-summary
|
||||||
= errors
|
= errors
|
||||||
|
|
|
@ -3,16 +3,32 @@ class TypesDeChampEditor::ErrorsSummary < ApplicationComponent
|
||||||
@revision = revision
|
@revision = revision
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def invalid?
|
||||||
|
@revision.invalid?
|
||||||
|
end
|
||||||
|
|
||||||
|
def condition_errors?
|
||||||
|
@revision.errors.include?(:condition)
|
||||||
|
end
|
||||||
|
|
||||||
|
def header_section_errors?
|
||||||
|
@revision.errors.include?(:header_section)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def error_message
|
def errors_for(key)
|
||||||
@revision.errors
|
@revision.errors.filter { _1.attribute == key }
|
||||||
|
end
|
||||||
|
|
||||||
|
def error_message_for(key)
|
||||||
|
errors_for(key)
|
||||||
.map { |error| error.options[:type_de_champ] }
|
.map { |error| error.options[:type_de_champ] }
|
||||||
.map { |tdc| tag.li(tdc_anchor(tdc)) }
|
.map { |tdc| tag.li(tdc_anchor(tdc, key)) }
|
||||||
.then { |lis| tag.ul(lis.reduce(&:+)) }
|
.then { |lis| tag.ul(lis.reduce(&:+)) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def tdc_anchor(tdc)
|
def tdc_anchor(tdc, key)
|
||||||
tag.a(tdc.libelle, href: champs_admin_procedure_path(@revision.procedure_id, anchor: dom_id(tdc.stable_self, :conditions)), data: { turbo: false })
|
tag.a(tdc.libelle, href: champs_admin_procedure_path(@revision.procedure_id, anchor: dom_id(tdc.stable_self, key)), data: { turbo: false })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
fr:
|
fr:
|
||||||
fix:
|
fix_conditional:
|
||||||
one: 'Corrigez le champ suivant :'
|
one: 'La logique conditionnelle du champ suivant est invalide, veuillez la corriger :'
|
||||||
other: 'Corrigez les champs suivants :'
|
other: 'La logique conditionnelle des champs suivants sont invalides, veuillez les corriger :'
|
||||||
|
|
||||||
|
fix_header_section:
|
||||||
|
one: 'Le titre de section suivant est invalide, veuillez le corriger :'
|
||||||
|
other: 'Les titres de section suivants sont invalides, veuillez les corriger :'
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
#errors-summary
|
#errors-summary
|
||||||
- if @revision.invalid?
|
- if invalid?
|
||||||
.card.warning
|
.card.warning
|
||||||
.card-title La logique conditionnelle est devenue invalide
|
.card-title Le formulaire contient des erreurs
|
||||||
|
|
||||||
%p.mb-2= t('.fix', count: @revision.errors.count)
|
- if condition_errors?
|
||||||
= error_message
|
%p.mb-2= t('.fix_conditional', count: errors_for(:condition).size)
|
||||||
|
= error_message_for(:condition)
|
||||||
|
|
||||||
|
- if header_section_errors?
|
||||||
|
%p.mb-2= t('.fix_header_section', count: errors_for(:header_section).size)
|
||||||
|
= error_message_for(:header_section)
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
class TypesDeChampEditor::HeaderSectionComponent < ApplicationComponent
|
||||||
|
MAX_LEVEL = 3
|
||||||
|
|
||||||
|
def initialize(form:, tdc:, upper_tdcs:)
|
||||||
|
@form = form
|
||||||
|
@tdc = tdc
|
||||||
|
@upper_tdcs = upper_tdcs
|
||||||
|
end
|
||||||
|
|
||||||
|
def header_section_options_for_select
|
||||||
|
closest_level = @tdc.previous_section_level(@upper_tdcs)
|
||||||
|
next_level = [closest_level + 1, MAX_LEVEL].min
|
||||||
|
|
||||||
|
available_levels = (1..next_level).map(&method(:option_for_level))
|
||||||
|
disabled_levels = errors? ? (next_level + 1..MAX_LEVEL).map(&method(:option_for_level)) : []
|
||||||
|
options_for_select(
|
||||||
|
available_levels + disabled_levels,
|
||||||
|
disabled: disabled_levels.map(&:second),
|
||||||
|
selected: @tdc.header_section_level_value
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def errors
|
||||||
|
@tdc.check_coherent_header_level(@upper_tdcs)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def option_for_level(level)
|
||||||
|
[translate(".select_option", level: level), level]
|
||||||
|
end
|
||||||
|
|
||||||
|
def errors?
|
||||||
|
!errors.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_html_list(messages)
|
||||||
|
messages
|
||||||
|
.map { |message| tag.li(message) }
|
||||||
|
.then { |lis| tag.ul(lis.reduce(&:+)) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
en:
|
||||||
|
select_option: "Header section %{level}"
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
fr:
|
||||||
|
select_option: "Titre de niveau %{level}"
|
|
@ -0,0 +1,5 @@
|
||||||
|
%div{ id: dom_id(@tdc.stable_self, :header_section) }
|
||||||
|
- if errors?
|
||||||
|
.errors-summary= to_html_list(errors)
|
||||||
|
= @form.label :header_section_level, "Niveau du titre", for: dom_id(@tdc, :header_section_level)
|
||||||
|
= @form.select :header_section_level, header_section_options_for_select, {}, id: dom_id(@tdc, :header_section_level)
|
|
@ -128,6 +128,7 @@ module Administrateurs
|
||||||
:drop_down_secondary_description,
|
:drop_down_secondary_description,
|
||||||
:collapsible_explanation_enabled,
|
:collapsible_explanation_enabled,
|
||||||
:collapsible_explanation_text,
|
:collapsible_explanation_text,
|
||||||
|
:header_section_level,
|
||||||
editable_options: [
|
editable_options: [
|
||||||
:cadastres,
|
:cadastres,
|
||||||
:unesco,
|
:unesco,
|
||||||
|
|
|
@ -83,7 +83,12 @@ class RootController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@dossier.association(:revision).target = @dossier.procedure.build_draft_revision
|
draft_revision = @dossier.procedure.build_draft_revision(types_de_champ_public: all_champs.map(&:type_de_champ))
|
||||||
|
@dossier.association(:revision).target = draft_revision
|
||||||
|
@dossier.champs_public.map(&:type_de_champ).map do |tdc|
|
||||||
|
tdc.association(:revision_type_de_champ).target = tdc.build_revision_type_de_champ(revision: draft_revision)
|
||||||
|
tdc.association(:revision).target = draft_revision
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def suivi
|
def suivi
|
||||||
|
|
|
@ -48,6 +48,8 @@ class Champ < ApplicationRecord
|
||||||
:drop_down_secondary_description,
|
:drop_down_secondary_description,
|
||||||
:collapsible_explanation_enabled?,
|
:collapsible_explanation_enabled?,
|
||||||
:collapsible_explanation_text,
|
:collapsible_explanation_text,
|
||||||
|
:header_section_level_value,
|
||||||
|
:current_section_level,
|
||||||
:exclude_from_export?,
|
:exclude_from_export?,
|
||||||
:exclude_from_view?,
|
:exclude_from_view?,
|
||||||
:repetition?,
|
:repetition?,
|
||||||
|
|
|
@ -21,6 +21,16 @@
|
||||||
# type_de_champ_id :integer
|
# type_de_champ_id :integer
|
||||||
#
|
#
|
||||||
class Champs::HeaderSectionChamp < Champ
|
class Champs::HeaderSectionChamp < Champ
|
||||||
|
def level
|
||||||
|
if parent.present?
|
||||||
|
header_section_level_value.to_i + parent.current_section_level
|
||||||
|
elsif header_section_level_value
|
||||||
|
header_section_level_value.to_i
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def search_terms
|
def search_terms
|
||||||
# The user cannot enter any information here so it doesn’t make much sense to search
|
# The user cannot enter any information here so it doesn’t make much sense to search
|
||||||
end
|
end
|
||||||
|
|
37
app/models/concerns/treeable_concern.rb
Normal file
37
app/models/concerns/treeable_concern.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
module TreeableConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
MAX_DEPTH = 6 # deepest level for header_sections is 3.
|
||||||
|
# but a repetition can be nested an header_section, so 3+3=6=MAX_DEPTH
|
||||||
|
|
||||||
|
included do
|
||||||
|
# as we progress in the list of ordered champs
|
||||||
|
# we keep a reference to each level of nesting (walk)
|
||||||
|
# when we encounter an header_section, it depends of its own depth of nesting minus 1, ie:
|
||||||
|
# h1 belongs to prior (rooted_tree)
|
||||||
|
# h2 belongs to prior h1
|
||||||
|
# h3 belongs to prior h2
|
||||||
|
# h1 belongs to prior (rooted_tree)
|
||||||
|
# then, each and every champs which are not an header_section
|
||||||
|
# are added to the current_tree
|
||||||
|
# given a root_depth at 0, we build a full tree
|
||||||
|
# given a root_depth > 0, we build a partial tree (aka, a repetition)
|
||||||
|
def to_tree(champs:)
|
||||||
|
rooted_tree = []
|
||||||
|
walk = Array.new(MAX_DEPTH)
|
||||||
|
walk[0] = rooted_tree
|
||||||
|
current_tree = rooted_tree
|
||||||
|
|
||||||
|
champs.each do |champ|
|
||||||
|
if champ.header_section?
|
||||||
|
new_tree = [champ]
|
||||||
|
walk[champ.header_section_level_value - 1].push(new_tree)
|
||||||
|
current_tree = walk[champ.header_section_level_value] = new_tree
|
||||||
|
else
|
||||||
|
current_tree.push(champ)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rooted_tree
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,6 +31,7 @@ class ProcedureRevision < ApplicationRecord
|
||||||
scope :ordered, -> { order(:created_at) }
|
scope :ordered, -> { order(:created_at) }
|
||||||
|
|
||||||
validate :conditions_are_valid?
|
validate :conditions_are_valid?
|
||||||
|
validate :header_sections_are_valid?
|
||||||
|
|
||||||
delegate :path, to: :procedure, prefix: true
|
delegate :path, to: :procedure, prefix: true
|
||||||
|
|
||||||
|
@ -401,4 +402,24 @@ class ProcedureRevision < ApplicationRecord
|
||||||
.filter { |_tdc, errors| errors.present? }
|
.filter { |_tdc, errors| errors.present? }
|
||||||
.each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
|
.each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def header_sections_are_valid?
|
||||||
|
public_tdcs = types_de_champ_public.to_a
|
||||||
|
|
||||||
|
root_tdcs_errors = errors_for_header_sections_order(public_tdcs)
|
||||||
|
repetition_tdcs_errors = public_tdcs
|
||||||
|
.filter_map { _1.repetition? ? children_of(_1) : nil }
|
||||||
|
.map { errors_for_header_sections_order(_1) }
|
||||||
|
|
||||||
|
repetition_tdcs_errors + root_tdcs_errors
|
||||||
|
end
|
||||||
|
|
||||||
|
def errors_for_header_sections_order(tdcs)
|
||||||
|
tdcs
|
||||||
|
.map.with_index
|
||||||
|
.filter_map { |tdc, i| tdc.header_section? ? [tdc, i] : nil }
|
||||||
|
.map { |tdc, i| [tdc, tdc.check_coherent_header_level(tdcs.take(i))] }
|
||||||
|
.filter { |_tdc, errors| errors.present? }
|
||||||
|
.each { |tdc, message| errors.add(:header_section, message, type_de_champ: tdc) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -119,7 +119,8 @@ class TypeDeChamp < ApplicationRecord
|
||||||
:drop_down_secondary_description,
|
:drop_down_secondary_description,
|
||||||
:drop_down_other,
|
:drop_down_other,
|
||||||
:collapsible_explanation_enabled,
|
:collapsible_explanation_enabled,
|
||||||
:collapsible_explanation_text
|
:collapsible_explanation_text,
|
||||||
|
:header_section_level
|
||||||
|
|
||||||
has_many :revision_types_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ
|
has_many :revision_types_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ
|
||||||
has_one :revision_type_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', inverse_of: false
|
has_one :revision_type_de_champ, -> { revision_ordered }, class_name: 'ProcedureRevisionTypeDeChamp', inverse_of: false
|
||||||
|
@ -411,6 +412,39 @@ class TypeDeChamp < ApplicationRecord
|
||||||
self.drop_down_options = parse_drop_down_list_value(value)
|
self.drop_down_options = parse_drop_down_list_value(value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def header_section_level_value
|
||||||
|
if header_section_level.presence
|
||||||
|
header_section_level.to_i
|
||||||
|
else
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def previous_section_level(upper_tdcs)
|
||||||
|
previous_header_section = upper_tdcs.reverse.find(&:header_section?)
|
||||||
|
|
||||||
|
return 0 if !previous_header_section
|
||||||
|
previous_header_section.header_section_level_value.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_coherent_header_level(upper_tdcs)
|
||||||
|
errs = []
|
||||||
|
previous_level = previous_section_level(upper_tdcs)
|
||||||
|
|
||||||
|
current_level = header_section_level_value.to_i
|
||||||
|
difference = current_level - previous_level
|
||||||
|
if current_level > previous_level && difference != 1
|
||||||
|
errs << I18n.t('activerecord.errors.type_de_champ.attributes.header_section_level.gap_error', level: current_level - previous_level - 1)
|
||||||
|
end
|
||||||
|
errs
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_section_level
|
||||||
|
tdcs = private? ? revision.type_champs_private.to_a : revision.types_de_champ_public.to_a
|
||||||
|
|
||||||
|
previous_section_level(tdcs.take(tdcs.find_index(self)))
|
||||||
|
end
|
||||||
|
|
||||||
def self.options_for_select?(type_champs)
|
def self.options_for_select?(type_champs)
|
||||||
[
|
[
|
||||||
TypeDeChamp.type_champs.fetch(:departements),
|
TypeDeChamp.type_champs.fetch(:departements),
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
- rendered = render @condition_component
|
- rendered = render @condition_component
|
||||||
|
|
||||||
- if rendered.present?
|
- if rendered.present?
|
||||||
= turbo_stream.replace dom_id(@tdc.stable_self, :conditions) do
|
= turbo_stream.replace dom_id(@tdc.stable_self, :condition) do
|
||||||
- rendered
|
- rendered
|
||||||
- else
|
- else
|
||||||
= turbo_stream.remove dom_id(@tdc.stable_self, :conditions)
|
= turbo_stream.remove dom_id(@tdc.stable_self, :condition)
|
||||||
|
|
|
@ -42,9 +42,5 @@
|
||||||
= f.select :groupe_instructeur_id,
|
= f.select :groupe_instructeur_id,
|
||||||
dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] },
|
dossier.procedure.groupe_instructeurs.active.map { |gi| [gi.label, gi.id] },
|
||||||
{ include_blank: dossier.brouillon? }
|
{ include_blank: dossier.brouillon? }
|
||||||
|
= render EditableChamp::SectionComponent.new(champs: dossier.champs_public)
|
||||||
- dossier.champs_public.each do |champ|
|
|
||||||
= fields_for champ.input_name, champ do |form|
|
|
||||||
= render EditableChamp::EditableChampComponent.new form: form, champ: champ
|
|
||||||
|
|
||||||
= render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: false)
|
= render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: false)
|
||||||
|
|
|
@ -3,9 +3,7 @@
|
||||||
%section.counter-start-header-section
|
%section.counter-start-header-section
|
||||||
= render NestedForms::FormOwnerComponent.new
|
= render NestedForms::FormOwnerComponent.new
|
||||||
= form_for dossier, url: annotations_instructeur_dossier_path(dossier.procedure, dossier), html: { class: 'form', multipart: true } do |f|
|
= form_for dossier, url: annotations_instructeur_dossier_path(dossier.procedure, dossier), html: { class: 'form', multipart: true } do |f|
|
||||||
- dossier.champs_private.each do |champ|
|
= render EditableChamp::SectionComponent.new(champs: dossier.champs_private)
|
||||||
= fields_for champ.input_name, champ do |form|
|
|
||||||
= render EditableChamp::EditableChampComponent.new form: form, champ: champ, seen_at: seen_at
|
|
||||||
|
|
||||||
= render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: true)
|
= render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: true)
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -50,3 +50,10 @@ en:
|
||||||
pole_emploi: 'Pôle emploi status'
|
pole_emploi: 'Pôle emploi status'
|
||||||
mesri: "Data from Ministère de l’Enseignement Supérieur, de la Recherche et de l’Innovation"
|
mesri: "Data from Ministère de l’Enseignement Supérieur, de la Recherche et de l’Innovation"
|
||||||
epci: "EPCI"
|
epci: "EPCI"
|
||||||
|
errors:
|
||||||
|
type_de_champ:
|
||||||
|
attributes:
|
||||||
|
header_section_level:
|
||||||
|
gap_error: "An header section with %{level} is missing."
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -50,3 +50,8 @@ fr:
|
||||||
pole_emploi: 'Situation Pôle emploi'
|
pole_emploi: 'Situation Pôle emploi'
|
||||||
mesri: "Données du Ministère de l’Enseignement Supérieur, de la Recherche et de l’Innovation"
|
mesri: "Données du Ministère de l’Enseignement Supérieur, de la Recherche et de l’Innovation"
|
||||||
epci: "EPCI"
|
epci: "EPCI"
|
||||||
|
errors:
|
||||||
|
type_de_champ:
|
||||||
|
attributes:
|
||||||
|
header_section_level:
|
||||||
|
gap_error: "Un titre de section avec le niveau %{level} est manquant."
|
||||||
|
|
113
spec/components/editable_champ/section_component_spec.rb
Normal file
113
spec/components/editable_champ/section_component_spec.rb
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
describe EditableChamp::SectionComponent, type: :component do
|
||||||
|
include TreeableConcern
|
||||||
|
let(:component) { described_class.new(champs: champs) }
|
||||||
|
before { render_inline(component).to_html }
|
||||||
|
|
||||||
|
context 'list of champs without an header_section' do
|
||||||
|
let(:champs) { [build(:champ_text), build(:champ_textarea)] }
|
||||||
|
|
||||||
|
it 'does not render fieldset' do
|
||||||
|
expect(page).not_to have_selector("fieldset")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders champs' do
|
||||||
|
expect(page).to have_selector("input[type=text]", count: 1)
|
||||||
|
expect(page).to have_selector("textarea", count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'list of champs with an header_section' do
|
||||||
|
let(:champs) { [build(:champ_header_section_level_1), build(:champ_text), build(:champ_textarea)] }
|
||||||
|
|
||||||
|
it 'renders fieldset' do
|
||||||
|
expect(page).to have_selector("fieldset")
|
||||||
|
expect(page).to have_selector("legend h2")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders champs within fieldset' do
|
||||||
|
expect(page).to have_selector("fieldset input[type=text]")
|
||||||
|
expect(page).to have_selector("fieldset textarea")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'list of champs without section and an header_section having champs' do
|
||||||
|
let(:champs) { [build(:champ_text), build(:champ_header_section_level_1), build(:champ_text)] }
|
||||||
|
|
||||||
|
it 'renders fieldset' do
|
||||||
|
expect(page).to have_selector("fieldset")
|
||||||
|
expect(page).to have_selector("legend h2")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders all champs, one outside fieldset, one within fieldset' do
|
||||||
|
expect(page).to have_selector("input[type=text]", count: 2)
|
||||||
|
expect(page).to have_selector("fieldset input[type=text]", count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'list of header_section without champs' do
|
||||||
|
let(:champs) { [build(:champ_header_section_level_1), build(:champ_header_section_level_2), build(:champ_header_section_level_3)] }
|
||||||
|
|
||||||
|
it 'does not render header within fieldset' do
|
||||||
|
expect(page).not_to have_selector("fieldset")
|
||||||
|
expect(page).to have_selector("h2")
|
||||||
|
expect(page).to have_selector("h3")
|
||||||
|
expect(page).to have_selector("h4")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'header_section followed by explication and another fieldset' do
|
||||||
|
let(:champs) { [build(:champ_header_section_level_1), build(:champ_explication), build(:champ_header_section_level_1), build(:champ_text)] }
|
||||||
|
|
||||||
|
it 'render fieldset, header_section (one within fieldset, one outside), also render explication' do
|
||||||
|
expect(page).to have_selector("h2", count: 2)
|
||||||
|
expect(page).to have_selector("h3") # explication
|
||||||
|
expect(page).to have_selector("fieldset h2", count: 1)
|
||||||
|
expect(page).to have_selector("fieldset input[type=text]", count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'nested fieldsset' do
|
||||||
|
let(:champs) { [build(:champ_header_section_level_1), build(:champ_text), build(:champ_header_section_level_2), build(:champ_textarea)] }
|
||||||
|
|
||||||
|
it 'render nested fieldsets' do
|
||||||
|
expect(page).to have_selector("fieldset")
|
||||||
|
expect(page).to have_selector("legend h2")
|
||||||
|
expect(page).to have_selector("fieldset fieldset")
|
||||||
|
expect(page).to have_selector("fieldset fieldset legend h3")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains all champs' do
|
||||||
|
expect(page).to have_selector("fieldset input[type=text]", count: 1)
|
||||||
|
expect(page).to have_selector("fieldset fieldset textarea", count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with repetition' do
|
||||||
|
let(:procedure) do
|
||||||
|
create(:procedure, types_de_champ_public: [
|
||||||
|
{ type: :header_section, header_section_level: 1 },
|
||||||
|
{
|
||||||
|
type: :repetition,
|
||||||
|
libelle: 'repetition',
|
||||||
|
children: [
|
||||||
|
{ type: :header_section, header_section_level: 1, libelle: 'child_1' },
|
||||||
|
{ type: :text, libelle: 'child_2' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
end
|
||||||
|
let(:dossier) { create(:dossier, :with_populated_champs, procedure: procedure) }
|
||||||
|
let(:champs) { dossier.champs_public }
|
||||||
|
|
||||||
|
it 'render nested fieldsets, increase heading level for repetition header_section' do
|
||||||
|
expect(page).to have_selector("fieldset")
|
||||||
|
expect(page).to have_selector("legend h2")
|
||||||
|
expect(page).to have_selector("fieldset fieldset")
|
||||||
|
expect(page).to have_selector("fieldset fieldset legend h3")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains as many text champ as repetition.rows' do
|
||||||
|
expect(page).to have_selector("fieldset fieldset input[type=text]", count: dossier.champs_public.find(&:repetition?).rows.size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
83
spec/components/header_section_component_spec.rb
Normal file
83
spec/components/header_section_component_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
RSpec.describe TypesDeChampEditor::HeaderSectionComponent, type: :component do
|
||||||
|
include ActionView::Context
|
||||||
|
include ActionView::Helpers::FormHelper
|
||||||
|
include ActionView::Helpers::FormOptionsHelper
|
||||||
|
|
||||||
|
let(:component) do
|
||||||
|
cmp = nil
|
||||||
|
form_for(tdc, url: '/') do |form|
|
||||||
|
cmp = described_class.new(form: form, tdc: tdc, upper_tdcs: upper_tdcs)
|
||||||
|
end
|
||||||
|
cmp
|
||||||
|
end
|
||||||
|
subject { render_inline(component).to_html }
|
||||||
|
|
||||||
|
describe 'header_section_options_for_select' do
|
||||||
|
context 'without upper tdc' do
|
||||||
|
let(:tdc) { header.type_de_champ }
|
||||||
|
let(:header) { build(:champ_header_section) }
|
||||||
|
let(:upper_tdcs) { [] }
|
||||||
|
|
||||||
|
it 'allows up to level 1 header section' do
|
||||||
|
expect(subject).to have_selector("option", count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with upper tdc of level 1' do
|
||||||
|
let(:tdc) { header.type_de_champ }
|
||||||
|
let(:header) { build(:champ_header_section_level_1) }
|
||||||
|
let(:upper_tdcs) { [build(:champ_header_section_level_1).type_de_champ] }
|
||||||
|
|
||||||
|
it 'allows up to level 2 header section' do
|
||||||
|
expect(subject).to have_selector("option", count: 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with upper tdc of level 2' do
|
||||||
|
let(:tdc) { header.type_de_champ }
|
||||||
|
let(:header) { build(:champ_header_section_level_1) }
|
||||||
|
let(:upper_tdcs) { [build(:champ_header_section_level_1), build(:champ_header_section_level_2)].map(&:type_de_champ) }
|
||||||
|
|
||||||
|
it 'allows up to level 3 header section' do
|
||||||
|
expect(subject).to have_selector("option", count: 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with upper tdc of level 3' do
|
||||||
|
let(:tdc) { header.type_de_champ }
|
||||||
|
let(:header) { build(:champ_header_section_level_1) }
|
||||||
|
let(:upper_tdcs) do
|
||||||
|
[
|
||||||
|
build(:champ_header_section_level_1),
|
||||||
|
build(:champ_header_section_level_2),
|
||||||
|
build(:champ_header_section_level_3)
|
||||||
|
].map(&:type_de_champ)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reaches limit of at most 3 section level' do
|
||||||
|
expect(subject).to have_selector("option", count: 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with error' do
|
||||||
|
let(:tdc) { header.type_de_champ }
|
||||||
|
let(:header) { build(:champ_header_section_level_2) }
|
||||||
|
let(:upper_tdcs) { [] }
|
||||||
|
|
||||||
|
it 'includes disabled levels' do
|
||||||
|
expect(subject).to have_selector("option", count: 3)
|
||||||
|
expect(subject).to have_selector("option[disabled]", count: 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'errors' do
|
||||||
|
let(:tdc) { header.type_de_champ }
|
||||||
|
let(:header) { build(:champ_header_section_level_2) }
|
||||||
|
let(:upper_tdcs) { [] }
|
||||||
|
|
||||||
|
it 'returns errors' do
|
||||||
|
expect(subject).to have_selector('.errors-summary')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,7 +8,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do
|
||||||
before { render_inline(described_class.new(conditions: conditions, upper_tdcs: upper_tdcs)) }
|
before { render_inline(described_class.new(conditions: conditions, upper_tdcs: upper_tdcs)) }
|
||||||
|
|
||||||
context 'when there are no condition' do
|
context 'when there are no condition' do
|
||||||
it { expect(page).to have_no_css('.condition-error') }
|
it { expect(page).to have_no_css('.errors-summary') }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the targeted_champ is not available' do
|
context 'when the targeted_champ is not available' do
|
||||||
|
@ -16,7 +16,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do
|
||||||
let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(1))] }
|
let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(1))] }
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect(page).to have_css('.condition-error')
|
expect(page).to have_css('.errors-summary')
|
||||||
expect(page).to have_content("Un champ cible n'est plus disponible")
|
expect(page).to have_content("Un champ cible n'est plus disponible")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,7 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do
|
||||||
let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(1))] }
|
let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(1))] }
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect(page).to have_css('.condition-error')
|
expect(page).to have_css('.errors-summary')
|
||||||
expect(page).to have_content("Le champ « #{tdc.libelle} » est de type « adresse » et ne peut pas être utilisé comme champ cible.")
|
expect(page).to have_content("Le champ « #{tdc.libelle} » est de type « adresse » et ne peut pas être utilisé comme champ cible.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -137,6 +137,18 @@ FactoryBot.define do
|
||||||
type_de_champ { association :type_de_champ_header_section, procedure: dossier.procedure }
|
type_de_champ { association :type_de_champ_header_section, procedure: dossier.procedure }
|
||||||
value { 'une section' }
|
value { 'une section' }
|
||||||
end
|
end
|
||||||
|
factory :champ_header_section_level_1, class: 'Champs::HeaderSectionChamp' do
|
||||||
|
type_de_champ { association :type_de_champ_header_section_level_1, procedure: dossier.procedure }
|
||||||
|
value { 'une section' }
|
||||||
|
end
|
||||||
|
factory :champ_header_section_level_2, class: 'Champs::HeaderSectionChamp' do
|
||||||
|
type_de_champ { association :type_de_champ_header_section_level_2, procedure: dossier.procedure }
|
||||||
|
value { 'une section' }
|
||||||
|
end
|
||||||
|
factory :champ_header_section_level_3, class: 'Champs::HeaderSectionChamp' do
|
||||||
|
type_de_champ { association :type_de_champ_header_section_level_3, procedure: dossier.procedure }
|
||||||
|
value { 'une section' }
|
||||||
|
end
|
||||||
|
|
||||||
factory :champ_explication, class: 'Champs::ExplicationChamp' do
|
factory :champ_explication, class: 'Champs::ExplicationChamp' do
|
||||||
type_de_champ { association :type_de_champ_explication, procedure: dossier.procedure }
|
type_de_champ { association :type_de_champ_explication, procedure: dossier.procedure }
|
||||||
|
|
|
@ -114,6 +114,20 @@ FactoryBot.define do
|
||||||
factory :type_de_champ_header_section do
|
factory :type_de_champ_header_section do
|
||||||
type_champ { TypeDeChamp.type_champs.fetch(:header_section) }
|
type_champ { TypeDeChamp.type_champs.fetch(:header_section) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :type_de_champ_header_section_level_1 do
|
||||||
|
type_champ { TypeDeChamp.type_champs.fetch(:header_section) }
|
||||||
|
header_section_level { 1 }
|
||||||
|
end
|
||||||
|
factory :type_de_champ_header_section_level_2 do
|
||||||
|
type_champ { TypeDeChamp.type_champs.fetch(:header_section) }
|
||||||
|
header_section_level { 2 }
|
||||||
|
end
|
||||||
|
factory :type_de_champ_header_section_level_3 do
|
||||||
|
type_champ { TypeDeChamp.type_champs.fetch(:header_section) }
|
||||||
|
header_section_level { 3 }
|
||||||
|
end
|
||||||
|
|
||||||
factory :type_de_champ_explication do
|
factory :type_de_champ_explication do
|
||||||
type_champ { TypeDeChamp.type_champs.fetch(:explication) }
|
type_champ { TypeDeChamp.type_champs.fetch(:explication) }
|
||||||
end
|
end
|
||||||
|
|
163
spec/models/concern/treeable_concern_spec.rb
Normal file
163
spec/models/concern/treeable_concern_spec.rb
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
describe TreeableConcern do
|
||||||
|
class ChampsToTree
|
||||||
|
include TreeableConcern
|
||||||
|
|
||||||
|
attr_reader :root
|
||||||
|
def initialize(champs:)
|
||||||
|
@root = to_tree(champs:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { ChampsToTree.new(champs: champs).root }
|
||||||
|
describe "to_tree" do
|
||||||
|
let(:header_1) { build(:champ_header_section_level_1) }
|
||||||
|
let(:header_1_2) { build(:champ_header_section_level_2) }
|
||||||
|
let(:header_2) { build(:champ_header_section_level_1) }
|
||||||
|
let(:champ_text) { build(:champ_text) }
|
||||||
|
let(:champ_textarea) { build(:champ_textarea) }
|
||||||
|
let(:champ_explication) { build(:champ_explication) }
|
||||||
|
let(:champ_communes) { build(:champ_communes) }
|
||||||
|
|
||||||
|
context 'without section' do
|
||||||
|
let(:champs) do
|
||||||
|
[
|
||||||
|
champ_text, champ_textarea
|
||||||
|
]
|
||||||
|
end
|
||||||
|
it 'inlines champs at root level' do
|
||||||
|
expect(subject.size).to eq(champs.size)
|
||||||
|
expect(subject).to eq(champs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with header_section and champs' do
|
||||||
|
let(:champs) do
|
||||||
|
[
|
||||||
|
header_1,
|
||||||
|
champ_explication,
|
||||||
|
champ_text,
|
||||||
|
header_2,
|
||||||
|
champ_textarea
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'wraps champs within preview header section' do
|
||||||
|
expect(subject.size).to eq(2)
|
||||||
|
expect(subject).to eq([
|
||||||
|
[header_1, champ_explication, champ_text],
|
||||||
|
[header_2, champ_textarea]
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'leading champs, and in between sections only' do
|
||||||
|
let(:champs) do
|
||||||
|
[
|
||||||
|
champ_text,
|
||||||
|
champ_textarea,
|
||||||
|
header_1,
|
||||||
|
champ_explication,
|
||||||
|
champ_communes,
|
||||||
|
header_2,
|
||||||
|
champ_textarea
|
||||||
|
]
|
||||||
|
end
|
||||||
|
it 'chunk by uniq champs' do
|
||||||
|
expect(subject.size).to eq(4)
|
||||||
|
expect(subject).to eq([
|
||||||
|
champ_text,
|
||||||
|
champ_textarea,
|
||||||
|
[header_1, champ_explication, champ_communes],
|
||||||
|
[header_2, champ_textarea]
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with one sub sections' do
|
||||||
|
let(:champs) do
|
||||||
|
[
|
||||||
|
header_1,
|
||||||
|
champ_explication,
|
||||||
|
header_1_2,
|
||||||
|
champ_communes,
|
||||||
|
header_2,
|
||||||
|
champ_textarea
|
||||||
|
]
|
||||||
|
end
|
||||||
|
it 'chunk by uniq champs' do
|
||||||
|
expect(subject.size).to eq(2)
|
||||||
|
expect(subject).to eq([
|
||||||
|
[header_1, champ_explication, [header_1_2, champ_communes]],
|
||||||
|
[header_2, champ_textarea]
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with consecutive subsection' do
|
||||||
|
let(:header_1) { build(:champ_header_section_level_1) }
|
||||||
|
let(:header_1_2_1) { build(:champ_header_section_level_2) }
|
||||||
|
let(:header_1_2_2) { build(:champ_header_section_level_2) }
|
||||||
|
let(:header_1_2_3) { build(:champ_header_section_level_2) }
|
||||||
|
let(:champs) do
|
||||||
|
[
|
||||||
|
header_1,
|
||||||
|
header_1_2_1,
|
||||||
|
champ_text,
|
||||||
|
header_1_2_2,
|
||||||
|
champ_textarea,
|
||||||
|
header_1_2_3,
|
||||||
|
champ_communes
|
||||||
|
]
|
||||||
|
end
|
||||||
|
it 'chunk by uniq champs' do
|
||||||
|
expect(subject.size).to eq(1)
|
||||||
|
expect(subject).to eq([
|
||||||
|
[
|
||||||
|
header_1,
|
||||||
|
[header_1_2_1, champ_text],
|
||||||
|
[header_1_2_2, champ_textarea],
|
||||||
|
[header_1_2_3, champ_communes]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with one sub sections and one subsub section' do
|
||||||
|
let(:header_1_2_3) { build(:champ_header_section_level_3) }
|
||||||
|
|
||||||
|
let(:champs) do
|
||||||
|
[
|
||||||
|
header_1,
|
||||||
|
champ_explication,
|
||||||
|
header_1_2,
|
||||||
|
champ_communes,
|
||||||
|
header_1_2_3,
|
||||||
|
champ_text,
|
||||||
|
header_2,
|
||||||
|
champ_textarea
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'chunk by uniq champs' do
|
||||||
|
expect(subject.size).to eq(2)
|
||||||
|
expect(subject).to eq([
|
||||||
|
[
|
||||||
|
header_1,
|
||||||
|
champ_explication,
|
||||||
|
[
|
||||||
|
header_1_2,
|
||||||
|
champ_communes,
|
||||||
|
[
|
||||||
|
header_1_2_3, champ_text
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
header_2,
|
||||||
|
champ_textarea
|
||||||
|
]
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -890,6 +890,24 @@ describe ProcedureRevision do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'header_sections_are_valid' do
|
||||||
|
let(:procedure) do
|
||||||
|
create(:procedure).tap do |p|
|
||||||
|
p.draft_revision.add_type_de_champ(type_champ: :header_section, libelle: 'hs', header_section_level: '2')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
let(:draft_revision) { procedure.draft_revision }
|
||||||
|
|
||||||
|
subject do
|
||||||
|
draft_revision.save
|
||||||
|
draft_revision.errors
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'find error' do
|
||||||
|
expect(subject.errors).not_to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#dependent_conditions" do
|
describe "#dependent_conditions" do
|
||||||
include Logic
|
include Logic
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do
|
||||||
expect(page).to have_css('label', text: type_de_champ_phone.libelle)
|
expect(page).to have_css('label', text: type_de_champ_phone.libelle)
|
||||||
expect(page).to have_field(type_de_champ_rna.libelle, with: rna_value)
|
expect(page).to have_field(type_de_champ_rna.libelle, with: rna_value)
|
||||||
expect(page).to have_field(type_de_champ_siret.libelle, with: siret_value)
|
expect(page).to have_field(type_de_champ_siret.libelle, with: siret_value)
|
||||||
expect(page).to have_css('h3', text: type_de_champ_repetition.libelle)
|
expect(page).to have_css('legend', text: type_de_champ_repetition.libelle)
|
||||||
expect(page).to have_field(text_repetition_libelle, with: text_repetition_value)
|
expect(page).to have_field(text_repetition_libelle, with: text_repetition_value)
|
||||||
expect(page).to have_field(integer_repetition_libelle, with: integer_repetition_value)
|
expect(page).to have_field(integer_repetition_libelle, with: integer_repetition_value)
|
||||||
expect(page).to have_field(type_de_champ_datetime.libelle, with: datetime_value)
|
expect(page).to have_field(type_de_champ_datetime.libelle, with: datetime_value)
|
||||||
|
|
|
@ -87,19 +87,19 @@ describe 'As an administrateur I can edit types de champ condition', js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "changing target champ to a not managed type" do
|
scenario "changing target champ to a not managed type" do
|
||||||
expect(page).to have_no_selector('.condition-error')
|
expect(page).to have_no_selector('.errors-summary')
|
||||||
|
|
||||||
within '.type-de-champ:nth-child(1)' do
|
within '.type-de-champ:nth-child(1)' do
|
||||||
select('Départements', from: 'Type de champ')
|
select('Départements', from: 'Type de champ')
|
||||||
end
|
end
|
||||||
|
|
||||||
within '.type-de-champ:nth-child(2)' do
|
within '.type-de-champ:nth-child(2)' do
|
||||||
expect(page).to have_selector('.condition-error')
|
expect(page).to have_selector('.errors-summary')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "moving a target champ below the condition" do
|
scenario "moving a target champ below the condition" do
|
||||||
expect(page).to have_no_selector('.condition-error')
|
expect(page).to have_no_selector('.errors-summary')
|
||||||
|
|
||||||
within '.type-de-champ:nth-child(1)' do
|
within '.type-de-champ:nth-child(1)' do
|
||||||
click_on 'Déplacer le champ vers le bas'
|
click_on 'Déplacer le champ vers le bas'
|
||||||
|
@ -107,12 +107,12 @@ describe 'As an administrateur I can edit types de champ condition', js: true do
|
||||||
|
|
||||||
# the now first champ has an error
|
# the now first champ has an error
|
||||||
within '.type-de-champ:nth-child(1)' do
|
within '.type-de-champ:nth-child(1)' do
|
||||||
expect(page).to have_selector('.condition-error')
|
expect(page).to have_selector('.errors-summary')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario "moving the condition champ above the target" do
|
scenario "moving the condition champ above the target" do
|
||||||
expect(page).to have_no_selector('.condition-error')
|
expect(page).to have_no_selector('.errors-summary')
|
||||||
|
|
||||||
within '.type-de-champ:nth-child(2)' do
|
within '.type-de-champ:nth-child(2)' do
|
||||||
click_on 'Déplacer le champ vers le haut'
|
click_on 'Déplacer le champ vers le haut'
|
||||||
|
@ -120,7 +120,7 @@ describe 'As an administrateur I can edit types de champ condition', js: true do
|
||||||
|
|
||||||
# the now first champ has an error
|
# the now first champ has an error
|
||||||
within '.type-de-champ:nth-child(1)' do
|
within '.type-de-champ:nth-child(1)' do
|
||||||
expect(page).to have_selector('.condition-error')
|
expect(page).to have_selector('.errors-summary')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
describe 'As an administrateur I can edit types de champ', js: true do
|
describe 'As an administrateur I can edit types de champ', js: true do
|
||||||
|
include ActionView::RecordIdentifier
|
||||||
|
|
||||||
let(:administrateur) { procedure.administrateurs.first }
|
let(:administrateur) { procedure.administrateurs.first }
|
||||||
let(:estimated_duration_visible) { true }
|
let(:estimated_duration_visible) { true }
|
||||||
let(:procedure) { create(:procedure, estimated_duration_visible:) }
|
let(:procedure) { create(:procedure, estimated_duration_visible:) }
|
||||||
|
@ -192,4 +194,28 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
||||||
expect(page).not_to have_content('Durée de remplissage estimée')
|
expect(page).not_to have_content('Durée de remplissage estimée')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'header section' do
|
||||||
|
scenario 'invalid order, it pops up errors summary' do
|
||||||
|
add_champ
|
||||||
|
select('Titre de section', from: 'Type de champ')
|
||||||
|
first_header = procedure.active_revision.types_de_champ_public.first
|
||||||
|
select('Titre de niveau 1', from: dom_id(first_header, :header_section_level))
|
||||||
|
|
||||||
|
add_champ
|
||||||
|
wait_until { procedure.reload.active_revision.types_de_champ_public.count == 2 }
|
||||||
|
second_header = procedure.active_revision.types_de_champ_public.last
|
||||||
|
select('Titre de section', from: dom_id(second_header, :type_champ))
|
||||||
|
select('Titre de niveau 2', from: dom_id(second_header, :header_section_level))
|
||||||
|
|
||||||
|
within(".types-de-champ-block li:first-child") do
|
||||||
|
page.accept_alert do
|
||||||
|
click_on 'Supprimer'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content("Le formulaire contient des erreurs")
|
||||||
|
expect(page).to have_content("Le titre de section suivant est invalide, veuillez le corriger :")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue