feat(type_de_champ): type_de_champ editor in stimulus/turbo
This commit is contained in:
parent
1573d20ee9
commit
6801b04b7b
34 changed files with 898 additions and 248 deletions
3
app/assets/images/icons/arrow-down.svg
Normal file
3
app/assets/images/icons/arrow-down.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 10.293a1 1 0 010 1.414l-6 6a1 1 0 01-1.414 0l-6-6a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l4.293-4.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
After Width: | Height: | Size: 296 B |
3
app/assets/images/icons/arrow-up.svg
Normal file
3
app/assets/images/icons/arrow-up.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M3.293 9.707a1 1 0 010-1.414l6-6a1 1 0 011.414 0l6 6a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L4.707 9.707a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
After Width: | Height: | Size: 294 B |
|
@ -87,6 +87,14 @@
|
|||
background-image: image-url("icons/lock.svg");
|
||||
}
|
||||
|
||||
&.arrow-up {
|
||||
background-image: image-url("icons/arrow-up.svg");
|
||||
}
|
||||
|
||||
&.arrow-down {
|
||||
background-image: image-url("icons/arrow-down.svg");
|
||||
}
|
||||
|
||||
&.add {
|
||||
background-image: image-url("icons/add.svg");
|
||||
margin-left: -5px;
|
||||
|
|
|
@ -1,129 +1,127 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
@import "placeholders";
|
||||
|
||||
.type-de-champ {
|
||||
width: 100%;
|
||||
background-color: #FAFDFF;
|
||||
border: 1px solid $border-grey;
|
||||
border-radius: 5px;
|
||||
margin-bottom: $default-padding * 2;
|
||||
box-shadow: 0px 2px 4px -4px;
|
||||
overflow: hidden;
|
||||
|
||||
.handle.icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-size: 32px;
|
||||
margin-left: 7px;
|
||||
margin-right: 16px;
|
||||
align-self: center;
|
||||
cursor: grab;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.types-de-champ-editor {
|
||||
> .types-de-champ-block {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.move {
|
||||
height: 44px;
|
||||
border-radius: 25px;
|
||||
margin-right: 10px;
|
||||
.type-de-champ {
|
||||
width: 100%;
|
||||
background-color: #FAFDFF;
|
||||
border: 1px solid $border-grey;
|
||||
border-radius: 5px;
|
||||
margin-bottom: $default-padding * 2;
|
||||
box-shadow: 0px 2px 4px -4px;
|
||||
overflow: hidden;
|
||||
|
||||
&:first-of-type {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.handle.icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-size: 32px;
|
||||
margin-left: 7px;
|
||||
margin-right: 16px;
|
||||
align-self: center;
|
||||
cursor: grab;
|
||||
opacity: 0.8;
|
||||
|
||||
&:last-of-type {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.head {
|
||||
background-color: #D9ECFF;
|
||||
|
||||
select {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.type-header-section {
|
||||
&,
|
||||
.head {
|
||||
background-color: $blue-france-500;
|
||||
}
|
||||
|
||||
.head .icon {
|
||||
filter: contrast(0%) brightness(200%);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $light-grey;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
&.section {
|
||||
padding: 10px 10px 0 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.hr {
|
||||
border-bottom: 1px solid $border-grey;
|
||||
|
||||
&.head {
|
||||
border-bottom: 1px solid #D4E5F5;
|
||||
padding-bottom: 10px;
|
||||
&:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&.shift-left {
|
||||
margin-left: 55px;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
.delete {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
margin-right: 20px;
|
||||
.move-up,
|
||||
.move-down {
|
||||
@extend %outline;
|
||||
|
||||
&.small {
|
||||
width: 90px;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
padding-bottom: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid $border-grey;
|
||||
font-family: "Muli";
|
||||
background-color: #FFFFFF;
|
||||
color: $black;
|
||||
text-align: center;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
background: $light-grey;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.libelle {
|
||||
width: 300px;
|
||||
&.first .move-up {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
&.last .move-down {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.carte-options {
|
||||
label {
|
||||
font-weight: initial;
|
||||
.head {
|
||||
background-color: #D9ECFF;
|
||||
|
||||
select {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
&.type-header-section {
|
||||
&,
|
||||
.head {
|
||||
background-color: $blue-france-500;
|
||||
}
|
||||
|
||||
.champs-editor {
|
||||
.footer {
|
||||
height: 50px;
|
||||
.handle.icon {
|
||||
filter: contrast(0%) brightness(200%);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $light-grey;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
&.section {
|
||||
padding: 10px 10px 0 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.hr {
|
||||
border-bottom: 1px solid $border-grey;
|
||||
|
||||
&.head {
|
||||
border-bottom: 1px solid #D4E5F5;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
margin-right: $default-padding;
|
||||
|
||||
label {
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.carte-options {
|
||||
label {
|
||||
font-weight: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
clear: both;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
// text
|
||||
.text-center,
|
||||
|
@ -68,6 +71,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.width-33 {
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
// who known
|
||||
.highlighted {
|
||||
background: $orange-bg;
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
class ApplicationComponent < ViewComponent::Base
|
||||
include ViewComponent::Translatable
|
||||
|
||||
def class_names(class_names)
|
||||
class_names.to_a.filter_map { |(class_name, flag)| class_name if flag }.join(' ')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
# Display a widget for uploading, editing and deleting a file attachment
|
||||
class Attachment::EditComponent < ApplicationComponent
|
||||
def initialize(form:, attached_file:, accept: nil, template: nil, user_can_destroy: false, direct_upload: true)
|
||||
def initialize(form:, attached_file:, accept: nil, template: nil, user_can_destroy: false, direct_upload: true, id: nil)
|
||||
@form = form
|
||||
@attached_file = attached_file
|
||||
@accept = accept
|
||||
@template = template
|
||||
@user_can_destroy = user_can_destroy
|
||||
@direct_upload = direct_upload
|
||||
@id = id
|
||||
end
|
||||
|
||||
attr_reader :template, :form
|
||||
|
@ -56,7 +57,7 @@ class Attachment::EditComponent < ApplicationComponent
|
|||
class: "attachment-input #{attachment_input_class} #{'hidden' if persisted?}",
|
||||
accept: @accept,
|
||||
direct_upload: @direct_upload,
|
||||
id: champ&.input_id,
|
||||
id: champ&.input_id || @id,
|
||||
aria: { describedby: champ&.describedby_id },
|
||||
data: { auto_attach_url: helpers.auto_attach_url(form.object) }
|
||||
}
|
||||
|
|
|
@ -64,8 +64,6 @@ class Dossiers::MessageComponent < ApplicationComponent
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def highlight?
|
||||
commentaire.created_at.present? && @messagerie_seen_at&.<(commentaire.created_at)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
class TypesDeChampEditor::AddChampButtonComponent < ApplicationComponent
|
||||
def initialize(revision:, parent: nil, is_annotation: false)
|
||||
@revision = revision
|
||||
@parent = parent
|
||||
@is_annotation = is_annotation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def annotations?
|
||||
@is_annotation
|
||||
end
|
||||
|
||||
def procedure
|
||||
@revision.procedure
|
||||
end
|
||||
|
||||
def button_title
|
||||
if annotations?
|
||||
"Ajouter une annotation"
|
||||
else
|
||||
"Ajouter un champ"
|
||||
end
|
||||
end
|
||||
|
||||
def button_options
|
||||
{
|
||||
class: "button",
|
||||
form: { class: @parent ? "add-to-block" : "add-to-root" },
|
||||
method: :post,
|
||||
params: {
|
||||
type_de_champ: {
|
||||
libelle: champ_libelle,
|
||||
type_champ: TypeDeChamp.type_champs.fetch(:text),
|
||||
private: annotations? ? true : nil,
|
||||
parent_id: @parent&.stable_id,
|
||||
after_id: ''
|
||||
}.compact
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def champ_libelle
|
||||
if annotations?
|
||||
"Nouvelle annotation"
|
||||
else
|
||||
"Nouveau champ"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
= button_to(button_title, admin_procedure_types_de_champ_path(procedure), button_options)
|
20
app/components/types_de_champ_editor/block_component.rb
Normal file
20
app/components/types_de_champ_editor/block_component.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
class TypesDeChampEditor::BlockComponent < ApplicationComponent
|
||||
def initialize(block:, coordinates:)
|
||||
@block = block
|
||||
@coordinates = coordinates
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sortable_options
|
||||
{
|
||||
controller: 'sortable',
|
||||
sortable_handle_value: '.handle',
|
||||
sortable_group_value: block_id
|
||||
}
|
||||
end
|
||||
|
||||
def block_id
|
||||
dom_id(@block, :types_de_champ_editor_block)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
%ul.types-de-champ-block{ id: block_id, data: sortable_options }
|
||||
- @coordinates.each do |coordinate|
|
||||
= render TypesDeChampEditor::ChampComponent.new(coordinate: coordinate)
|
111
app/components/types_de_champ_editor/champ_component.rb
Normal file
111
app/components/types_de_champ_editor/champ_component.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
||||
def initialize(coordinate:, focused: false)
|
||||
@coordinate = coordinate
|
||||
@focused = focused
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :coordinate
|
||||
delegate :type_de_champ, :revision, :procedure, to: :coordinate
|
||||
|
||||
def can_be_mandatory?
|
||||
type_de_champ.public? && !type_de_champ.non_fillable?
|
||||
end
|
||||
|
||||
def type_de_champ_path
|
||||
admin_procedure_type_de_champ_path(procedure, type_de_champ.stable_id)
|
||||
end
|
||||
|
||||
def html_options
|
||||
{
|
||||
id: dom_id(coordinate, :type_de_champ_editor),
|
||||
class: class_names('type-header-section': type_de_champ.header_section?,
|
||||
first: coordinate.first?,
|
||||
last: coordinate.last?),
|
||||
data: {
|
||||
controller: 'type-de-champ-editor',
|
||||
type_de_champ_editor_move_url_value: move_admin_procedure_type_de_champ_path(procedure, type_de_champ.stable_id),
|
||||
type_de_champ_editor_move_up_url_value: move_up_admin_procedure_type_de_champ_path(procedure, type_de_champ.stable_id),
|
||||
type_de_champ_editor_move_down_url_value: move_down_admin_procedure_type_de_champ_path(procedure, type_de_champ.stable_id),
|
||||
type_de_champ_editor_type_de_champ_id_value: coordinate.stable_id
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def form_options
|
||||
{
|
||||
url: type_de_champ_path,
|
||||
multipart: true,
|
||||
html: { id: nil, class: 'form width-100' }
|
||||
}
|
||||
end
|
||||
|
||||
def move_button_options(direction)
|
||||
{
|
||||
type: 'button',
|
||||
data: { action: 'type-de-champ-editor#onMoveButtonClick', type_de_champ_editor_direction_param: direction },
|
||||
title: direction == :up ? 'Déplacer le champ vers le haut' : 'Déplacer le champ vers le bas'
|
||||
}
|
||||
end
|
||||
|
||||
def input_autofocus
|
||||
@focused ? { controller: 'autofocus' } : nil
|
||||
end
|
||||
|
||||
def types_of_type_de_champ
|
||||
TypeDeChamp.type_champs
|
||||
.keys
|
||||
.filter(&method(:filter_type_champ))
|
||||
.filter(&method(:filter_featured_type_champ))
|
||||
.filter(&method(:filter_block_type_champ))
|
||||
.map { |type_champ| [t("activerecord.attributes.type_de_champ.type_champs.#{type_champ}"), type_champ] }
|
||||
.sort_by(&:first)
|
||||
end
|
||||
|
||||
def piece_justificative_options(form)
|
||||
{
|
||||
form: form,
|
||||
attached_file: type_de_champ.piece_justificative_template,
|
||||
user_can_destroy: true,
|
||||
id: dom_id(type_de_champ, :piece_justificative_template)
|
||||
}
|
||||
end
|
||||
|
||||
EXCLUDE_FROM_BLOCK = [
|
||||
TypeDeChamp.type_champs.fetch(:carte),
|
||||
TypeDeChamp.type_champs.fetch(:dossier_link),
|
||||
TypeDeChamp.type_champs.fetch(:repetition),
|
||||
TypeDeChamp.type_champs.fetch(:siret)
|
||||
]
|
||||
|
||||
def filter_block_type_champ(type_champ)
|
||||
!coordinate.child? || !EXCLUDE_FROM_BLOCK.include?(type_champ)
|
||||
end
|
||||
|
||||
def filter_featured_type_champ(type_champ)
|
||||
feature_name = TypeDeChamp::FEATURE_FLAGS[type_champ]
|
||||
feature_name.blank? || Flipper.enabled?(feature_name, helpers.current_user)
|
||||
end
|
||||
|
||||
def filter_type_champ(type_champ)
|
||||
case type_champ
|
||||
when TypeDeChamp.type_champs.fetch(:number)
|
||||
has_legacy_number?
|
||||
when TypeDeChamp.type_champs.fetch(:cnaf)
|
||||
procedure.cnaf_enabled?
|
||||
when TypeDeChamp.type_champs.fetch(:dgfip)
|
||||
procedure.dgfip_enabled?
|
||||
when TypeDeChamp.type_champs.fetch(:pole_emploi)
|
||||
procedure.pole_emploi_enabled?
|
||||
when TypeDeChamp.type_champs.fetch(:mesri)
|
||||
procedure.mesri_enabled?
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def has_legacy_number?
|
||||
revision.types_de_champ.any?(&:legacy_number?)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
fr:
|
||||
layers:
|
||||
cadastres: Cadastres
|
||||
unesco: UNESCO
|
||||
arretes_protection: Arrêtés de protection
|
||||
conservatoire_littoral: Conservatoire du Littoral
|
||||
reserves_chasse_faune_sauvage: Réserves nationales de chasse et de faune sauvage
|
||||
reserves_biologiques: Réserves biologiques
|
||||
reserves_naturelles: Réserves naturelles
|
||||
natura_2000: Natura 2000
|
||||
zones_humides: Zones humides d’importance internationale
|
||||
znieff: ZNIEFF
|
|
@ -0,0 +1,72 @@
|
|||
%li.type-de-champ.flex.column.justify-start{ html_options }
|
||||
.flex.justify-start.section.head{ class: type_de_champ.header_section? ? '' : 'hr'}
|
||||
.handle.small.icon-only.icon.move-handle{ title: "Déplacer le champ vers le haut ou vers le bas" }
|
||||
.flex.justify-start.delete
|
||||
= button_to type_de_champ_path, class: 'button small icon-only danger', method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
|
||||
.icon.delete
|
||||
%span.sr-only Supprimer
|
||||
|
||||
.flex.justify-start.section.ml-1
|
||||
= form_for(type_de_champ, form_options) do |form|
|
||||
.flex.justify-start
|
||||
.flex.justify-start.width-33
|
||||
.flex.justify-start.column
|
||||
%button.move-up.cell.mb-1{ move_button_options(:up) }
|
||||
.icon.arrow-up.small
|
||||
%span.sr-only Déplacer le champ vers le haut
|
||||
%button.move-down.cell{ move_button_options(:down) }
|
||||
.icon.arrow-down.small
|
||||
%span.sr-only Déplacer le champ vers le bas
|
||||
.cell.flex.justify-start.column.flex-grow
|
||||
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ)
|
||||
= form.select :type_champ, types_of_type_de_champ, {}, class: 'small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ)
|
||||
.flex.column.justify-start.flex-grow
|
||||
.cell
|
||||
.flex.align-center
|
||||
= form.label :libelle, "Libellé du champ", class: 'flex-grow', for: dom_id(type_de_champ, :libelle)
|
||||
- if can_be_mandatory?
|
||||
.cell.flex.align-center
|
||||
= form.check_box :mandatory, class: 'small-margin small', id: dom_id(type_de_champ, :mandatory)
|
||||
= form.label :mandatory, "Champ obligatoire", for: dom_id(type_de_champ, :mandatory)
|
||||
= form.text_field :libelle, class: 'small-margin small width-100', id: dom_id(type_de_champ, :libelle), data: input_autofocus
|
||||
- if !type_de_champ.header_section? && !type_de_champ.titre_identite?
|
||||
.cell.mt-1
|
||||
= form.label :description, "Description du champ (optionnel)", for: dom_id(type_de_champ, :description)
|
||||
= form.text_area :description, class: 'small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :description)
|
||||
|
||||
.flex.justify-start.mt-1
|
||||
- if type_de_champ.drop_down_list?
|
||||
.flex.column.justify-start.width-33
|
||||
.cell
|
||||
= form.label :drop_down_list_value, "Options de la liste", for: dom_id(type_de_champ, :drop_down_list_value)
|
||||
= form.text_area :drop_down_list_value, class: 'small-margin small width-100', rows: 7, id: dom_id(type_de_champ, :drop_down_list_value)
|
||||
- if type_de_champ.linked_drop_down_list?
|
||||
.flex.column.justify-start.flex-grow
|
||||
.cell
|
||||
= form.label :drop_down_secondary_libelle, "Libellé du champ secondaire", class: 'flex-grow', for: dom_id(type_de_champ, :drop_down_secondary_libelle)
|
||||
= form.text_field :drop_down_secondary_libelle, class: 'small-margin small width-100', id: dom_id(type_de_champ, :drop_down_secondary_libelle)
|
||||
.cell.mt-1
|
||||
= form.label :drop_down_secondary_description, "Description du champ secondaire (optionnel)", for: dom_id(type_de_champ, :drop_down_secondary_description)
|
||||
= form.text_area :drop_down_secondary_description, class: 'small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :drop_down_secondary_description)
|
||||
- if type_de_champ.piece_justificative?
|
||||
.cell
|
||||
= form.label :piece_justificative_template, "Modèle", for: dom_id(type_de_champ, :piece_justificative_template)
|
||||
= render Attachment::EditComponent.new(**piece_justificative_options(form))
|
||||
- if type_de_champ.titre_identite?
|
||||
.cell
|
||||
%p
|
||||
Dans le cadre de la RGPD, le titre d’identité sera supprimé lors de l’acceptation du dossier
|
||||
- if type_de_champ.carte?
|
||||
- type_de_champ.editable_options.each do |slice|
|
||||
.cell
|
||||
.carte-options
|
||||
= form.fields_for :editable_options do |form|
|
||||
- slice.each do |(name, checked)|
|
||||
= form.label name, for: dom_id(type_de_champ, "layer_#{name}") do
|
||||
= form.check_box name, checked: checked, class: 'small-margin small', id: dom_id(type_de_champ, "layer_#{name}")
|
||||
= t(".layers.#{name}")
|
||||
- if type_de_champ.repetition?
|
||||
.flex.justify-start.section.ml-1
|
||||
.editor-block.flex-grow.cell
|
||||
= render TypesDeChampEditor::BlockComponent.new(block: coordinate, coordinates: coordinate.revision_types_de_champ)
|
||||
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate, is_annotation: coordinate.private?)
|
20
app/components/types_de_champ_editor/editor_component.rb
Normal file
20
app/components/types_de_champ_editor/editor_component.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
class TypesDeChampEditor::EditorComponent < ApplicationComponent
|
||||
def initialize(revision:, is_annotation: false)
|
||||
@revision = revision
|
||||
@is_annotation = is_annotation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def annotations?
|
||||
@is_annotation
|
||||
end
|
||||
|
||||
def coordinates
|
||||
if annotations?
|
||||
@revision.revision_types_de_champ_private
|
||||
else
|
||||
@revision.revision_types_de_champ_public
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
.types-de-champ-editor.editor-root{ 'data-turbo': 'true', id: dom_id(@revision, :types_de_champ_editor) }
|
||||
= render TypesDeChampEditor::BlockComponent.new(block: @revision, coordinates: coordinates)
|
||||
.buttons
|
||||
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: @revision, is_annotation: annotations?)
|
||||
= render TypesDeChampEditor::EstimatedFillDurationComponent.new(revision: @revision, is_annotation: annotations?)
|
|
@ -0,0 +1,22 @@
|
|||
class TypesDeChampEditor::EstimatedFillDurationComponent < ApplicationComponent
|
||||
def initialize(revision:, is_annotation: false)
|
||||
@revision = revision
|
||||
@is_annotation = is_annotation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def annotations?
|
||||
@is_annotation
|
||||
end
|
||||
|
||||
def show?
|
||||
!annotations? && @revision.types_de_champ_public.present?
|
||||
end
|
||||
|
||||
def estimated_fill_duration_minutes
|
||||
seconds = @revision.estimated_fill_duration
|
||||
minutes = (seconds / 60.0).round
|
||||
[1, minutes].max
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
en:
|
||||
estimated_fill_duration: "Estimated fill time:"
|
||||
estimated_fill_minutes: "%{estimated_minutes} mn"
|
|
@ -0,0 +1,3 @@
|
|||
fr:
|
||||
estimated_fill_duration: "Durée de remplissage estimée :"
|
||||
estimated_fill_minutes: "%{estimated_minutes} mn"
|
|
@ -0,0 +1,5 @@
|
|||
%span.fill-duration{ id: dom_id(@revision, :estimated_fill_duration) }
|
||||
- if show?
|
||||
= t('.estimated_fill_duration')
|
||||
= link_to "https://doc.demarches-simplifiees.fr/tutoriels/tutoriel-administrateur#g.-estimation-de-la-duree-de-remplissage", target: "_blank", rel: "noopener noreferrer" do
|
||||
= t('.estimated_fill_minutes', estimated_minutes: estimated_fill_duration_minutes)
|
9
app/javascript/controllers/autofocus_controller.ts
Normal file
9
app/javascript/controllers/autofocus_controller.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export class AutofocusController extends Controller {
|
||||
connect() {
|
||||
const element = this.element as HTMLInputElement;
|
||||
element.focus();
|
||||
element.setSelectionRange(0, element.value.length);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { Application } from '@hotwired/stimulus';
|
||||
|
||||
import { AutofocusController } from './autofocus_controller';
|
||||
import { AutosaveController } from './autosave_controller';
|
||||
import { AutosaveStatusController } from './autosave_status_controller';
|
||||
import { GeoAreaController } from './geo_area_controller';
|
||||
|
@ -7,11 +8,14 @@ import { MenuButtonController } from './menu_button_controller';
|
|||
import { PersistedFormController } from './persisted_form_controller';
|
||||
import { ReactController } from './react_controller';
|
||||
import { ScrollToController } from './scroll_to_controller';
|
||||
import { SortableController } from './sortable_controller';
|
||||
import { TurboEventController } from './turbo_event_controller';
|
||||
import { TurboInputController } from './turbo_input_controller';
|
||||
import { TurboPollController } from './turbo_poll_controller';
|
||||
import { TypeDeChampEditorController } from './type_de_champ_editor_controller';
|
||||
|
||||
const Stimulus = Application.start();
|
||||
Stimulus.register('autofocus', AutofocusController);
|
||||
Stimulus.register('autosave-status', AutosaveStatusController);
|
||||
Stimulus.register('autosave', AutosaveController);
|
||||
Stimulus.register('geo-area', GeoAreaController);
|
||||
|
@ -19,6 +23,8 @@ Stimulus.register('menu-button', MenuButtonController);
|
|||
Stimulus.register('persisted-form', PersistedFormController);
|
||||
Stimulus.register('react', ReactController);
|
||||
Stimulus.register('scroll-to', ScrollToController);
|
||||
Stimulus.register('sortable', SortableController);
|
||||
Stimulus.register('turbo-event', TurboEventController);
|
||||
Stimulus.register('turbo-input', TurboInputController);
|
||||
Stimulus.register('turbo-poll', TurboPollController);
|
||||
Stimulus.register('type-de-champ-editor', TypeDeChampEditorController);
|
||||
|
|
68
app/javascript/controllers/sortable_controller.ts
Normal file
68
app/javascript/controllers/sortable_controller.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import Sortable from 'sortablejs';
|
||||
|
||||
import { ApplicationController } from './application_controller';
|
||||
|
||||
export class SortableController extends ApplicationController {
|
||||
declare readonly animationValue: number;
|
||||
declare readonly handleValue: string;
|
||||
declare readonly groupValue: string;
|
||||
|
||||
#sortable?: Sortable;
|
||||
|
||||
static values = {
|
||||
animation: Number,
|
||||
handle: String,
|
||||
group: String
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.#sortable = new Sortable(this.element as HTMLElement, {
|
||||
...this.defaultOptions,
|
||||
...this.options
|
||||
});
|
||||
this.onGlobal('sortable:sort', () => this.setEdgeClassNames());
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.#sortable?.destroy();
|
||||
}
|
||||
|
||||
private onEnd({ item, newIndex }: { item: HTMLElement; newIndex?: number }) {
|
||||
if (newIndex == null) return;
|
||||
|
||||
this.dispatch('end', {
|
||||
target: item,
|
||||
detail: { position: newIndex }
|
||||
});
|
||||
this.setEdgeClassNames();
|
||||
}
|
||||
|
||||
setEdgeClassNames() {
|
||||
const items = this.element.children;
|
||||
for (const item of items) {
|
||||
item.classList.remove('first', 'last');
|
||||
}
|
||||
if (items.length > 1) {
|
||||
const first = items[0];
|
||||
const last = items[items.length - 1];
|
||||
first?.classList.add('first');
|
||||
last?.classList.add('last');
|
||||
}
|
||||
}
|
||||
|
||||
get options(): Sortable.Options {
|
||||
return {
|
||||
animation: this.animationValue || this.defaultOptions.animation || 150,
|
||||
handle: this.handleValue || this.defaultOptions.handle || undefined,
|
||||
group: this.groupValue || this.defaultOptions.group || undefined,
|
||||
onEnd: (event) => this.onEnd(event)
|
||||
};
|
||||
}
|
||||
|
||||
get defaultOptions(): Sortable.Options {
|
||||
return {
|
||||
fallbackOnBody: true,
|
||||
swapThreshold: 0.65
|
||||
};
|
||||
}
|
||||
}
|
194
app/javascript/controllers/type_de_champ_editor_controller.ts
Normal file
194
app/javascript/controllers/type_de_champ_editor_controller.ts
Normal file
|
@ -0,0 +1,194 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { ActionEvent } from '@hotwired/stimulus';
|
||||
import { httpRequest } from '@utils';
|
||||
import { useIntersection } from 'stimulus-use';
|
||||
|
||||
import { ApplicationController } from './application_controller';
|
||||
|
||||
export class TypeDeChampEditorController extends ApplicationController {
|
||||
static values = {
|
||||
typeDeChampId: String,
|
||||
moveUrl: String,
|
||||
moveUpUrl: String,
|
||||
moveDownUrl: String
|
||||
};
|
||||
|
||||
declare readonly moveUrlValue: string;
|
||||
declare readonly moveUpUrlValue: string;
|
||||
declare readonly moveDownUrlValue: string;
|
||||
declare readonly typeDeChampIdValue: string;
|
||||
declare readonly isVisible: boolean;
|
||||
|
||||
#latestPromise = Promise.resolve();
|
||||
#dirtyForms: Set<HTMLFormElement> = new Set();
|
||||
#inFlightForms: Map<HTMLFormElement, AbortController> = new Map();
|
||||
|
||||
connect() {
|
||||
useIntersection(this, { threshold: 0.6 });
|
||||
|
||||
this.#latestPromise = Promise.resolve();
|
||||
this.on('change', (event) => this.onChange(event));
|
||||
this.on('input', (event) => this.onInput(event));
|
||||
this.on('sortable:end', (event) =>
|
||||
this.onSortableEnd(event as CustomEvent)
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.#latestPromise = Promise.resolve();
|
||||
for (const [form] of this.#inFlightForms) {
|
||||
this.abortForm(form);
|
||||
}
|
||||
this.#inFlightForms.clear();
|
||||
}
|
||||
|
||||
onMoveButtonClick(event: ActionEvent) {
|
||||
const { direction } = event.params;
|
||||
const action =
|
||||
direction == 'up' ? this.moveUpUrlValue : this.moveDownUrlValue;
|
||||
const form = createForm(action, 'patch');
|
||||
this.requestSubmitForm(form);
|
||||
}
|
||||
|
||||
appear() {
|
||||
this.updateAfterId();
|
||||
}
|
||||
|
||||
private onChange(event: Event) {
|
||||
const target = event.target as HTMLElement & { form?: HTMLFormElement };
|
||||
|
||||
if (
|
||||
target.form &&
|
||||
(isSelectElement(target) || isCheckboxOrRadioInputElement(target))
|
||||
) {
|
||||
this.save(target.form);
|
||||
}
|
||||
}
|
||||
|
||||
private onInput(event: Event) {
|
||||
const target = event.target as HTMLElement & { form?: HTMLFormElement };
|
||||
|
||||
// mark input as touched so we know to not overwrite it's value with next re-render
|
||||
target.setAttribute('data-touched', 'true');
|
||||
|
||||
if (target.form && isTextInputElement(target)) {
|
||||
this.#dirtyForms.add(target.form);
|
||||
this.debounce(this.save, 600);
|
||||
}
|
||||
}
|
||||
|
||||
private onSortableEnd(event: CustomEvent<{ position: number }>) {
|
||||
const position = event.detail.position;
|
||||
if (event.target == this.element) {
|
||||
const form = createForm(this.moveUrlValue, 'patch');
|
||||
createHiddenInput(form, 'position', position);
|
||||
this.requestSubmitForm(form);
|
||||
}
|
||||
}
|
||||
|
||||
private save(form?: HTMLFormElement | null): void {
|
||||
if (form) {
|
||||
createHiddenInput(form, 'should_render', true);
|
||||
} else {
|
||||
this.element.querySelector('input[name="should_render"]')?.remove();
|
||||
}
|
||||
|
||||
this.requestSubmitForm(form);
|
||||
}
|
||||
|
||||
private requestSubmitForm(form?: HTMLFormElement | null) {
|
||||
if (form) {
|
||||
this.submitForm(form);
|
||||
} else {
|
||||
const forms = [...this.#dirtyForms];
|
||||
this.#dirtyForms.clear();
|
||||
|
||||
for (const form of forms) {
|
||||
this.submitForm(form);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private submitForm(form: HTMLFormElement) {
|
||||
const controller = this.abortForm(form);
|
||||
|
||||
this.#latestPromise = this.#latestPromise.finally(() =>
|
||||
httpRequest(form.action, {
|
||||
method: form.getAttribute('method') ?? '',
|
||||
body: new FormData(form),
|
||||
controller: controller
|
||||
})
|
||||
.turbo()
|
||||
.catch(() => null)
|
||||
);
|
||||
}
|
||||
|
||||
private abortForm(form: HTMLFormElement) {
|
||||
const controller = new AbortController();
|
||||
this.#inFlightForms.get(form)?.abort();
|
||||
this.#inFlightForms.set(form, controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private updateAfterId() {
|
||||
const parent = this.element.closest<HTMLElement>(
|
||||
'.editor-block, .editor-root'
|
||||
);
|
||||
if (parent) {
|
||||
const selector = parent.classList.contains('editor-block')
|
||||
? '.add-to-block'
|
||||
: '.add-to-root';
|
||||
const input = parent.querySelector<HTMLInputElement>(
|
||||
`${selector} ${AFTER_ID_INPUT_SELECTOR}`
|
||||
);
|
||||
if (input) {
|
||||
input.value = this.typeDeChampIdValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const AFTER_ID_INPUT_SELECTOR = 'input[name="type_de_champ[after_id]"]';
|
||||
|
||||
function createForm(action: string, method: string) {
|
||||
const form = document.createElement('form');
|
||||
form.action = action;
|
||||
form.method = 'post';
|
||||
createHiddenInput(form, '_method', method);
|
||||
return form;
|
||||
}
|
||||
|
||||
function createHiddenInput(
|
||||
form: HTMLFormElement,
|
||||
name: string,
|
||||
value: unknown
|
||||
) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = String(value);
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
function isSelectElement(element: HTMLElement): element is HTMLSelectElement {
|
||||
return element.tagName == 'SELECT';
|
||||
}
|
||||
|
||||
function isCheckboxOrRadioInputElement(
|
||||
element: HTMLElement & { type?: string }
|
||||
): element is HTMLInputElement {
|
||||
return (
|
||||
element.tagName == 'INPUT' &&
|
||||
(element.type == 'checkbox' || element.type == 'radio')
|
||||
);
|
||||
}
|
||||
|
||||
function isTextInputElement(
|
||||
element: HTMLElement & { type?: string }
|
||||
): element is HTMLInputElement {
|
||||
return (
|
||||
['INPUT', 'TEXTAREA'].includes(element.tagName) &&
|
||||
element.type != 'checkbox' &&
|
||||
element.type != 'radio'
|
||||
);
|
||||
}
|
|
@ -49,6 +49,10 @@ class Champ < ApplicationRecord
|
|||
:dossier_link?,
|
||||
:titre_identite?,
|
||||
:header_section?,
|
||||
:cnaf?,
|
||||
:dgfip?,
|
||||
:pole_emploi?,
|
||||
:mesri?,
|
||||
:siret?,
|
||||
:stable_id,
|
||||
to: :type_de_champ
|
||||
|
|
|
@ -112,7 +112,7 @@ class TypeDeChamp < ApplicationRecord
|
|||
|
||||
before_validation :check_mandatory
|
||||
before_save :remove_piece_justificative_template, if: -> { type_champ_changed? }
|
||||
before_save :remove_drop_down_list, if: -> { type_champ_changed? }
|
||||
before_validation :remove_drop_down_list, if: -> { type_champ_changed? }
|
||||
before_save :remove_repetition, if: -> { type_champ_changed? }
|
||||
|
||||
after_save if: -> { @remove_piece_justificative_template } do
|
||||
|
@ -225,6 +225,22 @@ class TypeDeChamp < ApplicationRecord
|
|||
type_champ == TypeDeChamp.type_champs.fetch(:carte)
|
||||
end
|
||||
|
||||
def cnaf?
|
||||
type_champ == TypeDeChamp.type_champs.fetch(:cnaf)
|
||||
end
|
||||
|
||||
def dgfip?
|
||||
type_champ == TypeDeChamp.type_champs.fetch(:dgfip)
|
||||
end
|
||||
|
||||
def pole_emploi?
|
||||
type_champ == TypeDeChamp.type_champs.fetch(:pole_emploi)
|
||||
end
|
||||
|
||||
def mesri?
|
||||
type_champ == TypeDeChamp.type_champs.fetch(:mesri)
|
||||
end
|
||||
|
||||
def public?
|
||||
!private?
|
||||
end
|
||||
|
@ -233,12 +249,6 @@ class TypeDeChamp < ApplicationRecord
|
|||
"TypesDeChamp::#{type_champ.classify}TypeDeChamp"
|
||||
end
|
||||
|
||||
def piece_justificative_template_url
|
||||
if piece_justificative_template.attached?
|
||||
Rails.application.routes.url_helpers.url_for(piece_justificative_template)
|
||||
end
|
||||
end
|
||||
|
||||
def piece_justificative_template_filename
|
||||
if piece_justificative_template.attached?
|
||||
piece_justificative_template.filename
|
||||
|
@ -298,7 +308,10 @@ class TypeDeChamp < ApplicationRecord
|
|||
end
|
||||
|
||||
def editable_options
|
||||
options.slice(*TypesDeChamp::CarteTypeDeChamp::LAYERS)
|
||||
layers = TypesDeChamp::CarteTypeDeChamp::LAYERS.map do |layer|
|
||||
[layer, layer_enabled?(layer)]
|
||||
end
|
||||
layers.each_slice((layers.size / 2.0).round).to_a
|
||||
end
|
||||
|
||||
def read_attribute_for_serialization(name)
|
||||
|
@ -338,6 +351,12 @@ class TypeDeChamp < ApplicationRecord
|
|||
def remove_drop_down_list
|
||||
if !drop_down_list?
|
||||
self.drop_down_options = nil
|
||||
elsif !drop_down_options_changed?
|
||||
self.drop_down_options = if linked_drop_down_list?
|
||||
['', '--Fromage--', 'bleu de sassenage', 'picodon', '--Dessert--', 'éclair', 'tarte aux pommes']
|
||||
else
|
||||
['', 'Premier choix', 'Deuxième choix']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -7,4 +7,4 @@
|
|||
%h1 Configuration des annotations privées
|
||||
%br
|
||||
|
||||
= react_component("TypesDeChampEditor", types_de_champ_private_data(@procedure))
|
||||
= render TypesDeChampEditor::EditorComponent.new(revision: @procedure.draft_revision, is_annotation: true)
|
||||
|
|
|
@ -7,4 +7,4 @@
|
|||
%h1 Configuration des champs
|
||||
%br
|
||||
|
||||
= react_component("TypesDeChampEditor", types_de_champ_data(@procedure))
|
||||
= render TypesDeChampEditor::EditorComponent.new(revision: @procedure.draft_revision)
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"@rails/activestorage": "^6.1.4-1",
|
||||
"@rails/ujs": "^6.1.4-1",
|
||||
"@rails/webpacker": "5.4.3",
|
||||
"@reach/auto-id": "^0.16.0",
|
||||
"@reach/combobox": "^0.16.5",
|
||||
"@reach/slider": "^0.16.0",
|
||||
"@sentry/browser": "6.12.0",
|
||||
|
@ -39,13 +38,12 @@
|
|||
"react": "^18.0.0",
|
||||
"react-coordinate-input": "^1.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-intersection-observer": "^8.31.0",
|
||||
"react-popper": "^2.2.5",
|
||||
"react-query": "^3.34.19",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"stimulus-use": "^0.50.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"trix": "^1.2.3",
|
||||
"use-debounce": "^5.2.0",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
|
@ -62,6 +60,7 @@
|
|||
"@types/rails__ujs": "^6.0.1",
|
||||
"@types/react": "^17.0.43",
|
||||
"@types/react-dom": "^17.0.14",
|
||||
"@types/sortablejs": "^1.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.8.1",
|
||||
"@typescript-eslint/parser": "^5.8.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
|
|
|
@ -35,12 +35,12 @@ describe 'Creating a new procedure', js: true do
|
|||
visit champs_admin_procedure_path(procedure)
|
||||
|
||||
add_champ(remove_flash_message: true)
|
||||
fill_in 'champ-0-libelle', with: 'libelle de champ'
|
||||
fill_in 'Libellé du champ', with: 'libelle de champ'
|
||||
blur
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
add_champ
|
||||
expect(page).to have_selector('#champ-1-libelle')
|
||||
expect(page).to have_selector('.type-de-champ', count: 1)
|
||||
|
||||
click_on Procedure.last.libelle
|
||||
|
||||
|
@ -56,8 +56,8 @@ describe 'Creating a new procedure', js: true do
|
|||
|
||||
# Add an empty repetition type de champ
|
||||
add_champ(remove_flash_message: true)
|
||||
select('Bloc répétable', from: 'champ-0-type_champ')
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||
select('Bloc répétable', from: 'Type de champ')
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ'
|
||||
blur
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
|
|
|
@ -10,8 +10,7 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
|||
scenario "adding a new champ" do
|
||||
add_champ
|
||||
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||
blur
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ'
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
end
|
||||
|
||||
|
@ -25,21 +24,25 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
|||
expect(page).to have_selector('.type-de-champ', count: 3)
|
||||
|
||||
# Multiple champs can be edited
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ 0'
|
||||
fill_in 'champ-1-libelle', with: 'libellé de champ 1'
|
||||
blur
|
||||
within '.type-de-champ:nth-child(1)' do
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ 0'
|
||||
end
|
||||
within '.type-de-champ:nth-child(2)' do
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ 1'
|
||||
end
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
# Champs can be deleted
|
||||
within '.type-de-champ[data-index="2"]' do
|
||||
within '.type-de-champ:nth-child(3)' do
|
||||
page.accept_alert do
|
||||
click_on 'Supprimer'
|
||||
end
|
||||
end
|
||||
expect(page).not_to have_selector('#champ-2-libelle')
|
||||
expect(page).to have_content('Supprimer', count: 2)
|
||||
|
||||
fill_in 'champ-1-libelle', with: 'edited libellé de champ 1'
|
||||
blur
|
||||
within '.type-de-champ:nth-child(2)' do
|
||||
fill_in 'Libellé du champ', with: 'edited libellé de champ 1'
|
||||
end
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
expect(page).to have_content('Supprimer', count: 2)
|
||||
|
||||
|
@ -50,8 +53,7 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
|||
scenario "removing champs" do
|
||||
add_champ(remove_flash_message: true)
|
||||
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||
blur
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ'
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
page.refresh
|
||||
|
@ -69,32 +71,28 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
|||
scenario "adding an invalid champ" do
|
||||
add_champ(remove_flash_message: true)
|
||||
|
||||
fill_in 'champ-0-libelle', with: ''
|
||||
fill_in 'champ-0-description', with: 'description du champ'
|
||||
blur
|
||||
fill_in 'Libellé du champ', with: ''
|
||||
fill_in 'Description du champ (optionnel)', with: 'description du champ'
|
||||
expect(page).not_to have_content('Formulaire enregistré')
|
||||
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||
blur
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ'
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
end
|
||||
|
||||
scenario "adding a repetition champ" do
|
||||
add_champ(remove_flash_message: true)
|
||||
|
||||
select('Bloc répétable', from: 'champ-0-type_champ')
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||
blur
|
||||
select('Bloc répétable', from: 'Type de champ')
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ'
|
||||
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
page.refresh
|
||||
|
||||
within '.type-de-champ .repetition' do
|
||||
within '.type-de-champ .editor-block' do
|
||||
click_on 'Ajouter un champ'
|
||||
end
|
||||
|
||||
fill_in 'repetition-0-champ-0-libelle', with: 'libellé de champ 1'
|
||||
blur
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ 1'
|
||||
end
|
||||
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
expect(page).to have_content('Supprimer', count: 2)
|
||||
|
@ -103,21 +101,23 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
|||
click_on 'Ajouter un champ'
|
||||
end
|
||||
|
||||
select('Bloc répétable', from: 'champ-0-type_champ')
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ 2'
|
||||
blur
|
||||
within '.type-de-champ:nth-child(2)' do
|
||||
select('Bloc répétable', from: 'Type de champ')
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ 2'
|
||||
end
|
||||
|
||||
expect(page).to have_content('Supprimer', count: 3)
|
||||
end
|
||||
|
||||
scenario "adding a carte champ" do
|
||||
add_champ
|
||||
add_champ(remove_flash_message: true)
|
||||
|
||||
select('Carte', from: 'champ-0-type_champ')
|
||||
fill_in 'champ-0-libelle', with: 'Libellé de champ carte', fill_options: { clear: :backspace }
|
||||
select('Carte', from: 'Type de champ')
|
||||
fill_in 'Libellé du champ', with: 'Libellé de champ carte', fill_options: { clear: :backspace }
|
||||
check 'Cadastres'
|
||||
|
||||
wait_until { procedure.draft_types_de_champ.first.cadastres == true }
|
||||
wait_until { procedure.draft_types_de_champ.first.layer_enabled?(:cadastres) }
|
||||
wait_until { procedure.draft_types_de_champ.first.libelle == 'Libellé de champ carte' }
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
preview_window = window_opened_by { click_on 'Prévisualiser le formulaire' }
|
||||
|
@ -130,11 +130,11 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
|||
end
|
||||
|
||||
scenario "adding a dropdown champ" do
|
||||
add_champ
|
||||
add_champ(remove_flash_message: true)
|
||||
|
||||
select('Choix parmi une liste', from: 'champ-0-type_champ')
|
||||
fill_in 'champ-0-libelle', with: 'Libellé de champ menu déroulant', fill_options: { clear: :backspace }
|
||||
fill_in 'champ-0-drop_down_list_value', with: 'Un menu', fill_options: { clear: :backspace }
|
||||
select('Choix parmi une liste', from: 'Type de champ')
|
||||
fill_in 'Libellé du champ', with: 'Libellé de champ menu déroulant', fill_options: { clear: :backspace }
|
||||
fill_in 'Options de la liste', with: 'Un menu', fill_options: { clear: :backspace }
|
||||
|
||||
wait_until { procedure.draft_types_de_champ.first.drop_down_list_options == ['', 'Un menu'] }
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
@ -146,15 +146,15 @@ describe 'As an administrateur I can edit types de champ', js: true do
|
|||
|
||||
scenario "displaying the estimated fill duration" do
|
||||
# It doesn't display anything when there are no champs
|
||||
expect(page).not_to have_content('Durée de remplissage estimé')
|
||||
expect(page).not_to have_content('Durée de remplissage estimée')
|
||||
|
||||
# It displays the estimate when adding a new champ
|
||||
add_champ
|
||||
select('Pièce justificative', from: 'champ-0-type_champ')
|
||||
expect(page).to have_content('Durée de remplissage estimée : 1 mn')
|
||||
select('Pièce justificative', from: 'Type de champ')
|
||||
expect(page).to have_content('Durée de remplissage estimée : 2 mn')
|
||||
|
||||
# It updates the estimate when updating the champ
|
||||
check 'Obligatoire'
|
||||
check 'Champ obligatoire'
|
||||
expect(page).to have_content('Durée de remplissage estimée : 3 mn')
|
||||
|
||||
# It updates the estimate when removing the champ
|
||||
|
|
|
@ -219,8 +219,8 @@ describe 'fetch API Particulier Data', js: true do
|
|||
visit champs_admin_procedure_path(procedure)
|
||||
|
||||
add_champ
|
||||
select('Données de la Caisse nationale des allocations familiales', from: 'champ-0-type_champ')
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||
select('Données de la Caisse nationale des allocations familiales', from: 'Type de champ')
|
||||
fill_in 'Libellé du champ', with: 'libellé de champ'
|
||||
blur
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
|
@ -279,7 +279,9 @@ describe 'fetch API Particulier Data', js: true do
|
|||
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
|
||||
|
||||
dossier = Dossier.last
|
||||
expect(dossier.champs.first.code_postal).to eq('wrong_code')
|
||||
cnaf_champ = dossier.champs.find(&:cnaf?)
|
||||
|
||||
expect(cnaf_champ.code_postal).to eq('wrong_code')
|
||||
|
||||
click_on 'Déposer le dossier'
|
||||
expect(page).to have_content(/code postal doit posséder 5 caractères/)
|
||||
|
@ -332,7 +334,7 @@ describe 'fetch API Particulier Data', js: true do
|
|||
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
|
||||
|
||||
dossier = Dossier.last
|
||||
pole_emploi_champ = dossier.champs.third
|
||||
pole_emploi_champ = dossier.champs.find(&:pole_emploi?)
|
||||
|
||||
expect(pole_emploi_champ.identifiant).to eq('wrong code')
|
||||
|
||||
|
@ -400,7 +402,7 @@ describe 'fetch API Particulier Data', js: true do
|
|||
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
|
||||
|
||||
dossier = Dossier.last
|
||||
mesri_champ = dossier.champs.fourth
|
||||
mesri_champ = dossier.champs.find(&:mesri?)
|
||||
|
||||
expect(mesri_champ.ine).to eq('wrong code')
|
||||
|
||||
|
@ -442,68 +444,72 @@ describe 'fetch API Particulier Data', js: true do
|
|||
end
|
||||
end
|
||||
|
||||
scenario 'it can fill a DGFiP field' do
|
||||
visit commencer_path(path: procedure.path)
|
||||
click_on 'Commencer la démarche'
|
||||
context 'DGFiP' do
|
||||
scenario 'it can fill a DGFiP field' do
|
||||
visit commencer_path(path: procedure.path)
|
||||
click_on 'Commencer la démarche'
|
||||
|
||||
choose 'Madame'
|
||||
fill_in 'individual_nom', with: 'FERRI'
|
||||
fill_in 'individual_prenom', with: 'Karine'
|
||||
choose 'Madame'
|
||||
fill_in 'individual_nom', with: 'FERRI'
|
||||
fill_in 'individual_prenom', with: 'Karine'
|
||||
|
||||
click_button('Continuer')
|
||||
click_button('Continuer')
|
||||
|
||||
fill_in 'Le numéro fiscal', with: numero_fiscal
|
||||
fill_in "La référence d'avis d'imposition", with: 'wrong_code'
|
||||
fill_in 'Le numéro fiscal', with: numero_fiscal
|
||||
fill_in "La référence d'avis d'imposition", with: 'wrong_code'
|
||||
|
||||
blur
|
||||
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
|
||||
blur
|
||||
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
|
||||
|
||||
dossier = Dossier.last
|
||||
expect(dossier.champs.second.reference_avis).to eq('wrong_code')
|
||||
dossier = Dossier.last
|
||||
dgfip_champ = dossier.champs.find(&:dgfip?)
|
||||
|
||||
click_on 'Déposer le dossier'
|
||||
expect(page).to have_content(/reference avis doit posséder 13 ou 14 caractères/)
|
||||
expect(dgfip_champ.reference_avis).to eq('wrong_code')
|
||||
|
||||
fill_in "La référence d'avis d'imposition", with: reference_avis
|
||||
click_on 'Déposer le dossier'
|
||||
expect(page).to have_content(/reference avis doit posséder 13 ou 14 caractères/)
|
||||
|
||||
VCR.use_cassette('api_particulier/success/avis_imposition') do
|
||||
perform_enqueued_jobs { click_on 'Déposer le dossier' }
|
||||
fill_in "La référence d'avis d'imposition", with: reference_avis
|
||||
|
||||
VCR.use_cassette('api_particulier/success/avis_imposition') do
|
||||
perform_enqueued_jobs { click_on 'Déposer le dossier' }
|
||||
end
|
||||
|
||||
visit demande_dossier_path(dossier)
|
||||
expect(page).to have_content(/Des données.*ont été reçues depuis la DGFiP/)
|
||||
|
||||
log_out
|
||||
|
||||
login_as instructeur.user, scope: :user
|
||||
|
||||
visit instructeur_dossier_path(procedure, dossier)
|
||||
|
||||
expect(page).to have_content('nom FERRI')
|
||||
expect(page).to have_content('nom de naissance FERRI')
|
||||
expect(page).to have_content('prénoms Karine')
|
||||
expect(page).to have_content('date de naissance 12/08/1978')
|
||||
|
||||
expect(page).to have_content('date de recouvrement 09/10/2020')
|
||||
expect(page).to have_content("date d’établissement 07/07/2020")
|
||||
|
||||
expect(page).to have_content('année 2020')
|
||||
expect(page).to have_content("adresse fiscale de l’année passée 13 rue de la Plage 97615 Pamanzi")
|
||||
expect(page).to have_content('nombre de parts 1')
|
||||
expect(page).to have_content('situation familiale Célibataire')
|
||||
expect(page).to have_content('nombre de personnes à charge 0')
|
||||
|
||||
expect(page).to have_content('revenu brut global 38814')
|
||||
expect(page).to have_content('revenu imposable 38814')
|
||||
expect(page).to have_content('impôt sur le revenu net avant correction 38814')
|
||||
expect(page).to have_content("montant de l’impôt 38814")
|
||||
expect(page).to have_content('revenu fiscal de référence 38814')
|
||||
expect(page).to have_content("année d’imposition 2020")
|
||||
expect(page).to have_content('année des revenus 2020')
|
||||
|
||||
expect(page).to have_content('situation partielle SUP DOM')
|
||||
|
||||
expect(page).not_to have_content('erreur correctif')
|
||||
end
|
||||
|
||||
visit demande_dossier_path(dossier)
|
||||
expect(page).to have_content(/Des données.*ont été reçues depuis la DGFiP/)
|
||||
|
||||
log_out
|
||||
|
||||
login_as instructeur.user, scope: :user
|
||||
|
||||
visit instructeur_dossier_path(procedure, dossier)
|
||||
|
||||
expect(page).to have_content('nom FERRI')
|
||||
expect(page).to have_content('nom de naissance FERRI')
|
||||
expect(page).to have_content('prénoms Karine')
|
||||
expect(page).to have_content('date de naissance 12/08/1978')
|
||||
|
||||
expect(page).to have_content('date de recouvrement 09/10/2020')
|
||||
expect(page).to have_content("date d’établissement 07/07/2020")
|
||||
|
||||
expect(page).to have_content('année 2020')
|
||||
expect(page).to have_content("adresse fiscale de l’année passée 13 rue de la Plage 97615 Pamanzi")
|
||||
expect(page).to have_content('nombre de parts 1')
|
||||
expect(page).to have_content('situation familiale Célibataire')
|
||||
expect(page).to have_content('nombre de personnes à charge 0')
|
||||
|
||||
expect(page).to have_content('revenu brut global 38814')
|
||||
expect(page).to have_content('revenu imposable 38814')
|
||||
expect(page).to have_content('impôt sur le revenu net avant correction 38814')
|
||||
expect(page).to have_content("montant de l’impôt 38814")
|
||||
expect(page).to have_content('revenu fiscal de référence 38814')
|
||||
expect(page).to have_content("année d’imposition 2020")
|
||||
expect(page).to have_content('année des revenus 2020')
|
||||
|
||||
expect(page).to have_content('situation partielle SUP DOM')
|
||||
|
||||
expect(page).not_to have_content('erreur correctif')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
54
yarn.lock
54
yarn.lock
|
@ -1092,7 +1092,7 @@
|
|||
"@babel/helper-validator-option" "^7.16.7"
|
||||
"@babel/plugin-transform-typescript" "^7.16.7"
|
||||
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.15.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.15.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
|
||||
version "7.16.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a"
|
||||
integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==
|
||||
|
@ -2017,7 +2017,7 @@
|
|||
webpack-cli "^3.3.12"
|
||||
webpack-sources "^1.4.3"
|
||||
|
||||
"@reach/auto-id@0.16.0", "@reach/auto-id@^0.16.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==
|
||||
|
@ -2538,6 +2538,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc"
|
||||
integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==
|
||||
|
||||
"@types/sortablejs@^1.10.7":
|
||||
version "1.10.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.7.tgz#ab9039c85429f0516955ec6dbc0bb20139417b15"
|
||||
integrity sha512-lGCwwgpj8zW/ZmaueoPVSP7nnc9t8VqVWXS+ASX3eoUUENmiazv0rlXyTRludXzuX9ALjPsMqBu85TgJNWbTOg==
|
||||
|
||||
"@types/yargs-parser@*":
|
||||
version "20.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129"
|
||||
|
@ -7261,6 +7266,11 @@ hosted-git-info@^4.0.1:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
hotkeys-js@>=3:
|
||||
version "3.9.4"
|
||||
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.9.4.tgz#ce1aa4c3a132b6a63a9dd5644fc92b8a9b9cbfb9"
|
||||
integrity sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q==
|
||||
|
||||
hpack.js@^2.1.6:
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
|
||||
|
@ -7611,13 +7621,6 @@ into-stream@^3.1.0:
|
|||
from2 "^2.1.1"
|
||||
p-is-promise "^1.1.0"
|
||||
|
||||
invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
ip-regex@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
|
||||
|
@ -11258,7 +11261,7 @@ promise-inflight@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||
|
||||
prop-types@>=15.0.0, prop-types@^15.5.7, prop-types@^15.7.2:
|
||||
prop-types@>=15.0.0, prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
|
@ -11498,11 +11501,6 @@ react-fast-compare@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||
|
||||
react-intersection-observer@^8.31.0:
|
||||
version "8.33.1"
|
||||
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.33.1.tgz#8e6442cac7052ed63056e191b7539e423e7d5c64"
|
||||
integrity sha512-3v+qaJvp3D1MlGHyM+KISVg/CMhPiOlO6FgPHcluqHkx4YFCLuyXNlQ/LE6UkbODXlQcLOppfX6UMxCEkUhDLw==
|
||||
|
||||
react-is@^16.12.0, react-is@^16.8.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
@ -11530,15 +11528,6 @@ react-query@^3.34.19:
|
|||
broadcast-channel "^3.4.1"
|
||||
match-sorter "^6.0.2"
|
||||
|
||||
react-sortable-hoc@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7"
|
||||
integrity sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.2.0"
|
||||
invariant "^2.2.4"
|
||||
prop-types "^15.5.7"
|
||||
|
||||
react@^18.0.0:
|
||||
version "18.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96"
|
||||
|
@ -12402,6 +12391,11 @@ sort-keys@^2.0.0:
|
|||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
sortablejs@^1.15.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a"
|
||||
integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==
|
||||
|
||||
source-list-map@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||
|
@ -12609,6 +12603,13 @@ statsd-client@0.4.7:
|
|||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||
|
||||
stimulus-use@^0.50.0:
|
||||
version "0.50.0"
|
||||
resolved "https://registry.yarnpkg.com/stimulus-use/-/stimulus-use-0.50.0.tgz#0bae92fbb1fd961cbb23569f9edd12ae642ce4a6"
|
||||
integrity sha512-9NScZQiOycQdzh8VZ15pxk6ep/a22fgha2halOvZFpJITC4nsTbWlO7D1hm+9LspFxa5b28tQhm3XkbH/qAlGw==
|
||||
dependencies:
|
||||
hotkeys-js ">=3"
|
||||
|
||||
stream-browserify@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
|
||||
|
@ -13631,11 +13632,6 @@ url@^0.11.0:
|
|||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
use-debounce@^5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-5.2.1.tgz#7366543c769f1de3e92104dee64de5c4dfddfd33"
|
||||
integrity sha512-BQG5uEypYHd/ASF6imzYR8tJHh5qGn28oZG/5iVAbljV6MUrfyT4jzxA8co+L+WLCT1U8VBwzzvlb3CHmUDpEA==
|
||||
|
||||
use@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
|
|
Loading…
Reference in a new issue