Merge pull request #5903 from betagouv/4865-multi-select-accessible
rend accessible le multi-select
This commit is contained in:
commit
62a1716067
26 changed files with 459 additions and 213 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,5 @@
|
|||
// = require ./utils
|
||||
// = require ./fonts
|
||||
// = require leaflet
|
||||
// = require select2
|
||||
// = require_tree .
|
||||
// = stub ./print.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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -305,39 +304,26 @@
|
|||
border-color: $blue;
|
||||
}
|
||||
|
||||
.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 {
|
||||
[data-reach-combobox-token-list] {
|
||||
padding: $default-padding;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.select2-selection__choice {
|
||||
background-color: #FFFFFF;
|
||||
[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;
|
||||
}
|
||||
// scss-lint:enable
|
||||
|
||||
[data-reach-combobox-token]:focus {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.editable-champ {
|
||||
|
@ -481,11 +467,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;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,50 @@
|
|||
@import "constants";
|
||||
@import "colors";
|
||||
|
||||
.personnes-impliquees {
|
||||
padding-bottom: 50px;
|
||||
|
||||
ul {
|
||||
ul.tab-list {
|
||||
list-style-type: disc;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
// scss-lint:disable SelectorFormat
|
||||
.form .select2-container .select2-selection__rendered {
|
||||
padding: 12px;
|
||||
[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;
|
||||
}
|
||||
}
|
||||
// scss-lint:enable
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
250
app/javascript/components/ComboMultipleDropdownList.js
Normal file
250
app/javascript/components/ComboMultipleDropdownList.js
Normal file
|
@ -0,0 +1,250 @@
|
|||
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,
|
||||
acceptNewValues = false
|
||||
}) {
|
||||
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 [newValues, setNewValues] = useState([]);
|
||||
const results = useMemo(
|
||||
() =>
|
||||
(term
|
||||
? matchSorter(
|
||||
options.filter((o) => !o[0].startsWith('--')),
|
||||
term
|
||||
)
|
||||
: options
|
||||
).filter((o) => o[0] && !selections.includes(o[1])),
|
||||
[term, selections.join(',')]
|
||||
);
|
||||
const hiddenField = useMemo(
|
||||
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
|
||||
[hiddenFieldId]
|
||||
);
|
||||
|
||||
const handleChange = (event) => {
|
||||
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) {
|
||||
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) =>
|
||||
newValues.includes(value)
|
||||
? s != value
|
||||
: s !== options.find((o) => o[0] == value)[1]
|
||||
)
|
||||
);
|
||||
inputRef.current.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<Combobox openOnFocus={true} onSelect={onSelect} aria-label={label}>
|
||||
<ComboboxTokenLabel onRemove={onRemove}>
|
||||
<ul
|
||||
aria-live="polite"
|
||||
aria-atomic={true}
|
||||
data-reach-combobox-token-list
|
||||
>
|
||||
{selections.map((selection) => (
|
||||
<ComboboxToken
|
||||
key={selection}
|
||||
value={
|
||||
newValues.find((newValue) => newValue == selection) ||
|
||||
options.find((o) => o[1] == selection)[0]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<ComboboxInput
|
||||
ref={inputRef}
|
||||
value={term}
|
||||
onChange={handleChange}
|
||||
onKeyDown={onKeyDown}
|
||||
autocomplete={false}
|
||||
/>
|
||||
</ComboboxTokenLabel>
|
||||
{results && (
|
||||
<ComboboxPopover portal={false}>
|
||||
{results.length === 0 && (
|
||||
<p>
|
||||
Aucun résultat{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
setTerm('');
|
||||
}}
|
||||
>
|
||||
Effacer
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
<ComboboxList>
|
||||
{results.map((value, index) => {
|
||||
if (value[0].startsWith('--')) {
|
||||
return <ComboboxSeparator key={index} value={value[0]} />;
|
||||
}
|
||||
return <ComboboxOption key={index} value={value[0]} />;
|
||||
})}
|
||||
</ComboboxList>
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxTokenLabel({ onRemove, ...props }) {
|
||||
const selectionsRef = useRef([]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
selectionsRef.current = [];
|
||||
return () => (selectionsRef.current = []);
|
||||
});
|
||||
|
||||
const context = {
|
||||
onRemove,
|
||||
selectionsRef
|
||||
};
|
||||
|
||||
return (
|
||||
<Context.Provider value={context}>
|
||||
<div data-combobox-token-label {...props} />
|
||||
</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
ComboboxTokenLabel.propTypes = {
|
||||
onRemove: PropTypes.func
|
||||
};
|
||||
|
||||
function ComboboxSeparator({ value }) {
|
||||
return (
|
||||
<li aria-disabled="true" role="option" data-combobox-separator>
|
||||
{value.slice(2, -2)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
ComboboxSeparator.propTypes = {
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
function ComboboxToken({ value, ...props }) {
|
||||
const { selectionsRef, onRemove } = useContext(Context);
|
||||
useEffect(() => {
|
||||
selectionsRef.current.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<li
|
||||
data-reach-combobox-token
|
||||
tabIndex="0"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
onRemove(value);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
role="presentation"
|
||||
data-combobox-remove-token
|
||||
onClick={() => {
|
||||
onRemove(value);
|
||||
}}
|
||||
>
|
||||
x
|
||||
</span>
|
||||
{value}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
acceptNewValues: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ComboMultipleDropdownList;
|
5
app/javascript/loaders/ComboMultipleDropdownList.js
Normal file
5
app/javascript/loaders/ComboMultipleDropdownList.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Loadable from '../components/Loadable';
|
||||
|
||||
export default Loadable(() =>
|
||||
import('../components/ComboMultipleDropdownList')
|
||||
);
|
|
@ -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 }) =>
|
||||
$(
|
||||
`<span class="custom-select2-option"><span class="icon person"></span>${text}</span>`
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -25,10 +25,13 @@
|
|||
.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',
|
||||
acceptNewValues: true)
|
||||
|
||||
= f.submit 'Affecter', class: 'button primary send'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -30,14 +30,14 @@ 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)
|
||||
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 +50,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é")
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue