commit
9e184c915b
67 changed files with 744 additions and 629 deletions
|
@ -317,7 +317,7 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-token] {
|
[data-reach-combobox-token] button {
|
||||||
border: solid 1px $border-grey;
|
border: solid 1px $border-grey;
|
||||||
color: $black;
|
color: $black;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -328,14 +328,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-token]:focus {
|
[data-reach-combobox-token] button:focus {
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
color: $white;
|
color: $white;
|
||||||
|
|
||||||
[data-combobox-remove-token] {
|
|
||||||
background-color: $black;
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editable-champ {
|
.editable-champ {
|
||||||
|
@ -493,13 +488,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-react-class]:not([data-react-class="ComboMultipleDropdownList"]) {
|
[data-react-class]:not([data-react-class^="ComboMultiple"]) {
|
||||||
[data-reach-combobox-input]:not(.no-margin) {
|
[data-reach-combobox-input]:not(.no-margin) {
|
||||||
margin-bottom: $default-fields-spacer;
|
margin-bottom: $default-fields-spacer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-react-class="ComboMultipleDropdownList"] {
|
[data-react-class^="ComboMultiple"] {
|
||||||
margin-bottom: $default-fields-spacer;
|
margin-bottom: $default-fields-spacer;
|
||||||
|
|
||||||
[data-reach-combobox-input] {
|
[data-reach-combobox-input] {
|
||||||
|
@ -516,7 +511,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-combobox-token-label] {
|
[data-reach-combobox-token-label] {
|
||||||
border: 1px solid #CCCCCC;
|
border: 1px solid #CCCCCC;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -533,14 +528,14 @@
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-combobox-separator] {
|
[data-reach-combobox-separator] {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: $dark-grey;
|
color: $dark-grey;
|
||||||
background: $light-grey;
|
background: $light-grey;
|
||||||
padding: $default-spacer;
|
padding: $default-spacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-combobox-remove-token] {
|
[data-reach-combobox-token] button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
|
@ -552,7 +547,7 @@
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-input]:focus {
|
[data-reach-combobox-input] button:focus {
|
||||||
outline-color: $light-blue;
|
outline-color: $light-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-react-class="ComboMultipleDropdownList"] {
|
[data-react-class^="ComboMultiple"] {
|
||||||
margin-bottom: $default-fields-spacer;
|
margin-bottom: $default-fields-spacer;
|
||||||
|
|
||||||
[data-reach-combobox-token-list] {
|
[data-reach-combobox-token-list] {
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-token] {
|
[data-reach-combobox-token] button {
|
||||||
border: solid 1px $border-grey;
|
border: solid 1px $border-grey;
|
||||||
color: $black;
|
color: $black;
|
||||||
margin-top: 0.5 * $default-padding;
|
margin-top: 0.5 * $default-padding;
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-token]:focus {
|
[data-reach-combobox-token] button:focus {
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-react-class="ComboMultipleDropdownList"] {
|
[data-react-class^="ComboMultiple"] {
|
||||||
margin-bottom: $default-fields-spacer;
|
margin-bottom: $default-fields-spacer;
|
||||||
|
|
||||||
[data-reach-combobox-token-list] {
|
[data-reach-combobox-token-list] {
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-token] {
|
[data-reach-combobox-token] button {
|
||||||
border: solid 1px $border-grey;
|
border: solid 1px $border-grey;
|
||||||
color: $black;
|
color: $black;
|
||||||
margin: 0.25 * $default-padding;
|
margin: 0.25 * $default-padding;
|
||||||
|
@ -83,14 +83,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-token]:focus {
|
[data-reach-combobox-token] button:focus {
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
color: $white;
|
color: $white;
|
||||||
|
|
||||||
[data-combobox-remove-token] {
|
|
||||||
background-color: $black;
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,9 @@ class Champs::PieceJustificativeController < ApplicationController
|
||||||
def attach_piece_justificative
|
def attach_piece_justificative
|
||||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||||
@champ.piece_justificative_file.attach(params[:blob_signed_id])
|
@champ.piece_justificative_file.attach(params[:blob_signed_id])
|
||||||
@champ.save
|
save_succeed = @champ.save
|
||||||
|
@champ.dossier.update(last_champ_updated_at: Time.zone.now.utc) if save_succeed
|
||||||
|
save_succeed
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_piece_justificative_or_retry
|
def attach_piece_justificative_or_retry
|
||||||
|
|
|
@ -50,7 +50,7 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_champ(champ)
|
def render_champ(champ)
|
||||||
champ_selector = ".editable-champ[data-champ-id=\"#{champ.id}\"]"
|
champ_selector = "##{champ.input_group_id}"
|
||||||
form_html = render 'shared/dossiers/edit', dossier: champ.dossier, apercu: false
|
form_html = render 'shared/dossiers/edit', dossier: champ.dossier, apercu: false
|
||||||
champ_html = Nokogiri::HTML.fragment(form_html).at_css(champ_selector).to_s
|
champ_html = Nokogiri::HTML.fragment(form_html).at_css(champ_selector).to_s
|
||||||
# rubocop:disable Rails/OutputSafety
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
|
|
@ -20,12 +20,6 @@ module ChampHelper
|
||||||
simple_format(auto_linked_text, {}, sanitize: false)
|
simple_format(auto_linked_text, {}, sanitize: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def describedby_id(champ)
|
|
||||||
if champ.description.present?
|
|
||||||
"desc-#{champ.type_de_champ.id}-#{champ.row}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def auto_attach_url(form, object)
|
def auto_attach_url(form, object)
|
||||||
if object.is_a?(Champ) && object.persisted? && object.public?
|
if object.is_a?(Champ) && object.persisted? && object.public?
|
||||||
champs_piece_justificative_url(object.id)
|
champs_piece_justificative_url(object.id)
|
||||||
|
|
|
@ -6,42 +6,29 @@ import ComboSearch from './ComboSearch';
|
||||||
import { queryClient } from './shared/queryClient';
|
import { queryClient } from './shared/queryClient';
|
||||||
|
|
||||||
function ComboAdresseSearch({
|
function ComboAdresseSearch({
|
||||||
mandatory,
|
|
||||||
placeholder,
|
|
||||||
hiddenFieldId,
|
|
||||||
onChange,
|
|
||||||
transformResult = ({ properties: { label } }) => [label, label],
|
transformResult = ({ properties: { label } }) => [label, label],
|
||||||
allowInputValues = true,
|
allowInputValues = true,
|
||||||
className
|
...props
|
||||||
}) {
|
}) {
|
||||||
const transformResults = useCallback((_, { features }) => features);
|
const transformResults = useCallback((_, { features }) => features);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ComboSearch
|
<ComboSearch
|
||||||
className={className}
|
|
||||||
placeholder={placeholder}
|
|
||||||
required={mandatory}
|
|
||||||
hiddenFieldId={hiddenFieldId}
|
|
||||||
onChange={onChange}
|
|
||||||
allowInputValues={allowInputValues}
|
allowInputValues={allowInputValues}
|
||||||
scope="adresse"
|
scope="adresse"
|
||||||
minimumInputLength={2}
|
minimumInputLength={2}
|
||||||
transformResult={transformResult}
|
transformResult={transformResult}
|
||||||
transformResults={transformResults}
|
transformResults={transformResults}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboAdresseSearch.propTypes = {
|
ComboAdresseSearch.propTypes = {
|
||||||
className: PropTypes.string,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
mandatory: PropTypes.bool,
|
|
||||||
hiddenFieldId: PropTypes.string,
|
|
||||||
transformResult: PropTypes.func,
|
transformResult: PropTypes.func,
|
||||||
allowInputValues: PropTypes.bool,
|
allowInputValues: PropTypes.bool
|
||||||
onChange: PropTypes.func
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComboAdresseSearch;
|
export default ComboAdresseSearch;
|
||||||
|
|
|
@ -4,12 +4,10 @@ import { QueryClientProvider } from 'react-query';
|
||||||
import ComboSearch from './ComboSearch';
|
import ComboSearch from './ComboSearch';
|
||||||
import { queryClient } from './shared/queryClient';
|
import { queryClient } from './shared/queryClient';
|
||||||
|
|
||||||
function ComboAnnuaireEducationSearch(params) {
|
function ComboAnnuaireEducationSearch(props) {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ComboSearch
|
<ComboSearch
|
||||||
required={params.mandatory}
|
|
||||||
hiddenFieldId={params.hiddenFieldId}
|
|
||||||
scope="annuaire-education"
|
scope="annuaire-education"
|
||||||
minimumInputLength={3}
|
minimumInputLength={3}
|
||||||
transformResults={(_, { records }) => records}
|
transformResults={(_, { records }) => records}
|
||||||
|
@ -20,6 +18,7 @@ function ComboAnnuaireEducationSearch(params) {
|
||||||
nom_commune
|
nom_commune
|
||||||
}
|
}
|
||||||
}) => [id, `${nom_etablissement}, ${nom_commune} (${id})`]}
|
}) => [id, `${nom_etablissement}, ${nom_commune} (${id})`]}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import React, { useState, useMemo } from 'react';
|
import React from 'react';
|
||||||
import { QueryClientProvider } from 'react-query';
|
import { QueryClientProvider } from 'react-query';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import ComboSearch from './ComboSearch';
|
import ComboSearch from './ComboSearch';
|
||||||
import { queryClient } from './shared/queryClient';
|
import { queryClient } from './shared/queryClient';
|
||||||
import { ComboDepartementsSearch } from './ComboDepartementsSearch';
|
import { ComboDepartementsSearch } from './ComboDepartementsSearch';
|
||||||
|
import { useHiddenField, groupId } from './shared/hooks';
|
||||||
|
|
||||||
// Avoid hiding similar matches for precise queries (like "Sainte Marie")
|
// Avoid hiding similar matches for precise queries (like "Sainte Marie")
|
||||||
function searchResultsLimit(term) {
|
function searchResultsLimit(term) {
|
||||||
|
@ -48,34 +50,18 @@ const [placeholderDepartement, placeholderCommune] =
|
||||||
Math.floor(Math.random() * (placeholderDepartements.length - 1))
|
Math.floor(Math.random() * (placeholderDepartements.length - 1))
|
||||||
];
|
];
|
||||||
|
|
||||||
function ComboCommunesSearch(params) {
|
function ComboCommunesSearch({ id, ...props }) {
|
||||||
const hiddenDepartementFieldId = `${params.hiddenFieldId}:departement`;
|
const group = groupId(id);
|
||||||
const hiddenDepartementField = useMemo(
|
const [departementValue, setDepartementValue] = useHiddenField(
|
||||||
() =>
|
group,
|
||||||
document.querySelector(`input[data-attr="${hiddenDepartementFieldId}"]`),
|
'departement'
|
||||||
[params.hiddenFieldId]
|
|
||||||
);
|
);
|
||||||
const hiddenCodeDepartementField = useMemo(
|
const [codeDepartement, setCodeDepartement] = useHiddenField(
|
||||||
() =>
|
group,
|
||||||
document.querySelector(
|
'code_departement'
|
||||||
`input[data-attr="${params.hiddenFieldId}:code_departement"]`
|
|
||||||
),
|
|
||||||
[params.hiddenFieldId]
|
|
||||||
);
|
);
|
||||||
const inputId = useMemo(
|
const departementDescribedBy = `${id}_departement_notice`;
|
||||||
() =>
|
const communeDescribedBy = `${id}_commune_notice`;
|
||||||
document.querySelector(`input[data-uuid="${params.hiddenFieldId}"]`)?.id,
|
|
||||||
[params.hiddenFieldId]
|
|
||||||
);
|
|
||||||
const [departementCode, setDepartementCode] = useState(
|
|
||||||
() => hiddenCodeDepartementField?.value
|
|
||||||
);
|
|
||||||
const departementValue = useMemo(
|
|
||||||
() => hiddenDepartementField?.value,
|
|
||||||
[hiddenDepartementField]
|
|
||||||
);
|
|
||||||
const departementDescribedBy = `${inputId}_departement_notice`;
|
|
||||||
const communeDescribedBy = `${inputId}_commune_notice`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
@ -87,22 +73,19 @@ function ComboCommunesSearch(params) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ComboDepartementsSearch
|
<ComboDepartementsSearch
|
||||||
value={departementValue}
|
{...props}
|
||||||
inputId={!departementCode ? inputId : null}
|
id={!codeDepartement ? id : null}
|
||||||
aria-describedby={departementDescribedBy}
|
describedby={departementDescribedBy}
|
||||||
placeholder={placeholderDepartement}
|
placeholder={placeholderDepartement}
|
||||||
addForeignDepartement={false}
|
addForeignDepartement={false}
|
||||||
required={params.mandatory}
|
value={departementValue}
|
||||||
onChange={(_, result) => {
|
onChange={(_, result) => {
|
||||||
setDepartementCode(result?.code);
|
setDepartementValue(result?.nom);
|
||||||
if (hiddenDepartementField && hiddenCodeDepartementField) {
|
setCodeDepartement(result?.code);
|
||||||
hiddenDepartementField.setAttribute('value', result?.nom);
|
|
||||||
hiddenCodeDepartementField.setAttribute('value', result?.code);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{departementCode ? (
|
{codeDepartement ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="notice" id={communeDescribedBy}>
|
<div className="notice" id={communeDescribedBy}>
|
||||||
<p>
|
<p>
|
||||||
|
@ -111,14 +94,12 @@ function ComboCommunesSearch(params) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ComboSearch
|
<ComboSearch
|
||||||
autoFocus
|
{...props}
|
||||||
inputId={inputId}
|
id={id}
|
||||||
aria-describedby={communeDescribedBy}
|
describedby={communeDescribedBy}
|
||||||
placeholder={placeholderCommune}
|
placeholder={placeholderCommune}
|
||||||
required={params.mandatory}
|
|
||||||
hiddenFieldId={params.hiddenFieldId}
|
|
||||||
scope="communes"
|
scope="communes"
|
||||||
scopeExtra={departementCode}
|
scopeExtra={codeDepartement}
|
||||||
minimumInputLength={2}
|
minimumInputLength={2}
|
||||||
transformResult={({ code, nom, codesPostaux }) => [
|
transformResult={({ code, nom, codesPostaux }) => [
|
||||||
code,
|
code,
|
||||||
|
@ -132,4 +113,8 @@ function ComboCommunesSearch(params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboCommunesSearch.propTypes = {
|
||||||
|
id: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
export default ComboCommunesSearch;
|
export default ComboCommunesSearch;
|
||||||
|
|
|
@ -19,11 +19,11 @@ function expandResultsWithForeignDepartement(term, results) {
|
||||||
|
|
||||||
export function ComboDepartementsSearch({
|
export function ComboDepartementsSearch({
|
||||||
addForeignDepartement = true,
|
addForeignDepartement = true,
|
||||||
...params
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ComboSearch
|
<ComboSearch
|
||||||
{...params}
|
{...props}
|
||||||
scope="departements"
|
scope="departements"
|
||||||
minimumInputLength={1}
|
minimumInputLength={1}
|
||||||
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
|
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
|
||||||
|
@ -37,10 +37,7 @@ export function ComboDepartementsSearch({
|
||||||
function ComboDepartementsSearchDefault(params) {
|
function ComboDepartementsSearchDefault(params) {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ComboDepartementsSearch
|
<ComboDepartementsSearch {...params} />
|
||||||
required={params.mandatory}
|
|
||||||
hiddenFieldId={params.hiddenFieldId}
|
|
||||||
/>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
314
app/javascript/components/ComboMultiple.jsx
Normal file
314
app/javascript/components/ComboMultiple.jsx
Normal file
|
@ -0,0 +1,314 @@
|
||||||
|
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 { useId } from '@reach/auto-id';
|
||||||
|
import '@reach/combobox/styles.css';
|
||||||
|
import { matchSorter } from 'match-sorter';
|
||||||
|
import { XIcon } from '@heroicons/react/outline';
|
||||||
|
import isHotkey from 'is-hotkey';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
|
||||||
|
import { useDeferredSubmit, useHiddenField } from './shared/hooks';
|
||||||
|
|
||||||
|
const Context = createContext();
|
||||||
|
|
||||||
|
function ComboMultiple({
|
||||||
|
options,
|
||||||
|
id,
|
||||||
|
labelledby,
|
||||||
|
describedby,
|
||||||
|
label,
|
||||||
|
group,
|
||||||
|
name = 'value',
|
||||||
|
selected,
|
||||||
|
acceptNewValues = false
|
||||||
|
}) {
|
||||||
|
invariant(id || label, 'ComboMultiple: `id` or a `label` are required');
|
||||||
|
invariant(group, 'ComboMultiple: `group` is required');
|
||||||
|
|
||||||
|
if (!Array.isArray(options[0])) {
|
||||||
|
options = options.filter((o) => o).map((o) => [o, o]);
|
||||||
|
}
|
||||||
|
const inputRef = useRef();
|
||||||
|
const [term, setTerm] = useState('');
|
||||||
|
const [selections, setSelections] = useState(selected);
|
||||||
|
const [newValues, setNewValues] = useState([]);
|
||||||
|
const inputId = useId(id);
|
||||||
|
const removedLabelledby = `${inputId}-remove`;
|
||||||
|
const selectedLabelledby = `${inputId}-selected`;
|
||||||
|
|
||||||
|
const optionValueByLabel = (label) => {
|
||||||
|
const maybeOption = newValues.includes(label)
|
||||||
|
? [label, label]
|
||||||
|
: options.find(([optionLabel]) => optionLabel == label);
|
||||||
|
return maybeOption ? maybeOption[1] : undefined;
|
||||||
|
};
|
||||||
|
const optionLabelByValue = (value) => {
|
||||||
|
const maybeOption = newValues.includes(value)
|
||||||
|
? [value, value]
|
||||||
|
: options.find(([, optionValue]) => optionValue == value);
|
||||||
|
return maybeOption ? maybeOption[0] : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extraOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
|
||||||
|
? [[term, term]]
|
||||||
|
: [],
|
||||||
|
[acceptNewValues, term, newValues.join(',')]
|
||||||
|
);
|
||||||
|
const results = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
...extraOptions,
|
||||||
|
...(term
|
||||||
|
? matchSorter(
|
||||||
|
options.filter(([label]) => !label.startsWith('--')),
|
||||||
|
term
|
||||||
|
)
|
||||||
|
: options)
|
||||||
|
].filter(([, value]) => !selections.includes(value)),
|
||||||
|
[term, selections.join(','), newValues.join(',')]
|
||||||
|
);
|
||||||
|
const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name);
|
||||||
|
const awaitFormSubmit = useDeferredSubmit(hiddenField);
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
setTerm(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSelection = (fn) => {
|
||||||
|
setSelections((selections) => {
|
||||||
|
selections = fn(selections);
|
||||||
|
setHiddenFieldValue(JSON.stringify(selections));
|
||||||
|
return selections;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (value) => {
|
||||||
|
const maybeValue = [...extraOptions, ...options].find(
|
||||||
|
([val]) => val == value
|
||||||
|
);
|
||||||
|
const selectedValue = maybeValue && maybeValue[1];
|
||||||
|
if (selectedValue) {
|
||||||
|
if (
|
||||||
|
acceptNewValues &&
|
||||||
|
extraOptions[0] &&
|
||||||
|
extraOptions[0][0] == selectedValue
|
||||||
|
) {
|
||||||
|
setNewValues((newValues) => [...newValues, selectedValue]);
|
||||||
|
}
|
||||||
|
saveSelection((selections) => [...selections, selectedValue]);
|
||||||
|
}
|
||||||
|
setTerm('');
|
||||||
|
awaitFormSubmit.done();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemove = (label) => {
|
||||||
|
const optionValue = optionValueByLabel(label);
|
||||||
|
if (optionValue) {
|
||||||
|
saveSelection((selections) =>
|
||||||
|
selections.filter((value) => value != optionValue)
|
||||||
|
);
|
||||||
|
setNewValues((newValues) =>
|
||||||
|
newValues.filter((value) => value != optionValue)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
inputRef.current.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
if (
|
||||||
|
isHotkey('enter', event) ||
|
||||||
|
isHotkey(' ', event) ||
|
||||||
|
isHotkey(',', event) ||
|
||||||
|
isHotkey(';', event)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
term &&
|
||||||
|
[...extraOptions, ...options].map(([label]) => label).includes(term)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSelect(term);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
if (
|
||||||
|
term &&
|
||||||
|
[...extraOptions, ...options].map(([label]) => label).includes(term)
|
||||||
|
) {
|
||||||
|
awaitFormSubmit(() => {
|
||||||
|
onSelect(term);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox openOnFocus={true} onSelect={onSelect}>
|
||||||
|
<ComboboxTokenLabel onRemove={onRemove}>
|
||||||
|
<span id={removedLabelledby} className="hidden">
|
||||||
|
désélectionner
|
||||||
|
</span>
|
||||||
|
<ul
|
||||||
|
id={selectedLabelledby}
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic={true}
|
||||||
|
data-reach-combobox-token-list
|
||||||
|
>
|
||||||
|
{selections.map((selection) => (
|
||||||
|
<ComboboxToken
|
||||||
|
key={selection}
|
||||||
|
describedby={removedLabelledby}
|
||||||
|
value={optionLabelByValue(selection)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<ComboboxInput
|
||||||
|
ref={inputRef}
|
||||||
|
value={term}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={onBlur}
|
||||||
|
autocomplete={false}
|
||||||
|
id={inputId}
|
||||||
|
aria-label={label}
|
||||||
|
aria-labelledby={[labelledby, selectedLabelledby]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
aria-describedby={describedby}
|
||||||
|
/>
|
||||||
|
</ComboboxTokenLabel>
|
||||||
|
{results && (results.length > 0 || !acceptNewValues) && (
|
||||||
|
<ComboboxPopover className="shadow-popup">
|
||||||
|
{results.length === 0 && (
|
||||||
|
<p>
|
||||||
|
Aucun résultat{' '}
|
||||||
|
<button onClick={() => setTerm('')}>Effacer</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<ComboboxList>
|
||||||
|
{results.map(([label, value], index) => {
|
||||||
|
if (label.startsWith('--')) {
|
||||||
|
return <ComboboxSeparator key={index} value={label} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ComboboxOption
|
||||||
|
key={index}
|
||||||
|
value={label}
|
||||||
|
data-option-value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</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-reach-combobox-token-label {...props} />
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboboxTokenLabel.propTypes = {
|
||||||
|
onRemove: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
function ComboboxSeparator({ value }) {
|
||||||
|
return (
|
||||||
|
<li aria-disabled="true" role="option" data-reach-combobox-separator>
|
||||||
|
{value.slice(2, -2)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboboxSeparator.propTypes = {
|
||||||
|
value: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function ComboboxToken({ value, describedby, ...props }) {
|
||||||
|
const { selectionsRef, onRemove } = useContext(Context);
|
||||||
|
useEffect(() => {
|
||||||
|
selectionsRef.current.push(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li data-reach-combobox-token {...props}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onRemove(value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Backspace') {
|
||||||
|
onRemove(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-describedby={describedby}
|
||||||
|
>
|
||||||
|
<XIcon className="icon-size mr-1" aria-hidden="true" />
|
||||||
|
{value}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboboxToken.propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
describedby: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
ComboMultiple.propTypes = {
|
||||||
|
options: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.string),
|
||||||
|
PropTypes.arrayOf(
|
||||||
|
PropTypes.arrayOf(
|
||||||
|
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
selected: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
arraySelected: PropTypes.arrayOf(PropTypes.array),
|
||||||
|
acceptNewValues: PropTypes.bool,
|
||||||
|
mandatory: PropTypes.bool,
|
||||||
|
id: PropTypes.string,
|
||||||
|
group: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
labelledby: PropTypes.string,
|
||||||
|
describedby: PropTypes.string,
|
||||||
|
label: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComboMultiple;
|
|
@ -1,302 +1,15 @@
|
||||||
import React, {
|
import React from 'react';
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
useContext,
|
|
||||||
createContext,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
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';
|
|
||||||
import { XIcon } from '@heroicons/react/outline';
|
|
||||||
import isHotkey from 'is-hotkey';
|
|
||||||
|
|
||||||
import { useDeferredSubmit } from './shared/hooks';
|
import { groupId } from './shared/hooks';
|
||||||
|
import ComboMultiple from './ComboMultiple';
|
||||||
|
|
||||||
const Context = createContext();
|
function ComboMultipleDropdownList({ id, ...props }) {
|
||||||
|
return <ComboMultiple group={groupId(id)} id={id} {...props} />;
|
||||||
function ComboMultipleDropdownList({
|
|
||||||
options,
|
|
||||||
hiddenFieldId,
|
|
||||||
selected,
|
|
||||||
label,
|
|
||||||
acceptNewValues = false
|
|
||||||
}) {
|
|
||||||
if (label == undefined) {
|
|
||||||
label = 'Choisir une option';
|
|
||||||
}
|
}
|
||||||
if (!Array.isArray(options[0])) {
|
|
||||||
options = options.filter((o) => o).map((o) => [o, o]);
|
|
||||||
}
|
|
||||||
const inputRef = useRef();
|
|
||||||
const [term, setTerm] = useState('');
|
|
||||||
const [selections, setSelections] = useState(selected);
|
|
||||||
const [newValues, setNewValues] = useState([]);
|
|
||||||
|
|
||||||
const optionValueByLabel = (label) => {
|
|
||||||
const maybeOption = newValues.includes(label)
|
|
||||||
? [label, label]
|
|
||||||
: options.find(([optionLabel]) => optionLabel == label);
|
|
||||||
return maybeOption ? maybeOption[1] : undefined;
|
|
||||||
};
|
|
||||||
const optionLabelByValue = (value) => {
|
|
||||||
const maybeOption = newValues.includes(value)
|
|
||||||
? [value, value]
|
|
||||||
: options.find(([, optionValue]) => optionValue == value);
|
|
||||||
return maybeOption ? maybeOption[0] : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extraOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
|
|
||||||
? [[term, term]]
|
|
||||||
: [],
|
|
||||||
[acceptNewValues, term, newValues.join(',')]
|
|
||||||
);
|
|
||||||
const results = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
...extraOptions,
|
|
||||||
...(term
|
|
||||||
? matchSorter(
|
|
||||||
options.filter(([label]) => !label.startsWith('--')),
|
|
||||||
term
|
|
||||||
)
|
|
||||||
: options)
|
|
||||||
].filter(([, value]) => !selections.includes(value)),
|
|
||||||
[term, selections.join(','), newValues.join(',')]
|
|
||||||
);
|
|
||||||
const hiddenField = useMemo(
|
|
||||||
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
|
|
||||||
[hiddenFieldId]
|
|
||||||
);
|
|
||||||
const awaitFormSubmit = useDeferredSubmit(hiddenField);
|
|
||||||
|
|
||||||
const handleChange = (event) => {
|
|
||||||
setTerm(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSelection = (fn) => {
|
|
||||||
setSelections((selections) => {
|
|
||||||
selections = fn(selections);
|
|
||||||
if (hiddenField) {
|
|
||||||
hiddenField.setAttribute('value', JSON.stringify(selections));
|
|
||||||
fire(hiddenField, 'autosave:trigger');
|
|
||||||
}
|
|
||||||
return selections;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelect = (value) => {
|
|
||||||
const maybeValue = [...extraOptions, ...options].find(
|
|
||||||
([val]) => val == value
|
|
||||||
);
|
|
||||||
const selectedValue = maybeValue && maybeValue[1];
|
|
||||||
if (selectedValue) {
|
|
||||||
if (
|
|
||||||
acceptNewValues &&
|
|
||||||
extraOptions[0] &&
|
|
||||||
extraOptions[0][0] == selectedValue
|
|
||||||
) {
|
|
||||||
setNewValues((newValues) => [...newValues, selectedValue]);
|
|
||||||
}
|
|
||||||
saveSelection((selections) => [...selections, selectedValue]);
|
|
||||||
}
|
|
||||||
setTerm('');
|
|
||||||
awaitFormSubmit.done();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemove = (label) => {
|
|
||||||
const optionValue = optionValueByLabel(label);
|
|
||||||
if (optionValue) {
|
|
||||||
saveSelection((selections) =>
|
|
||||||
selections.filter((value) => value != optionValue)
|
|
||||||
);
|
|
||||||
setNewValues((newValues) =>
|
|
||||||
newValues.filter((value) => value != optionValue)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
inputRef.current.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event) => {
|
|
||||||
if (
|
|
||||||
isHotkey('enter', event) ||
|
|
||||||
isHotkey(' ', event) ||
|
|
||||||
isHotkey(',', event) ||
|
|
||||||
isHotkey(';', event)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
term &&
|
|
||||||
[...extraOptions, ...options].map(([label]) => label).includes(term)
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
onSelect(term);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBlur = () => {
|
|
||||||
if (
|
|
||||||
term &&
|
|
||||||
[...extraOptions, ...options].map(([label]) => label).includes(term)
|
|
||||||
) {
|
|
||||||
awaitFormSubmit(() => {
|
|
||||||
onSelect(term);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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={optionLabelByValue(selection)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<ComboboxInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={term}
|
|
||||||
onChange={handleChange}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onBlur={onBlur}
|
|
||||||
autocomplete={false}
|
|
||||||
/>
|
|
||||||
</ComboboxTokenLabel>
|
|
||||||
{results && (results.length > 0 || !acceptNewValues) && (
|
|
||||||
<ComboboxPopover className="shadow-popup">
|
|
||||||
{results.length === 0 && (
|
|
||||||
<p>
|
|
||||||
Aucun résultat{' '}
|
|
||||||
<button onClick={() => setTerm('')}>Effacer</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<ComboboxList>
|
|
||||||
{results.map(([label, value], index) => {
|
|
||||||
if (label.startsWith('--')) {
|
|
||||||
return <ComboboxSeparator key={index} value={label} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ComboboxOption
|
|
||||||
key={index}
|
|
||||||
value={label}
|
|
||||||
data-option-value={value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</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}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
data-combobox-remove-token
|
|
||||||
onClick={() => {
|
|
||||||
onRemove(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XIcon className="icon-size" />
|
|
||||||
<span className="screen-reader-text">Désélectionner</span>
|
|
||||||
</button>
|
|
||||||
{value}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ComboboxToken.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
label: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
ComboMultipleDropdownList.propTypes = {
|
ComboMultipleDropdownList.propTypes = {
|
||||||
options: PropTypes.oneOfType([
|
id: PropTypes.string
|
||||||
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;
|
export default ComboMultipleDropdownList;
|
||||||
|
|
|
@ -4,15 +4,14 @@ import { QueryClientProvider } from 'react-query';
|
||||||
import ComboSearch from './ComboSearch';
|
import ComboSearch from './ComboSearch';
|
||||||
import { queryClient } from './shared/queryClient';
|
import { queryClient } from './shared/queryClient';
|
||||||
|
|
||||||
function ComboPaysSearch(params) {
|
function ComboPaysSearch(props) {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ComboSearch
|
<ComboSearch
|
||||||
required={params.mandatory}
|
|
||||||
hiddenFieldId={params.hiddenFieldId}
|
|
||||||
scope="pays"
|
scope="pays"
|
||||||
minimumInputLength={0}
|
minimumInputLength={0}
|
||||||
transformResult={({ code, value, label }) => [code, value, label]}
|
transformResult={({ code, value, label }) => [code, value, label]}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,15 +4,14 @@ import { QueryClientProvider } from 'react-query';
|
||||||
import ComboSearch from './ComboSearch';
|
import ComboSearch from './ComboSearch';
|
||||||
import { queryClient } from './shared/queryClient';
|
import { queryClient } from './shared/queryClient';
|
||||||
|
|
||||||
function ComboRegionsSearch(params) {
|
function ComboRegionsSearch(props) {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ComboSearch
|
<ComboSearch
|
||||||
required={params.mandatory}
|
|
||||||
hiddenFieldId={params.hiddenFieldId}
|
|
||||||
scope="regions"
|
scope="regions"
|
||||||
minimumInputLength={0}
|
minimumInputLength={0}
|
||||||
transformResult={({ code, nom }) => [code, nom]}
|
transformResult={({ code, nom }) => [code, nom]}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import React, {
|
import React, { useState, useCallback, useRef } from 'react';
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
useEffect
|
|
||||||
} from 'react';
|
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -16,42 +10,33 @@ import {
|
||||||
ComboboxOption
|
ComboboxOption
|
||||||
} from '@reach/combobox';
|
} from '@reach/combobox';
|
||||||
import '@reach/combobox/styles.css';
|
import '@reach/combobox/styles.css';
|
||||||
import { fire } from '@utils';
|
import invariant from 'tiny-invariant';
|
||||||
|
|
||||||
import { useDeferredSubmit } from './shared/hooks';
|
import { useDeferredSubmit, useHiddenField, groupId } from './shared/hooks';
|
||||||
|
|
||||||
function defaultTransformResults(_, results) {
|
function defaultTransformResults(_, results) {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComboSearch({
|
function ComboSearch({
|
||||||
hiddenFieldId,
|
|
||||||
onChange,
|
onChange,
|
||||||
|
value: controlledValue,
|
||||||
scope,
|
scope,
|
||||||
inputId,
|
|
||||||
scopeExtra,
|
scopeExtra,
|
||||||
minimumInputLength,
|
minimumInputLength,
|
||||||
transformResult,
|
transformResult,
|
||||||
allowInputValues = false,
|
allowInputValues = false,
|
||||||
transformResults = defaultTransformResults,
|
transformResults = defaultTransformResults,
|
||||||
|
id,
|
||||||
|
describedby,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const hiddenValueField = useMemo(
|
invariant(id || onChange, 'ComboSearch: `id` or `onChange` are required');
|
||||||
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
|
|
||||||
[hiddenFieldId]
|
const group = !onChange ? groupId(id) : null;
|
||||||
);
|
const [externalValue, setExternalValue, hiddenField] = useHiddenField(group);
|
||||||
const comboInputId = useMemo(
|
const [, setExternalId] = useHiddenField(group, 'external_id');
|
||||||
() => hiddenValueField?.id || inputId,
|
const initialValue = externalValue ? externalValue : controlledValue;
|
||||||
[inputId, hiddenValueField]
|
|
||||||
);
|
|
||||||
const hiddenIdField = useMemo(
|
|
||||||
() =>
|
|
||||||
document.querySelector(
|
|
||||||
`input[data-uuid="${hiddenFieldId}"] + input[data-reference]`
|
|
||||||
),
|
|
||||||
[hiddenFieldId]
|
|
||||||
);
|
|
||||||
const initialValue = hiddenValueField ? hiddenValueField.value : props.value;
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
|
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
|
@ -60,41 +45,26 @@ function ComboSearch({
|
||||||
const [, value, label] = transformResult(result);
|
const [, value, label] = transformResult(result);
|
||||||
return label ?? value;
|
return label ?? value;
|
||||||
};
|
};
|
||||||
const setExternalValue = useCallback(
|
|
||||||
(value) => {
|
|
||||||
if (hiddenValueField) {
|
|
||||||
hiddenValueField.setAttribute('value', value);
|
|
||||||
fire(hiddenValueField, 'autosave:trigger');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[hiddenValueField]
|
|
||||||
);
|
|
||||||
const setExternalId = useCallback(
|
|
||||||
(key) => {
|
|
||||||
if (hiddenIdField) {
|
|
||||||
hiddenIdField.setAttribute('value', key);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[hiddenIdField]
|
|
||||||
);
|
|
||||||
const setExternalValueAndId = useCallback((label) => {
|
const setExternalValueAndId = useCallback((label) => {
|
||||||
const { key, value, result } = resultsMap.current[label];
|
const { key, value, result } = resultsMap.current[label];
|
||||||
setExternalId(key);
|
|
||||||
setExternalValue(value);
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(value, result);
|
onChange(value, result);
|
||||||
|
} else {
|
||||||
|
setExternalId(key);
|
||||||
|
setExternalValue(value);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const awaitFormSubmit = useDeferredSubmit(hiddenValueField);
|
const awaitFormSubmit = useDeferredSubmit(hiddenField);
|
||||||
|
|
||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
({ target: { value } }) => {
|
({ target: { value } }) => {
|
||||||
setValue(value);
|
setValue(value);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
setExternalId('');
|
|
||||||
setExternalValue('');
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(null);
|
onChange(null);
|
||||||
|
} else {
|
||||||
|
setExternalId('');
|
||||||
|
setExternalValue('');
|
||||||
}
|
}
|
||||||
} else if (value.length >= minimumInputLength) {
|
} else if (value.length >= minimumInputLength) {
|
||||||
setSearchTerm(value.trim());
|
setSearchTerm(value.trim());
|
||||||
|
@ -133,20 +103,16 @@ function ComboSearch({
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document
|
|
||||||
.querySelector(`#${comboInputId}[type="hidden"]`)
|
|
||||||
?.removeAttribute('id');
|
|
||||||
}, [comboInputId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox onSelect={handleOnSelect}>
|
<Combobox onSelect={handleOnSelect}>
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
{...props}
|
{...props}
|
||||||
id={comboInputId}
|
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
value={value}
|
value={value}
|
||||||
|
autocomplete={false}
|
||||||
|
id={id}
|
||||||
|
aria-describedby={describedby}
|
||||||
/>
|
/>
|
||||||
{isSuccess && (
|
{isSuccess && (
|
||||||
<ComboboxPopover className="shadow-popup">
|
<ComboboxPopover className="shadow-popup">
|
||||||
|
@ -178,15 +144,16 @@ function ComboSearch({
|
||||||
|
|
||||||
ComboSearch.propTypes = {
|
ComboSearch.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
hiddenFieldId: PropTypes.string,
|
|
||||||
scope: PropTypes.string,
|
scope: PropTypes.string,
|
||||||
minimumInputLength: PropTypes.number,
|
minimumInputLength: PropTypes.number,
|
||||||
transformResult: PropTypes.func,
|
transformResult: PropTypes.func,
|
||||||
transformResults: PropTypes.func,
|
transformResults: PropTypes.func,
|
||||||
allowInputValues: PropTypes.bool,
|
allowInputValues: PropTypes.bool,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
inputId: PropTypes.string,
|
scopeExtra: PropTypes.string,
|
||||||
scopeExtra: PropTypes.string
|
mandatory: PropTypes.bool,
|
||||||
|
id: PropTypes.string,
|
||||||
|
describedby: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComboSearch;
|
export default ComboSearch;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useRef, useCallback } from 'react';
|
import { useRef, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { fire } from '@utils';
|
||||||
|
|
||||||
export function useDeferredSubmit(input) {
|
export function useDeferredSubmit(input) {
|
||||||
const calledRef = useRef(false);
|
const calledRef = useRef(false);
|
||||||
|
@ -31,3 +32,35 @@ export function useDeferredSubmit(input) {
|
||||||
};
|
};
|
||||||
return awaitFormSubmit;
|
return awaitFormSubmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupId(id) {
|
||||||
|
return `#champ-${id.replace(/-input$/, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHiddenField(group, name = 'value') {
|
||||||
|
const hiddenField = useMemo(
|
||||||
|
() => selectInputInGroup(group, name),
|
||||||
|
[group, name]
|
||||||
|
);
|
||||||
|
const [value, setValue] = useState(() => hiddenField?.value);
|
||||||
|
|
||||||
|
return [
|
||||||
|
value,
|
||||||
|
(value) => {
|
||||||
|
if (hiddenField) {
|
||||||
|
hiddenField.setAttribute('value', value);
|
||||||
|
setValue(value);
|
||||||
|
fire(hiddenField, 'autosave:trigger');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hiddenField
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectInputInGroup(group, name) {
|
||||||
|
if (group) {
|
||||||
|
return document.querySelector(
|
||||||
|
`${group} input[type="hidden"][name$="[${name}]"], ${group} input[type="hidden"][name="${name}"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ registerReactComponents({
|
||||||
ComboMultipleDropdownList: Loadable(() =>
|
ComboMultipleDropdownList: Loadable(() =>
|
||||||
import('../components/ComboMultipleDropdownList')
|
import('../components/ComboMultipleDropdownList')
|
||||||
),
|
),
|
||||||
|
ComboMultiple: Loadable(() => import('../components/ComboMultiple')),
|
||||||
ComboPaysSearch: Loadable(() => import('../components/ComboPaysSearch')),
|
ComboPaysSearch: Loadable(() => import('../components/ComboPaysSearch')),
|
||||||
ComboRegionsSearch: Loadable(() =>
|
ComboRegionsSearch: Loadable(() =>
|
||||||
import('../components/ComboRegionsSearch')
|
import('../components/ComboRegionsSearch')
|
||||||
|
|
|
@ -139,6 +139,22 @@ class Champ < ApplicationRecord
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def input_group_id
|
||||||
|
"champ-#{html_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def input_id
|
||||||
|
"#{html_id}-input"
|
||||||
|
end
|
||||||
|
|
||||||
|
def labelledby_id
|
||||||
|
"#{html_id}-label"
|
||||||
|
end
|
||||||
|
|
||||||
|
def describedby_id
|
||||||
|
"#{html_id}-description" if description.present?
|
||||||
|
end
|
||||||
|
|
||||||
def stable_id
|
def stable_id
|
||||||
type_de_champ.stable_id
|
type_de_champ.stable_id
|
||||||
end
|
end
|
||||||
|
@ -159,6 +175,10 @@ class Champ < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def html_id
|
||||||
|
"#{stable_id}-#{id}"
|
||||||
|
end
|
||||||
|
|
||||||
def needs_dossier_id?
|
def needs_dossier_id?
|
||||||
!dossier_id && parent_id
|
!dossier_id && parent_id
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,10 +19,6 @@
|
||||||
- else
|
- else
|
||||||
%span.text-inactive désactivée
|
%span.text-inactive désactivée
|
||||||
|
|
||||||
- if @attestation_template.activated && @procedure.locked?
|
|
||||||
.card.warning
|
|
||||||
%p L’attestation ne peut plus être désactivée car la démarche est publiée.
|
|
||||||
|
|
||||||
%p.notice
|
%p.notice
|
||||||
L’attestation, si elle est activée, est émise au moment où un dossier est accepté.
|
L’attestation, si elle est activée, est émise au moment où un dossier est accepté.
|
||||||
%br
|
%br
|
||||||
|
@ -33,8 +29,6 @@
|
||||||
|
|
||||||
.procedure-form__actions.sticky--bottom
|
.procedure-form__actions.sticky--bottom
|
||||||
.actions-left
|
.actions-left
|
||||||
-# Admins cannot disactivate the Attestation if it is activated and the procedure is published
|
|
||||||
- if !(@attestation_template.activated && @procedure.locked?)
|
|
||||||
%label.toggle-switch
|
%label.toggle-switch
|
||||||
= f.check_box :activated, class: 'toggle-switch-checkbox'
|
= f.check_box :activated, class: 'toggle-switch-checkbox'
|
||||||
%span.toggle-switch-control.round
|
%span.toggle-switch-control.round
|
||||||
|
|
|
@ -44,14 +44,15 @@
|
||||||
|
|
||||||
.instructeur-wrapper
|
.instructeur-wrapper
|
||||||
%p.notice Pendant l'instruction d’un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
|
%p.notice Pendant l'instruction d’un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
|
||||||
%p.notice Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
|
%p#experts-emails.notice Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= hidden_field_tag :emails, nil
|
||||||
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
|
= react_component("ComboMultiple",
|
||||||
= react_component("ComboMultipleDropdownList",
|
|
||||||
options: [],
|
options: [],
|
||||||
selected: [], disabled: [],
|
selected: [], disabled: [],
|
||||||
hiddenFieldId: hidden_field_id,
|
group: '.instructeur-wrapper',
|
||||||
label: 'email expert',
|
name: 'emails',
|
||||||
|
label: 'Emails',
|
||||||
|
describedby: 'experts-emails',
|
||||||
acceptNewValues: true)
|
acceptNewValues: true)
|
||||||
|
|
||||||
= f.submit 'Affecter à la démarche', class: 'button primary send'
|
= f.submit 'Affecter à la démarche', class: 'button primary send'
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
.instructeur-wrapper
|
.instructeur-wrapper
|
||||||
- if !procedure.routee?
|
- if !procedure.routee?
|
||||||
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
|
%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
|
||||||
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
|
= react_component("ComboMultiple",
|
||||||
= react_component("ComboMultipleDropdownList",
|
|
||||||
options: available_instructeur_emails, selected: [], disabled: [],
|
options: available_instructeur_emails, selected: [], disabled: [],
|
||||||
hiddenFieldId: hidden_field_id,
|
group: '.instructeur-wrapper',
|
||||||
label: 'email instructeur',
|
name: 'emails',
|
||||||
|
label: 'Emails',
|
||||||
acceptNewValues: true)
|
acceptNewValues: true)
|
||||||
|
|
||||||
= f.submit 'Affecter', class: 'button primary send'
|
= f.submit 'Affecter', class: 'button primary send'
|
||||||
|
|
|
@ -4,13 +4,12 @@
|
||||||
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
|
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
|
||||||
|
|
||||||
= form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "expert-ask-avis-for-dossier-#{@avis.dossier.id}" } } do |f|
|
= form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "expert-ask-avis-for-dossier-#{@avis.dossier.id}" } } do |f|
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= hidden_field_tag 'avis[emails]', nil
|
||||||
= hidden_field_tag 'avis[emails]', nil, data: { uuid: hidden_field_id }
|
= react_component("ComboMultiple",
|
||||||
= react_component("ComboMultipleDropdownList",
|
options: [], selected: [], disabled: [],
|
||||||
options: [],
|
group: '.ask-avis',
|
||||||
selected: [], disabled: [],
|
name: 'emails',
|
||||||
hiddenFieldId: hidden_field_id,
|
label: 'Emails',
|
||||||
label: 'avis_emails',
|
|
||||||
acceptNewValues: true)
|
acceptNewValues: true)
|
||||||
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: 'persisted-input'
|
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: 'persisted-input'
|
||||||
%p.tab-title Ajouter une pièce jointe
|
%p.tab-title Ajouter une pièce jointe
|
||||||
|
|
|
@ -8,8 +8,12 @@
|
||||||
Le destinataire suivra automatiquement le dossier
|
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|
|
= 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
|
.flex.justify-start.align-start
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= hidden_field_tag :recipients, nil
|
||||||
= hidden_field_tag :recipients, nil, data: { uuid: hidden_field_id }
|
= react_component("ComboMultiple",
|
||||||
= react_component("ComboMultipleDropdownList", options: potential_recipients.map{|r| [r.email, r.id]}, selected: [], disabled: [], hiddenFieldId: hidden_field_id, label: "email instructeur")
|
options: potential_recipients.map{|r| [r.email, r.id]},
|
||||||
|
selected: [], disabled: [],
|
||||||
|
group: '.recipients-form',
|
||||||
|
name: 'recipients',
|
||||||
|
label: 'Emails')
|
||||||
|
|
||||||
= f.submit "Envoyer", class: "button large send gap-left"
|
= f.submit "Envoyer", class: "button large send gap-left"
|
||||||
|
|
|
@ -96,9 +96,14 @@
|
||||||
Personnaliser
|
Personnaliser
|
||||||
#custom-menu.dropdown-content.fade-in-down
|
#custom-menu.dropdown-content.fade-in-down
|
||||||
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
|
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= hidden_field_tag :values, nil
|
||||||
= hidden_field_tag :values, nil, data: { uuid: hidden_field_id }
|
= react_component("ComboMultiple",
|
||||||
= react_component("ComboMultipleDropdownList", options: @displayed_fields_options, selected: @displayed_fields_selected, disabled: [], hiddenFieldId: hidden_field_id, label: 'colonne')
|
options: @displayed_fields_options,
|
||||||
|
selected: @displayed_fields_selected,
|
||||||
|
disabled: [],
|
||||||
|
label: 'Colonne à afficher',
|
||||||
|
group: '.columns-form',
|
||||||
|
name: 'values')
|
||||||
|
|
||||||
= submit_tag "Enregistrer", class: 'button'
|
= submit_tag "Enregistrer", class: 'button'
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,21 @@
|
||||||
%h1.tab-title Inviter des personnes à donner leur avis
|
%h1.tab-title Inviter des personnes à donner leur avis
|
||||||
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
|
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
|
||||||
- if @dossier.procedure.experts_require_administrateur_invitation
|
- if @dossier.procedure.experts_require_administrateur_invitation
|
||||||
%p.avis-notice Choisissez des experts à qui vous souhaitez demander un avis parmi la liste prédéfinie par les administrateurs de la démarche
|
%p#avis-emails-description.avis-notice
|
||||||
|
Choisissez des experts à qui vous souhaitez demander un avis parmi la liste prédéfinie par les administrateurs de la démarche
|
||||||
- else
|
- else
|
||||||
%p.avis-notice Entrez les adresses email des experts à qui vous souhaitez demander un avis
|
%p#avis-emails-description.avis-notice
|
||||||
|
Entrez les adresses email des experts à qui vous souhaitez demander un avis
|
||||||
|
|
||||||
= form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "instructeur-ask-avis-for-dossier-#{@dossier.id}" } } do |f|
|
= form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "instructeur-ask-avis-for-dossier-#{@dossier.id}" } } do |f|
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= hidden_field_tag 'avis[emails]', nil
|
||||||
= hidden_field_tag 'avis[emails]', nil, data: { uuid: hidden_field_id }
|
= react_component("ComboMultiple",
|
||||||
= react_component("ComboMultipleDropdownList",
|
|
||||||
options: @dossier.procedure.experts_require_administrateur_invitation ? @experts_emails : [],
|
options: @dossier.procedure.experts_require_administrateur_invitation ? @experts_emails : [],
|
||||||
selected: [],
|
selected: [], disabled: [],
|
||||||
disabled: [],
|
label: 'Emails',
|
||||||
hiddenFieldId: hidden_field_id,
|
group: '.ask-avis',
|
||||||
label: 'avis_emails',
|
name: 'emails',
|
||||||
id: 'avis_emails',
|
describedby: 'avis-emails-description',
|
||||||
acceptNewValues: !@dossier.procedure.experts_require_administrateur_invitation)
|
acceptNewValues: !@dossier.procedure.experts_require_administrateur_invitation)
|
||||||
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: "persisted-input"
|
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: "persisted-input"
|
||||||
%p.tab-title Ajouter une pièce jointe
|
%p.tab-title Ajouter une pièce jointe
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
- accept = defined?(accept) ? accept : nil
|
- accept = defined?(accept) ? accept : nil
|
||||||
- user_can_destroy = defined?(user_can_destroy) ? user_can_destroy : false
|
- user_can_destroy = defined?(user_can_destroy) ? user_can_destroy : false
|
||||||
- direct_upload = direct_upload != nil ? false : true
|
- direct_upload = direct_upload != nil ? false : true
|
||||||
|
- champ = form.object.is_a?(Champ) ? form.object : nil
|
||||||
|
|
||||||
.attachment
|
.attachment
|
||||||
- if defined?(template) && template.attached?
|
- if defined?(template) && template.attached?
|
||||||
|
@ -36,4 +37,6 @@
|
||||||
class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
|
class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
|
||||||
accept: accept,
|
accept: accept,
|
||||||
direct_upload: direct_upload,
|
direct_upload: direct_upload,
|
||||||
|
id: champ&.input_id,
|
||||||
|
aria: { describedby: champ&.describedby_id },
|
||||||
data: { 'auto-attach-url': auto_attach_url(form, form.object) }
|
data: { 'auto-attach-url': auto_attach_url(form, form.object) }
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= form.hidden_field :value
|
||||||
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
|
= form.hidden_field :external_id
|
||||||
= form.hidden_field :external_id, { data: { reference: true } }
|
= react_component("ComboAdresseSearch",
|
||||||
= react_component("ComboAdresseSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
|
required: champ.mandatory?,
|
||||||
|
id: champ.input_id,
|
||||||
|
describedby: champ.describedby_id)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= form.hidden_field :value
|
||||||
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
|
= form.hidden_field :external_id
|
||||||
= form.hidden_field :external_id, { data: { reference: true } }
|
= react_component("ComboAnnuaireEducationSearch",
|
||||||
= react_component("ComboAnnuaireEducationSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id )
|
required: champ.mandatory?,
|
||||||
|
id: champ.input_id,
|
||||||
|
describedby: champ.describedby_id)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
= # we do this trick because some html elements should use 'label' and some should be plain paragraphs
|
= # we do this trick because some html elements should use 'label' and some should be plain paragraphs
|
||||||
- if champ.html_label?
|
- if champ.html_label?
|
||||||
= form.label champ.main_value_name do
|
= form.label champ.main_value_name, id: champ.labelledby_id, for: champ.input_id do
|
||||||
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
||||||
- else
|
- else
|
||||||
.form-label.mb-4
|
.form-label.mb-4
|
||||||
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
||||||
|
|
||||||
- if champ.description.present?
|
- if champ.description.present?
|
||||||
.notice{ id: describedby_id(champ) }= string_to_html(champ.description)
|
.notice{ id: champ.describedby_id }= string_to_html(champ.description)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
= form.check_box :value,
|
= form.check_box :value,
|
||||||
{ required: champ.mandatory? },
|
{ required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id } },
|
||||||
'on',
|
'on',
|
||||||
'off'
|
'off'
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
= form.text_field :numero_allocataire,
|
= form.text_field :numero_allocataire,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
size: 7,
|
size: 7,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
||||||
%div
|
%div
|
||||||
= form.label :code_postal, t('.code_postal_label')
|
= form.label :code_postal, t('.code_postal_label')
|
||||||
|
@ -13,4 +13,4 @@
|
||||||
= form.text_field :code_postal,
|
= form.text_field :code_postal,
|
||||||
size: 5,
|
size: 5,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= form.hidden_field :value
|
||||||
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
|
= form.hidden_field :external_id
|
||||||
= form.hidden_field :external_id, { data: { reference: true } }
|
= form.hidden_field :departement
|
||||||
= form.hidden_field :departement, { data: { attr: "#{hidden_field_id}:departement" } }
|
= form.hidden_field :code_departement
|
||||||
= form.hidden_field :code_departement, { data: { attr: "#{hidden_field_id}:code_departement" } }
|
= react_component("ComboCommunesSearch",
|
||||||
= react_component("ComboCommunesSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
|
required: champ.mandatory?,
|
||||||
|
id: champ.input_id,
|
||||||
|
describedby: champ.describedby_id)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
= form.date_field :value,
|
= form.date_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
value: champ.value,
|
value: champ.value,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
placeholder: 'aaaa-mm-jj'
|
placeholder: 'aaaa-mm-jj'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- parsed_value = champ.value.present? ? Time.zone.parse(champ.value) : nil
|
- parsed_value = champ.value.present? ? Time.zone.parse(champ.value) : nil
|
||||||
|
|
||||||
.datetime
|
.datetime
|
||||||
= form.datetime_select(:value, selected: parsed_value, start_year: datetime_start_year(parsed_value), end_year: Date.today.year + 50, minute_step: 5, include_blank: true)
|
= form.datetime_select(:value, id: champ.input_id, aria: { describedby: champ.describedby_id }, selected: parsed_value, start_year: datetime_start_year(parsed_value), end_year: Date.today.year + 50, minute_step: 5, include_blank: true)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
= form.number_field :value,
|
= form.number_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
step: :any,
|
step: :any,
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
required: champ.mandatory?
|
required: champ.mandatory?
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= form.hidden_field :value
|
||||||
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
|
= form.hidden_field :external_id
|
||||||
= form.hidden_field :external_id, { data: { reference: true } }
|
= react_component("ComboDepartementsSearch",
|
||||||
= react_component("ComboDepartementsSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
|
required: champ.mandatory?,
|
||||||
|
id: champ.input_id,
|
||||||
|
describedby: champ.describedby_id)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
= form.text_field :numero_fiscal,
|
= form.text_field :numero_fiscal,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
size: 14,
|
size: 14,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
||||||
%div
|
%div
|
||||||
= form.label :reference_avis, t('.reference_avis_label')
|
= form.label :reference_avis, t('.reference_avis_label')
|
||||||
|
@ -13,4 +13,4 @@
|
||||||
= form.text_field :reference_avis,
|
= form.text_field :reference_avis,
|
||||||
size: 14,
|
size: 14,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
.dossier-link{ class: "dossier-link-#{form.index}" }
|
.dossier-link{ class: "dossier-link-#{form.index}" }
|
||||||
= form.number_field :value,
|
= form.number_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: "Numéro de dossier",
|
placeholder: "Numéro de dossier",
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
= form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: champ.other_value_present?
|
= form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: champ.other_value_present?
|
||||||
Autre
|
Autre
|
||||||
- else
|
- else
|
||||||
= form.select :value, champ.options, selected: champ.selected, required: champ.mandatory?
|
= form.select :value, champ.options, { selected: champ.selected}, required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id }
|
||||||
|
|
||||||
- if champ.drop_down_other?
|
- if champ.drop_down_other?
|
||||||
= render partial: "shared/dossiers/drop_down_other_input", locals: { form: form, champ: champ }
|
= render partial: "shared/dossiers/drop_down_other_input", locals: { form: form, champ: champ }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.editable-champ{ class: "editable-champ-#{champ.type_champ}", data: { 'champ-id': champ.id } }
|
.editable-champ{ class: "editable-champ-#{champ.type_champ}", id: champ.input_group_id }
|
||||||
- if champ.repetition?
|
- if champ.repetition?
|
||||||
%h3.header-subsection= champ.libelle
|
%h3.header-subsection= champ.libelle
|
||||||
- if champ.description.present?
|
- if champ.description.present?
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
= form.email_field :value,
|
= form.email_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
required: champ.mandatory?
|
required: champ.mandatory?
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
= form.check_box :value,
|
= form.check_box :value,
|
||||||
{ required: champ.mandatory? },
|
{ required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id } },
|
||||||
'on',
|
'on',
|
||||||
'off'
|
'off'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
= form.text_field :value,
|
= form.text_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
placeholder: "27 caractères au format FR7630006000011234567890189",
|
placeholder: "27 caractères au format FR7630006000011234567890189",
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
= form.number_field :value,
|
= form.number_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
required: champ.mandatory?
|
required: champ.mandatory?
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
- if champ.options?
|
- if champ.options?
|
||||||
= form.select :primary_value,
|
= form.select :primary_value,
|
||||||
champ.primary_options,
|
champ.primary_options,
|
||||||
{ required: champ.mandatory? },
|
{},
|
||||||
{ data: { secondary_options: champ.secondary_options } }
|
{ data: { secondary_options: champ.secondary_options }, required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id } }
|
||||||
%span
|
|
||||||
= form.label :secondary_value do
|
= form.label :secondary_value, for: "#{champ.input_id}-secondary" do
|
||||||
= champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
|
= champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
|
||||||
- if champ.mandatory?
|
- if champ.mandatory?
|
||||||
%span.mandatory *
|
%span.mandatory *
|
||||||
- if champ.drop_down_secondary_description.present?
|
- if champ.drop_down_secondary_description.present?
|
||||||
.notice= string_to_html(champ.drop_down_secondary_description)
|
.notice{ id: "#{champ.describedby_id}-secondary" }= string_to_html(champ.drop_down_secondary_description)
|
||||||
= form.select :secondary_value,
|
= form.select :secondary_value,
|
||||||
champ.secondary_options[champ.primary_value],
|
champ.secondary_options[champ.primary_value],
|
||||||
{ required: champ.mandatory? },
|
{},
|
||||||
{ data: { secondary: true } }
|
{ data: { secondary: true }, required: champ.mandatory?, id: "#{champ.input_id}-secondary", aria: { describedby: "#{champ.describedby_id}-secondary" } }
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
%p.notice= t('.ine_notice')
|
%p.notice= t('.ine_notice')
|
||||||
= form.text_field :ine,
|
= form.text_field :ine,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
|
@ -3,10 +3,15 @@
|
||||||
= form.collection_check_boxes(:value, champ.enabled_non_empty_options, :to_s, :to_s) do |b|
|
= form.collection_check_boxes(:value, champ.enabled_non_empty_options, :to_s, :to_s) do |b|
|
||||||
.editable-champ.editable-champ-checkbox
|
.editable-champ.editable-champ-checkbox
|
||||||
= b.label do
|
= b.label do
|
||||||
= b.check_box({ multiple: true, checked: champ&.selected_options&.include?(b.value) })
|
= b.check_box({ multiple: true, checked: champ&.selected_options&.include?(b.value), aria: { describedby: champ.describedby_id } })
|
||||||
= b.text
|
= b.text
|
||||||
|
|
||||||
- else
|
- else
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= form.hidden_field :value
|
||||||
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
|
= react_component("ComboMultipleDropdownList",
|
||||||
= react_component("ComboMultipleDropdownList", options: champ.options, selected: champ.selected_options, disabled: champ.disabled_options, hiddenFieldId: hidden_field_id, label: champ.libelle)
|
options: champ.options,
|
||||||
|
selected: champ.selected_options,
|
||||||
|
disabled: champ.disabled_options,
|
||||||
|
id: champ.input_id,
|
||||||
|
labelledby: champ.labelledby_id,
|
||||||
|
describedby: champ.describedby_id)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
= form.number_field :value,
|
= form.number_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
required: champ.mandatory?
|
required: champ.mandatory?
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= form.hidden_field :value
|
||||||
= form.hidden_field :value, { value: champ.localized_value, data: { uuid: hidden_field_id } }
|
= form.hidden_field :external_id
|
||||||
= form.hidden_field :external_id, { data: { reference: true } }
|
= react_component("ComboPaysSearch",
|
||||||
= react_component("ComboPaysSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
|
required: champ.mandatory?,
|
||||||
|
id: champ.input_id,
|
||||||
|
describedby: champ.describedby_id)
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
-# very light validation is made client-side
|
-# very light validation is made client-side
|
||||||
-# stronger validation is made server-side
|
-# stronger validation is made server-side
|
||||||
= form.phone_field :value,
|
= form.phone_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
pattern: "[^a-z^A-Z]+"
|
pattern: "[^a-z^A-Z]+"
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
%p.notice= t('.identifiant_notice')
|
%p.notice= t('.identifiant_notice')
|
||||||
= form.text_field :identifiant,
|
= form.text_field :identifiant,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
- hidden_field_id = SecureRandom.uuid
|
= form.hidden_field :value
|
||||||
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
|
= form.hidden_field :external_id
|
||||||
= form.hidden_field :external_id, { data: { reference: true } }
|
= react_component("ComboRegionsSearch",
|
||||||
= react_component("ComboRegionsSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
|
required: champ.mandatory?,
|
||||||
|
id: champ.input_id,
|
||||||
|
describedby: champ.describedby_id)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
= form.text_field :value,
|
= form.text_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
data: { remote: true, debounce: true, url: champs_siret_path(form.index), params: { champ_id: champ&.id }.to_query, spinner: true },
|
data: { remote: true, debounce: true, url: champs_siret_path(form.index), params: { champ_id: champ&.id }.to_query, spinner: true },
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
= form.text_field :value,
|
= form.text_field :value,
|
||||||
|
id: champ.input_id,
|
||||||
placeholder: champ.libelle,
|
placeholder: champ.libelle,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
aria: { describedby: describedby_id(champ) }
|
aria: { describedby: champ.describedby_id }
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
~ form.text_area :value,
|
~ form.text_area :value,
|
||||||
|
id: champ.input_id,
|
||||||
|
aria: { describedby: champ.describedby_id },
|
||||||
rows: 6,
|
rows: 6,
|
||||||
required: champ.mandatory?,
|
required: champ.mandatory?,
|
||||||
value: html_to_string(champ.value)
|
value: html_to_string(champ.value)
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"@rails/activestorage": "^6.1.4-1",
|
"@rails/activestorage": "^6.1.4-1",
|
||||||
"@rails/ujs": "^6.1.4-1",
|
"@rails/ujs": "^6.1.4-1",
|
||||||
"@rails/webpacker": "5.4.3",
|
"@rails/webpacker": "5.4.3",
|
||||||
|
"@reach/auto-id": "^0.16.0",
|
||||||
"@reach/combobox": "^0.13.0",
|
"@reach/combobox": "^0.13.0",
|
||||||
"@reach/slider": "^0.15.0",
|
"@reach/slider": "^0.15.0",
|
||||||
"@reach/visually-hidden": "^0.15.2",
|
"@reach/visually-hidden": "^0.15.2",
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
"react-popper": "^2.2.5",
|
"react-popper": "^2.2.5",
|
||||||
"react-query": "^3.9.7",
|
"react-query": "^3.9.7",
|
||||||
"react-sortable-hoc": "^1.11.0",
|
"react-sortable-hoc": "^1.11.0",
|
||||||
|
"tiny-invariant": "^1.2.0",
|
||||||
"trix": "^1.2.3",
|
"trix": "^1.2.3",
|
||||||
"use-debounce": "^5.2.0",
|
"use-debounce": "^5.2.0",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
|
|
|
@ -29,7 +29,11 @@ describe Champs::PieceJustificativeController, type: :controller do
|
||||||
it 'renders the attachment template as Javascript' do
|
it 'renders the attachment template as Javascript' do
|
||||||
subject
|
subject
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(response.body).to include("editable-champ[data-champ-id=\"#{champ.id}\"]")
|
expect(response.body).to include("##{champ.input_group_id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates dossier.last_champ_updated_at' do
|
||||||
|
expect { subject }.to change { dossier.reload.last_champ_updated_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,8 @@ Capybara.default_max_wait_time = 2
|
||||||
|
|
||||||
Capybara.ignore_hidden_elements = false
|
Capybara.ignore_hidden_elements = false
|
||||||
|
|
||||||
|
Capybara.enable_aria_label = true
|
||||||
|
|
||||||
# Save a snapshot of the HTML page when an integration test fails
|
# Save a snapshot of the HTML page when an integration test fails
|
||||||
Capybara::Screenshot.autosave_on_failure = true
|
Capybara::Screenshot.autosave_on_failure = true
|
||||||
# Keep only the screenshots generated from the last failing test suite
|
# Keep only the screenshots generated from the last failing test suite
|
||||||
|
|
|
@ -103,41 +103,27 @@ module SystemHelpers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_combobox(champ, fill_with, value)
|
def select_combobox(libelle, fill_with, value, check: true)
|
||||||
fill_in champ, with: fill_with
|
fill_in libelle, with: fill_with
|
||||||
selector = "li[data-option-value=\"#{value}\"]"
|
selector = "li[data-option-value=\"#{value}\"]"
|
||||||
find(selector).click
|
find(selector).click
|
||||||
expect(page).to have_css(selector)
|
if check
|
||||||
expect(page).to have_css("[type=\"hidden\"][value=\"#{value}\"]")
|
check_selected_value(libelle, with: value)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_multi_combobox(champ, fill_with, value)
|
def check_selected_value(libelle, with:)
|
||||||
input = find("input[aria-label=\"#{champ}\"")
|
field = find_hidden_field_for(libelle)
|
||||||
input.click
|
value = field.value.starts_with?('[') ? JSON.parse(field.value) : field.value
|
||||||
input.fill_in with: fill_with
|
if value.is_a?(Array)
|
||||||
selector = "li[data-option-value=\"#{value}\"]"
|
if with.is_a?(Array)
|
||||||
find(selector).click
|
expect(value.sort).to eq(with.sort)
|
||||||
check_selected_value(champ, value)
|
else
|
||||||
|
expect(value).to include(with)
|
||||||
end
|
end
|
||||||
|
else
|
||||||
def check_selected_values(champ, values)
|
expect(value).to eq(with)
|
||||||
combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
|
|
||||||
hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
|
|
||||||
hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]")
|
|
||||||
hidden_field_values = JSON.parse(hidden_field.value)
|
|
||||||
expect(values.sort).to eq(hidden_field_values.sort)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_selected_value(champ, value)
|
|
||||||
combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
|
|
||||||
hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
|
|
||||||
hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]")
|
|
||||||
hidden_field_values = JSON.parse(hidden_field.value)
|
|
||||||
expect(hidden_field_values).to include(value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def have_hidden_field(libelle, with:)
|
|
||||||
have_css("##{form_id_for(libelle)}[value=\"#{with}\"]")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_out
|
def log_out
|
||||||
|
@ -172,6 +158,18 @@ module SystemHelpers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_hidden_field_for(libelle, name: 'value')
|
||||||
|
find("#{form_group_id_for(libelle)} input[type=\"hidden\"][name$=\"[#{name}]\"]")
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_group_id_for(libelle)
|
||||||
|
"#champ-#{form_id_for(libelle).gsub('-input', '')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_id_for(libelle)
|
||||||
|
find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
require 'system/administrateurs/procedure_spec_helper'
|
||||||
|
|
||||||
|
describe 'As an administrateur, I want to manage the procedure’s attestation', js: true do
|
||||||
|
include ProcedureSpecHelper
|
||||||
|
|
||||||
|
let(:administrateur) { create(:administrateur) }
|
||||||
|
let(:procedure) do
|
||||||
|
create(:procedure, :with_service, :with_instructeur,
|
||||||
|
aasm_state: :brouillon,
|
||||||
|
administrateurs: [administrateur],
|
||||||
|
libelle: 'libellé de la procédure',
|
||||||
|
path: 'libelle-de-la-procedure')
|
||||||
|
end
|
||||||
|
before { login_as(administrateur.user, scope: :user) }
|
||||||
|
|
||||||
|
def find_attestation_card(with_nested_selector: nil)
|
||||||
|
full_selector = [
|
||||||
|
".card-admin[href=\"#{edit_admin_procedure_attestation_template_path(procedure)}\"]",
|
||||||
|
with_nested_selector
|
||||||
|
].compact.join(" ")
|
||||||
|
page.find(full_selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Enable, publish, Disable' do
|
||||||
|
scenario do
|
||||||
|
visit admin_procedure_path(procedure)
|
||||||
|
# start with no attestation
|
||||||
|
find_attestation_card(with_nested_selector: ".icon.clock")
|
||||||
|
|
||||||
|
# now process to enable attestation
|
||||||
|
find_attestation_card.click
|
||||||
|
fill_in "Titre de l'attestation", with: 'BOOM'
|
||||||
|
fill_in "Corps du document", with: 'BOOM'
|
||||||
|
find('.toggle-switch-control').click
|
||||||
|
click_on 'Enregistrer'
|
||||||
|
page.find(".alert-success", text: "L'attestation a bien été sauvegardée")
|
||||||
|
|
||||||
|
# check attestation
|
||||||
|
visit admin_procedure_path(procedure)
|
||||||
|
find_attestation_card(with_nested_selector: ".icon.accept")
|
||||||
|
|
||||||
|
# publish procedure
|
||||||
|
# click CTA for publication screen
|
||||||
|
click_on("Publier")
|
||||||
|
# validate publication
|
||||||
|
click_on("Publier")
|
||||||
|
|
||||||
|
# now process to disable attestation
|
||||||
|
find_attestation_card.click
|
||||||
|
find('.toggle-switch-control').click
|
||||||
|
click_on 'Enregistrer'
|
||||||
|
page.find(".alert-success", text: "L'attestation a bien été modifiée")
|
||||||
|
|
||||||
|
# check attestation is now disabled
|
||||||
|
visit admin_procedure_path(procedure)
|
||||||
|
find_attestation_card(with_nested_selector: ".icon.clock")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,7 +9,7 @@ describe 'As an instructeur', js: true do
|
||||||
visit admin_procedure_path(procedure)
|
visit admin_procedure_path(procedure)
|
||||||
find('#groupe-instructeurs').click
|
find('#groupe-instructeurs').click
|
||||||
|
|
||||||
find("input[aria-label='email instructeur'").send_keys(instructeur_email, :enter)
|
fill_in 'Emails', with: instructeur_email
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
|
|
||||||
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
|
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
|
||||||
|
|
|
@ -165,8 +165,8 @@ describe 'Instructing a dossier:', js: true do
|
||||||
|
|
||||||
click_on 'Personnes impliquées'
|
click_on 'Personnes impliquées'
|
||||||
|
|
||||||
select_multi_combobox('email instructeur', instructeur_2.email, instructeur_2.id)
|
select_combobox('Emails', instructeur_2.email, instructeur_2.id, check: false)
|
||||||
select_multi_combobox('email instructeur', instructeur_3.email, instructeur_3.id)
|
select_combobox('Emails', instructeur_3.email, instructeur_3.id, check: false)
|
||||||
|
|
||||||
click_on 'Envoyer'
|
click_on 'Envoyer'
|
||||||
|
|
||||||
|
|
|
@ -125,13 +125,13 @@ describe "procedure filters" do
|
||||||
|
|
||||||
def add_column(column_name, column_path)
|
def add_column(column_name, column_path)
|
||||||
click_on 'Personnaliser'
|
click_on 'Personnaliser'
|
||||||
select_multi_combobox('colonne', column_name, column_path)
|
select_combobox('Colonne à afficher', column_name, column_path, check: false)
|
||||||
click_button "Enregistrer"
|
click_button "Enregistrer"
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_column(column_name)
|
def remove_column(column_name)
|
||||||
click_on 'Personnaliser'
|
click_on 'Personnaliser'
|
||||||
find(:xpath, ".//li[contains(text(), \"#{column_name}\")]/button", text: 'Désélectionner').click
|
click_button column_name
|
||||||
find("body").native.send_key("Escape")
|
find("body").native.send_key("Escape")
|
||||||
click_button "Enregistrer"
|
click_button "Enregistrer"
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,14 +30,14 @@ describe 'The routing', js: true do
|
||||||
expect(page).to have_field('Nom du groupe', with: 'littéraire')
|
expect(page).to have_field('Nom du groupe', with: 'littéraire')
|
||||||
|
|
||||||
# add victor to littéraire groupe
|
# add victor to littéraire groupe
|
||||||
find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter)
|
fill_in 'Emails', with: 'victor@inst.com'
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("L’instructeur victor@inst.com a été affecté au groupe « littéraire »")
|
expect(page).to have_text("L’instructeur victor@inst.com a été affecté au groupe « littéraire »")
|
||||||
|
|
||||||
victor = User.find_by(email: 'victor@inst.com').instructeur
|
victor = User.find_by(email: 'victor@inst.com').instructeur
|
||||||
|
|
||||||
# add superwoman to littéraire groupe
|
# add superwoman to littéraire groupe
|
||||||
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
|
fill_in 'Emails', with: 'superwoman@inst.com'
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté au groupe « littéraire »")
|
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté au groupe « littéraire »")
|
||||||
|
|
||||||
|
@ -50,14 +50,14 @@ describe 'The routing', js: true do
|
||||||
expect(page).to have_text('Le groupe d’instructeurs « scientifique » a été créé.')
|
expect(page).to have_text('Le groupe d’instructeurs « scientifique » a été créé.')
|
||||||
|
|
||||||
# add marie to scientifique groupe
|
# add marie to scientifique groupe
|
||||||
find("input[aria-label='email instructeur'").send_keys('marie@inst.com', :enter)
|
fill_in 'Emails', with: 'marie@inst.com'
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("L’instructeur marie@inst.com a été affecté")
|
expect(page).to have_text("L’instructeur marie@inst.com a été affecté")
|
||||||
|
|
||||||
marie = User.find_by(email: 'marie@inst.com').instructeur
|
marie = User.find_by(email: 'marie@inst.com').instructeur
|
||||||
|
|
||||||
# add superwoman to scientifique groupe
|
# add superwoman to scientifique groupe
|
||||||
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
|
fill_in 'Emails', with: 'superwoman@inst.com'
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté")
|
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté")
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ describe 'The routing', js: true do
|
||||||
click_on litteraire_user.dossiers.first.id.to_s
|
click_on litteraire_user.dossiers.first.id.to_s
|
||||||
click_on 'Modifier mon dossier'
|
click_on 'Modifier mon dossier'
|
||||||
|
|
||||||
fill_in 'dossier_champs_attributes_0_value', with: 'some value'
|
fill_in litteraire_user.dossiers.first.champs.first.libelle, with: 'some value'
|
||||||
click_on 'Enregistrer les modifications du dossier'
|
click_on 'Enregistrer les modifications du dossier'
|
||||||
log_out
|
log_out
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,13 @@ describe 'The user' do
|
||||||
check('val1')
|
check('val1')
|
||||||
check('val3')
|
check('val3')
|
||||||
select('bravo', from: form_id_for('simple_choice_drop_down_list_long'))
|
select('bravo', from: form_id_for('simple_choice_drop_down_list_long'))
|
||||||
select_multi_combobox('multiple_choice_drop_down_list_long', 'alp', 'alpha')
|
select_combobox('multiple_choice_drop_down_list_long', 'alp', 'alpha')
|
||||||
select_multi_combobox('multiple_choice_drop_down_list_long', 'cha', 'charly')
|
select_combobox('multiple_choice_drop_down_list_long', 'cha', 'charly')
|
||||||
|
|
||||||
select_combobox('pays', 'aust', 'Australie')
|
select_combobox('pays', 'aust', 'Australie')
|
||||||
select_combobox('regions', 'Ma', 'Martinique')
|
select_combobox('regions', 'Ma', 'Martinique')
|
||||||
select_combobox('departements', 'Ai', '02 - Aisne')
|
select_combobox('departements', 'Ai', '02 - Aisne')
|
||||||
select_combobox('communes', 'Ai', '02 - Aisne')
|
select_combobox('communes', 'Ai', '02 - Aisne', check: false)
|
||||||
select_combobox('communes', 'Ambl', 'Ambléon (01300)')
|
select_combobox('communes', 'Ambl', 'Ambléon (01300)')
|
||||||
|
|
||||||
check('engagement')
|
check('engagement')
|
||||||
|
@ -87,11 +87,11 @@ describe 'The user' do
|
||||||
expect(page).to have_checked_field('val1')
|
expect(page).to have_checked_field('val1')
|
||||||
expect(page).to have_checked_field('val3')
|
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('simple_choice_drop_down_list_long', selected: 'bravo')
|
||||||
check_selected_values('multiple_choice_drop_down_list_long', ['alpha', 'charly'])
|
check_selected_value('multiple_choice_drop_down_list_long', with: ['alpha', 'charly'])
|
||||||
expect(page).to have_hidden_field('pays', with: 'Australie')
|
check_selected_value('pays', with: 'Australie')
|
||||||
expect(page).to have_hidden_field('regions', with: 'Martinique')
|
check_selected_value('regions', with: 'Martinique')
|
||||||
expect(page).to have_hidden_field('departements', with: '02 - Aisne')
|
check_selected_value('departements', with: '02 - Aisne')
|
||||||
expect(page).to have_hidden_field('communes', with: 'Ambléon (01300)')
|
check_selected_value('communes', with: 'Ambléon (01300)')
|
||||||
expect(page).to have_checked_field('engagement')
|
expect(page).to have_checked_field('engagement')
|
||||||
expect(page).to have_field('dossier_link', with: '123')
|
expect(page).to have_field('dossier_link', with: '123')
|
||||||
expect(page).to have_text('file.pdf')
|
expect(page).to have_text('file.pdf')
|
||||||
|
@ -337,10 +337,6 @@ describe 'The user' do
|
||||||
expect(page).to have_current_path(identite_dossier_path(user_dossier))
|
expect(page).to have_current_path(identite_dossier_path(user_dossier))
|
||||||
end
|
end
|
||||||
|
|
||||||
def form_id_for(libelle)
|
|
||||||
find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
|
|
||||||
end
|
|
||||||
|
|
||||||
def form_id_for_datetime(libelle)
|
def form_id_for_datetime(libelle)
|
||||||
# The HTML for datetime is a bit specific since it has 5 selects, below here is a sample HTML
|
# The HTML for datetime is a bit specific since it has 5 selects, below here is a sample HTML
|
||||||
# So, we want to find the partial id of a datetime (partial because there are 5 ids:
|
# So, we want to find the partial id of a datetime (partial because there are 5 ids:
|
||||||
|
|
|
@ -27,16 +27,16 @@ describe 'linked dropdown lists' do
|
||||||
fill_individual
|
fill_individual
|
||||||
|
|
||||||
# Select a primary value
|
# Select a primary value
|
||||||
select('Primary 2', from: primary_id_for('linked dropdown'))
|
select('Primary 2', from: 'linked dropdown')
|
||||||
|
|
||||||
# Secondary menu reflects chosen primary value
|
# Secondary menu reflects chosen primary value
|
||||||
expect(page).to have_select(secondary_id_for('linked dropdown'), options: ['', 'Secondary 2.1', 'Secondary 2.2', 'Secondary 2.3'])
|
expect(page).to have_select("Valeur secondaire dépendant de la première", options: ['', 'Secondary 2.1', 'Secondary 2.2', 'Secondary 2.3'])
|
||||||
|
|
||||||
# Select another primary value
|
# Select another primary value
|
||||||
select('Primary 1', from: primary_id_for('linked dropdown'))
|
select('Primary 1', from: 'linked dropdown')
|
||||||
|
|
||||||
# Secondary menu gets updated
|
# Secondary menu gets updated
|
||||||
expect(page).to have_select(secondary_id_for('linked dropdown'), options: ['', 'Secondary 1.1', 'Secondary 1.2'])
|
expect(page).to have_select("Valeur secondaire dépendant de la première", options: ['', 'Secondary 1.1', 'Secondary 1.2'])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -63,15 +63,4 @@ describe 'linked dropdown lists' do
|
||||||
click_on 'Continuer'
|
click_on 'Continuer'
|
||||||
expect(page).to have_current_path(brouillon_dossier_path(user_dossier))
|
expect(page).to have_current_path(brouillon_dossier_path(user_dossier))
|
||||||
end
|
end
|
||||||
|
|
||||||
def primary_id_for(libelle)
|
|
||||||
find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
|
|
||||||
end
|
|
||||||
|
|
||||||
def secondary_id_for(libelle)
|
|
||||||
primary_id = primary_id_for(libelle)
|
|
||||||
find("\##{primary_id}")
|
|
||||||
.ancestor('.editable-champ')
|
|
||||||
.find("[data-secondary]")['id']
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -1838,6 +1838,14 @@
|
||||||
"@reach/utils" "0.15.3"
|
"@reach/utils" "0.15.3"
|
||||||
tslib "^2.3.0"
|
tslib "^2.3.0"
|
||||||
|
|
||||||
|
"@reach/auto-id@^0.16.0":
|
||||||
|
version "0.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.16.0.tgz#dfabc3227844e8c04f8e6e45203a8e14a8edbaed"
|
||||||
|
integrity sha512-5ssbeP5bCkM39uVsfQCwBBL+KT8YColdnMN5/Eto6Rj7929ql95R3HZUOkKIvj7mgPtEb60BLQxd1P3o6cjbmg==
|
||||||
|
dependencies:
|
||||||
|
"@reach/utils" "0.16.0"
|
||||||
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@reach/combobox@^0.13.0":
|
"@reach/combobox@^0.13.0":
|
||||||
version "0.13.2"
|
version "0.13.2"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.13.2.tgz#f0a38c685a2704f6a189709e63cd7e25809da1e4"
|
resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.13.2.tgz#f0a38c685a2704f6a189709e63cd7e25809da1e4"
|
||||||
|
@ -1922,6 +1930,14 @@
|
||||||
tiny-warning "^1.0.3"
|
tiny-warning "^1.0.3"
|
||||||
tslib "^2.3.0"
|
tslib "^2.3.0"
|
||||||
|
|
||||||
|
"@reach/utils@0.16.0":
|
||||||
|
version "0.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.16.0.tgz#5b0777cf16a7cab1ddd4728d5d02762df0ba84ce"
|
||||||
|
integrity sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q==
|
||||||
|
dependencies:
|
||||||
|
tiny-warning "^1.0.3"
|
||||||
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@reach/visually-hidden@^0.15.2":
|
"@reach/visually-hidden@^0.15.2":
|
||||||
version "0.15.2"
|
version "0.15.2"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.15.2.tgz#07794cb53f4bd23a9452d53a0ad7778711ee323f"
|
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.15.2.tgz#07794cb53f4bd23a9452d53a0ad7778711ee323f"
|
||||||
|
@ -12445,6 +12461,11 @@ timsort@^0.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||||
|
|
||||||
|
tiny-invariant@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
||||||
|
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
|
||||||
|
|
||||||
tiny-warning@^1.0.3:
|
tiny-warning@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
|
|
Loading…
Add table
Reference in a new issue