diff --git a/app/controllers/new_administrateur/types_de_champ_controller.rb b/app/controllers/new_administrateur/types_de_champ_controller.rb index f81298751..49cc93507 100644 --- a/app/controllers/new_administrateur/types_de_champ_controller.rb +++ b/app/controllers/new_administrateur/types_de_champ_controller.rb @@ -57,6 +57,7 @@ module NewAdministrateur ], methods: [ :drop_down_list_value, + :drop_down_other, :drop_down_secondary_libelle, :drop_down_secondary_description, :piece_justificative_template_filename, @@ -75,6 +76,7 @@ module NewAdministrateur :parent_id, :private, :drop_down_list_value, + :drop_down_other, :drop_down_secondary_libelle, :drop_down_secondary_description, :piece_justificative_template, @@ -98,6 +100,7 @@ module NewAdministrateur :description, :mandatory, :drop_down_list_value, + :drop_down_other, :drop_down_secondary_libelle, :drop_down_secondary_description, :piece_justificative_template, diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 721ddf54b..6ba476c78 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -337,8 +337,8 @@ module Users def champs_params params.permit(dossier: { champs_attributes: [ - :id, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: [], - champs_attributes: [:id, :_destroy, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: []] + :id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: [], + champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: []] ] }) end diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx index 5679ebedf..bce4e7103 100644 --- a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx +++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx @@ -11,6 +11,7 @@ import MoveButton from './MoveButton'; import TypeDeChampCarteOption from './TypeDeChampCarteOption'; import TypeDeChampCarteOptions from './TypeDeChampCarteOptions'; import TypeDeChampDropDownOptions from './TypeDeChampDropDownOptions'; +import TypeDeChampDropDownOther from './TypeDeChampDropDownOther'; import TypeDeChampPieceJustificative from './TypeDeChampPieceJustificative'; import TypeDeChampRepetitionOptions from './TypeDeChampRepetitionOptions'; import TypeDeChampTypesSelect from './TypeDeChampTypesSelect'; @@ -24,6 +25,7 @@ const TypeDeChamp = sortableElement( 'linked_drop_down_list' ].includes(typeDeChamp.type_champ); const isLinkedDropDown = typeDeChamp.type_champ === 'linked_drop_down_list'; + const isSimpleDropDown = typeDeChamp.type_champ === 'drop_down_list'; const isFile = typeDeChamp.type_champ === 'piece_justificative'; const isCarte = typeDeChamp.type_champ === 'carte'; const isExplication = typeDeChamp.type_champ === 'explication'; @@ -137,6 +139,10 @@ const TypeDeChamp = sortableElement( libelleHandler={updateHandlers.drop_down_secondary_libelle} descriptionHandler={updateHandlers.drop_down_secondary_description} /> + + + + ); + } + return null; +} + +TypeDeChampDropDownOther.propTypes = { + isVisible: PropTypes.bool, + handler: PropTypes.object +}; + +export default TypeDeChampDropDownOther; diff --git a/app/javascript/new_design/champs/drop-down-list.js b/app/javascript/new_design/champs/drop-down-list.js new file mode 100644 index 000000000..874946ae2 --- /dev/null +++ b/app/javascript/new_design/champs/drop-down-list.js @@ -0,0 +1,20 @@ +import { delegate, show, hide } from '@utils'; + +delegate( + 'change', + '.editable-champ-drop_down_list select, .editable-champ-drop_down_list input[type="radio"]', + (event) => { + const parent = event.target.closest('.editable-champ-drop_down_list'); + const inputGroup = parent?.querySelector('.drop_down_other'); + if (inputGroup) { + const input = inputGroup.querySelector('input'); + if (event.target.value === '__other__') { + show(inputGroup); + input.disabled = false; + } else { + hide(inputGroup); + input.disabled = true; + } + } + } +); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index bfaa7cda4..f6a7fb267 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -27,6 +27,7 @@ import '../new_design/dossiers/auto-upload'; import '../new_design/champs/carte'; import '../new_design/champs/linked-drop-down-list'; import '../new_design/champs/repetition'; +import '../new_design/champs/drop-down-list'; import { toggleCondidentielExplanation, diff --git a/app/models/champ.rb b/app/models/champ.rb index 131cbe976..552d0676d 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -38,6 +38,7 @@ class Champ < ApplicationRecord :mandatory?, :description, :drop_down_list_options, + :drop_down_other, :drop_down_list_options?, :drop_down_list_disabled_options, :drop_down_list_enabled_non_empty_options, diff --git a/app/models/champs/drop_down_list_champ.rb b/app/models/champs/drop_down_list_champ.rb index bf9e1b70d..248eb443f 100644 --- a/app/models/champs/drop_down_list_champ.rb +++ b/app/models/champs/drop_down_list_champ.rb @@ -21,6 +21,7 @@ # class Champs::DropDownListChamp < Champ THRESHOLD_NB_OPTIONS_AS_RADIO = 5 + OTHER = '__other__' def render_as_radios? enabled_non_empty_options.size <= THRESHOLD_NB_OPTIONS_AS_RADIO @@ -31,7 +32,15 @@ class Champs::DropDownListChamp < Champ end def options - drop_down_list_options + if drop_down_other? + drop_down_list_options + [["Autre", OTHER]] + else + drop_down_list_options + end + end + + def selected + other_value_present? ? OTHER : value end def disabled_options @@ -41,4 +50,26 @@ class Champs::DropDownListChamp < Champ def enabled_non_empty_options drop_down_list_enabled_non_empty_options end + + def other_value_present? + drop_down_other? && value.present? && drop_down_list_options.exclude?(value) + end + + def drop_down_other? + drop_down_other + end + + def value=(value) + if value != OTHER + write_attribute(:value, value) + end + end + + def value_other=(value) + write_attribute(:value, value) + end + + def value_other + other_value_present? ? value : "" + end end diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index e3856f4f6..25865fc96 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -243,6 +243,17 @@ class ProcedureRevision < ApplicationRecord } end end + if from_type_de_champ.drop_down_other != to_type_de_champ.drop_down_other + changes << { + op: :update, + attribute: :drop_down_other, + label: from_type_de_champ.libelle, + private: from_type_de_champ.private?, + from: from_type_de_champ.drop_down_other, + to: to_type_de_champ.drop_down_other, + stable_id: from_type_de_champ.stable_id + } + end elsif to_type_de_champ.carte? if from_type_de_champ.carte_optional_layers != to_type_de_champ.carte_optional_layers changes << { diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 97b329b7b..a415595ec 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -58,7 +58,7 @@ class TypeDeChamp < ApplicationRecord belongs_to :parent, class_name: 'TypeDeChamp', optional: true has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', inverse_of: :parent, dependent: :destroy - store_accessor :options, :cadastres, :old_pj, :drop_down_options, :skip_pj_validation, :skip_content_type_pj_validation, :drop_down_secondary_libelle, :drop_down_secondary_description + store_accessor :options, :cadastres, :old_pj, :drop_down_options, :skip_pj_validation, :skip_content_type_pj_validation, :drop_down_secondary_libelle, :drop_down_secondary_description, :drop_down_other has_many :revision_types_de_champ, class_name: 'ProcedureRevisionTypeDeChamp', dependent: :destroy, inverse_of: :type_de_champ has_many :revisions, through: :revision_types_de_champ @@ -331,6 +331,7 @@ class TypeDeChamp < ApplicationRecord ], methods: [ :drop_down_list_value, + :drop_down_other, :piece_justificative_template_filename, :piece_justificative_template_url, :editable_options, diff --git a/app/views/new_administrateur/procedures/_revision_changes.html.haml b/app/views/new_administrateur/procedures/_revision_changes.html.haml index cf0ead74f..1c7e936c5 100644 --- a/app/views/new_administrateur/procedures/_revision_changes.html.haml +++ b/app/views/new_administrateur/procedures/_revision_changes.html.haml @@ -41,6 +41,11 @@ %li= t(:add_option, scope: [:new_administrateur, :revision_changes], items: added.map{ |term| "« #{term.strip} »" }.join(", ")) - if removed.present? %li= t(:remove_option, scope: [:new_administrateur, :revision_changes], items: removed.map{ |term| "« #{term.strip} »" }.join(", ")) + - when :drop_down_other + - if change[:from] == false + %li.mb-1= t("new_administrateur.revision_changes.update_drop_down_other#{postfix}.enabled", label: change[:label]) + - else + %li.mb-1= t("new_administrateur.revision_changes.update_drop_down_other#{postfix}.disabled", label: change[:label]) - when :carte_layers - added = change[:to].sort - change[:from].sort - removed = change[:from].sort - change[:to].sort diff --git a/app/views/shared/dossiers/_drop_down_other_input.html.haml b/app/views/shared/dossiers/_drop_down_other_input.html.haml new file mode 100644 index 000000000..349e8c458 --- /dev/null +++ b/app/views/shared/dossiers/_drop_down_other_input.html.haml @@ -0,0 +1,4 @@ +.drop_down_other{ class: champ.other_value_present? ? '' : 'hidden' } + .notice + %p Veuillez saisir votre autre choix + = form.text_field :value_other, maxlength: "200", placeholder: "Saisissez ici", disabled: !champ.other_value_present? diff --git a/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml index 153217cff..ec7f81ee7 100644 --- a/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml +++ b/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml @@ -5,13 +5,18 @@ %label = form.radio_button :value, option = option + - if !champ.mandatory? %label.blank-radio = form.radio_button :value, '' Non renseigné + - if champ.drop_down_other? + %label + = form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: champ.other_value_present? + Autre - else - = form.select :value, - champ.options, - disabled: champ.disabled_options, - required: champ.mandatory? + = form.select :value, champ.options, selected: champ.selected, required: champ.mandatory? + + - if champ.drop_down_other? + = render partial: "shared/dossiers/drop_down_other_input", locals: { form: form, champ: champ } diff --git a/config/locales/views/new_administrateur/revision_changes/fr.yml b/config/locales/views/new_administrateur/revision_changes/fr.yml index b86ff6619..495f1728f 100644 --- a/config/locales/views/new_administrateur/revision_changes/fr.yml +++ b/config/locales/views/new_administrateur/revision_changes/fr.yml @@ -17,6 +17,9 @@ fr: disabled: Le champ « %{label} » n’est plus obligatoire update_piece_justificative_template: Le modèle de pièce justificative du champ « %{label} » a été modifié update_drop_down_options: Les options de sélection du champ « %{label} » ont été modifiées + update_drop_down_other: + enabled: Le champ « %{label} » comporte maintenant un choix « Autre » + disabled: Le champ « %{label} » ne comporte plus de choix « Autre » update_carte_layers: Les référentiels cartographiques du champ « %{label} » ont été modifiés add_private: L’annotation privée « %{label} » a été ajoutée remove_private: L’annotation privée « %{label} » a été supprimée diff --git a/spec/system/users/dropdown_spec.rb b/spec/system/users/dropdown_spec.rb new file mode 100644 index 000000000..67e6d3494 --- /dev/null +++ b/spec/system/users/dropdown_spec.rb @@ -0,0 +1,43 @@ +describe 'dropdown list with other option activated' do + let(:password) { 'my-s3cure-p4ssword' } + let!(:user) { create(:user, password: password) } + + let(:list_items) do + <<~END_OF_LIST + --Primary 1-- + Secondary 1.1 + Secondary 1.2 + END_OF_LIST + end + + let(:type_de_champ) { build(:type_de_champ_drop_down_list, libelle: 'simple dropdown other', drop_down_list_value: list_items, drop_down_other: true) } + + let(:procedure) do + create(:procedure, :published, :for_individual, types_de_champ: [type_de_champ]) + end + + let(:user_dossier) { user.dossiers.first } + + before do + login_as(user, scope: :user) + visit "/commencer/#{procedure.path}" + click_on 'Commencer la démarche' + end + + scenario 'Select other option and the other input hidden must appear', js: true do + fill_individual + + find('.radios').find('label:last-child').find('input').select_option + expect(page).to have_selector('.drop_down_other', visible: true) + end + + private + + def fill_individual + choose 'Monsieur' + fill_in('individual_prenom', with: 'prenom') + fill_in('individual_nom', with: 'nom') + click_on 'Continuer' + expect(page).to have_current_path(brouillon_dossier_path(user_dossier)) + end +end