From 0ba0fd5058bc7b7a3f2f5d7fb18406d512bc6bd2 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 1 Sep 2023 10:50:17 +0200 Subject: [PATCH] feat(type_de_champ): insert an add champ button after each type de champ --- .../stylesheets/procedure_champs_editor.scss | 23 +- app/components/application_component.rb | 7 - .../add_champ_button_component.rb | 8 +- .../types_de_champ_editor/champ_component.rb | 3 +- .../champ_component/champ_component.html.haml | 219 +++++++++--------- .../type_de_champ_editor_controller.ts | 28 --- .../procedure_revision_type_de_champ.rb | 4 + .../types_de_champ/_insert.turbo_stream.haml | 5 + 8 files changed, 140 insertions(+), 157 deletions(-) diff --git a/app/assets/stylesheets/procedure_champs_editor.scss b/app/assets/stylesheets/procedure_champs_editor.scss index 72e8cf4bb..e788ec727 100644 --- a/app/assets/stylesheets/procedure_champs_editor.scss +++ b/app/assets/stylesheets/procedure_champs_editor.scss @@ -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 { diff --git a/app/components/application_component.rb b/app/components/application_component.rb index 3f32fe9e6..2cfb80769 100644 --- a/app/components/application_component.rb +++ b/app/components/application_component.rb @@ -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 diff --git a/app/components/types_de_champ_editor/add_champ_button_component.rb b/app/components/types_de_champ_editor/add_champ_button_component.rb index ad23aaf41..ac1c28af0 100644 --- a/app/components/types_de_champ_editor/add_champ_button_component.rb +++ b/app/components/types_de_champ_editor/add_champ_button_component.rb @@ -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 } } diff --git a/app/components/types_de_champ_editor/champ_component.rb b/app/components/types_de_champ_editor/champ_component.rb index 6fe4fb9a1..d2e6e39b5 100644 --- a/app/components/types_de_champ_editor/champ_component.rb +++ b/app/components/types_de_champ_editor/champ_component.rb @@ -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 diff --git a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml index b2dd9f91b..56da3b30e 100644 --- a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml +++ b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml @@ -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 lorsqu’aucun 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 lorsqu’aucun de vos titres ne commence par un chiffre. - - if type_de_champ.titre_identite? - %p Dans le cadre de la RGPD, le titre d’identité sera supprimé lors de l’acceptation 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 d’identité sera supprimé lors de l’acceptation 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) diff --git a/app/javascript/controllers/type_de_champ_editor_controller.ts b/app/javascript/controllers/type_de_champ_editor_controller.ts index 9f9a0f01e..6f6af508d 100644 --- a/app/javascript/controllers/type_de_champ_editor_controller.ts +++ b/app/javascript/controllers/type_de_champ_editor_controller.ts @@ -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 = 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( - '.editor-block, .editor-root' - ); - if (parent) { - const selector = parent.classList.contains('editor-block') - ? '.add-to-block' - : '.add-to-root'; - const input = parent.querySelector( - `${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; diff --git a/app/models/procedure_revision_type_de_champ.rb b/app/models/procedure_revision_type_de_champ.rb index b5396351e..9e7b8464d 100644 --- a/app/models/procedure_revision_type_de_champ.rb +++ b/app/models/procedure_revision_type_de_champ.rb @@ -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 diff --git a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml index fe1769549..fee164d69 100644 --- a/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml +++ b/app/views/administrateurs/types_de_champ/_insert.turbo_stream.haml @@ -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)