Merge pull request #8836 from demarches-simplifiees/improve-routing-rules-ux
feat(routing): nicer and safer ?
This commit is contained in:
commit
c252748833
27 changed files with 451 additions and 107 deletions
|
@ -5,6 +5,11 @@
|
|||
.types-de-champ-editor {
|
||||
> .types-de-champ-block {
|
||||
padding-bottom: 50px;
|
||||
|
||||
.types-de-champ-errors {
|
||||
background-color: $background-red;
|
||||
padding: $default-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.type-de-champ {
|
||||
|
|
59
app/assets/stylesheets/routing_rules_component.scss
Normal file
59
app/assets/stylesheets/routing_rules_component.scss
Normal file
|
@ -0,0 +1,59 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
#routing-rules {
|
||||
|
||||
.routing-rules-table {
|
||||
table-layout: fixed;
|
||||
|
||||
.far-left {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.if {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.target {
|
||||
width: 350px;
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.operator {
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: $default-spacer;
|
||||
|
||||
}
|
||||
|
||||
td {
|
||||
padding: $default-spacer;
|
||||
|
||||
input,
|
||||
select {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input.alert,
|
||||
select.alert {
|
||||
border-color: $dark-red;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,18 +4,27 @@ class Procedure::RoutingRulesComponent < ApplicationComponent
|
|||
def initialize(revision:, groupe_instructeurs:)
|
||||
@revision = revision
|
||||
@groupe_instructeurs = groupe_instructeurs
|
||||
@procedure_id = revision.procedure_id
|
||||
end
|
||||
|
||||
def rows
|
||||
@groupe_instructeurs.active.map do |gi|
|
||||
[gi.routing_rule&.left, gi.routing_rule&.right, gi]
|
||||
if gi.routing_rule.present?
|
||||
[gi.routing_rule.left, gi.routing_rule.right, gi]
|
||||
else
|
||||
[empty, empty, gi]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def can_route?
|
||||
available_targets_for_select.present?
|
||||
end
|
||||
|
||||
def targeted_champ_tag(targeted_champ, row_index)
|
||||
select_tag(
|
||||
'targeted_champ',
|
||||
options_for_select(targeted_champs_for_select, selected: targeted_champ&.stable_id),
|
||||
options_for_select(targeted_champs_for_select, selected: targeted_champ.to_json),
|
||||
id: input_id_for('targeted_champ', row_index)
|
||||
)
|
||||
end
|
||||
|
@ -23,7 +32,10 @@ class Procedure::RoutingRulesComponent < ApplicationComponent
|
|||
def value_tag(targeted_champ, value, row_index)
|
||||
select_tag(
|
||||
'value',
|
||||
options_for_select(values_for_select(targeted_champ), selected: value),
|
||||
options_for_select(
|
||||
values_for_select(targeted_champ, row_index),
|
||||
selected: value.to_json
|
||||
),
|
||||
id: input_id_for('value', row_index)
|
||||
)
|
||||
end
|
||||
|
@ -48,16 +60,20 @@ class Procedure::RoutingRulesComponent < ApplicationComponent
|
|||
def available_targets_for_select
|
||||
@revision.types_de_champ_public
|
||||
.filter { |tdc| [:drop_down_list].include?(tdc.type_champ.to_sym) }
|
||||
.map { |tdc| [tdc.libelle, tdc.stable_id] }
|
||||
.map { |tdc| [tdc.libelle, champ_value(tdc.stable_id).to_json] }
|
||||
end
|
||||
|
||||
def available_values_for_select(targeted_champ)
|
||||
return [] if targeted_champ.nil?
|
||||
targeted_champ.options(@revision.types_de_champ_public)
|
||||
return [] if targeted_champ.is_a?(Logic::Empty)
|
||||
targeted_champ
|
||||
.options(@revision.types_de_champ_public)
|
||||
.map { |tdc| [tdc.first, constant(tdc.first).to_json] }
|
||||
end
|
||||
|
||||
def values_for_select(targeted_champ)
|
||||
empty_target_for_select + available_values_for_select(targeted_champ)
|
||||
def values_for_select(targeted_champ, row_index)
|
||||
(empty_target_for_select + available_values_for_select(targeted_champ))
|
||||
# add id to help morph render selected option
|
||||
.map { |(libelle, json)| [libelle, json, { id: "#{row_index}-option-#{libelle}" }] }
|
||||
end
|
||||
|
||||
def input_id_for(name, row_index)
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
fr:
|
||||
select: Sélectionner
|
||||
apply_routing_rules: Appliquer des règles de routage
|
||||
routing_rules_notice: |
|
||||
Ajoutez des règles de routage à partir de champs créés dans le formulaire.
|
||||
Si les mêmes règles de routage sont appliquées à plusieurs groupes,
|
||||
les dossiers seront routés vers le premier groupe affiché dans la liste.
|
||||
routing_rules_notice_html: |
|
||||
<p>Ajoutez des règles de routage à partir de champs « choix simple » créés dans le <a href="%{path}">formulaire</a>.</p>
|
||||
<p>Les dossiers seront routées vers le premier groupe affiché dont la règle correspond.</p>
|
||||
routing_rules_warning_html: |
|
||||
<p>Pour appliquer des règles de routage, votre formulaire doit comporter
|
||||
au moins un champ « choix simple ».</p>
|
||||
<p>Ajoutez ce champ dans la page <a href="%{path}">« Configuration des champs »</a>.</p>
|
||||
|
|
|
@ -1,26 +1,32 @@
|
|||
.card#routing-rules
|
||||
.card-title
|
||||
= t('.apply_routing_rules')
|
||||
%p.notice
|
||||
= t('.routing_rules_notice')
|
||||
.conditionnel.mt-2.width-100
|
||||
%table.condition-table.mt-2.width-100
|
||||
%thead
|
||||
%tr
|
||||
%th.far-left
|
||||
%th.target Champ cible du routage
|
||||
%th.operator Opérateur
|
||||
%th.value Valeur
|
||||
%th.delete-column
|
||||
.conditionnel.mt-2.width-100
|
||||
- rows.each.with_index do |(targeted_champ, value, groupe_instructeur), row_index|
|
||||
= form_tag admin_procedure_routing_rules_path, method: :post, class: "form width-100 gi-#{groupe_instructeur.id}" do
|
||||
%table.condition-table.mt-2.width-100
|
||||
%tbody
|
||||
%tr{ data: { controller: 'autosave' } }
|
||||
%td.far-left Router vers « #{groupe_instructeur.label} » si
|
||||
%td.target= targeted_champ_tag(targeted_champ, row_index)
|
||||
%td.operator Est égal à
|
||||
%td.value= value_tag(targeted_champ, value, row_index)
|
||||
%td.delete-column
|
||||
= hidden_groupe_instructeur_tag(groupe_instructeur.id)
|
||||
- if can_route?
|
||||
.notice
|
||||
= t('.routing_rules_notice_html', path: champs_admin_procedure_path(@procedure_id))
|
||||
.mt-2.width-100
|
||||
%table.routing-rules-table.mt-2.width-100
|
||||
%thead
|
||||
%tr
|
||||
%th.far-left Router vers
|
||||
%th.if
|
||||
%th.target Champ cible du routage
|
||||
%th.operator
|
||||
%th.value Valeur
|
||||
.mt-2.width-100
|
||||
- rows.each.with_index do |(targeted_champ, value, groupe_instructeur), row_index|
|
||||
= form_tag admin_procedure_routing_rules_path(@procedure_id),
|
||||
method: :post,
|
||||
class: "form width-100 gi-#{groupe_instructeur.id}",
|
||||
data: { controller: 'autosave' } do
|
||||
= hidden_groupe_instructeur_tag(groupe_instructeur.id)
|
||||
%table.routing-rules-table.condition-table.mt-2.width-100
|
||||
%tbody
|
||||
%tr
|
||||
%td.far-left= groupe_instructeur.label
|
||||
%td.if si
|
||||
%td.target= targeted_champ_tag(targeted_champ, row_index)
|
||||
%td.operator est égal à
|
||||
%td.value= value_tag(targeted_champ, value, row_index)
|
||||
- else
|
||||
.notice= t('.routing_rules_warning_html', path: champs_admin_procedure_path(@procedure_id))
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
||||
attr_reader :coordinate, :upper_coordinates
|
||||
|
||||
def initialize(coordinate:, upper_coordinates:, focused: false)
|
||||
def initialize(coordinate:, upper_coordinates:, focused: false, errors: '')
|
||||
@coordinate = coordinate
|
||||
@focused = focused
|
||||
@upper_coordinates = upper_coordinates
|
||||
@errors = errors
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
%li.type-de-champ.flex.column.justify-start{ html_options }
|
||||
.flex.justify-start.section.head.hr
|
||||
.flex.justify-between.section.head.hr
|
||||
.handle.small.icon-only.icon.move-handle{ title: "Déplacer le champ vers le haut ou vers le bas" }
|
||||
.flex.justify-start.delete
|
||||
= button_to type_de_champ_path, class: 'button small icon-only danger', method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
|
||||
.icon.delete
|
||||
%span.sr-only Supprimer
|
||||
|
||||
- if coordinate.used_by_routing_rules?
|
||||
.flex.align-center
|
||||
%span
|
||||
utilisé pour
|
||||
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
|
||||
- else
|
||||
.flex.justify-start.delete
|
||||
= button_to type_de_champ_path, class: 'button small icon-only danger', method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
|
||||
.icon.delete
|
||||
%span.sr-only Supprimer
|
||||
|
||||
- if @errors.present?
|
||||
.types-de-champ-errors
|
||||
= @errors
|
||||
|
||||
.flex.justify-start.section.ml-1
|
||||
= form_for(type_de_champ, form_options) do |form|
|
||||
|
@ -19,7 +30,7 @@
|
|||
%span.sr-only Déplacer le champ vers le bas
|
||||
.cell.flex.justify-start.column.flex-grow
|
||||
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ)
|
||||
= form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ)
|
||||
= form.select :type_champ, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules?
|
||||
.flex.column.justify-start.flex-grow
|
||||
.cell
|
||||
.flex.align-center
|
||||
|
|
|
@ -5,26 +5,29 @@ module Administrateurs
|
|||
before_action :retrieve_procedure
|
||||
|
||||
def update
|
||||
left = champ_value(targeted_champ)
|
||||
right = parsed_value
|
||||
left = targeted_champ
|
||||
|
||||
@procedure.groupe_instructeurs.find(groupe_instructeur_id).update!(routing_rule: ds_eq(left, right))
|
||||
right = targeted_champ_changed? ? empty : value
|
||||
|
||||
groupe_instructeur.update!(routing_rule: ds_eq(left, right))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def targeted_champ_changed?
|
||||
targeted_champ != groupe_instructeur.routing_rule&.left
|
||||
end
|
||||
|
||||
def targeted_champ
|
||||
routing_params[:targeted_champ].to_i
|
||||
Logic.from_json(routing_params[:targeted_champ])
|
||||
end
|
||||
|
||||
def value
|
||||
routing_params[:value]
|
||||
Logic.from_json(routing_params[:value])
|
||||
end
|
||||
|
||||
def parsed_value
|
||||
term = Logic.from_json(value) rescue nil
|
||||
|
||||
term.presence || constant(value)
|
||||
def groupe_instructeur
|
||||
@groupe_instructeur ||= @procedure.groupe_instructeurs.find(groupe_instructeur_id)
|
||||
end
|
||||
|
||||
def groupe_instructeur_id
|
||||
|
|
|
@ -20,7 +20,12 @@ module Administrateurs
|
|||
def update
|
||||
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.revision_type_de_champ.used_by_routing_rules? && changing_of_type?(type_de_champ)
|
||||
coordinate = draft.coordinate_for(type_de_champ)
|
||||
errors = "« #{type_de_champ.libelle} » est utilisé pour le routage, vous ne pouvez pas modifier son type."
|
||||
@morphed = [champ_component_from(coordinate, focused: false, errors:)]
|
||||
flash.alert = errors
|
||||
elsif type_de_champ.update(type_de_champ_update_params)
|
||||
@coordinate = draft.coordinate_for(type_de_champ)
|
||||
@morphed = champ_components_starting_at(@coordinate)
|
||||
|
||||
|
@ -67,17 +72,29 @@ module Administrateurs
|
|||
end
|
||||
|
||||
def destroy
|
||||
@coordinate = draft.remove_type_de_champ(params[:stable_id])
|
||||
flash.notice = "Formulaire enregistré"
|
||||
coordinate, type_de_champ = draft.coordinate_and_tdc(params[:stable_id])
|
||||
|
||||
if @coordinate.present?
|
||||
@destroyed = @coordinate
|
||||
@morphed = champ_components_starting_at(@coordinate)
|
||||
if coordinate.used_by_routing_rules?
|
||||
errors = "« #{type_de_champ.libelle} » est utilisé pour le routage, vous ne pouvez pas le supprimer."
|
||||
@morphed = [champ_component_from(coordinate, focused: false, errors:)]
|
||||
flash.alert = errors
|
||||
else
|
||||
@coordinate = draft.remove_type_de_champ(params[:stable_id])
|
||||
flash.notice = "Formulaire enregistré"
|
||||
|
||||
if @coordinate.present?
|
||||
@destroyed = @coordinate
|
||||
@morphed = champ_components_starting_at(@coordinate)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def changing_of_type?(type_de_champ)
|
||||
type_de_champ_update_params['type_champ'].present? && (type_de_champ_update_params['type_champ'] != type_de_champ.type_champ)
|
||||
end
|
||||
|
||||
def champ_components_starting_at(coordinate, offset = 0)
|
||||
coordinate
|
||||
.siblings_starting_at(offset)
|
||||
|
@ -85,11 +102,12 @@ module Administrateurs
|
|||
.map { |c| champ_component_from(c) }
|
||||
end
|
||||
|
||||
def champ_component_from(coordinate, focused: false)
|
||||
def champ_component_from(coordinate, focused: false, errors: '')
|
||||
TypesDeChampEditor::ChampComponent.new(
|
||||
coordinate: coordinate,
|
||||
coordinate:,
|
||||
upper_coordinates: coordinate.upper_siblings,
|
||||
focused: focused
|
||||
focused: focused,
|
||||
errors:
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -101,27 +119,27 @@ module Administrateurs
|
|||
|
||||
def type_de_champ_update_params
|
||||
params.required(:type_de_champ).permit(:type_champ,
|
||||
:libelle,
|
||||
:description,
|
||||
:mandatory,
|
||||
:drop_down_list_value,
|
||||
:drop_down_other,
|
||||
:drop_down_secondary_libelle,
|
||||
:drop_down_secondary_description,
|
||||
:collapsible_explanation_enabled,
|
||||
:collapsible_explanation_text,
|
||||
editable_options: [
|
||||
:cadastres,
|
||||
:unesco,
|
||||
:arretes_protection,
|
||||
:conservatoire_littoral,
|
||||
:reserves_chasse_faune_sauvage,
|
||||
:reserves_biologiques,
|
||||
:reserves_naturelles,
|
||||
:natura_2000,
|
||||
:zones_humides,
|
||||
:znieff
|
||||
])
|
||||
:libelle,
|
||||
:description,
|
||||
:mandatory,
|
||||
:drop_down_list_value,
|
||||
:drop_down_other,
|
||||
:drop_down_secondary_libelle,
|
||||
:drop_down_secondary_description,
|
||||
:collapsible_explanation_enabled,
|
||||
:collapsible_explanation_text,
|
||||
editable_options: [
|
||||
:cadastres,
|
||||
:unesco,
|
||||
:arretes_protection,
|
||||
:conservatoire_littoral,
|
||||
:reserves_chasse_faune_sauvage,
|
||||
:reserves_biologiques,
|
||||
:reserves_naturelles,
|
||||
:natura_2000,
|
||||
:zones_humides,
|
||||
:znieff
|
||||
])
|
||||
end
|
||||
|
||||
def draft
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# canonical_procedure_id :bigint
|
||||
# defaut_groupe_instructeur_id :bigint
|
||||
# draft_revision_id :bigint
|
||||
# parent_procedure_id :bigint
|
||||
# published_revision_id :bigint
|
||||
|
@ -917,7 +918,12 @@ class Procedure < ApplicationRecord
|
|||
|
||||
def ensure_defaut_groupe_instructeur
|
||||
if self.groupe_instructeurs.empty?
|
||||
groupe_instructeurs.create(label: GroupeInstructeur::DEFAUT_LABEL)
|
||||
gi = groupe_instructeurs.create(label: GroupeInstructeur::DEFAUT_LABEL)
|
||||
self.update(defaut_groupe_instructeur_id: gi.id)
|
||||
end
|
||||
end
|
||||
|
||||
def stable_ids_used_by_routing_rules
|
||||
@stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact
|
||||
end
|
||||
end
|
||||
|
|
|
@ -225,6 +225,16 @@ class ProcedureRevision < ApplicationRecord
|
|||
types_de_champ_public.any?(&:carte?)
|
||||
end
|
||||
|
||||
def coordinate_and_tdc(stable_id)
|
||||
return [nil, nil] if stable_id.blank?
|
||||
|
||||
coordinate = revision_types_de_champ
|
||||
.joins(:type_de_champ)
|
||||
.find_by(type_de_champ: { stable_id: stable_id })
|
||||
|
||||
[coordinate, coordinate&.type_de_champ]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_estimated_fill_duration
|
||||
|
@ -245,16 +255,6 @@ class ProcedureRevision < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def coordinate_and_tdc(stable_id)
|
||||
return [nil, nil] if stable_id.blank?
|
||||
|
||||
coordinate = revision_types_de_champ
|
||||
.joins(:type_de_champ)
|
||||
.find_by(type_de_champ: { stable_id: stable_id })
|
||||
|
||||
[coordinate, coordinate&.type_de_champ]
|
||||
end
|
||||
|
||||
def renumber(siblings)
|
||||
siblings.to_a.compact.each.with_index do |sibling, position|
|
||||
sibling.update_column(:position, position)
|
||||
|
|
|
@ -69,4 +69,8 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
|
|||
revision
|
||||
end
|
||||
end
|
||||
|
||||
def used_by_routing_rules?
|
||||
stable_id.in?(procedure.stable_ids_used_by_routing_rules)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
= vite_client_tag
|
||||
= vite_javascript_tag 'application'
|
||||
|
||||
= preload_link_tag(asset_url("Muli-Regular.woff2"))
|
||||
= preload_link_tag(asset_url("Muli-Bold.woff2"))
|
||||
= preload_link_tag(asset_url("Marianne-Regular.woff2"))
|
||||
= preload_link_tag(asset_url("Spectral-Regular.ttf"))
|
||||
|
||||
= vite_stylesheet_tag 'main', media: 'all'
|
||||
= stylesheet_link_tag 'application', media: 'all'
|
||||
|
||||
%body{ class: browser.platform.ios? ? 'ios' : nil }
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
class AddDefautGroupeInstructeurIdToProcedures < ActiveRecord::Migration[6.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_reference :procedures, :defaut_groupe_instructeur, index: { algorithm: :concurrently }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddDefautGroupeInstructeurForeignKeyToProcedures < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_foreign_key :procedures, :groupe_instructeurs, column: :defaut_groupe_instructeur_id, validate: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class ValidateAddDefautGroupeInstructeurForeignKeyToProcedures < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
validate_foreign_key :procedures, :groupe_instructeurs
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2023_03_31_075755) do
|
||||
ActiveRecord::Schema.define(version: 2023_03_31_125931) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
|
@ -699,6 +699,7 @@ ActiveRecord::Schema.define(version: 2023_03_31_075755) do
|
|||
t.datetime "closed_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.string "declarative_with_state"
|
||||
t.bigint "defaut_groupe_instructeur_id"
|
||||
t.string "description"
|
||||
t.string "direction"
|
||||
t.datetime "dossiers_count_computed_at"
|
||||
|
@ -744,6 +745,7 @@ ActiveRecord::Schema.define(version: 2023_03_31_075755) do
|
|||
t.bigint "zone_id"
|
||||
t.index ["api_particulier_sources"], name: "index_procedures_on_api_particulier_sources", using: :gin
|
||||
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
|
||||
t.index ["defaut_groupe_instructeur_id"], name: "index_procedures_on_defaut_groupe_instructeur_id"
|
||||
t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id"
|
||||
t.index ["hidden_at"], name: "index_procedures_on_hidden_at"
|
||||
t.index ["libelle"], name: "index_procedures_on_libelle"
|
||||
|
@ -1019,6 +1021,7 @@ ActiveRecord::Schema.define(version: 2023_03_31_075755) do
|
|||
add_foreign_key "procedure_revisions", "attestation_templates"
|
||||
add_foreign_key "procedure_revisions", "dossier_submitted_messages"
|
||||
add_foreign_key "procedure_revisions", "procedures"
|
||||
add_foreign_key "procedures", "groupe_instructeurs", column: "defaut_groupe_instructeur_id"
|
||||
add_foreign_key "procedures", "procedure_revisions", column: "draft_revision_id"
|
||||
add_foreign_key "procedures", "procedure_revisions", column: "published_revision_id"
|
||||
add_foreign_key "procedures", "services"
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
namespace :after_party do
|
||||
desc 'Deployment task: back_fill_procedure_defaut_groupe_instructeur_id'
|
||||
task back_fill_procedure_defaut_groupe_instructeur_id: :environment do
|
||||
puts "Running deploy task 'back_fill_procedure_defaut_groupe_instructeur_id'"
|
||||
|
||||
# Put your task implementation HERE.
|
||||
#
|
||||
|
||||
# rubocop:disable DS/Unscoped
|
||||
progress = ProgressReport.new(Procedure.unscoped.where(defaut_groupe_instructeur_id: nil).count)
|
||||
|
||||
Procedure.unscoped.where(defaut_groupe_instructeur_id: nil).find_each do |p|
|
||||
p.update_columns(defaut_groupe_instructeur_id: p.defaut_groupe_instructeur.id)
|
||||
progress.inc
|
||||
end
|
||||
# rubocop:enable DS/Unscoped
|
||||
|
||||
progress.finish
|
||||
|
||||
# Update task as completed. If you remove the line below, the task will
|
||||
# run with every deploy (or every time you call after_party:run).
|
||||
AfterParty::TaskRecord
|
||||
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
class TypesDeChampEditor::ChampComponentPreview < ViewComponent::Preview
|
||||
include Logic
|
||||
|
||||
def nominal
|
||||
tdc = TypeDeChamp.new(type_champ: 'text', stable_id: 123)
|
||||
procedure = Procedure.new(id: 123)
|
||||
coordinate = ProcedureRevisionTypeDeChamp.new(type_de_champ: tdc, procedure:)
|
||||
upper_coordinates = []
|
||||
errors = 'une grosse erreur'
|
||||
|
||||
render_with_template(locals: {
|
||||
coordinate:,
|
||||
upper_coordinates:,
|
||||
errors:
|
||||
})
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
%main
|
||||
.container
|
||||
.types-de-champ-editor
|
||||
%ul.types-de-champ-block
|
||||
= render TypesDeChampEditor::ChampComponent.new(coordinate:,
|
||||
upper_coordinates:,
|
||||
focused: false,
|
||||
errors:)
|
38
spec/components/procedures/routing_rules_component_spec.rb
Normal file
38
spec/components/procedures/routing_rules_component_spec.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
describe Procedure::RoutingRulesComponent, type: :component do
|
||||
include Logic
|
||||
|
||||
describe 'render' do
|
||||
let(:procedure) do
|
||||
create(:procedure, types_de_champ_public: [{ type: :integer_number, libelle: 'Age' }])
|
||||
.tap { _1.groupe_instructeurs.create(label: 'groupe 2') }
|
||||
end
|
||||
|
||||
subject do
|
||||
render_inline(described_class.new(revision: procedure.active_revision,
|
||||
groupe_instructeurs: procedure.groupe_instructeurs))
|
||||
end
|
||||
|
||||
context 'when there are no types de champ that can be routed' do
|
||||
before do
|
||||
procedure.publish_revision!
|
||||
procedure.reload
|
||||
subject
|
||||
end
|
||||
it { expect(page).to have_text('Ajoutez ce champ dans la page') }
|
||||
end
|
||||
|
||||
context 'when there are types de champ that can be routed' do
|
||||
before do
|
||||
procedure.draft_revision.add_type_de_champ({
|
||||
type_champ: :drop_down_list,
|
||||
libelle: 'Votre ville',
|
||||
drop_down_list_value: "Paris\nLyon\nMarseille"
|
||||
})
|
||||
procedure.publish_revision!
|
||||
procedure.reload
|
||||
subject
|
||||
end
|
||||
it { expect(page).to have_text('Router vers') }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
describe TypesDeChampEditor::ChampComponent, type: :component do
|
||||
describe 'render' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }]) }
|
||||
let(:drop_down_tdc) { procedure.draft_revision.types_de_champ.first }
|
||||
let(:coordinate) { drop_down_tdc.revision_type_de_champ }
|
||||
let(:component) { described_class.new(coordinate:, upper_coordinates: []) }
|
||||
let(:routing_rules_stable_ids) { [] }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Procedure).to receive(:stable_ids_used_by_routing_rules).and_return(routing_rules_stable_ids)
|
||||
render_inline(component)
|
||||
end
|
||||
|
||||
context 'drop down tdc not used for routing' do
|
||||
it do
|
||||
expect(page).not_to have_text(/utilisé pour\nle routage/)
|
||||
expect(page).not_to have_css("select[disabled=\"disabled\"]")
|
||||
end
|
||||
end
|
||||
|
||||
context 'drop down tdc used for routing' do
|
||||
let(:routing_rules_stable_ids) { [drop_down_tdc.stable_id] }
|
||||
|
||||
it do
|
||||
expect(page).to have_css("select[disabled=\"disabled\"]")
|
||||
expect(page).to have_text(/utilisé pour\nle routage/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,26 +3,50 @@ describe Administrateurs::RoutingController, type: :controller do
|
|||
|
||||
before { sign_in(procedure.administrateurs.first.user) }
|
||||
|
||||
describe '#update' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }]) }
|
||||
describe '#update targeted champ' do
|
||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }, { type: :text, libelle: 'Un champ texte' }]) }
|
||||
let(:gi_2) { procedure.groupe_instructeurs.create(label: 'groupe 2') }
|
||||
let(:drop_down_tdc) { procedure.draft_revision.types_de_champ.first }
|
||||
let(:params) do
|
||||
{
|
||||
procedure_id: procedure.id,
|
||||
targeted_champ: drop_down_tdc.stable_id,
|
||||
value: 'Lyon',
|
||||
targeted_champ: champ_value(drop_down_tdc.stable_id).to_json,
|
||||
value: empty.to_json,
|
||||
groupe_instructeur_id: gi_2.id
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(procedure.administrateurs.first.user)
|
||||
post :update, params: params, format: :turbo_stream
|
||||
end
|
||||
|
||||
it do
|
||||
expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon')))
|
||||
expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), empty))
|
||||
end
|
||||
|
||||
context '#update value' do
|
||||
let(:value_updated_params) { params.merge(value: constant('Lyon').to_json) }
|
||||
|
||||
before do
|
||||
post :update, params: value_updated_params, format: :turbo_stream
|
||||
end
|
||||
|
||||
it do
|
||||
expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(drop_down_tdc.stable_id), constant('Lyon')))
|
||||
end
|
||||
|
||||
context 'targeted champ changed' do
|
||||
let(:last_tdc) { procedure.draft_revision.types_de_champ.last }
|
||||
|
||||
before do
|
||||
targeted_champ_updated_params = value_updated_params.merge(targeted_champ: champ_value(last_tdc.stable_id).to_json)
|
||||
post :update, params: targeted_champ_updated_params, format: :turbo_stream
|
||||
end
|
||||
|
||||
it do
|
||||
expect(gi_2.reload.routing_rule).to eq(ds_eq(champ_value(last_tdc.stable_id), empty))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ describe Administrateurs::TypesDeChampController, type: :controller 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: :drop_down_list, libelle: 'l3')
|
||||
p.draft_revision.add_type_de_champ(type_champ: :yes_no, libelle: 'bon dossier', private: true)
|
||||
end
|
||||
end
|
||||
|
@ -97,6 +97,21 @@ describe Administrateurs::TypesDeChampController, type: :controller do
|
|||
expect(flash.alert).to eq(["Le champ « Libelle » doit être rempli"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'rejected if type changed and routing involved' do
|
||||
let(:params) do
|
||||
default_params.deep_merge(type_de_champ: { type_champ: 'text', stable_id: third_coordinate.stable_id })
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(ProcedureRevisionTypeDeChamp).to receive(:used_by_routing_rules?).and_return(true)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to have_http_status(:ok)
|
||||
expect(flash.alert).to include("utilisé pour le routage")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# l1, l2, l3 => l1, l3, l2
|
||||
|
@ -157,5 +172,20 @@ describe Administrateurs::TypesDeChampController, type: :controller do
|
|||
expect(assigns(:destroyed).libelle).to eq('l2')
|
||||
expect(morpheds).to eq([['l3', ['l1']]])
|
||||
end
|
||||
|
||||
context 'rejected if type changed and routing involved' do
|
||||
let(:params) do
|
||||
{ procedure_id: procedure.id, stable_id: third_coordinate.stable_id }
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(ProcedureRevisionTypeDeChamp).to receive(:used_by_routing_rules?).and_return(true)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to have_http_status(:ok)
|
||||
expect(flash.alert).to include("utilisé pour le routage")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
describe '20230407124517_back_fill_procedure_defaut_groupe_instructeur_id' do
|
||||
let(:rake_task) { Rake::Task['after_party:back_fill_procedure_defaut_groupe_instructeur_id'] }
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
subject(:run_task) { rake_task.invoke }
|
||||
after(:each) { rake_task.reenable }
|
||||
|
||||
it 'populates defaut_groupe_instructeur_id' do
|
||||
expect(procedure.defaut_groupe_instructeur_id).to be_nil
|
||||
|
||||
run_task
|
||||
|
||||
procedure.reload
|
||||
expect(procedure.defaut_groupe_instructeur_id).to eq(procedure.defaut_groupe_instructeur.id)
|
||||
end
|
||||
end
|
|
@ -1274,10 +1274,13 @@ describe Procedure do
|
|||
end
|
||||
|
||||
describe '.ensure_a_groupe_instructeur_exists' do
|
||||
let!(:procedure) { create(:procedure) }
|
||||
let(:procedure) { create(:procedure, groupe_instructeurs: []) }
|
||||
|
||||
it { expect(procedure.groupe_instructeurs.count).to eq(1) }
|
||||
it { expect(procedure.groupe_instructeurs.first.label).to eq(GroupeInstructeur::DEFAUT_LABEL) }
|
||||
it do
|
||||
expect(procedure.groupe_instructeurs.count).to eq(1)
|
||||
expect(procedure.groupe_instructeurs.first.label).to eq(GroupeInstructeur::DEFAUT_LABEL)
|
||||
expect(procedure.defaut_groupe_instructeur_id).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.missing_instructeurs?' do
|
||||
|
|
|
@ -41,8 +41,8 @@ describe 'As an administrateur I can manage procedure routing', js: true do
|
|||
visit admin_procedure_groupe_instructeurs_path(procedure)
|
||||
|
||||
within('.condition-table tbody tr:nth-child(1)', match: :first) do
|
||||
expect(page).to have_content 'Router vers « a second group »'
|
||||
expect(page).not_to have_content 'Router vers « défaut »'
|
||||
expect(page).to have_content 'second group'
|
||||
expect(page).not_to have_content 'défaut'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue