Merge pull request #9445 from tchak/feat-add-champ-buttons

feat(type_de_champ): insert an add champ button after each type de champ
This commit is contained in:
mfo 2023-09-04 07:55:41 +00:00 committed by GitHub
commit 70b57257eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 140 additions and 157 deletions

View file

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

View file

@ -2,13 +2,6 @@ class ApplicationComponent < ViewComponent::Base
include ViewComponent::Translatable include ViewComponent::Translatable
include FlipperHelper 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 def current_user
controller.current_user controller.current_user
end end

View file

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

View file

@ -30,8 +30,7 @@ class TypesDeChampEditor::ChampComponent < ApplicationComponent
controller: 'type-de-champ-editor', 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_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_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_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
} }
} }
end end

View file

@ -1,117 +1,122 @@
%li.type-de-champ.flex.column.justify-start{ html_options } %li.type-de-champ.flex.column.justify-start{ html_options }
.flex.justify-between.section.head.hr .type-de-champ-container
.handle.small.icon-only.icon.move-handle{ title: "Déplacer le champ vers le haut ou vers le bas" } .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? - if coordinate.used_by_routing_rules?
.flex.align-center .flex.align-center
%span %span
utilisé pour utilisé pour
= link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules')) = link_to('le routage', admin_procedure_groupe_instructeurs_path(revision.procedure_id, anchor: 'routing-rules'))
- else - else
.flex.justify-start.delete .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 = 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 .icon.delete
%span.sr-only Supprimer %span.sr-only Supprimer
- if @errors.present? - if @errors.present?
.types-de-champ-errors .types-de-champ-errors
= @errors = @errors
.flex.justify-start.section.ml-1 .flex.justify-start.section.ml-1
= form_for(type_de_champ, form_options) do |form| = form_for(type_de_champ, form_options) do |form|
.flex.justify-start .flex.justify-start
.flex.justify-start.width-33 .flex.justify-start.width-33
.flex.justify-start.column .flex.justify-start.column
%button.move-up.cell.mb-1{ move_button_options(:up) } %button.move-up.cell.mb-1{ move_button_options(:up) }
.icon.arrow-up.small .icon.arrow-up.small
%span.sr-only Déplacer le champ vers le haut %span.sr-only Déplacer le champ vers le haut
%button.move-down.cell{ move_button_options(:down) } %button.move-down.cell{ move_button_options(:down) }
.icon.arrow-down.small .icon.arrow-down.small
%span.sr-only Déplacer le champ vers le bas %span.sr-only Déplacer le champ vers le bas
.cell.flex.justify-start.column.flex-grow .cell.flex.justify-start.column.flex-grow
= form.label :type_champ, "Type de champ", for: dom_id(type_de_champ, :type_champ) = 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? = 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
.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.column.justify-start.flex-grow .flex.column.justify-start.flex-grow
.cell .cell
= form.label :drop_down_secondary_libelle, "Libellé du champ secondaire", class: 'flex-grow', for: dom_id(type_de_champ, :drop_down_secondary_libelle) .flex.align-center
= form.text_field :drop_down_secondary_libelle, class: 'small-margin small width-100', id: dom_id(type_de_champ, :drop_down_secondary_libelle) = form.label :libelle, "Libellé du champ", class: 'flex-grow', for: dom_id(type_de_champ, :libelle)
.cell.mt-1 - if can_be_mandatory?
= form.label :drop_down_secondary_description, "Description du champ secondaire (optionnel)", for: dom_id(type_de_champ, :drop_down_secondary_description) .cell.flex.align-center
= 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) = form.check_box :mandatory, class: 'small-margin small', id: dom_id(type_de_champ, :mandatory)
- if type_de_champ.piece_justificative? = form.label :mandatory, "Champ obligatoire", for: dom_id(type_de_champ, :mandatory)
.cell = form.text_field :libelle, class: 'fr-input small-margin small width-100', id: dom_id(type_de_champ, :libelle), data: input_autofocus
= form.label :piece_justificative_template, "Modèle", for: dom_id(type_de_champ, :piece_justificative_template) - if type_de_champ.header_section?
= render Attachment::EditComponent.new(**piece_justificative_template_options) %p
%small Nous numérotons automatiquement les titres lorsquaucun de vos titres ne commence par un chiffre.
- if type_de_champ.titre_identite? - if !type_de_champ.header_section? && !type_de_champ.titre_identite?
%p Dans le cadre de la RGPD, le titre didentité sera supprimé lors de lacceptation du dossier .cell.mt-1
- elsif procedure.piece_justificative_multiple? = form.label :description, "Description du champ (optionnel)", for: dom_id(type_de_champ, :description)
%p Les usagers pourront envoyer plusieurs fichiers si nécessaire. = 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.carte? - if type_de_champ.header_section?
- type_de_champ.editable_options.each do |slice| .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 .cell
.carte-options = form.label :piece_justificative_template, "Modèle", for: dom_id(type_de_champ, :piece_justificative_template)
= form.fields_for :editable_options do |form| = render Attachment::EditComponent.new(**piece_justificative_template_options)
- 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)
- if type_de_champ.block? - if type_de_champ.titre_identite?
.flex.justify-start.section.ml-1 %p Dans le cadre de la RGPD, le titre didentité sera supprimé lors de lacceptation du dossier
.editor-block.flex-grow.cell - elsif procedure.piece_justificative_multiple?
= render TypesDeChampEditor::BlockComponent.new(block: coordinate, coordinates: coordinate.revision_types_de_champ, upper_coordinates: @upper_coordinates) %p Les usagers pourront envoyer plusieurs fichiers si nécessaire.
= render TypesDeChampEditor::AddChampButtonComponent.new(revision: coordinate.revision, parent: coordinate, is_annotation: coordinate.private?) - 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? - if type_de_champ.block?
= render(TypesDeChampEditor::ConditionsComponent.new(tdc: type_de_champ, upper_tdcs: @upper_coordinates.map(&:type_de_champ), procedure_id: procedure.id)) .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, isTextInputElement,
getConfig getConfig
} from '@utils'; } from '@utils';
import { useIntersection } from 'stimulus-use';
import { AutoUpload } from '../shared/activestorage/auto-upload'; import { AutoUpload } from '../shared/activestorage/auto-upload';
import { ApplicationController } from './application_controller'; import { ApplicationController } from './application_controller';
@ -28,7 +27,6 @@ export class TypeDeChampEditorController extends ApplicationController {
declare readonly moveUrlValue: string; declare readonly moveUrlValue: string;
declare readonly moveUpUrlValue: string; declare readonly moveUpUrlValue: string;
declare readonly moveDownUrlValue: string; declare readonly moveDownUrlValue: string;
declare readonly typeDeChampStableIdValue: string;
declare readonly isVisible: boolean; declare readonly isVisible: boolean;
#latestPromise = Promise.resolve(); #latestPromise = Promise.resolve();
@ -36,8 +34,6 @@ export class TypeDeChampEditorController extends ApplicationController {
#inFlightForms: Map<HTMLFormElement, AbortController> = new Map(); #inFlightForms: Map<HTMLFormElement, AbortController> = new Map();
connect() { connect() {
useIntersection(this, { threshold: 0.6 });
this.#latestPromise = Promise.resolve(); this.#latestPromise = Promise.resolve();
this.on('change', (event) => this.onChange(event)); this.on('change', (event) => this.onChange(event));
this.on('input', (event) => this.onInput(event)); this.on('input', (event) => this.onInput(event));
@ -62,10 +58,6 @@ export class TypeDeChampEditorController extends ApplicationController {
this.requestSubmitForm(form); this.requestSubmitForm(form);
} }
appear() {
this.updateAfterId();
}
private onChange(event: Event) { private onChange(event: Event) {
const target = event.target as HTMLElement & { form?: HTMLFormElement }; const target = event.target as HTMLElement & { form?: HTMLFormElement };
@ -144,28 +136,8 @@ export class TypeDeChampEditorController extends ApplicationController {
this.#inFlightForms.set(form, controller); this.#inFlightForms.set(form, controller);
return 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) { function createForm(action: string, method: string) {
const form = document.createElement('form'); const form = document.createElement('form');
form.action = action; form.action = action;

View file

@ -25,6 +25,10 @@ class ProcedureRevisionTypeDeChamp < ApplicationRecord
siblings.last == self siblings.last == self
end end
def empty?
revision_types_de_champ.empty?
end
def siblings def siblings
if parent_id.present? if parent_id.present?
revision.revision_types_de_champ.where(parent_id: parent_id).ordered 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?) - render TypesDeChampEditor::EstimatedFillDurationComponent.new(revision: @coordinate.revision, is_annotation: @coordinate.private?)
= turbo_stream.dispatch 'sortable:sort' = 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)