From e048f482410ab9fba8fbffe077955467fb1a71c6 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 11 Feb 2021 15:25:30 +0100 Subject: [PATCH 1/9] create ComboMultipleDropdownList component --- .../components/ComboMultipleDropdownList.js | 217 ++++++++++++++++++ .../loaders/ComboMultipleDropdownList.js | 5 + 2 files changed, 222 insertions(+) create mode 100644 app/javascript/components/ComboMultipleDropdownList.js create mode 100644 app/javascript/loaders/ComboMultipleDropdownList.js diff --git a/app/javascript/components/ComboMultipleDropdownList.js b/app/javascript/components/ComboMultipleDropdownList.js new file mode 100644 index 000000000..4665fe2a6 --- /dev/null +++ b/app/javascript/components/ComboMultipleDropdownList.js @@ -0,0 +1,217 @@ +import React, { + useMemo, + useState, + useRef, + useContext, + createContext, + useEffect, + useLayoutEffect +} from 'react'; +import PropTypes from 'prop-types'; +import { + Combobox, + ComboboxInput, + ComboboxList, + ComboboxOption, + ComboboxPopover +} from '@reach/combobox'; +import '@reach/combobox/styles.css'; +import matchSorter from 'match-sorter'; +import { fire } from '@utils'; + +const Context = createContext(); + +function ComboMultipleDropdownList({ + options, + hiddenFieldId, + selected, + label +}) { + if (label == undefined) { + label = 'Choisir une option'; + } + if (Array.isArray(options[0]) == false) { + options = options.map((o) => [o, o]); + } + const inputRef = useRef(); + const [term, setTerm] = useState(''); + const [selections, setSelections] = useState(selected); + const results = useMemo( + () => + (term + ? matchSorter( + options.filter((o) => !o[0].startsWith('--')), + term + ) + : options + ).filter((o) => o[0] && !selections.includes(o[1])), + [term, selections] + ); + const hiddenField = useMemo( + () => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`), + [hiddenFieldId] + ); + + const handleChange = (event) => { + setTerm(event.target.value); + }; + + const saveSelection = (selections) => { + setSelections(selections); + if (hiddenField) { + hiddenField.setAttribute('value', JSON.stringify(selections)); + fire(hiddenField, 'autosave:trigger'); + } + }; + + const onSelect = (value) => { + let sel = options.find((o) => o[0] == value)[1]; + saveSelection([...selections, sel]); + setTerm(''); + }; + + const onRemove = (value) => { + saveSelection( + selections.filter((s) => s !== options.find((o) => o[0] == value)[1]) + ); + inputRef.current.focus(); + }; + + return ( + + +
    + {selections.map((selection) => ( + o[1] == selection)[0]} + /> + ))} +
+ +
+ {results && ( + + {results.length === 0 && ( +

+ Aucun résultat{' '} + +

+ )} + + {results.map((value, index) => { + if (value[0].startsWith('--')) { + return ; + } + return ; + })} + +
+ )} +
+ ); +} + +function ComboboxTokenLabel({ onRemove, ...props }) { + const selectionsRef = useRef([]); + + useLayoutEffect(() => { + selectionsRef.current = []; + return () => (selectionsRef.current = []); + }); + + const context = { + onRemove, + selectionsRef + }; + + return ( + +
+ + ); +} + +ComboboxTokenLabel.propTypes = { + onRemove: PropTypes.func +}; + +function ComboboxSeparator({ value }) { + return ( +
  • + {value.slice(2, -2)} +
  • + ); +} + +ComboboxSeparator.propTypes = { + value: PropTypes.string +}; + +function ComboboxToken({ value, ...props }) { + const { selectionsRef, onRemove } = useContext(Context); + useEffect(() => { + selectionsRef.current.push(value); + }); + + return ( +
  • { + if (event.key === 'Backspace') { + onRemove(value); + } + }} + {...props} + > + { + onRemove(value); + }} + > + x + + {value} +
  • + ); +} + +ComboboxToken.propTypes = { + value: PropTypes.string, + label: PropTypes.string +}; + +ComboMultipleDropdownList.propTypes = { + options: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string), + PropTypes.arrayOf( + PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ) + ) + ]), + hiddenFieldId: PropTypes.string, + selected: PropTypes.arrayOf(PropTypes.string), + arraySelected: PropTypes.arrayOf(PropTypes.array), + label: PropTypes.string +}; + +export default ComboMultipleDropdownList; diff --git a/app/javascript/loaders/ComboMultipleDropdownList.js b/app/javascript/loaders/ComboMultipleDropdownList.js new file mode 100644 index 000000000..db778b339 --- /dev/null +++ b/app/javascript/loaders/ComboMultipleDropdownList.js @@ -0,0 +1,5 @@ +import Loadable from '../components/Loadable'; + +export default Loadable(() => + import('../components/ComboMultipleDropdownList') +); From b8097e782a2e11339572cdb73db69cc525f8b1be Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 11 Feb 2021 15:27:39 +0100 Subject: [PATCH 2/9] rend accessible la selection multiple usager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit en remplaçant select2 par ComboMultipleDropdownList --- .../_multiple_drop_down_list.html.haml | 9 +++------ spec/features/users/brouillon_spec.rb | 7 ++++--- spec/support/feature_helpers.rb | 19 +++++++++++++++++++ .../shared/dossiers/_edit.html.haml_spec.rb | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml index e227cf775..4911753ee 100644 --- a/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml +++ b/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml @@ -7,10 +7,7 @@ = b.text - else - = form.select :value, - champ.options, - { selected: champ.selected_options, - disabled: champ.disabled_options }, - multiple: true, - class: 'select2' + - hidden_field_id = SecureRandom.uuid + = form.hidden_field :value, { data: { uuid: hidden_field_id } } + = react_component("ComboMultipleDropdownList", options: champ.options, selected: champ.selected_options, disabled: champ.disabled_options, hiddenFieldId: hidden_field_id, label: champ.libelle) diff --git a/spec/features/users/brouillon_spec.rb b/spec/features/users/brouillon_spec.rb index cb02a6d55..9f3b9fa0f 100644 --- a/spec/features/users/brouillon_spec.rb +++ b/spec/features/users/brouillon_spec.rb @@ -25,8 +25,9 @@ feature 'The user' do check('val1') check('val3') select('bravo', from: form_id_for('simple_choice_drop_down_list_long')) - select('alpha', from: form_id_for('multiple_choice_drop_down_list_long')) - select('charly', from: form_id_for('multiple_choice_drop_down_list_long')) + select_multi('multiple_choice_drop_down_list_long', 'alpha') + select_multi('multiple_choice_drop_down_list_long', 'charly') + select_champ_geo('pays', 'aust', 'AUSTRALIE') select_champ_geo('regions', 'Ma', 'Martinique') @@ -83,7 +84,7 @@ feature 'The user' do expect(page).to have_checked_field('val1') expect(page).to have_checked_field('val3') expect(page).to have_selected_value('simple_choice_drop_down_list_long', selected: 'bravo') - expect(page).to have_selected_value('multiple_choice_drop_down_list_long', selected: ['alpha', 'charly']) + check_selected_values('multiple_choice_drop_down_list_long', ['alpha', 'charly']) expect(page).to have_hidden_field('pays', with: 'AUSTRALIE') expect(page).to have_hidden_field('regions', with: 'Martinique') expect(page).to have_hidden_field('departements', with: '02 - Aisne') diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 56347bbfa..cb1bd6c9c 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -103,6 +103,25 @@ module FeatureHelpers end end + def select_multi(champ, with) + input = find("input[aria-label='#{champ}'") + input.click + + # hack because for unknown reason, the click on input doesn't show combobox-popover with selenium driver + script = "document.evaluate(\"//input[@aria-label='#{champ}']//ancestor::div[@data-reach-combobox]/div[@data-reach-combobox-popover]\", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext().removeAttribute(\"hidden\")" + execute_script(script) + + element = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-reach-combobox]//div[@data-reach-combobox-popover]//li/span[normalize-space(text())='#{with}']") + element.click + end + + def check_selected_values(champ, values) + combobox = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-react-class='ComboMultipleDropdownList']") + hiddenFieldId = JSON.parse(combobox["data-react-props"])["hiddenFieldId"] + hiddenField = find("input[data-uuid='#{hiddenFieldId}']") + expect(values.sort).to eq(JSON.parse(hiddenField.value).sort) + end + # Keep the brower window open after a test success of failure, to # allow inspecting the page or the console. # diff --git a/spec/views/shared/dossiers/_edit.html.haml_spec.rb b/spec/views/shared/dossiers/_edit.html.haml_spec.rb index 12e24fad4..531f2d32c 100644 --- a/spec/views/shared/dossiers/_edit.html.haml_spec.rb +++ b/spec/views/shared/dossiers/_edit.html.haml_spec.rb @@ -82,7 +82,7 @@ describe 'shared/dossiers/edit.html.haml', type: :view do let(:type_de_champ) { create(:type_de_champ_multiple_drop_down_list, :long, procedure: dossier.procedure) } it 'renders the list as a multiple-selection dropdown' do - expect(subject).to have_selector('select.select2') + expect(subject).to have_selector('[data-react-class="ComboMultipleDropdownList"]') end end end From c855d13994b82197b4ba968dbd4c004fd4a9740c Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 11 Feb 2021 15:31:10 +0100 Subject: [PATCH 3/9] rend accessible l'envoi de dossier par un instructeur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit en remplaçant select2 par ComboMultipleDropdownList --- app/controllers/instructeurs/dossiers_controller.rb | 2 +- .../dossiers/_envoyer_dossier_block.html.haml | 9 ++++----- .../controllers/instructeurs/dossiers_controller_spec.rb | 2 +- spec/features/instructeurs/instruction_spec.rb | 6 ++---- .../dossiers/_envoyer_dossier_block.html.haml_spec.rb | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 00ccfefdc..09e9a432b 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -72,7 +72,7 @@ module Instructeurs end def send_to_instructeurs - recipients = Instructeur.find(params[:recipients]) + recipients = Instructeur.find(JSON.parse(params[:recipients])) recipients.each do |recipient| recipient.follow(dossier) diff --git a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml index e5e0afd78..8405c620d 100644 --- a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml +++ b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml @@ -8,9 +8,8 @@ Le destinataire suivra automatiquement le dossier = form_for dossier, url: send_to_instructeurs_instructeur_dossier_path(dossier.procedure, dossier), method: :post, html: { class: 'form recipients-form' } do |f| .flex.justify-start.align-start - = select_tag(:recipients, - options_from_collection_for_select(potential_recipients, :id, :email), - multiple: true, - class: 'select2-limited', - placeholder: '') + - hidden_field_id = SecureRandom.uuid + = hidden_field_tag :recipients, nil, data: { uuid: hidden_field_id } + = react_component("ComboMultipleDropdownList", options: potential_recipients.map{|r| [r.email, r.id]}, selected: [], disabled: [], hiddenFieldId: hidden_field_id, label: "email instructeur") + = f.submit "Envoyer", class: "button large send gap-left" diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index f95471b26..650725dec 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -38,7 +38,7 @@ describe Instructeurs::DossiersController, type: :controller do post( :send_to_instructeurs, params: { - recipients: [recipient], + recipients: [recipient.id].to_json, procedure_id: procedure.id, dossier_id: dossier.id } diff --git a/spec/features/instructeurs/instruction_spec.rb b/spec/features/instructeurs/instruction_spec.rb index fb0b5acd6..13f2a2669 100644 --- a/spec/features/instructeurs/instruction_spec.rb +++ b/spec/features/instructeurs/instruction_spec.rb @@ -128,10 +128,8 @@ feature 'Instructing a dossier:' do click_on 'Personnes impliquées' - first('.select2-container', minimum: 1).click - find('li.select2-results__option[role="option"]', text: instructeur_2.email).click - first('.select2-container', minimum: 1).click - find('li.select2-results__option[role="option"]', text: instructeur_3.email).click + select_multi('email instructeur', instructeur_2.email) + select_multi('email instructeur', instructeur_3.email) click_on 'Envoyer' diff --git a/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb b/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb index 7b690e803..c094c5f16 100644 --- a/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb +++ b/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb @@ -13,7 +13,7 @@ describe 'instructeurs/dossiers/envoyer_dossier_block.html.haml', type: :view do let(:instructeur) { create(:instructeur, email: 'yop@totomail.fr') } let(:potential_recipients) { [instructeur] } - it { is_expected.to have_css("select > option[value='#{instructeur.id}']") } + it { is_expected.to match(/data-react-props.*#{instructeur.email}/) } it { is_expected.to have_css(".button.send") } end From 3fc7b57b8c3436d745763cec36515ef9c887d34f Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 11 Feb 2021 15:32:00 +0100 Subject: [PATCH 4/9] rend accessible la personnalisation des filtres instructeurs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit en remplaçant select2 par ComboMultipleDropdownList --- app/controllers/instructeurs/procedures_controller.rb | 2 +- app/views/instructeurs/procedures/show.html.haml | 8 ++++---- spec/features/instructeurs/procedure_filters_spec.rb | 6 ++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index e389ab07d..8e780b5e3 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -138,7 +138,7 @@ module Instructeurs end def update_displayed_fields - procedure_presentation.update_displayed_fields(params[:values]) + procedure_presentation.update_displayed_fields(JSON.parse(params[:values])) redirect_back(fallback_location: instructeur_procedure_url(procedure)) end diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index 6f59875bf..bae011e94 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -122,10 +122,10 @@ Personnaliser #custom-menu.dropdown-content.fade-in-down = form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form columns-form' do - = select_tag :values, - options_for_select(@displayed_fields_options, selected: @displayed_fields_selected), - multiple: true, - class: 'select2-limited' + - hidden_field_id = SecureRandom.uuid + = hidden_field_tag :values, nil, data: { uuid: hidden_field_id } + = react_component("ComboMultipleDropdownList", options: @displayed_fields_options, selected: @displayed_fields_selected, disabled: [], hiddenFieldId: hidden_field_id, label: 'colonne') + = submit_tag "Enregistrer", class: 'button' %tbody diff --git a/spec/features/instructeurs/procedure_filters_spec.rb b/spec/features/instructeurs/procedure_filters_spec.rb index faaa5f9b4..2e3ab6519 100644 --- a/spec/features/instructeurs/procedure_filters_spec.rb +++ b/spec/features/instructeurs/procedure_filters_spec.rb @@ -125,15 +125,13 @@ feature "procedure filters" do def add_column(column_name) click_on 'Personnaliser' - find("span.select2-container").click - find(:xpath, "//li[text()='#{column_name}']").click + select_multi('colonne', column_name) click_button "Enregistrer" end def remove_column(column_name) click_on 'Personnaliser' - find(:xpath, "//li[contains(@title, '#{column_name}')]/span[contains(text(), '×')]").click - find(:xpath, "//form[contains(@class, 'columns-form')]//span[contains(@class, 'select2-container')]").click + find(:xpath, "//li[contains(text(), '#{column_name}')]/span[contains(text(), 'x')]").click click_button "Enregistrer" end end From 7565a25b516c44164c74dfc2d0b822057294b98a Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 11 Feb 2021 15:36:11 +0100 Subject: [PATCH 5/9] rend accessible l'affectation d'un instructeur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit en remplaçant select2 par ComboMultipleDropdownList --- .../groupe_instructeurs_controller.rb | 4 ++-- .../groupe_instructeurs/show.html.haml | 9 ++++----- .../groupe_instructeurs_controller_spec.rb | 12 ++++++------ spec/features/routing/full_scenario_spec.rb | 10 ++++++---- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb index e90523e76..91375a9a4 100644 --- a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb +++ b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb @@ -80,8 +80,8 @@ module NewAdministrateur end def add_instructeur - emails = params['emails'].presence || [] - emails = emails.map(&:strip).map(&:downcase) + emails = params['emails'].presence || [].to_json + emails = JSON.parse(emails).map(&:strip).map(&:downcase) correct_emails, bad_emails = emails .partition { |email| URI::MailTo::EMAIL_REGEXP.match?(email) } diff --git a/app/views/new_administrateur/groupe_instructeurs/show.html.haml b/app/views/new_administrateur/groupe_instructeurs/show.html.haml index c026fa1c9..1ada176e6 100644 --- a/app/views/new_administrateur/groupe_instructeurs/show.html.haml +++ b/app/views/new_administrateur/groupe_instructeurs/show.html.haml @@ -25,12 +25,11 @@ .instructeur-wrapper - if !@procedure.routee? %p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche - = select_tag :emails, - options_for_select(@available_instructeur_emails), - multiple: true, - class: 'select-instructeurs select2-limited' + - hidden_field_id = SecureRandom.uuid + = hidden_field_tag :emails, nil, data: { uuid: hidden_field_id } + = react_component("ComboMultipleDropdownList", options: @available_instructeur_emails, selected: [], disabled: [], hiddenFieldId: hidden_field_id, label: 'email instructeur') - = f.submit 'Affecter', class: 'button primary send' + = f.submit 'Affecter', class: 'button primary send' %table.table.mt-2 %thead diff --git a/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb b/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb index a4bf90d06..697409c6b 100644 --- a/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb @@ -207,11 +207,11 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do describe '#add_instructeur_procedure_non_routee' do let(:procedure) { create :procedure, administrateur: admin } - let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'] } + let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'].to_json } subject { post :add_instructeur, params: { emails: emails, procedure_id: procedure.id, id: gi_1_1.id } } context 'when all emails are valid' do - let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'] } + let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'].to_json } it { expect(response.status).to eq(200) } it { expect(subject.request.flash[:alert]).to be_nil } it { expect(subject.request.flash[:notice]).to be_present } @@ -219,7 +219,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do end context 'when there is at least one bad email' do - let(:emails) { ['badmail', 'instructeur2@gmail.com'] } + let(:emails) { ['badmail', 'instructeur2@gmail.com'].to_json } it { expect(response.status).to eq(200) } it { expect(subject.request.flash[:alert]).to be_present } it { expect(subject.request.flash[:notice]).to be_present } @@ -227,7 +227,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do end context 'when the admin wants to assign an instructor who is already assigned on this procedure' do - let(:emails) { ['instructeur_1@ministere_a.gouv.fr'] } + let(:emails) { ['instructeur_1@ministere_a.gouv.fr'].to_json } it { expect(subject.request.flash[:alert]).to be_present } it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) } end @@ -247,7 +247,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do params: { procedure_id: procedure.id, id: gi_1_2.id, - emails: new_instructeur_emails + emails: new_instructeur_emails.to_json } end @@ -281,7 +281,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do end context 'of an empty string' do - let(:new_instructeur_emails) { '' } + let(:new_instructeur_emails) { [''] } it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) } end diff --git a/spec/features/routing/full_scenario_spec.rb b/spec/features/routing/full_scenario_spec.rb index ba9a5607a..d8438d5b2 100644 --- a/spec/features/routing/full_scenario_spec.rb +++ b/spec/features/routing/full_scenario_spec.rb @@ -30,14 +30,16 @@ feature 'The routing', js: true do expect(page).to have_field('Nom du groupe', with: 'littéraire') # add victor to littéraire groupe - find('input.select2-search__field').send_keys('victor@inst.com', :enter) + # find('input.select2-search__field').send_keys('victor@inst.com', :enter) + find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter) + click_on 'Affecter' perform_enqueued_jobs { click_on 'Affecter' } expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche") victor = User.find_by(email: 'victor@inst.com').instructeur # add superwoman to littéraire groupe - find('input.select2-search__field').send_keys('superwoman@inst.com', :enter) + find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter) perform_enqueued_jobs { click_on 'Affecter' } expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche") @@ -50,14 +52,14 @@ feature 'The routing', js: true do expect(page).to have_text('Le groupe d’instructeurs « scientifique » a été créé.') # add marie to scientifique groupe - find('input.select2-search__field').send_keys('marie@inst.com', :enter) + find("input[aria-label='email instructeur'").send_keys('marie@inst.com', :enter) perform_enqueued_jobs { click_on 'Affecter' } expect(page).to have_text("L’instructeur marie@inst.com a été affecté") marie = User.find_by(email: 'marie@inst.com').instructeur # add superwoman to scientifique groupe - find('input.select2-search__field').send_keys('superwoman@inst.com', :enter) + find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter) perform_enqueued_jobs { click_on 'Affecter' } expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté") From f9ad9444a9862ffd18d97f693941bdd54744a63e Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Thu, 11 Feb 2021 15:37:45 +0100 Subject: [PATCH 6/9] add style for ComboMultipleDropdownList --- app/assets/stylesheets/forms.scss | 66 +++++++++++++++++++ .../stylesheets/personnes_impliquees.scss | 43 +++++++++++- app/assets/stylesheets/procedure_show.scss | 38 +++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index 3d6afeccf..d37c4c8df 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -305,6 +305,28 @@ border-color: $blue; } + [data-reach-combobox-token-list] { + padding: $default-padding; + display: flex; + } + + [data-reach-combobox-token] { + border: solid 1px $border-grey; + color: $black; + margin-top: $default-padding; + margin-bottom: $default-padding; + margin-right: 0.5 * $default-padding; + border-radius: 4px; + padding: $default-padding; + cursor: pointer; + list-style: none; + } + + [data-reach-combobox-token]:focus { + background-color: $black; + color: $white; + } + .select2 { min-width: 50%; } @@ -481,11 +503,55 @@ } } +[data-react-class="ComboMultipleDropdownList"] { + margin-bottom: $default-fields-spacer; + + [data-reach-combobox-input] { + outline: none; + border: none; + flex-grow: 1; + margin: 0.25rem; + background-image: image-url("icons/chevron-down"); + background-size: 14px; + background-repeat: no-repeat; + background-position: right 10px center; + } + + [data-reach-combobox-input]:focus { + outline: solid; + outline-color: $light-blue; + } +} + +[data-combobox-token-label] { + border: 1px solid #CCCCCC; + border-radius: 4px; + display: flex; + flex-wrap: wrap; +} + [data-reach-combobox-option] { font-size: 16px; + list-style-type: none; } [data-reach-combobox-option][aria-selected="true"] { background: $light-blue !important; color: $white; } + +[data-combobox-separator] { + font-size: 16px; + color: $dark-grey; + margin-top: 6px; +} + +[data-combobox-remove-token] { + color: $dark-grey; + padding-right: 4px; +} + + +[data-reach-combobox-input]:focus { + outline-color: $light-blue; +} diff --git a/app/assets/stylesheets/personnes_impliquees.scss b/app/assets/stylesheets/personnes_impliquees.scss index ac908f538..829abbf38 100644 --- a/app/assets/stylesheets/personnes_impliquees.scss +++ b/app/assets/stylesheets/personnes_impliquees.scss @@ -1,7 +1,10 @@ +@import "constants"; +@import "colors"; + .personnes-impliquees { padding-bottom: 50px; - ul { + ul.tab-list { list-style-type: disc; margin-left: 16px; } @@ -11,4 +14,42 @@ padding: 12px; } // scss-lint:enable + [data-react-class="ComboMultipleDropdownList"] { + margin-bottom: $default-fields-spacer; + + [data-reach-combobox-token-list] { + padding: 0.5 * $default-padding; + display: flex; + } + + [data-reach-combobox-token] { + border: solid 1px $border-grey; + color: $black; + margin-top: 0.5 * $default-padding; + margin-bottom: 0.5 * $default-padding; + margin-right: 0.5 * $default-padding; + border-radius: 4px; + padding: 0.5 * $default-padding; + cursor: pointer; + list-style: none; + } + + [data-reach-combobox-token]:focus { + background-color: $black; + color: $white; + } + + + [data-reach-combobox-input] { + outline: none; + border: none; + flex-grow: 1; + margin: 0.25rem; + } + + [data-reach-combobox-input]:focus { + outline: solid; + outline-color: $light-blue; + } + } } diff --git a/app/assets/stylesheets/procedure_show.scss b/app/assets/stylesheets/procedure_show.scss index 9303ad8ca..c05969248 100644 --- a/app/assets/stylesheets/procedure_show.scss +++ b/app/assets/stylesheets/procedure_show.scss @@ -61,4 +61,42 @@ margin-bottom: 3 * $default-spacer; text-align: center; } + + [data-react-class="ComboMultipleDropdownList"] { + margin-bottom: $default-fields-spacer; + + [data-reach-combobox-token-list] { + padding: 0.25 * $default-padding; + display: inline-block; + width: 100%; + } + + [data-reach-combobox-token] { + border: solid 1px $border-grey; + color: $black; + margin: 0.25 * $default-padding; + border-radius: 2px; + padding: 0.25 * $default-padding; + cursor: pointer; + list-style: none; + } + + [data-reach-combobox-token]:focus { + background-color: $black; + color: $white; + } + + + [data-reach-combobox-input] { + outline: none; + border: none; + flex-grow: 1; + margin: 0.25rem; + } + + [data-reach-combobox-input]:focus { + outline: solid; + outline-color: $light-blue; + } + } } From 06e282b8398fb88e6d25bcf9c3efe0667dd66a68 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 16 Feb 2021 16:46:16 +0100 Subject: [PATCH 7/9] supprime select2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit select2 n'est plus utilisé. Il est remplacé par ComboMultipleDropdownList --- app/assets/stylesheets/add_instructeur.scss | 30 ------- app/assets/stylesheets/application.scss | 1 - app/assets/stylesheets/buttons.scss | 16 ---- app/assets/stylesheets/forms.scss | 38 +-------- .../stylesheets/personnes_impliquees.scss | 5 -- app/javascript/new_design/select2.js | 80 ------------------- app/javascript/packs/application.js | 1 - 7 files changed, 1 insertion(+), 170 deletions(-) delete mode 100644 app/javascript/new_design/select2.js diff --git a/app/assets/stylesheets/add_instructeur.scss b/app/assets/stylesheets/add_instructeur.scss index 8e446e281..ba7d7b3cf 100644 --- a/app/assets/stylesheets/add_instructeur.scss +++ b/app/assets/stylesheets/add_instructeur.scss @@ -5,34 +5,4 @@ .select-instructeurs { width: 100%; } - - .select2-container--default { - .select2-selection--multiple { - border: solid 1px $border-grey; - - .select2-selection__choice, // scss-lint:disable SelectorFormat - .select2-search--inline { - padding: $default-spacer; - } - } - - &.select2-container--focus { - .select2-selection--multiple { - border: 1px solid $blue; - box-shadow: 0px 0px 2px 1px $blue; - } - } - - .select2-results__option { // scss-lint:disable SelectorFormat - padding: $default-spacer; - } - - .custom-select2-option { - .icon { - margin-right: $default-spacer; - } - } - } } - - diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index af88e9f77..eaf0c0b98 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -4,6 +4,5 @@ // = require ./utils // = require ./fonts // = require leaflet -// = require select2 // = require_tree . // = stub ./print.scss diff --git a/app/assets/stylesheets/buttons.scss b/app/assets/stylesheets/buttons.scss index f7bfe4ea1..4e399e8ef 100644 --- a/app/assets/stylesheets/buttons.scss +++ b/app/assets/stylesheets/buttons.scss @@ -281,18 +281,6 @@ .dropdown-form { padding: 2 * $default-spacer; - .select2-container { - margin-bottom: 2 * $default-spacer; - } - - .select2-selection { - border: 1px solid $border-grey; - - &.select2-selection--multiple { - border: 1px solid $border-grey; - } - } - &.large { width: 340px; } @@ -310,10 +298,6 @@ } } -.select2-dropdown { - border: 1px solid $border-grey; -} - .link { color: $blue; } diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index d37c4c8df..c473aca8d 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -260,8 +260,7 @@ max-width: 180px; } - select, - .select2-selection { + select { // hack found here: https://stackoverflow.com/questions/1895476/how-to-style-a-select-dropdown-with-css-only-without-javascript -webkit-appearance: none; -moz-appearance: none; @@ -327,41 +326,6 @@ color: $white; } - .select2 { - min-width: 50%; - } - - .select2-container { - display: block; - margin-bottom: $default-fields-spacer; - - &.select2-container--focus { - .select2-selection { - border-color: $border-grey; - } - } - - .select2-selection--single { - min-height: 62px; - - // scss-lint:disable SelectorFormat - .select2-selection__arrow { - display: none; - } - // scss-lint:enable - } - - // scss-lint:disable SelectorFormat - .select2-selection__rendered { - padding: $default-padding; - } - - .select2-selection__choice { - background-color: #FFFFFF; - } - // scss-lint:enable - } - .editable-champ { &:not(.editable-champ-carte) .algolia-autocomplete { margin-bottom: 2 * $default-padding; diff --git a/app/assets/stylesheets/personnes_impliquees.scss b/app/assets/stylesheets/personnes_impliquees.scss index 829abbf38..381886823 100644 --- a/app/assets/stylesheets/personnes_impliquees.scss +++ b/app/assets/stylesheets/personnes_impliquees.scss @@ -9,11 +9,6 @@ margin-left: 16px; } - // scss-lint:disable SelectorFormat - .form .select2-container .select2-selection__rendered { - padding: 12px; - } - // scss-lint:enable [data-react-class="ComboMultipleDropdownList"] { margin-bottom: $default-fields-spacer; diff --git a/app/javascript/new_design/select2.js b/app/javascript/new_design/select2.js deleted file mode 100644 index e69e1c92e..000000000 --- a/app/javascript/new_design/select2.js +++ /dev/null @@ -1,80 +0,0 @@ -import $ from 'jquery'; -import 'select2'; - -const language = { - errorLoading: function () { - return 'Les résultats ne peuvent pas être chargés.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Supprimez ' + overChars + ' caractère' + (overChars > 1 ? 's' : ''); - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return ( - 'Saisissez au moins ' + - remainingChars + - ' caractère' + - (remainingChars > 1 ? 's' : '') - ); - }, - loadingMore: function () { - return 'Chargement de résultats supplémentaires…'; - }, - maximumSelected: function (args) { - return ( - 'Vous pouvez seulement sélectionner ' + - args.maximum + - ' élément' + - (args.maximum > 1 ? 's' : '') - ); - }, - noResults: function () { - return 'Aucun résultat trouvé'; - }, - searching: function () { - return 'Recherche en cours…'; - }, - removeAllItems: function () { - return 'Supprimer tous les éléments'; - } -}; - -const baseOptions = { - language, - width: '100%' -}; - -const templateOption = ({ text }) => - $( - `${text}` - ); - -addEventListener('ds:page:update', () => { - $('select.select2').select2(baseOptions); - - $('.columns-form select.select2-limited').select2({ - width: '300px', - placeholder: 'Sélectionnez des colonnes', - maximumSelectionLength: '5' - }); - - $('.recipients-form select.select2-limited').select2({ - language, - width: '300px', - placeholder: 'Sélectionnez des instructeurs', - maximumSelectionLength: '30' - }); - - $('select.select2-limited.select-instructeurs').select2({ - language, - dropdownParent: $('.instructeur-wrapper'), - placeholder: 'Saisir l’adresse email de l’instructeur', - tags: true, - tokenSeparators: [',', ' '], - templateResult: templateOption, - templateSelection: templateOption - }); -}); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 4cf6989f0..940713e7b 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -17,7 +17,6 @@ import '../new_design/dropdown'; import '../new_design/form-validation'; import '../new_design/procedure-context'; import '../new_design/procedure-form'; -import '../new_design/select2'; import '../new_design/spinner'; import '../new_design/support'; import '../new_design/dossiers/auto-save'; From 8b55f67964d9fca46ecaa144194e0506a5b5d02e Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Tue, 16 Feb 2021 14:15:34 +0100 Subject: [PATCH 8/9] accepte nouvelles valeurs pour ComboMultipleDropdownList --- .../components/ComboMultipleDropdownList.js | 41 +++++++++++++++++-- .../groupe_instructeurs/show.html.haml | 6 ++- spec/features/routing/full_scenario_spec.rb | 2 - 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/app/javascript/components/ComboMultipleDropdownList.js b/app/javascript/components/ComboMultipleDropdownList.js index 4665fe2a6..45dbc69e7 100644 --- a/app/javascript/components/ComboMultipleDropdownList.js +++ b/app/javascript/components/ComboMultipleDropdownList.js @@ -25,7 +25,8 @@ function ComboMultipleDropdownList({ options, hiddenFieldId, selected, - label + label, + acceptNewValues = false }) { if (label == undefined) { label = 'Choisir une option'; @@ -36,6 +37,7 @@ function ComboMultipleDropdownList({ const inputRef = useRef(); const [term, setTerm] = useState(''); const [selections, setSelections] = useState(selected); + const [newValues, setNewValues] = useState([]); const results = useMemo( () => (term @@ -56,6 +58,28 @@ function ComboMultipleDropdownList({ setTerm(event.target.value); }; + const onKeyDown = (event) => { + if (event.key === 'Enter') { + if (term && options.map((o) => o[0]).includes(term)) { + event.preventDefault(); + return onSelect(term); + } + if ( + acceptNewValues && + term && + matchSorter( + options.map((o) => o[0]), + term + ).length == 0 // ignore when was pressed for selecting popover option + ) { + event.preventDefault(); + setNewValues([...newValues, term]); + saveSelection([...selections, term]); + setTerm(''); + } + } + }; + const saveSelection = (selections) => { setSelections(selections); if (hiddenField) { @@ -72,7 +96,11 @@ function ComboMultipleDropdownList({ const onRemove = (value) => { saveSelection( - selections.filter((s) => s !== options.find((o) => o[0] == value)[1]) + selections.filter((s) => + newValues.includes(value) + ? s != value + : s !== options.find((o) => o[0] == value)[1] + ) ); inputRef.current.focus(); }; @@ -88,7 +116,10 @@ function ComboMultipleDropdownList({ {selections.map((selection) => ( o[1] == selection)[0]} + value={ + newValues.find((newValue) => newValue == selection) || + options.find((o) => o[1] == selection)[0] + } /> ))} @@ -96,6 +127,7 @@ function ComboMultipleDropdownList({ ref={inputRef} value={term} onChange={handleChange} + onKeyDown={onKeyDown} autocomplete={false} /> @@ -211,7 +243,8 @@ ComboMultipleDropdownList.propTypes = { hiddenFieldId: PropTypes.string, selected: PropTypes.arrayOf(PropTypes.string), arraySelected: PropTypes.arrayOf(PropTypes.array), - label: PropTypes.string + label: PropTypes.string, + acceptNewValues: PropTypes.bool }; export default ComboMultipleDropdownList; diff --git a/app/views/new_administrateur/groupe_instructeurs/show.html.haml b/app/views/new_administrateur/groupe_instructeurs/show.html.haml index 1ada176e6..19edc6b40 100644 --- a/app/views/new_administrateur/groupe_instructeurs/show.html.haml +++ b/app/views/new_administrateur/groupe_instructeurs/show.html.haml @@ -27,7 +27,11 @@ %p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche - hidden_field_id = SecureRandom.uuid = hidden_field_tag :emails, nil, data: { uuid: hidden_field_id } - = react_component("ComboMultipleDropdownList", options: @available_instructeur_emails, selected: [], disabled: [], hiddenFieldId: hidden_field_id, label: 'email instructeur') + = react_component("ComboMultipleDropdownList", + options: @available_instructeur_emails, selected: [], disabled: [], + hiddenFieldId: hidden_field_id, + label: 'email instructeur', + acceptNewValues: true) = f.submit 'Affecter', class: 'button primary send' diff --git a/spec/features/routing/full_scenario_spec.rb b/spec/features/routing/full_scenario_spec.rb index d8438d5b2..516da20dd 100644 --- a/spec/features/routing/full_scenario_spec.rb +++ b/spec/features/routing/full_scenario_spec.rb @@ -30,9 +30,7 @@ feature 'The routing', js: true do expect(page).to have_field('Nom du groupe', with: 'littéraire') # add victor to littéraire groupe - # find('input.select2-search__field').send_keys('victor@inst.com', :enter) find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter) - click_on 'Affecter' perform_enqueued_jobs { click_on 'Affecter' } expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche") From 881820888a5d5d105d38e98a35132115aed02c81 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 16 Feb 2021 16:37:01 +0100 Subject: [PATCH 9/9] Use stable cache key for useMemo --- app/javascript/components/ComboMultipleDropdownList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/components/ComboMultipleDropdownList.js b/app/javascript/components/ComboMultipleDropdownList.js index 45dbc69e7..2b4eaf5e4 100644 --- a/app/javascript/components/ComboMultipleDropdownList.js +++ b/app/javascript/components/ComboMultipleDropdownList.js @@ -47,7 +47,7 @@ function ComboMultipleDropdownList({ ) : options ).filter((o) => o[0] && !selections.includes(o[1])), - [term, selections] + [term, selections.join(',')] ); const hiddenField = useMemo( () => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),