diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component.rb b/app/components/dossiers/invalid_ineligibilite_rules_component.rb new file mode 100644 index 000000000..fe45272f6 --- /dev/null +++ b/app/components/dossiers/invalid_ineligibilite_rules_component.rb @@ -0,0 +1,16 @@ +class Dossiers::InvalidIneligibiliteRulesComponent < ApplicationComponent + delegate :can_passer_en_construction?, :ineligibilite_rules_computable?, to: :@dossier + + def initialize(dossier:) + @dossier = dossier + @revision = dossier.revision + end + + def render? + ineligibilite_rules_computable? && !can_passer_en_construction? + end + + def error_message + @dossier.revision.ineligibilite_message + end +end diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml new file mode 100644 index 000000000..1a377763c --- /dev/null +++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.en.yml @@ -0,0 +1,6 @@ +fr: + modal: + title: "Your file does not match submission criteria" + close: "Close" + close_alt: "Close this modal" + body: "The procedure « %{procedure_libelle} » have submission criteria, unfortunately your file does not match them. You can not submit your file" \ No newline at end of file diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml new file mode 100644 index 000000000..d191f03d4 --- /dev/null +++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.fr.yml @@ -0,0 +1,5 @@ +fr: + modal: + title: "Vous ne pouvez pas déposer votre dossier" + close: "Fermer" + close_alt: "Fermer la fenêtre modale" \ No newline at end of file diff --git a/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml new file mode 100644 index 000000000..dd39925cd --- /dev/null +++ b/app/components/dossiers/invalid_ineligibilite_rules_component/invalid_ineligibilite_rules_component.html.haml @@ -0,0 +1,16 @@ +%div{ id: dom_id(@dossier, :ineligibilite_rules_broken), data: { controller: 'ineligibilite-rules-match', turbo_force: :server } } + %button.fr-sr-only{ aria: {controls: 'modal-eligibilite-rules-dialog' }, data: {'fr-opened': "false" } } + show modal + + %dialog.fr-modal{ "aria-labelledby" => "fr-modal-title-modal-1", role: "dialog", id: 'modal-eligibilite-rules-dialog', data: { 'ineligibilite-rules-match-target' => 'dialog' } } + .fr-container.fr-container--fluid.fr-container-md + .fr-grid-row.fr-grid-row--center + .fr-col-12.fr-col-md-8.fr-col-lg-6 + .fr-modal__body + .fr-modal__header + %button.fr-btn--close.fr-btn{ aria: { controls: 'modal-eligibilite-rules-dialog' }, title: t('.modal.close_alt') }= t('.modal.close') + .fr-modal__content + %h1#fr-modal-title-modal-1.fr-modal__title + %span.fr-icon-arrow-right-line.fr-icon--lg> + = t('.modal.title') + %p= error_message diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index acdfd1332..e686683ff 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -303,10 +303,13 @@ module Users def update @dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier @dossier = dossier_with_champs(pj_template: false) - @errors = update_dossier_and_compute_errors - - @dossier.index_search_terms_later if @errors.empty? - + @ineligibilite_rules_was_computable = @dossier.ineligibilite_rules_computable? + @can_passer_en_construction_was = @dossier.can_passer_en_construction? + update_dossier_and_compute_errors + @dossier.index_search_terms_later if @dossier.errors.empty? + @ineligibilite_rules_is_computable = @dossier.ineligibilite_rules_computable? + @can_passer_en_construction_is = @dossier.can_passer_en_construction? + @ineligibilite_rules_computable_changed = !@ineligibilite_rules_was_computable && @ineligibilite_rules_is_computable respond_to do |format| format.turbo_stream do @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?)) diff --git a/app/javascript/controllers/ineligibilite_rules_match_controller.ts b/app/javascript/controllers/ineligibilite_rules_match_controller.ts new file mode 100644 index 000000000..5b47d79b5 --- /dev/null +++ b/app/javascript/controllers/ineligibilite_rules_match_controller.ts @@ -0,0 +1,19 @@ +import { ApplicationController } from './application_controller'; +declare interface modal { + disclose: () => void; +} +declare interface dsfr { + modal: modal; +} +declare const window: Window & + typeof globalThis & { dsfr: (elem: HTMLElement) => dsfr }; + +export class InvalidIneligibiliteRulesController extends ApplicationController { + static targets = ['dialog']; + + declare dialogTarget: HTMLElement; + + connect() { + setTimeout(() => window.dsfr(this.dialogTarget).modal.disclose(), 100); + } +} diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index d5fff3262..1962951e8 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -25,4 +25,6 @@ = render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier) + = render Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: dossier) + = render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false) diff --git a/app/views/users/dossiers/update.turbo_stream.haml b/app/views/users/dossiers/update.turbo_stream.haml index 91a898ab0..374291733 100644 --- a/app/views/users/dossiers/update.turbo_stream.haml +++ b/app/views/users/dossiers/update.turbo_stream.haml @@ -1 +1,8 @@ = render partial: 'shared/dossiers/update_champs', locals: { to_show: @to_show, to_hide: @to_hide, to_update: @to_update, dossier: @dossier } + +- if !params.key?(:validate) + - if @ineligibilite_rules_is_computable + = turbo_stream.remove(dom_id(@dossier, :ineligibilite_rules_broken)) + + - if (@ineligibilite_rules_computable_changed && !@can_passer_en_construction_is) || (@can_passer_en_construction_was && !@can_passer_en_construction_is) + = turbo_stream.append('contenu', render(Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: @dossier))) diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index 220bd6722..1f78b2f4a 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -398,7 +398,9 @@ describe Users::DossiersController, type: :controller do describe '#submit_brouillon' do before { sign_in(user) } - let!(:dossier) { create(:dossier, user: user) } + let(:procedure) { create(:procedure, :published, types_de_champ_public:) } + let(:types_de_champ_public) { [{ type: :text }] } + let!(:dossier) { create(:dossier, user:, procedure:) } let(:first_champ) { dossier.champs_public.first } let(:anchor_to_first_champ) { controller.helpers.link_to first_champ.libelle, brouillon_dossier_path(anchor: first_champ.labelledby_id), class: 'error-anchor' } let(:value) { 'beautiful value' } @@ -439,9 +441,9 @@ describe Users::DossiersController, type: :controller do render_views let(:error_message) { 'nop' } before do - expect_any_instance_of(Dossier).to receive(:validate).and_return(false) - expect_any_instance_of(Dossier).to receive(:errors).and_return( - [double(inner_error: double(base: first_champ), message: 'nop')] + allow_any_instance_of(Dossier).to receive(:validate).and_return(false) + allow_any_instance_of(Dossier).to receive(:errors).and_return( + [instance_double(ActiveModel::NestedError, inner_error: double(base: first_champ), message: 'nop')] ) subject end @@ -461,11 +463,8 @@ describe Users::DossiersController, type: :controller do render_views let(:value) { nil } - - before do - first_champ.type_de_champ.update(mandatory: true, libelle: 'l') - subject - end + let(:types_de_champ_public) { [{ type: :text, mandatory: true, libelle: 'l' }] } + before { subject } it { expect(response).to render_template(:brouillon) } it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") } @@ -548,8 +547,8 @@ describe Users::DossiersController, type: :controller do render_views before do - expect_any_instance_of(Dossier).to receive(:validate).and_return(false) - expect_any_instance_of(Dossier).to receive(:errors).and_return( + allow_any_instance_of(Dossier).to receive(:validate).and_return(false) + allow_any_instance_of(Dossier).to receive(:errors).and_return( [double(inner_error: double(base: first_champ), message: 'nop')] ) @@ -661,7 +660,8 @@ describe Users::DossiersController, type: :controller do describe '#update brouillon' do before { sign_in(user) } - let(:procedure) { create(:procedure, :published, types_de_champ_public: [{}, { type: :piece_justificative }]) } + let(:procedure) { create(:procedure, :published, types_de_champ_public:) } + let(:types_de_champ_public) { [{}, { type: :piece_justificative }] } let(:dossier) { create(:dossier, user:, procedure:) } let(:first_champ) { dossier.champs_public.first } let(:piece_justificative_champ) { dossier.champs_public.last } @@ -754,13 +754,65 @@ describe Users::DossiersController, type: :controller do end end - it "debounce search terms indexation" do - # dossier creation trigger a first indexation and flag, - # so we we have to remove this flag - dossier.debounce_index_search_terms_flag.remove + context 'having ineligibilite_rules setup' do + include Logic + render_views - assert_enqueued_jobs(1, only: DossierIndexSearchTermsJob) do - 3.times { patch :update, params: payload, format: :turbo_stream } + let(:types_de_champ_public) { [{ type: :text }, { type: :integer_number }] } + let(:text_champ) { dossier.champs_public.first } + let(:number_champ) { dossier.champs_public.last } + let(:submit_payload) do + { + id: dossier.id, + dossier: { + groupe_instructeur_id: dossier.groupe_instructeur_id, + champs_public_attributes: { + text_champ.public_id => { + with_public_id: true, + value: "hello world" + }, + number_champ.public_id => { + with_public_id: true, + value: + } + } + } + } + end + let(:must_be_greater_than) { 10 } + + before do + procedure.published_revision.update( + ineligibilite_enabled: true, + ineligibilite_message: 'lol', + ineligibilite_rules: greater_than(champ_value(number_champ.stable_id), constant(must_be_greater_than)) + ) + procedure.published_revision.save! + end + render_views + + context 'when it pass from undefined to true' do + let(:value) { must_be_greater_than + 1 } + + it 'raises popup' do + subject + dossier.reload + expect(dossier.can_passer_en_construction?).to be_falsey + expect(assigns(:ineligibilite_rules_was_computable)).to eq(false) + expect(assigns(:ineligibilite_rules_is_computable)).to eq(true) + expect(response.body).to match(ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)) + end + end + context 'when it pass from undefined to false' do + let(:value) { must_be_greater_than - 1 } + it 'does nothing' do + subject + dossier.reload + expect(dossier.can_passer_en_construction?).to be_truthy + expect(assigns(:ineligibilite_rules_was_computable)).to eq(false) + expect(assigns(:ineligibilite_rules_is_computable)).to eq(true) + expect(response.body).not_to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}") + end end end end @@ -868,8 +920,8 @@ describe Users::DossiersController, type: :controller do context 'classic error' do before do - expect_any_instance_of(Dossier).to receive(:save).and_return(false) - expect_any_instance_of(Dossier).to receive(:errors).and_return( + allow_any_instance_of(Dossier).to receive(:save).and_return(false) + allow_any_instance_of(Dossier).to receive(:errors).and_return( [message: 'nop', inner_error: double(base: first_champ)] ) subject diff --git a/spec/views/shared/dossiers/_edit.html.haml_spec.rb b/spec/views/shared/dossiers/_edit.html.haml_spec.rb index c242f3ec8..5183554d8 100644 --- a/spec/views/shared/dossiers/_edit.html.haml_spec.rb +++ b/spec/views/shared/dossiers/_edit.html.haml_spec.rb @@ -149,4 +149,18 @@ describe 'shared/dossiers/edit', type: :view do end end end + + context 'when dossier transitions rules are computable and passer_en_construction is false' do + let(:types_de_champ_public) { [] } + let(:dossier) { create(:dossier, procedure:) } + + before do + allow_any_instance_of(Dossiers::InvalidIneligibiliteRulesComponent).to receive(:ineligibilite_rules_computable?).and_return(true) + allow(dossier).to receive(:can_passer_en_construction?).and_return(false) + end + + it 'renders broken transitions rules dialog' do + expect(subject).to have_selector("##{ActionView::RecordIdentifier.dom_id(dossier, :ineligibilite_rules_broken)}") + end + end end