Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ETQ administrateur je peux configurer un champ référentiel à configurer (avancé) #11202

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion app/assets/stylesheets/procedure_champs_editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
}

.cell {
label {
label,
.fake-label {
margin-bottom: 8px;
text-transform: uppercase;
font-size: 12px;
Expand Down
87 changes: 87 additions & 0 deletions app/components/referentiels/mapping_form_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# frozen_string_literal: true

class Referentiels::MappingFormComponent < ApplicationComponent
TYPES = {
String => "Chaine de caractère",
Float => "Nombre à virgule",
Integer => "Nombre Entier",
TrueClass => "Booléen",
FalseClass => "Booléen"
}

PREFIX = "type_de_champ[referentiel_mapping][]"

attr_reader :procedure, :type_de_champ, :referentiel

def initialize(procedure:, type_de_champ:, referentiel:)
@procedure = procedure
@type_de_champ = type_de_champ
@referentiel = referentiel
end

def last_request_keys
hash_to_jsonpath(referentiel.last_response_body)
end

def error_title
"¡Ay, caramba! 💣💥"
end

def back_url
edit_admin_procedure_referentiel_path(procedure, type_de_champ.stable_id, referentiel.id)
end

def cast_tag(jsonpath, value)
attribute = "type"
current_value = lookup_existing_value(jsonpath, attribute) || value_to_type(value)

select_tag "#{PREFIX}[#{attribute}]", options_for_select(TYPES.values.uniq, current_value), class: "fr-select"
end

def prefill_tag(jsonpath)
attribute = "prefill"
current_value = lookup_existing_value(jsonpath, attribute) || false
tag.div(class: "fr-checkbox-group") do
safe_join([
check_box_tag("#{PREFIX}[#{attribute}]", "1", current_value, class: "fr-checkbox", id: jsonpath.parameterize, data: { "action": "change->referentiel-mapping#onCheckboxChange" }),
tag.label(for: jsonpath.parameterize, class: "fr-label") { "&nbsp;".html_safe }
])
end
end

def libelle_tag(jsonpath)
attribute = "libelle"
current_value = lookup_existing_value(jsonpath, attribute) || jsonpath
options = { class: 'fr-input', data: { "referentiel-mapping-target": "input" } }
options[:disabled] = :disabled if lookup_existing_value(jsonpath, "prefill") == "1"

text_field_tag "#{PREFIX}[#{attribute}]", current_value, options
end

private

def lookup_existing_value(jsonpath, attribute)
type_de_champ.referentiel_mapping
&.find { _1["jsonpath"] == jsonpath }
&.fetch(attribute) { nil }

Check warning on line 66 in app/components/referentiels/mapping_form_component.rb

View check run for this annotation

Codecov / codecov/patch

app/components/referentiels/mapping_form_component.rb#L65-L66

Added lines #L65 - L66 were not covered by tests
end

def value_to_type(value)
TYPES.fetch(value.class) { TYPES[String] }
end

def hash_to_jsonpath(hash, parent_path = '$')
hash.each_with_object({}) do |(key, value), result|
current_path = "#{parent_path}.#{key}"

if value.is_a?(Hash)
result.merge!(hash_to_jsonpath(value, current_path))
elsif value.is_a?(Array) && value[0].is_a?(Hash)

result.merge!(hash_to_jsonpath(value[0], "#{current_path}[0]"))
else
result[current_path] = value
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
- if [email protected]?
%h4 Impossible de contacter le référentiel

= render Dsfr::AlertComponent.new(title: error_title, state: :error, extra_class_names: 'fr-mb-3w') do |c|
- c.with_body do
%p L'API n'a pas retournée une réponse valide, veuillez vérifier les paramètres du referentiel :
%pre Endpoint: #{@referentiel.url}
%pre Body: #{JSON.pretty_generate(@referentiel.last_response_body)}

%ul.fr-btns-group.fr-btns-group--inline-sm
%li= link_to "Annuler", champs_admin_procedure_path(@procedure), class: 'fr-btn fr-btn--secondary'
%li= link_to "Étape précédente", back_url, class: 'fr-btn'
%li
%button.fr-btn{ disabled: true } Étape suivante

- else
%section.fr-accordion.fr-mb-3w
%h3.fr-accordion__title
%button.fr-accordion__btn{ "aria-controls" => "last_response", "aria-expanded" => "false" } Afficher la réponse récupérée à partir de la requête configurée

.fr-collapse#last_response
%pre= @referentiel.url
%pre= JSON.pretty_generate(@referentiel.last_response_body)

%section
= form_with(model: @type_de_champ, url: update_mapping_type_de_champ_admin_procedure_referentiel_path(@procedure, @type_de_champ.stable_id, @referentiel)) do |f|
.fr-table
.fr-table__wrapper
.fr-table__container
.fr-table__content
%table
%caption Complétez le tableau de mapping ci-dessous en fonction des données que vous souhaitez exploiter
%thead
%tr
%th{ scope: "col" } Propriété
%th.fr-cell--fixed{ scope: "col" } Exemple de donnée
%th{ scope: "col" } Type de donnée
%th{ scope: "col" }
Utiliser la donnée
%br
pour préremplir
%br
un champ du
%br
formulaire
%th{ scope: "col" }
Libellé de la donnée récupérée
%br
(pour afficher à l'usager et/ou l'instructeur)
%tbody
- last_request_keys.each do |jsonpath,example_value|
%tr{ data: { controller: "referentiel-mapping" } }
%td
= jsonpath
= hidden_field_tag "type_de_champ[referentiel_mapping][][jsonpath]", jsonpath
%td.fr-cell--multiline= example_value
%td= cast_tag(jsonpath, example_value)
%td.text-center= prefill_tag(jsonpath)
%td= libelle_tag(jsonpath)

%ul.fr-btns-group.fr-btns-group--inline-sm
%li= link_to "Annuler", champs_admin_procedure_path(@procedure), class: 'fr-btn fr-btn--secondary'
%li= link_to "Étape précédente", back_url, class: 'fr-btn fr-btn--secondary'
%li= f.submit "Étape suivante", class: "fr-btn"
42 changes: 42 additions & 0 deletions app/components/referentiels/new_form_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

class Referentiels::NewFormComponent < ApplicationComponent
attr_reader :referentiel, :type_de_champ, :procedure
def initialize(referentiel:, type_de_champ:, procedure:)
@referentiel = referentiel
@type_de_champ = type_de_champ
@procedure = procedure
end

def id
:new_referentiel
end

def back_url
champs_admin_procedure_path(@procedure)
end

def form_url
if @referentiel.persisted?
admin_procedure_referentiel_path(@procedure, @type_de_champ.stable_id, @referentiel)
else
admin_procedure_referentiels_path(@procedure, @type_de_champ.stable_id)
end
end

def form_options
{
method: @referentiel.persisted? ? :patch : :post,
data: { turbo: 'true' },
html: { novalidate: 'novalidate', id: }
}
end

def submit_options
if referentiel.type.nil?
{ class: 'fr-btn', disabled: true }
else
{ class: 'fr-btn' }
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
= form_with model: @referentiel, scope: :referentiel, url: form_url, **form_options do |form|
= form.hidden_field :referentiel_id, value: params[:referentiel_id]
%div
= render Dsfr::RadioButtonListComponent.new(form:, target: :type,
buttons: [ { label: 'À partir d’une URL', value: 'Referentiels::APIReferentiel', hint: 'Connectez un champ à une API', data: {controller: 'autosubmit'} } ,
{ label: 'À partir d’un fichier CSV', value: 'Referentiels::CsvReferentiel', hint: 'Connectez un champ à un CSV', disabled: true }]) do ||
Comment intéroger votre référentiel ?

- if @referentiel.type == 'Referentiels::APIReferentiel'
= render Dsfr::InputComponent.new(form:, attribute: :url)
- elsif @referentiel.type == 'Referentiels::CsvReferentiel'
.fr-input-group
= form.label :piece_justificative_template, class: 'fr-label', for: dom_id(@type_de_champ, :piece_justificative_template) do
Fichier CSV
%span.fr-text-hint Utilisez le modèle du fichier CSV fourni ci-dessous pour construire votre référentiel (le nombre de colonne n’est pas limité).
= render Attachment::EditComponent.new(attached_file: @type_de_champ.piece_justificative_template, view_as: :link)

%hr.fr-hr.fr-my-5w

%div
= render Dsfr::RadioButtonListComponent.new(form:, target: :mode,
buttons: [ { label: 'Correspondance exacte', value: 'exact_match', hint: 'Vérification de l’existence de la donnée saisie dans la BDD du référentiel (exemple : plaque d’immatriculation, SIREN...)' } ,
{ label: 'Autosuggestion au fur et à mesure de la saisie de l’usager', value: 'autocomplete', hint: 'Affichage de données issues de la BDD du référentiel correspondant en partie ou en totalité à la donnée saisie par l’usager (exemple : BDD de médicaments, modèles de véhicules...)', disabled: true }]) do
Mode de remplissage du champ par l’usager

= render Dsfr::InputComponent.new(form:, attribute: :hint)
= render Dsfr::InputComponent.new(form:, attribute: :test_data)

%hr.fr-hr.fr-my-5w

%ul.fr-btns-group.fr-btns-group--inline-sm
%li= link_to "Annuler", back_url, class: 'fr-btn fr-btn--secondary'
%li= form.submit 'Étape suivante', submit_options
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@
Spécifier un nombre maximal conseillé de caractères :
= form.select :character_limit, options_for_character_limit, {}, { id: dom_id(type_de_champ, :character_limit), class: 'fr-select' }

- if type_de_champ.referentiel?
= render(TypesDeChampEditor::InfoReferentielComponent.new(procedure:, type_de_champ:))

- if type_de_champ.repetition?
.flex.justify-start.section.fr-ml-1w
.editor-block.flex-grow.cell
Expand Down
36 changes: 36 additions & 0 deletions app/components/types_de_champ_editor/info_referentiel_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

class TypesDeChampEditor::InfoReferentielComponent < ApplicationComponent
attr_reader :procedure, :type_de_champ
delegate :referentiel, to: :type_de_champ
delegate :ready?, to: :referentiel, allow_nil: true

def initialize(procedure:, type_de_champ:)
@procedure = procedure
@type_de_champ = type_de_champ
end

def edit_referentiel_on_draft_or_clone_url
if should_create_new_referentiel?
dup_existing_referentiel_options = referentiel ? { referentiel_id: referentiel.id } : {}
new_admin_procedure_referentiel_path(procedure, type_de_champ.stable_id, dup_existing_referentiel_options)
else
edit_admin_procedure_referentiel_path(procedure, type_de_champ.stable_id, type_de_champ.referentiel)
end
end

private

def should_create_new_referentiel?
return true if referentiel.nil?
return true if procedure.publiee?

already_in_use_in_another_procedure?
end

def already_in_use_in_another_procedure?
Procedure.joins(revisions: :types_de_champ)
.where.not(published_at: nil)
.exists?(types_de_champ: { referentiel_id: referentiel.id })
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.fr-px-2w

%hr.fr-hr

.flex.fr-mb-2w.cell
%p.fake-label.fr-text--bold.flex-grow.fr-mb-0 Configuration du champ référentiel
- if !ready?
%p.fr-badge.fr-badge--sm.fr-badge--error À configurer
- else
%p.fr-badge.fr-badge--sm.fr-badge--success Configuré

.fr-notice.fr-notice--info.fr-mb-2w
.fr-container
.fr-notice__body
%p.fr-notice__title Ce champ sera présenté à l’usager sous forme d’un champ de type « Texte court ».
%p.notice__desc.fr-mt-2w Lors de la configuration, vous pourrez choisir de :
%ul.fr-mt-0
%li proposer des autosuggestions au fur et à mesure de la saisie de l’usager (issue de la BDD du référentiel)
%li ou uniquement vérifier la correspondance exacte de saisie dans la BDD du référentiel

%p.flex.column.align-end
= link_to "Configurer le champ", edit_referentiel_on_draft_or_clone_url, class: 'fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-equalizer-fill'

%hr.fr-hr
80 changes: 80 additions & 0 deletions app/controllers/administrateurs/referentiels_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

module Administrateurs
class ReferentielsController < AdministrateurController
before_action :retrieve_procedure
before_action :retrieve_type_de_champ
before_action :retrieve_referentiel, except: [:new, :create]
layout 'empty_layout'

def new
@referentiel = @type_de_champ.build_referentiel(build_or_clone_by_id_params)
end

def edit
end

def create
referentiel = @type_de_champ.build_referentiel(referentiel_params)

if referentiel.configured? && referentiel.update(referentiel_params)
redirect_to mapping_type_de_champ_admin_procedure_referentiel_path(@procedure, @type_de_champ.stable_id, referentiel)
else
component = Referentiels::NewFormComponent.new(referentiel:, type_de_champ: @type_de_champ, procedure: @procedure)
render turbo_stream: turbo_stream.replace(component.id, component)
end
end

def update
if @referentiel.update(referentiel_params)
redirect_to mapping_type_de_champ_admin_procedure_referentiel_path(@procedure, @type_de_champ.stable_id, @referentiel)
else
render :edit
end
end

def mapping_type_de_champ
@service = ReferentielService.new(referentiel: @referentiel)
@service.test
end

def update_mapping_type_de_champ
flash = if @type_de_champ.update(type_de_champ_mapping_params)
{ notice: "La configuration du mapping a bien été enregistrée" }
else
{ alert: "Une erreur est survenue" }
end
redirect_to mapping_type_de_champ_admin_procedure_referentiel_path(@procedure, @type_de_champ.stable_id, @referentiel), flash:
end

private

def type_de_champ_mapping_params
params.require(:type_de_champ)
.permit(referentiel_mapping: [:jsonpath, :type, :prefill, :libelle])
end

def referentiel_params
params.require(:referentiel)
.permit(:type, :mode, :url, :hint, :test_data)
rescue ActionController::ParameterMissing
{}
end

def retrieve_type_de_champ
@type_de_champ = @procedure.draft_revision.find_and_ensure_exclusive_use(params[:stable_id])
end

def retrieve_referentiel
@referentiel = Referentiel.find(params[:id])
end

def build_or_clone_by_id_params
if params[:referentiel_id]
Referentiel.find(params[:referentiel_id]).attributes.slice(*%w[url test_data hint mode type])
else
referentiel_params
end
end
end
end
Loading
Loading