feat(type_de_champ): type_de_champ editor in stimulus/turbo

This commit is contained in:
Paul Chavard 2022-06-16 14:56:53 +02:00 committed by Paul Chavard
parent 1573d20ee9
commit 6801b04b7b
34 changed files with 898 additions and 248 deletions

View 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

View 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

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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

View file

@ -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) }
}

View file

@ -64,8 +64,6 @@ class Dossiers::MessageComponent < ApplicationComponent
end
end
private
def highlight?
commentaire.created_at.present? && @messagerie_seen_at&.<(commentaire.created_at)
end

View file

@ -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

View file

@ -0,0 +1 @@
= button_to(button_title, admin_procedure_types_de_champ_path(procedure), button_options)

View 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

View file

@ -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)

View 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

View file

@ -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 dimportance internationale
znieff: ZNIEFF

View file

@ -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 didentité sera supprimé lors de lacceptation 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?)

View 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

View file

@ -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?)

View file

@ -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

View file

@ -0,0 +1,3 @@
en:
estimated_fill_duration: "Estimated fill time:"
estimated_fill_minutes: "%{estimated_minutes} mn"

View file

@ -0,0 +1,3 @@
fr:
estimated_fill_duration: "Durée de remplissage estimée :"
estimated_fill_minutes: "%{estimated_minutes} mn"

View file

@ -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)

View 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);
}
}

View file

@ -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);

View 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
};
}
}

View 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'
);
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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",

View file

@ -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é')

View file

@ -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

View file

@ -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 lanné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 limpôt 38814")
expect(page).to have_content('revenu fiscal de référence 38814')
expect(page).to have_content("année dimposition 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 lanné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 limpôt 38814")
expect(page).to have_content('revenu fiscal de référence 38814')
expect(page).to have_content("année dimposition 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

View file

@ -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"