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