feat(type_de_champ): insert an add champ button after each type de champ

This commit is contained in:
Paul Chavard 2023-09-01 10:50:17 +02:00
parent 79224569a2
commit 0ba0fd5058
8 changed files with 140 additions and 157 deletions

View file

@ -14,13 +14,18 @@
.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;
margin-bottom: $default-padding;
overflow: hidden;
.type-de-champ-container {
width: 100%;
background-color: #FAFDFF;
border: 1px solid $border-grey;
border-radius: 5px;
margin-bottom: $default-padding;
box-shadow: 0px 2px 4px -4px;
}
.handle.icon {
width: 32px;
height: 32px;
@ -71,6 +76,10 @@
display: none;
}
&.last .type-de-champ-add-button.root {
display: none;
}
.head {
background-color: #FAFDFF;
@ -91,10 +100,6 @@
&.section {
padding: $default-spacer $default-spacer 0;
margin-bottom: 8px;
input {
background-color: $white;
}
}
&.hr {

View file

@ -2,13 +2,6 @@ class ApplicationComponent < ViewComponent::Base
include ViewComponent::Translatable
include FlipperHelper
# Takes a Hash of { class_name: boolean }.
# Returns truthy class names in an array. Array can be passed as-it in rails helpers,
# and is still manipulable if needed.
def class_names(class_names)
class_names.filter { _2 }.keys
end
def current_user
controller.current_user
end

View file

@ -1,8 +1,9 @@
class TypesDeChampEditor::AddChampButtonComponent < ApplicationComponent
def initialize(revision:, parent: nil, is_annotation: false)
def initialize(revision:, parent: nil, is_annotation: false, after_stable_id: nil)
@revision = revision
@parent = parent
@is_annotation = is_annotation
@after_stable_id = after_stable_id
end
private
@ -25,8 +26,7 @@ class TypesDeChampEditor::AddChampButtonComponent < ApplicationComponent
def button_options
{
class: "button",
form: { class: @parent ? "add-to-block" : "add-to-root" },
class: "fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-add-line",
method: :post,
params: {
type_de_champ: {
@ -34,7 +34,7 @@ class TypesDeChampEditor::AddChampButtonComponent < ApplicationComponent
type_champ: TypeDeChamp.type_champs.fetch(:text),
private: annotations? ? true : nil,
parent_stable_id: @parent&.stable_id,
after_stable_id: ''
after_stable_id: @after_stable_id
}.compact
}
}

View file

@ -30,8 +30,7 @@ class TypesDeChampEditor::ChampComponent < ApplicationComponent
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_stable_id_value: 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)
}
}
end

View file

@ -1,117 +1,122 @@
%li.type-de-champ.flex.column.justify-start{ html_options }
.flex.justify-between.section.head.hr
.handle.small.icon-only.icon.move-handle{ title: "Déplacer le champ vers le haut ou vers le bas" }
.type-de-champ-container
.flex.justify-between.section.head.hr
.handle.small.icon-only.icon.move-handle{ title: "Déplacer le champ vers le haut ou vers le bas" }
- if coordinate.used_by_routing_rules?
.flex.align-center
%span
utilisé pour
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
- else
.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
- if coordinate.used_by_routing_rules?
.flex.align-center
%span
utilisé pour
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
- else
.flex.justify-start.delete
= button_to type_de_champ_path, class: 'fr-btn fr-btn--sm fr-btn--secondary fr-icon-delete-line', title: "Supprimer le champ", method: :delete, form: { data: { turbo_confirm: 'Êtes vous sûr de vouloir supprimer ce champ ?' } } do
.icon.delete
%span.sr-only Supprimer
- if @errors.present?
.types-de-champ-errors
= @errors
- if @errors.present?
.types-de-champ-errors
= @errors
.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, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules?
.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?
%p
%small Nous numérotons automatiquement les titres lorsquaucun de vos titres ne commence par un chiffre.
- 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)
- if type_de_champ.header_section?
.cell.mt-1
= render TypesDeChampEditor::HeaderSectionComponent.new(form: form, tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ))
.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.simple_drop_down_list?
.cell
= form.label :drop_down_other, for: dom_id(type_de_champ, :drop_down_other) do
Proposer une option « autre » avec un texte libre
= form.check_box :drop_down_other, class: "small-margin small", id: dom_id(type_de_champ, :drop_down_other)
- if type_de_champ.linked_drop_down_list?
.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, grouped_options_for_select(types_of_type_de_champ, type_de_champ.type_champ), {}, class: 'fr-select small-margin small inline width-100', id: dom_id(type_de_champ, :type_champ), disabled: coordinate.used_by_routing_rules?
.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_template_options)
.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: 'fr-input small-margin small width-100', id: dom_id(type_de_champ, :libelle), data: input_autofocus
- if type_de_champ.header_section?
%p
%small Nous numérotons automatiquement les titres lorsquaucun de vos titres ne commence par un chiffre.
- if type_de_champ.titre_identite?
%p Dans le cadre de la RGPD, le titre didentité sera supprimé lors de lacceptation du dossier
- elsif procedure.piece_justificative_multiple?
%p Les usagers pourront envoyer plusieurs fichiers si nécessaire.
- if type_de_champ.carte?
- type_de_champ.editable_options.each do |slice|
- 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: 'fr-input small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :description)
- if type_de_champ.header_section?
.cell.mt-1
= render TypesDeChampEditor::HeaderSectionComponent.new(form: form, tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ))
.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: 'fr-input small-margin small width-100', rows: 7, id: dom_id(type_de_champ, :drop_down_list_value)
- if type_de_champ.simple_drop_down_list?
.cell
= form.label :drop_down_other, for: dom_id(type_de_champ, :drop_down_other) do
Proposer une option « autre » avec un texte libre
= form.check_box :drop_down_other, class: "small-margin small", id: dom_id(type_de_champ, :drop_down_other)
- 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: 'fr-input 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: 'fr-input small-margin small width-100', rows: 3, id: dom_id(type_de_champ, :drop_down_secondary_description)
- if type_de_champ.piece_justificative?
.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.explication?
.cell.width-66
= form.label :collapsible_explanation_enabled, for: dom_id(type_de_champ, :collapsible_explanation_enabled) do
Afficher un texte complementaire affichable au clic
= form.check_box :collapsible_explanation_enabled, class: "small-margin small", id: dom_id(type_de_champ, :collapsible_explanation_enabled)
- if form.object.collapsible_explanation_enabled?
= form.label :collapsible_explanation_text, for: dom_id(type_de_champ, :collapsible_explanation_text) do
= "Texte à afficher quand l'utiliser a choisi de l'afficher"
= form.text_area :collapsible_explanation_text, class: "small-margin small", id: dom_id(type_de_champ, :collapsible_explanation_text)
- if type_de_champ.textarea?
.cell
= form.label :character_limit, for: dom_id(type_de_champ, :character_limit) do
Spécifier un nombre maximal conseillé de caractères :
= form.select :character_limit, options_for_character_limit, id: dom_id(type_de_champ, :character_limit)
= form.label :piece_justificative_template, "Modèle", for: dom_id(type_de_champ, :piece_justificative_template)
= render Attachment::EditComponent.new(**piece_justificative_template_options)
- if type_de_champ.block?
.flex.justify-start.section.ml-1
.editor-block.flex-grow.cell
= render TypesDeChampEditor::BlockComponent.new(block: coordinate, coordinates: coordinate.revision_types_de_champ, upper_coordinates: @upper_coordinates)
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate, is_annotation: coordinate.private?)
- if type_de_champ.titre_identite?
%p Dans le cadre de la RGPD, le titre didentité sera supprimé lors de lacceptation du dossier
- elsif procedure.piece_justificative_multiple?
%p Les usagers pourront envoyer plusieurs fichiers si nécessaire.
- 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.explication?
.cell.width-66
= form.label :collapsible_explanation_enabled, for: dom_id(type_de_champ, :collapsible_explanation_enabled) do
Afficher un texte complementaire affichable au clic
= form.check_box :collapsible_explanation_enabled, class: "small-margin small", id: dom_id(type_de_champ, :collapsible_explanation_enabled)
- if form.object.collapsible_explanation_enabled?
= form.label :collapsible_explanation_text, for: dom_id(type_de_champ, :collapsible_explanation_text) do
= "Texte à afficher quand l'utiliser a choisi de l'afficher"
= form.text_area :collapsible_explanation_text, class: "fr-input small-margin small", id: dom_id(type_de_champ, :collapsible_explanation_text)
- if type_de_champ.textarea?
.cell
= form.label :character_limit, for: dom_id(type_de_champ, :character_limit) do
Spécifier un nombre maximal conseillé de caractères :
= form.select :character_limit, options_for_character_limit, id: dom_id(type_de_champ, :character_limit), class: 'fr-select'
- if conditional_enabled?
= render(TypesDeChampEditor::ConditionsComponent.new(tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ), procedure_id: procedure.id))
- if type_de_champ.block?
.flex.justify-start.section.ml-1
.editor-block.flex-grow.cell
= render TypesDeChampEditor::BlockComponent.new(block: coordinate, coordinates: coordinate.revision_types_de_champ, upper_coordinates: @upper_coordinates)
.type-de-champ-add-button{ id: dom_id(coordinate, :type_de_champ_add_button), class: class_names(hidden: !coordinate.empty?) }
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate, is_annotation: coordinate.private?)
- if conditional_enabled?
= render(TypesDeChampEditor::ConditionsComponent.new(tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ), procedure_id: procedure.id))
.type-de-champ-add-button{ class: class_names(root: !coordinate.child?) }
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate&.parent, is_annotation: coordinate.private?, after_stable_id: type_de_champ.stable_id)

