From 9b7b59f67e0491c7815cc97add330f64bced4194 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 17 Jan 2019 15:15:46 +0100 Subject: [PATCH 1/4] Champs editor should handle repetition type --- app/assets/stylesheets/new_design/flex.scss | 8 +++ .../new_design/procedure_champs_editor.scss | 19 +------ .../procedures_controller.rb | 11 ++-- app/helpers/procedure_helper.rb | 13 +++-- .../administrateur/DraggableItem.js | 40 +++++++++++++-- .../administrateur/DraggableItem.vue | 46 +++++++++++++---- .../administrateur/DraggableList.js | 1 + .../administrateur/DraggableList.vue | 1 - .../administrateur/champs-editor.js | 3 +- app/models/type_de_champ.rb | 4 +- .../procedures/update.js.erb | 2 +- .../features/admin/procedure_creation_spec.rb | 4 +- .../new_administrateur/procedures_spec.rb | 51 +++++++++++++++---- 13 files changed, 148 insertions(+), 55 deletions(-) diff --git a/app/assets/stylesheets/new_design/flex.scss b/app/assets/stylesheets/new_design/flex.scss index e61a57611..317ebf55c 100644 --- a/app/assets/stylesheets/new_design/flex.scss +++ b/app/assets/stylesheets/new_design/flex.scss @@ -28,4 +28,12 @@ &.wrap { flex-wrap: wrap; } + + &.column { + flex-direction: column; + } +} + +.flex-grow { + flex-grow: 1; } diff --git a/app/assets/stylesheets/new_design/procedure_champs_editor.scss b/app/assets/stylesheets/new_design/procedure_champs_editor.scss index 5e1241f38..daa960e59 100644 --- a/app/assets/stylesheets/new_design/procedure_champs_editor.scss +++ b/app/assets/stylesheets/new_design/procedure_champs_editor.scss @@ -9,10 +9,6 @@ } .draggable-item { - display: flex; - flex-direction: column; - justify-content: flex-start; - border: 1px solid $border-grey; border-radius: 5px; margin-bottom: 10px; @@ -55,20 +51,7 @@ } } - .column { - display: flex; - justify-content: flex-start; - flex-direction: column; - - &.shift-left { - margin-left: 35px; - } - } - - .row { - display: flex; - justify-content: flex-start; - + .flex { &.section { padding: 10px 10px 0 10px; margin-bottom: 8px; diff --git a/app/controllers/new_administrateur/procedures_controller.rb b/app/controllers/new_administrateur/procedures_controller.rb index 02566fe80..ff4734397 100644 --- a/app/controllers/new_administrateur/procedures_controller.rb +++ b/app/controllers/new_administrateur/procedures_controller.rb @@ -3,7 +3,7 @@ module NewAdministrateur before_action :retrieve_procedure, only: [:champs, :annotations, :update] before_action :procedure_locked?, only: [:champs, :annotations, :update] - TYPE_DE_CHAMP_ATTRIBUTES = [ + TYPE_DE_CHAMP_ATTRIBUTES_BASE = [ :_destroy, :libelle, :description, @@ -18,6 +18,11 @@ module NewAdministrateur drop_down_list_attributes: [:value] ] + TYPE_DE_CHAMP_ATTRIBUTES = TYPE_DE_CHAMP_ATTRIBUTES_BASE.dup + TYPE_DE_CHAMP_ATTRIBUTES << { + types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES_BASE + } + def apercu @dossier = procedure_without_control.new_dossier @tab = apercu_tab @@ -26,9 +31,9 @@ module NewAdministrateur def update if @procedure.update(procedure_params) flash.now.notice = if params[:procedure][:types_de_champ_attributes].present? - 'Champs enregistrés' + 'Formulaire mis à jour.' elsif params[:procedure][:types_de_champ_private_attributes].present? - 'Annotations enregistrés' + 'Annotations privées mises à jour.' else 'Démarche enregistrée.' end diff --git a/app/helpers/procedure_helper.rb b/app/helpers/procedure_helper.rb index c6d6db847..443626b8e 100644 --- a/app/helpers/procedure_helper.rb +++ b/app/helpers/procedure_helper.rb @@ -79,9 +79,16 @@ module ProcedureHelper types_de_champ end + TYPES_DE_CHAMP_INCLUDE = { drop_down_list: { only: :value } } + TYPES_DE_CHAMP_BASE = { + except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private], + methods: [:piece_justificative_template_filename, :piece_justificative_template_url], + include: TYPES_DE_CHAMP_INCLUDE + } + TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE + .merge(include: TYPES_DE_CHAMP_INCLUDE.merge(types_de_champ: TYPES_DE_CHAMP_BASE)) + def types_de_champ_as_json(types_de_champ) - types_de_champ.as_json(except: [:created_at, :updated_at], - methods: [:piece_justificative_template_filename, :piece_justificative_template_url], - include: { drop_down_list: { only: :value } }) + types_de_champ.as_json(TYPES_DE_CHAMP) end end diff --git a/app/javascript/new_design/administrateur/DraggableItem.js b/app/javascript/new_design/administrateur/DraggableItem.js index 70dee126f..e1ff2a357 100644 --- a/app/javascript/new_design/administrateur/DraggableItem.js +++ b/app/javascript/new_design/administrateur/DraggableItem.js @@ -1,5 +1,5 @@ export default { - props: ['state', 'update', 'index', 'item', 'prefix'], + props: ['state', 'update', 'index', 'item'], computed: { isDirty() { return ( @@ -56,6 +56,9 @@ export default { isHeaderSection() { return this.typeChamp === 'header_section'; }, + isRepetition() { + return this.typeChamp === 'repetition'; + }, options() { const options = this.item.options || {}; for (let key of Object.keys(options)) { @@ -69,6 +72,21 @@ export default { } else { return 'types_de_champ_attributes'; } + }, + typesDeChamp() { + return this.item.types_de_champ; + }, + typesDeChampOptions() { + return this.state.typesDeChampOptions.filter( + ([, typeChamp]) => !EXCLUDE_FROM_REPETITION.includes(typeChamp) + ); + }, + stateForRepetition() { + return Object.assign({}, this.state, { + typesDeChamp: this.typesDeChamp, + typesDeChampOptions: this.typesDeChampOptions, + prefix: `${this.state.prefix}[${this.attribute}][${this.index}]` + }); } }, data() { @@ -103,14 +121,30 @@ export default { } }, nameFor(name) { - return `${this.prefix}[${this.attribute}][${this.index}][${name}]`; + return `${this.state.prefix}[${this.attribute}][${this.index}][${name}]`; }, elementIdFor(name) { - return `${this.prefix}_${this.attribute}_${this.index}_${name}`; + const prefix = this.state.prefix.replace(/\[/g, '_').replace(/\]/g, ''); + return `${prefix}_${this.attribute}_${this.index}_${name}`; + }, + addChamp() { + this.typesDeChamp.push({ + type_champ: 'text', + drop_down_list: {}, + types_de_champ: [], + options: {} + }); } } }; +const EXCLUDE_FROM_REPETITION = [ + 'carte', + 'dossier_link', + 'repetition', + 'siret' +]; + const PATHS_TO_WATCH = [ 'typeChamp', 'libelle', diff --git a/app/javascript/new_design/administrateur/DraggableItem.vue b/app/javascript/new_design/administrateur/DraggableItem.vue index 93849ed8d..fbb70a5c8 100644 --- a/app/javascript/new_design/administrateur/DraggableItem.vue +++ b/app/javascript/new_design/administrateur/DraggableItem.vue @@ -4,19 +4,23 @@ -
-
+
+
- +
-
+
Le libellé doit être rempli. @@ -36,8 +40,8 @@
-
-
+
+
-
+
-
+
+
+ + + + + +
diff --git a/app/javascript/new_design/administrateur/DraggableList.js b/app/javascript/new_design/administrateur/DraggableList.js index b680daa52..f28746ffe 100644 --- a/app/javascript/new_design/administrateur/DraggableList.js +++ b/app/javascript/new_design/administrateur/DraggableList.js @@ -5,6 +5,7 @@ export default { this.state.typesDeChamp.push({ type_champ: 'text', drop_down_list: {}, + types_de_champ: [], options: {} }); } diff --git a/app/javascript/new_design/administrateur/DraggableList.vue b/app/javascript/new_design/administrateur/DraggableList.vue index 7832e565b..361d1b098 100644 --- a/app/javascript/new_design/administrateur/DraggableList.vue +++ b/app/javascript/new_design/administrateur/DraggableList.vue @@ -16,7 +16,6 @@ { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', dependent: :destroy store_accessor :options, :cadastres, :quartiers_prioritaires, :parcelles_agricoles, :old_pj @@ -84,7 +84,7 @@ class TypeDeChamp < ApplicationRecord has_one_attached :piece_justificative_template accepts_nested_attributes_for :drop_down_list, update_only: true - accepts_nested_attributes_for :types_de_champ, allow_destroy: true + accepts_nested_attributes_for :types_de_champ, reject_if: proc { |attributes| attributes['libelle'].blank? }, allow_destroy: true validates :libelle, presence: true, allow_blank: false, allow_nil: false validates :type_champ, presence: true, allow_blank: false, allow_nil: false diff --git a/app/views/new_administrateur/procedures/update.js.erb b/app/views/new_administrateur/procedures/update.js.erb index 4aa3bae04..d8423e18a 100644 --- a/app/views/new_administrateur/procedures/update.js.erb +++ b/app/views/new_administrateur/procedures/update.js.erb @@ -1,3 +1,3 @@ <%= render_flash timeout: 6000, fixed: true %> -<%= fire_event(:ProcedureUpdated, procedure_data(@procedure)) %> +<%= fire_event(:ProcedureUpdated, procedure_data(@procedure.reload)) %> diff --git a/spec/features/admin/procedure_creation_spec.rb b/spec/features/admin/procedure_creation_spec.rb index 646382773..f8fd9a74a 100644 --- a/spec/features/admin/procedure_creation_spec.rb +++ b/spec/features/admin/procedure_creation_spec.rb @@ -105,7 +105,7 @@ feature 'As an administrateur I wanna create a new procedure', js: true do end expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ' - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') within '.footer' do click_on 'Ajouter un champ' @@ -135,7 +135,7 @@ feature 'As an administrateur I wanna create a new procedure', js: true do click_on 'Ajouter un champ' end fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ' - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') click_on Procedure.last.libelle click_on 'onglet-pieces' diff --git a/spec/features/new_administrateur/procedures_spec.rb b/spec/features/new_administrateur/procedures_spec.rb index ead5f28da..47ba7e66f 100644 --- a/spec/features/new_administrateur/procedures_spec.rb +++ b/spec/features/new_administrateur/procedures_spec.rb @@ -5,6 +5,7 @@ feature 'As an administrateur I edit procedure', js: true do let(:procedure) { create(:procedure) } before do + Flipflop::FeatureSet.current.test!.switch!(:champ_repetition, true) login_as administrateur, scope: :administrateur visit champs_procedure_path(procedure) end @@ -15,14 +16,14 @@ feature 'As an administrateur I edit procedure', js: true do end expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') page.refresh within '.footer' do click_on 'Enregistrer' end - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') end it "Add multiple champs" do @@ -34,7 +35,7 @@ feature 'As an administrateur I edit procedure', js: true do end expect(page).not_to have_content('Le libellé doit être rempli.') expect(page).not_to have_content('Modifications non sauvegardées.') - expect(page).not_to have_content('Champs enregistrés') + expect(page).not_to have_content('Formulaire mis à jour') fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0' expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') @@ -44,7 +45,7 @@ feature 'As an administrateur I edit procedure', js: true do expect(page).to have_content('Le libellé doit être rempli.') expect(page).to have_content('Modifications non sauvegardées.') - expect(page).not_to have_content('Champs enregistrés') + expect(page).not_to have_content('Formulaire mis à jour') fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2' within '.draggable-item-3' do @@ -53,12 +54,12 @@ feature 'As an administrateur I edit procedure', js: true do expect(page).to have_content('Le libellé doit être rempli.') expect(page).to have_content('Modifications non sauvegardées.') - expect(page).not_to have_content('Champs enregistrés') + expect(page).not_to have_content('Formulaire mis à jour') fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1' expect(page).not_to have_content('Le libellé doit être rempli.') expect(page).not_to have_content('Modifications non sauvegardées.') - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') page.refresh expect(page).to have_content('Supprimer', count: 3) @@ -69,11 +70,11 @@ feature 'As an administrateur I edit procedure', js: true do click_on 'Ajouter un champ' end fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') page.refresh click_on 'Supprimer' - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') expect(page).not_to have_content('Supprimer') page.refresh @@ -87,9 +88,39 @@ feature 'As an administrateur I edit procedure', js: true do expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description') fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ' expect(page).to have_content('Le libellé doit être rempli.') - expect(page).not_to have_content('Champs enregistrés') + expect(page).not_to have_content('Formulaire mis à jour') fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' - expect(page).to have_content('Champs enregistrés') + expect(page).to have_content('Formulaire mis à jour') + end + + it "Add repetition champ" do + within '.footer' do + click_on 'Ajouter un champ' + end + expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') + select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ') + fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' + + expect(page).to have_content('Formulaire mis à jour') + page.refresh + + within '.flex-grow' do + click_on 'Ajouter un champ' + end + + fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1' + + expect(page).to have_content('Formulaire mis à jour') + expect(page).to have_content('Supprimer', count: 2) + + within '.footer' do + click_on 'Ajouter un champ' + end + + select('Bloc répétable', from: 'procedure_types_de_champ_attributes_1_type_champ') + fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2' + + expect(page).to have_content('Supprimer', count: 3) end end From 862ab4ed04bc695d91922eee70e296c0ff12b8fb Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 4 Feb 2019 15:46:11 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=80=9CBloc=20r=C3=A9p=C3=A9table?= =?UTF-8?q?=E2=80=9D=20is=20ready=20to=20be=20tested?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/features.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/features.rb b/config/features.rb index 2faf85f48..55d02a7bc 100644 --- a/config/features.rb +++ b/config/features.rb @@ -10,7 +10,7 @@ Flipflop.configure do feature :champ_integer_number, title: "Champ nombre entier" feature :champ_repetition, - title: "Bloc répétable (NE MARCHE PAS – NE PAS ACTIVER)" + title: "Bloc répétable" end feature :web_hook From a4a421a91a2b49a3569b223c4a2f00ccb73f0d5e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 30 Jan 2019 17:20:02 +0100 Subject: [PATCH 3/4] Champ Repetition dossier display --- app/assets/stylesheets/new_design/table.scss | 4 ++ app/models/type_de_champ.rb | 5 +-- .../shared/dossiers/_champ_row.html.haml | 40 +++++++++++++++++++ app/views/shared/dossiers/_champs.html.haml | 32 +-------------- .../shared/dossiers/_champs.html.haml_spec.rb | 2 +- 5 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 app/views/shared/dossiers/_champ_row.html.haml diff --git a/app/assets/stylesheets/new_design/table.scss b/app/assets/stylesheets/new_design/table.scss index 28afbb340..1f461e9e4 100644 --- a/app/assets/stylesheets/new_design/table.scss +++ b/app/assets/stylesheets/new_design/table.scss @@ -20,6 +20,10 @@ padding: (3 * $default-spacer) 2px; } + th.padded { + padding-left: (2 * $default-spacer); + } + &.hoverable { tbody tr:hover { background: $light-grey; diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 77ef1402f..f63d94f34 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -147,10 +147,7 @@ class TypeDeChamp < ApplicationRecord end def exclude_from_view? - type_champ.in?([ - TypeDeChamp.type_champs.fetch(:explication), - TypeDeChamp.type_champs.fetch(:repetition) - ]) + type_champ == TypeDeChamp.type_champs.fetch(:explication) end def public? diff --git a/app/views/shared/dossiers/_champ_row.html.haml b/app/views/shared/dossiers/_champ_row.html.haml new file mode 100644 index 000000000..361c6e786 --- /dev/null +++ b/app/views/shared/dossiers/_champ_row.html.haml @@ -0,0 +1,40 @@ +- champs.reject(&:exclude_from_view?).each do |c| + - if c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) + %tr + %th.libelle.repetition{ colspan: 3 } + = "#{c.libelle} :" + - c.rows.each do |champs| + = render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: true } + %tr + %th{ colspan: 4 } + - else + %tr + - if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section) + %th.header-section{ colspan: 3 } + = c.libelle + - else + %th.libelle{ class: repetition ? 'padded' : '' } + = "#{c.libelle} :" + %td.rich-text + %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } + - case c.type_champ + - when TypeDeChamp.type_champs.fetch(:carte) + = render partial: "shared/champs/carte/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:dossier_link) + = render partial: "shared/champs/dossier_link/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) + = render partial: "shared/champs/multiple_drop_down_list/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:piece_justificative) + = render partial: "shared/champs/piece_justificative/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:siret) + = render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile } + - when TypeDeChamp.type_champs.fetch(:textarea) + = render partial: "shared/champs/textarea/show", locals: { champ: c } + - else + = sanitize(c.to_s) + + - if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section) + %td.updated-at + %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } + modifié le + = c.updated_at.strftime("%d/%m/%Y à %H:%M") diff --git a/app/views/shared/dossiers/_champs.html.haml b/app/views/shared/dossiers/_champs.html.haml index 08c6a311b..1a511730f 100644 --- a/app/views/shared/dossiers/_champs.html.haml +++ b/app/views/shared/dossiers/_champs.html.haml @@ -1,33 +1,3 @@ %table.table.vertical.dossier-champs %tbody - - champs.reject(&:exclude_from_view?).each do |c| - %tr - - if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section) - %th.header-section{ colspan: 3 } - = c.libelle - - else - %th.libelle - = "#{c.libelle} :" - %td.rich-text - %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } - - case c.type_champ - - when TypeDeChamp.type_champs.fetch(:carte) - = render partial: "shared/champs/carte/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:dossier_link) - = render partial: "shared/champs/dossier_link/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) - = render partial: "shared/champs/multiple_drop_down_list/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:piece_justificative) - = render partial: "shared/champs/piece_justificative/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:siret) - = render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile } - - when TypeDeChamp.type_champs.fetch(:textarea) - = render partial: "shared/champs/textarea/show", locals: { champ: c } - - else - = sanitize(c.to_s) - - - if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section) - %td.updated-at - %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } - modifié le - = c.updated_at.strftime("%d/%m/%Y à %H:%M") + = render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: false } diff --git a/spec/views/shared/dossiers/_champs.html.haml_spec.rb b/spec/views/shared/dossiers/_champs.html.haml_spec.rb index d8a80ab50..58d6c5690 100644 --- a/spec/views/shared/dossiers/_champs.html.haml_spec.rb +++ b/spec/views/shared/dossiers/_champs.html.haml_spec.rb @@ -8,7 +8,7 @@ describe 'shared/dossiers/champs.html.haml', type: :view do allow(view).to receive(:current_gestionnaire).and_return(gestionnaire) end - subject { render 'shared/dossiers/champs.html.haml', champs: champs, demande_seen_at: demande_seen_at } + subject { render 'shared/dossiers/champs.html.haml', champs: champs, demande_seen_at: demande_seen_at, profile: nil } context "there are some champs" do let(:dossier) { create(:dossier) } From 071448e1d9ef78b83ff34a737811477f487ab1cb Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 30 Jan 2019 16:14:15 +0100 Subject: [PATCH 4/4] Champ Repetition dossier editor --- app/assets/stylesheets/new_design/forms.scss | 13 ++++ .../champs/repetition_controller.rb | 21 +++++++ .../new_user/dossiers_controller.rb | 6 +- app/helpers/application_helper.rb | 7 +++ .../new_design/champs/repetition.js | 24 ++++++++ app/javascript/packs/application.js | 1 + app/models/champ.rb | 2 +- app/models/champs/repetition_champ.rb | 12 ++++ app/models/dossier.rb | 16 ++++- app/models/type_de_champ.rb | 4 ++ app/views/champs/repetition/_show.html.haml | 10 ++++ app/views/champs/repetition/show.js.erb | 3 + .../editable_champs/_champ_label.html.haml | 2 +- .../editable_champs/_repetition.html.haml | 16 ++++- config/routes.rb | 1 + spec/features/new_user/brouillon_spec.rb | 35 +++++++++++ spec/models/dossier_spec.rb | 59 +++++++++++++++++++ 17 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 app/controllers/champs/repetition_controller.rb create mode 100644 app/javascript/new_design/champs/repetition.js create mode 100644 app/views/champs/repetition/_show.html.haml create mode 100644 app/views/champs/repetition/show.js.erb diff --git a/app/assets/stylesheets/new_design/forms.scss b/app/assets/stylesheets/new_design/forms.scss index 04617dbfe..e2202ecad 100644 --- a/app/assets/stylesheets/new_design/forms.scss +++ b/app/assets/stylesheets/new_design/forms.scss @@ -111,6 +111,10 @@ } } + .add-row { + margin-bottom: 2 * $default-padding; + } + input[type=checkbox] { &.small-margin { margin-bottom: $default-padding / 2; @@ -246,6 +250,15 @@ .geo-areas { margin-bottom: 2 * $default-padding; } + + &.editable-champ-repetition { + .row { + border-radius: 4px; + border: 1px solid $border-grey; + padding: $default-padding; + margin-bottom: 2 * $default-padding; + } + } } input.aa-input, diff --git a/app/controllers/champs/repetition_controller.rb b/app/controllers/champs/repetition_controller.rb new file mode 100644 index 000000000..c5d8b12ea --- /dev/null +++ b/app/controllers/champs/repetition_controller.rb @@ -0,0 +1,21 @@ +class Champs::RepetitionController < ApplicationController + before_action :authenticate_logged_user! + + def show + @champ = Champ + .joins(:dossier) + .where(dossiers: { user_id: logged_user_ids }) + .find(params[:champ_id]) + + @position = params[:position] + row = (@champ.champs.empty? ? 0 : @champ.champs.last.row) + 1 + + @champ.add_row(row) + + if @champ.private? + @attribute = "dossier[champs_private_attributes][#{@position}][champs_attributes]" + else + @attribute = "dossier[champs_attributes][#{@position}][champs_attributes]" + end + end +end diff --git a/app/controllers/new_user/dossiers_controller.rb b/app/controllers/new_user/dossiers_controller.rb index 7db5821fc..b848ff8ec 100644 --- a/app/controllers/new_user/dossiers_controller.rb +++ b/app/controllers/new_user/dossiers_controller.rb @@ -282,7 +282,8 @@ module NewUser params.permit(dossier: { champs_attributes: [ :id, :value, :primary_value, :secondary_value, :piece_justificative_file, value: [], - etablissement_attributes: Champs::SiretChamp::ETABLISSEMENT_ATTRIBUTES + etablissement_attributes: Champs::SiretChamp::ETABLISSEMENT_ATTRIBUTES, + champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, value: []] ] }) end @@ -303,8 +304,7 @@ module NewUser end if !save_draft? - errors += @dossier.champs.select(&:mandatory_and_blank?) - .map { |c| "Le champ #{c.libelle.truncate(200)} doit être rempli." } + errors += @dossier.check_mandatory_champs errors += PiecesJustificativesService.missing_pj_error_messages(@dossier) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d167a0609..28dc0af2a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -31,6 +31,13 @@ module ApplicationHelper # rubocop:enable Rails/OutputSafety end + def append_to_element(selector, partial:, locals: {}) + html = escape_javascript(render partial: partial, locals: locals) + # rubocop:disable Rails/OutputSafety + raw("document.querySelector('#{selector}').insertAdjacentHTML('beforeend', \"#{html}\");") + # rubocop:enable Rails/OutputSafety + end + def render_flash(timeout: false, sticky: false, fixed: false) if flash.any? html = render_to_element('#flash_messages', partial: 'layouts/flash_messages', locals: { sticky: sticky, fixed: fixed }, outer: true) diff --git a/app/javascript/new_design/champs/repetition.js b/app/javascript/new_design/champs/repetition.js new file mode 100644 index 000000000..a70b48c71 --- /dev/null +++ b/app/javascript/new_design/champs/repetition.js @@ -0,0 +1,24 @@ +import { delegate } from '@utils'; + +const BUTTON_SELECTOR = '.button.remove-row'; +const DESTROY_INPUT_SELECTOR = 'input[type=hidden][name*=_destroy]'; +const CHAMP_SELECTOR = '.editable-champ'; + +addEventListener('turbolinks:load', () => { + delegate('click', BUTTON_SELECTOR, evt => { + evt.preventDefault(); + + const row = evt.target.closest('.row'); + + for (let input of row.querySelectorAll(DESTROY_INPUT_SELECTOR)) { + input.disabled = false; + input.value = true; + } + for (let champ of row.querySelectorAll(CHAMP_SELECTOR)) { + champ.remove(); + } + + evt.target.remove(); + row.classList.remove('row'); + }); +}); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 61ff74ed2..128b7c1b1 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -21,6 +21,7 @@ import '../new_design/select2'; import '../new_design/champs/carte'; import '../new_design/champs/linked-drop-down-list'; +import '../new_design/champs/repetition'; import '../new_design/administrateur/champs-editor'; diff --git a/app/models/champ.rb b/app/models/champ.rb index 8a7a9b91c..e0d7b35f1 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -10,7 +10,7 @@ class Champ < ApplicationRecord has_many :geo_areas, dependent: :destroy belongs_to :etablissement, dependent: :destroy - delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, to: :type_de_champ + delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, :repetition?, to: :type_de_champ scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) } scope :public_only, -> { where(private: false) } diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb index 8b06ca396..a2f02fa04 100644 --- a/app/models/champs/repetition_champ.rb +++ b/app/models/champs/repetition_champ.rb @@ -9,10 +9,22 @@ class Champs::RepetitionChamp < Champ champs.group_by(&:row).values end + def add_row(row = 0) + type_de_champ.types_de_champ.each do |type_de_champ| + self.champs << type_de_champ.champ.build(row: row) + end + end + + def mandatory_and_blank? + mandatory? && champs.empty? + end + def search_terms # The user cannot enter any information here so it doesn’t make much sense to search end + private + def setup_dossier champs.each do |champ| champ.dossier = dossier diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 97a98442d..2dee58040 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -121,7 +121,13 @@ class Dossier < ApplicationRecord def build_default_champs procedure.types_de_champ.each do |type_de_champ| - champs << type_de_champ.champ.build + champ = type_de_champ.champ.build + + if type_de_champ.repetition? + champ.add_row + end + + champs << champ end procedure.types_de_champ_private.each do |type_de_champ| champs_private << type_de_champ.champ.build @@ -334,6 +340,14 @@ class Dossier < ApplicationRecord log_dossier_operation(gestionnaire, :classer_sans_suite) end + def check_mandatory_champs + (champs + champs.select(&:repetition?).flat_map(&:champs)) + .select(&:mandatory_and_blank?) + .map do |champ| + "Le champ #{champ.libelle.truncate(200)} doit être rempli." + end + end + private def log_dossier_operation(gestionnaire, operation, automatic_operation: false) diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index f63d94f34..1b167c67a 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -150,6 +150,10 @@ class TypeDeChamp < ApplicationRecord type_champ == TypeDeChamp.type_champs.fetch(:explication) end + def repetition? + type_champ == TypeDeChamp.type_champs.fetch(:repetition) + end + def public? !private? end diff --git a/app/views/champs/repetition/_show.html.haml b/app/views/champs/repetition/_show.html.haml new file mode 100644 index 000000000..508593712 --- /dev/null +++ b/app/views/champs/repetition/_show.html.haml @@ -0,0 +1,10 @@ +- champs = champ.rows.last +- index = (champ.rows.size - 1) * champs.size +%div{ class: "row row-#{champs.first.row}" } + - champs.each.with_index(index) do |champ, index| + = fields_for "#{attribute}[#{index}]", champ do |form| + = render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form } + = form.hidden_field :id + = form.hidden_field :_destroy, disabled: true + %button.button.danger.remove-row + Supprimer diff --git a/app/views/champs/repetition/show.js.erb b/app/views/champs/repetition/show.js.erb new file mode 100644 index 000000000..af552c482 --- /dev/null +++ b/app/views/champs/repetition/show.js.erb @@ -0,0 +1,3 @@ +<%= append_to_element(".repetition-#{@position}", + partial: 'champs/repetition/show', + locals: { champ: @champ, attribute: @attribute }) %> diff --git a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml index 6ba997ae9..43c6d2b11 100644 --- a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml +++ b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml @@ -1,4 +1,4 @@ -= form.label champ.main_value_name do += form.label champ.main_value_name, { class: champ.repetition? ? 'header-section' : '' } do #{champ.libelle} - if champ.mandatory? %span.mandatory * diff --git a/app/views/shared/dossiers/editable_champs/_repetition.html.haml b/app/views/shared/dossiers/editable_champs/_repetition.html.haml index b9fcfa711..d7e8717c7 100644 --- a/app/views/shared/dossiers/editable_champs/_repetition.html.haml +++ b/app/views/shared/dossiers/editable_champs/_repetition.html.haml @@ -1 +1,15 @@ -%h2.repetition-libelle= champ.libelle +%div{ class: "repetition-#{form.index}" } + - champ.rows.each do |champs| + %div{ class: "row row-#{champs.first.row}" } + - champs.each do |champ| + = form.fields_for :champs, champ do |form| + = render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form } + = form.hidden_field :_destroy, disabled: true + %button.button.danger.remove-row + Supprimer + +- if champ.persisted? + = link_to "Ajouter une ligne pour « #{champ.libelle} »", champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, method: 'POST', params: { champ_id: champ&.id }.to_query } +- else + %button.button.add-row{ disabled: true } + = "Ajouter une ligne pour « #{champ.libelle} »" diff --git a/config/routes.rb b/config/routes.rb index 570844167..c0b41153f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,6 +127,7 @@ Rails.application.routes.draw do get ':position/siret', to: 'siret#show', as: :siret get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link post ':position/carte', to: 'carte#show', as: :carte + post ':position/repetition', to: 'repetition#show', as: :repetition end get 'tour-de-france' => 'root#tour_de_france' diff --git a/spec/features/new_user/brouillon_spec.rb b/spec/features/new_user/brouillon_spec.rb index 660d084a8..16c2d82ba 100644 --- a/spec/features/new_user/brouillon_spec.rb +++ b/spec/features/new_user/brouillon_spec.rb @@ -83,6 +83,41 @@ feature 'The user' do expect(page).to have_field('dossier_link', with: '123') end + let(:procedure_with_repetition) do + tdc = create(:type_de_champ_repetition, libelle: 'repetition') + tdc.types_de_champ << create(:type_de_champ_text, libelle: 'text') + create(:procedure, :published, :for_individual, types_de_champ: [tdc]) + end + + scenario 'fill a dossier with repetition', js: true do + log_in(user.email, password, procedure_with_repetition) + + fill_individual + + fill_in('text', with: 'super texte') + expect(page).to have_field('text', with: 'super texte') + + click_on 'Ajouter une ligne pour' + + within '.row-1' do + fill_in('text', with: 'un autre texte') + end + + expect(page).to have_content('Supprimer', count: 2) + + click_on 'Enregistrer le brouillon' + + expect(page).to have_content('Supprimer', count: 2) + + within '.row-1' do + click_on 'Supprimer' + end + + click_on 'Enregistrer le brouillon' + + expect(page).to have_content('Supprimer', count: 1) + end + let(:simple_procedure) do tdcs = [create(:type_de_champ, mandatory: true, libelle: 'texte obligatoire')] create(:procedure, :published, :for_individual, types_de_champ: tdcs) diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index c5a7ee826..451fdffbe 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -832,4 +832,63 @@ describe Dossier do it { expect(dossier.followers_gestionnaires).not_to include(gestionnaire) } it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) } end + + describe "#check_mandatory_champs" do + let(:procedure) { create(:procedure, :with_type_de_champ) } + let(:dossier) { create(:dossier, :with_all_champs, procedure: procedure) } + + it 'no mandatory champs' do + expect(dossier.check_mandatory_champs).to be_empty + end + + context "with mandatory champs" do + let(:procedure) { create(:procedure, :with_type_de_champ_mandatory) } + let(:champ_with_error) { dossier.champs.first } + + before do + champ_with_error.value = nil + champ_with_error.save + end + + it 'should have errors' do + errors = dossier.check_mandatory_champs + expect(errors).not_to be_empty + expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.") + end + end + + context "with champ repetition" do + let(:procedure) { create(:procedure) } + let(:type_de_champ_repetition) { create(:type_de_champ_repetition, mandatory: true) } + + before do + procedure.types_de_champ << type_de_champ_repetition + type_de_champ_repetition.types_de_champ << create(:type_de_champ_text, mandatory: true) + end + + context "when no champs" do + let(:champ_with_error) { dossier.champs.first } + + it 'should have errors' do + errors = dossier.check_mandatory_champs + expect(errors).not_to be_empty + expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.") + end + end + + context "when mandatory champ inside repetition" do + let(:champ_with_error) { dossier.champs.first.champs.first } + + before do + dossier.champs.first.add_row + end + + it 'should have errors' do + errors = dossier.check_mandatory_champs + expect(errors).not_to be_empty + expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.") + end + end + end + end end