feat(Users/Dossiers#update): track changes live and pop modal when ineligibilite_rules matches

This commit is contained in:
mfo 2024-06-05 17:36:25 +02:00
parent 2210db3b81
commit be5f580237
No known key found for this signature in database
GPG key ID: 7CE3E1F5B794A8EC
10 changed files with 164 additions and 24 deletions

View file

@ -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

View file

@ -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"

View file

@ -0,0 +1,5 @@
fr:
modal:
title: "Vous ne pouvez pas déposer votre dossier"
close: "Fermer"
close_alt: "Fermer la fenêtre modale"

View file

@ -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

View file

@ -303,10 +303,13 @@ module Users
def update def update
@dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier @dossier = dossier.en_construction? ? dossier.find_editing_fork(dossier.user) : dossier
@dossier = dossier_with_champs(pj_template: false) @dossier = dossier_with_champs(pj_template: false)
@errors = update_dossier_and_compute_errors @ineligibilite_rules_was_computable = @dossier.ineligibilite_rules_computable?
@can_passer_en_construction_was = @dossier.can_passer_en_construction?
@dossier.index_search_terms_later if @errors.empty? 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| respond_to do |format|
format.turbo_stream do format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?)) @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_attributes_params, dossier.champs.filter(&:public?))

View file

@ -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);
}
}

View file

@ -25,4 +25,6 @@
= render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier) = render Dossiers::PendingCorrectionCheckboxComponent.new(dossier: dossier)
= render Dossiers::InvalidIneligibiliteRulesComponent.new(dossier: dossier)
= render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false) = render Dossiers::EditFooterComponent.new(dossier: dossier_for_editing, annotation: false)

View file

@ -1 +1,8 @@
= render partial: 'shared/dossiers/update_champs', locals: { to_show: @to_show, to_hide: @to_hide, to_update: @to_update, dossier: @dossier } = 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)))

View file

@ -398,7 +398,9 @@ describe Users::DossiersController, type: :controller do
describe '#submit_brouillon' do describe '#submit_brouillon' do
before { sign_in(user) } 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(: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(: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' } let(:value) { 'beautiful value' }
@ -439,9 +441,9 @@ describe Users::DossiersController, type: :controller do
render_views render_views
let(:error_message) { 'nop' } let(:error_message) { 'nop' }
before do before do
expect_any_instance_of(Dossier).to receive(:validate).and_return(false) allow_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(:errors).and_return(
[double(inner_error: double(base: first_champ), message: 'nop')] [instance_double(ActiveModel::NestedError, inner_error: double(base: first_champ), message: 'nop')]
) )
subject subject
end end
@ -461,11 +463,8 @@ describe Users::DossiersController, type: :controller do
render_views render_views
let(:value) { nil } let(:value) { nil }
let(:types_de_champ_public) { [{ type: :text, mandatory: true, libelle: 'l' }] }
before do before { subject }
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
subject
end
it { expect(response).to render_template(:brouillon) } it { expect(response).to render_template(:brouillon) }
it { expect(response.body).to have_link(first_champ.libelle, href: "##{first_champ.labelledby_id}") } 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 render_views
before do before do
expect_any_instance_of(Dossier).to receive(:validate).and_return(false) allow_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(:errors).and_return(
[double(inner_error: double(base: first_champ), message: 'nop')] [double(inner_error: double(base: first_champ), message: 'nop')]
) )
@ -661,7 +660,8 @@ describe Users::DossiersController, type: :controller do
describe '#update brouillon' do describe '#update brouillon' do
before { sign_in(user) } 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(:dossier) { create(:dossier, user:, procedure:) }
let(:first_champ) { dossier.champs_public.first } let(:first_champ) { dossier.champs_public.first }
let(:piece_justificative_champ) { dossier.champs_public.last } let(:piece_justificative_champ) { dossier.champs_public.last }
@ -754,13 +754,65 @@ describe Users::DossiersController, type: :controller do
end end
end end
it "debounce search terms indexation" do context 'having ineligibilite_rules setup' do
# dossier creation trigger a first indexation and flag, include Logic
# so we we have to remove this flag render_views
dossier.debounce_index_search_terms_flag.remove
assert_enqueued_jobs(1, only: DossierIndexSearchTermsJob) do let(:types_de_champ_public) { [{ type: :text }, { type: :integer_number }] }
3.times { patch :update, params: payload, format: :turbo_stream } 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 end
end end
@ -868,8 +920,8 @@ describe Users::DossiersController, type: :controller do
context 'classic error' do context 'classic error' do
before do before do
expect_any_instance_of(Dossier).to receive(:save).and_return(false) allow_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(:errors).and_return(
[message: 'nop', inner_error: double(base: first_champ)] [message: 'nop', inner_error: double(base: first_champ)]
) )
subject subject

View file

@ -149,4 +149,18 @@ describe 'shared/dossiers/edit', type: :view do
end end
end 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 end