View file

@ -7,7 +7,6 @@ import {
isTextInputElement,
getConfig
} from '@utils';
import { useIntersection } from 'stimulus-use';
import { AutoUpload } from '../shared/activestorage/auto-upload';
import { ApplicationController } from './application_controller';
@ -28,7 +27,6 @@ export class TypeDeChampEditorController extends ApplicationController {
declare readonly moveUrlValue: string;
declare readonly moveUpUrlValue: string;
declare readonly moveDownUrlValue: string;
declare readonly typeDeChampStableIdValue: string;
declare readonly isVisible: boolean;
#latestPromise = Promise.resolve();
@ -36,8 +34,6 @@ export class TypeDeChampEditorController extends ApplicationController {
#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));
@ -62,10 +58,6 @@ export class TypeDeChampEditorController extends ApplicationController {
this.requestSubmitForm(form);
}
appear() {
this.updateAfterId();
}
private onChange(event: Event) {
const target = event.target as HTMLElement & { form?: HTMLFormElement };
@ -144,28 +136,8 @@ export class TypeDeChampEditorController extends ApplicationController {
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_STABLE_ID_INPUT_SELECTOR}`
);
if (input) {
input.value = this.typeDeChampStableIdValue;
}
}
}
}
const AFTER_STABLE_ID_INPUT_SELECTOR =
'input[name="type_de_champ[after_stable_id]"]';
function createForm(action: string, method: string) {
const form = document.createElement('form');
form.action = action;

View file

@ -25,6 +25,10 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
siblings.last == self
end
def empty?
revision_types_de_champ.empty?
end
def siblings
if parent_id.present?
revision.revision_types_de_champ.where(parent_id: parent_id).ordered

View file

@ -26,3 +26,8 @@
- render TypesDeChampEditor::EstimatedFillDurationComponent.new(revision: @coordinate.revision, is_annotation: @coordinate.private?)
= turbo_stream.dispatch 'sortable:sort'
- if @created&.coordinate&.child?
= turbo_stream.hide dom_id(@created.coordinate.parent, :type_de_champ_add_button)
- elsif @destroyed&.child? && @destroyed.parent.empty?
= turbo_stream.show dom_id(@destroyed.parent, :type_de_champ_add_button)