Merge pull request #9363 from demarches-simplifiees/dashboard-usager/make-search-work-with-procedure-filter

[refonte usager] Tableau de bord - rendre recherche complémentaire avec filtre par procédure
This commit is contained in:
Lisa Durand 2023-09-13 12:55:42 +00:00 committed by GitHub
commit 7311bcebb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 85 additions and 64 deletions

View file

@ -1,4 +1,5 @@
= form_with(url: dossiers_path, method: :get, data: { controller: 'autosubmit' } ) do |f| = form_with(url: dossiers_path, method: :get, data: { controller: 'autosubmit' } ) do |f|
= f.hidden_field :q, value: params[:q], id: nil
= f.label :procedure_id, t('.procedure.label'), class: 'sr-only' = f.label :procedure_id, t('.procedure.label'), class: 'sr-only'
.fr-input-group .fr-input-group
= f.select :procedure_id, options_for_select(@procedures_for_select, params[:procedure_id]), { prompt: t('.procedures.prompt') }, class: 'fr-select' = f.select :procedure_id, options_for_select(@procedures_for_select, params[:procedure_id]), { prompt: t('.procedures.prompt') }, class: 'fr-select'

View file

@ -5,7 +5,7 @@ module Users
layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret] layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret]
ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all] ACTIONS_ALLOWED_TO_ANY_USER = [:index, :new, :transferer_all]
ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :destroy, :demande, :messagerie, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :modifier_legacy, :update, :create_commentaire, :papertrail, :restore, :champ] ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :destroy, :demande, :messagerie, :brouillon, :submit_brouillon, :submit_en_construction, :modifier, :modifier_legacy, :update, :create_commentaire, :papertrail, :restore, :champ]
before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE
@ -33,21 +33,28 @@ module Users
.distinct(:procedure_id) .distinct(:procedure_id)
.order(:libelle) .order(:libelle)
.pluck(:libelle, :id) .pluck(:libelle, :id)
@procedure_id = params[:procedure_id]
@procedure_id = params[:procedure_id]
if @procedure_id.present? if @procedure_id.present?
ordered_dossiers = ordered_dossiers.where(procedures: { id: @procedure_id }) ordered_dossiers = ordered_dossiers.where(procedures: { id: @procedure_id })
deleted_dossiers = deleted_dossiers.where(procedures: { id: @procedure_id }) deleted_dossiers = deleted_dossiers.where(procedures: { id: @procedure_id })
end end
dossiers_visibles = ordered_dossiers.visible_by_user @search_terms = params[:q]
if @search_terms.present?
dossiers_filter_by_search = DossierSearchService.matching_dossiers_for_user(@search_terms, current_user).page
ordered_dossiers = ordered_dossiers.merge(dossiers_filter_by_search)
deleted_dossiers = nil
end
@user_dossiers = current_user.dossiers.state_not_termine.merge(dossiers_visibles) @dossiers_visibles = ordered_dossiers.visible_by_user
@dossiers_traites = current_user.dossiers.state_termine.merge(dossiers_visibles)
@dossiers_invites = current_user.dossiers_invites.merge(dossiers_visibles) @user_dossiers = current_user.dossiers.state_not_termine.merge(@dossiers_visibles)
@dossiers_traites = current_user.dossiers.state_termine.merge(@dossiers_visibles)
@dossiers_invites = current_user.dossiers_invites.merge(@dossiers_visibles)
@dossiers_supprimes_recemment = current_user.dossiers.hidden_by_user.merge(ordered_dossiers) @dossiers_supprimes_recemment = current_user.dossiers.hidden_by_user.merge(ordered_dossiers)
@dossier_transferes = dossiers_visibles.where(dossier_transfer_id: DossierTransfer.for_email(current_user.email).ids) @dossier_transferes = @dossiers_visibles.where(dossier_transfer_id: DossierTransfer.for_email(current_user.email).ids)
@dossiers_close_to_expiration = current_user.dossiers.close_to_expiration.merge(dossiers_visibles) @dossiers_close_to_expiration = current_user.dossiers.close_to_expiration.merge(@dossiers_visibles)
@dossiers_supprimes_definitivement = deleted_dossiers @dossiers_supprimes_definitivement = deleted_dossiers
@statut = statut(@user_dossiers, @dossiers_traites, @dossiers_invites, @dossiers_supprimes_recemment, @dossiers_supprimes_definitivement, @dossier_transferes, @dossiers_close_to_expiration, params[:statut]) @statut = statut(@user_dossiers, @dossiers_traites, @dossiers_invites, @dossiers_supprimes_recemment, @dossiers_supprimes_definitivement, @dossier_transferes, @dossiers_close_to_expiration, params[:statut])
@ -337,28 +344,6 @@ module Users
end end
end end
def recherche
@procedures_for_select = nil
@search_terms = params[:q]
return redirect_to dossiers_path if @search_terms.blank?
@dossiers = DossierSearchService.matching_dossiers_for_user(@search_terms, current_user).page(page)
if @dossiers.present?
# we need the page condition when accessing page n with n>1 when the page has only 1 result
# in order to avoid an unpleasant redirection when changing page
if @dossiers.count == 1 && page == 1
redirect_to url_for_dossier(@dossiers.first)
else
render :index
end
else
flash.alert = "Vous navez pas de dossiers contenant « #{@search_terms} »."
redirect_to dossiers_path
end
end
def new def new
erase_user_location! erase_user_location!

View file

@ -10,7 +10,7 @@ class DossierSearchService
def self.matching_dossiers_for_user(search_terms, user) def self.matching_dossiers_for_user(search_terms, user)
dossier_by_exact_id_for_user(search_terms, user) dossier_by_exact_id_for_user(search_terms, user)
.presence || dossier_by_full_text_for_user(search_terms, Dossier.where(id: user.dossiers.ids + user.dossiers_invites.ids)) .presence || dossier_by_full_text_for_user(search_terms, Dossier.includes(:procedure).where(id: user.dossiers.ids + user.dossiers_invites.ids))
end end
private private
@ -38,7 +38,7 @@ class DossierSearchService
def self.dossier_by_full_text_for_user(search_terms, dossiers) def self.dossier_by_full_text_for_user(search_terms, dossiers)
ts_vector = "to_tsvector('french', search_terms)" ts_vector = "to_tsvector('french', search_terms)"
ts_query = "to_tsquery('french', #{Dossier.connection.quote(to_tsquery(search_terms))})" ts_query = "to_tsquery('french', #{Dossier.includes(:procedure).connection.quote(to_tsquery(search_terms))})"
dossiers dossiers
.visible_by_user .visible_by_user
@ -49,9 +49,9 @@ class DossierSearchService
def self.dossier_by_exact_id_for_user(search_terms, user) def self.dossier_by_exact_id_for_user(search_terms, user)
id = search_terms.to_i id = search_terms.to_i
if id != 0 && id_compatible?(id) # Sometimes user is searching dossiers with a big number (ex: SIRET), ActiveRecord can't deal with them and throws ActiveModel::RangeError. id_compatible? prevents this. if id != 0 && id_compatible?(id) # Sometimes user is searching dossiers with a big number (ex: SIRET), ActiveRecord can't deal with them and throws ActiveModel::RangeError. id_compatible? prevents this.
Dossier.where(id: user.dossiers.visible_by_user.where(id: id) + user.dossiers_invites.visible_by_user.where(id: id)).distinct Dossier.includes(:procedure).where(id: user.dossiers.visible_by_user.where(id: id) + user.dossiers_invites.visible_by_user.where(id: id)).distinct
else else
Dossier.none Dossier.includes(:procedure).none
end end
end end

View file

@ -94,7 +94,7 @@
- else - else
- if filter.filter_params.present? - if filter.present?
.blank-tab .blank-tab
%h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title') %h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title')
%p.empty-text-details %p.empty-text-details
@ -102,6 +102,13 @@
%br %br
= link_to t('views.users.dossiers.dossiers_list.no_result_reset_filter'), dossiers_path(statut: statut), class: 'fr-btn fr-btn--sm fr-mt-2w' = link_to t('views.users.dossiers.dossiers_list.no_result_reset_filter'), dossiers_path(statut: statut), class: 'fr-btn fr-btn--sm fr-mt-2w'
- elsif search
.blank-tab
%h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title')
%p.empty-text-details
= t('views.users.dossiers.dossiers_list.no_result_text_with_search')
%br
= link_to t('views.users.dossiers.dossiers_list.no_result_reset_search'), dossiers_path(), class: 'fr-btn fr-btn--sm fr-mt-2w'
- else - else
.blank-tab .blank-tab
%h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title') %h2.empty-text= t('views.users.dossiers.dossiers_list.no_result_title')

View file

@ -14,7 +14,8 @@
- if current_user.dossiers.count > 2 || current_user.dossiers_invites.count > 2 - if current_user.dossiers.count > 2 || current_user.dossiers_invites.count > 2
.fr-col .fr-col
#search-2.fr-search-bar #search-2.fr-search-bar
= form_tag recherche_dossiers_path, method: :get, :role => "search", class: "flex width-100 fr-mb-5w" do = form_tag dossiers_path, method: :get, :role => "search", class: "flex width-100 fr-mb-5w" do
= hidden_field_tag :procedure_id, params[:procedure_id]
= label_tag "q", t('views.users.dossiers.search.search_file'), class: 'fr-label' = label_tag "q", t('views.users.dossiers.search.search_file'), class: 'fr-label'
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.search_file'), class: "fr-input" = text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.search_file'), class: "fr-input"
%button.fr-btn.fr-btn--sm %button.fr-btn.fr-btn--sm
@ -71,7 +72,7 @@
.fr-container .fr-container
.fr-grid-row.fr-grid-row--center .fr-grid-row.fr-grid-row--center
.fr-col-xl-10 .fr-col-xl-10
- if @statut == "en-cours" - if @statut == "en-cours" && @search_terms.blank?
- if @first_brouillon_recently_updated.present? - if @first_brouillon_recently_updated.present?
= render Dsfr::CalloutComponent.new(title: t('users.dossiers.header.callout.first_brouillon_recently_updated_title'), heading_level: 'h2') do |c| = render Dsfr::CalloutComponent.new(title: t('users.dossiers.header.callout.first_brouillon_recently_updated_title'), heading_level: 'h2') do |c|
- c.with_body do - c.with_body do
@ -80,8 +81,11 @@
= link_to t('users.dossiers.header.callout.first_brouillon_recently_updated_button'), url_for_dossier(@first_brouillon_recently_updated), class: 'fr-btn' = link_to t('users.dossiers.header.callout.first_brouillon_recently_updated_button'), url_for_dossier(@first_brouillon_recently_updated), class: 'fr-btn'
- if @search_terms.present? - if @search_terms.present?
%h2.page-title Résultat de la recherche pour « #{@search_terms} » %h2.page-title
= render partial: "dossiers_list", locals: { dossiers: @dossiers } = t('views.users.dossiers.search.result_term_title', search_terms: @search_terms)
- if @procedure_id.present?
= t('views.users.dossiers.search.result_procedure_title', procedure_libelle: @procedures_for_select.rassoc(@procedure_id.to_i).first)
= render partial: "dossiers_list", locals: { dossiers: @dossiers_visibles, filter: nil, search: true }
- else - else
= render Dossiers::UserFilterComponent.new(statut: @statut, filter: @filter, procedure_id: @procedure_id ) = render Dossiers::UserFilterComponent.new(statut: @statut, filter: @filter, procedure_id: @procedure_id )
@ -90,4 +94,4 @@
-# /!\ in this context, @dossiers is a collection of DeletedDossier not Dossier -# /!\ in this context, @dossiers is a collection of DeletedDossier not Dossier
= render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers } = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers }
- else - else
= render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut } = render partial: "dossiers_list", locals: { dossiers: @dossiers, filter: @filter, statut: @statut, search: false }

View file

@ -471,6 +471,8 @@ en:
search: search:
search_file: Search a file (File number, keywords) search_file: Search a file (File number, keywords)
simple: Search simple: Search
result_term_title: Search result for « %{search_terms} »
result_procedure_title: and procedure « %{procedure_libelle} »
secondary_menu: Secondary menu secondary_menu: Secondary menu
index: index:
dossiers: "My files" dossiers: "My files"
@ -478,6 +480,8 @@ en:
n_dossier: "File n." n_dossier: "File n."
no_result_title: No files no_result_title: No files
no_result_text_html: "To fill a procedure, contact your administration asking for the procedure link. <br> It should look like %{app_base}/commencer/xxx." no_result_text_html: "To fill a procedure, contact your administration asking for the procedure link. <br> It should look like %{app_base}/commencer/xxx."
no_result_text_with_search: found with search terms
no_result_reset_search: Reset search
no_result_text_with_filter: found with selected filters no_result_text_with_filter: found with selected filters
no_result_reset_filter: Reset filters no_result_reset_filter: Reset filters
procedure_closed: This procedure has been closed, you will not be able to submit a file again from the procedure link, contact your administration for more information. procedure_closed: This procedure has been closed, you will not be able to submit a file again from the procedure link, contact your administration for more information.

View file

@ -473,6 +473,8 @@ fr:
search: search:
search_file: Rechercher un dossier (N° de dossier, mots-clés) search_file: Rechercher un dossier (N° de dossier, mots-clés)
simple: Rechercher simple: Rechercher
result_term_title: Résultat de la recherche pour « %{search_terms} »
result_procedure_title: et pour la procédure « %{procedure_libelle} »
secondary_menu: Menu secondaire secondary_menu: Menu secondaire
index: index:
dossiers: "Mes dossiers" dossiers: "Mes dossiers"
@ -480,6 +482,8 @@ fr:
n_dossier: "dossier Nº " n_dossier: "dossier Nº "
no_result_title: Aucun dossier no_result_title: Aucun dossier
no_result_text_html: "Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche. <br> Celui ci doit ressembler à %{app_base}/commencer/xxx." no_result_text_html: "Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche. <br> Celui ci doit ressembler à %{app_base}/commencer/xxx."
no_result_text_with_search: ne correspond aux termes recherchés
no_result_reset_search: Réinitialiser la recherche
no_result_text_with_filter: ne correspond aux filtres sélectionnés no_result_text_with_filter: ne correspond aux filtres sélectionnés
no_result_reset_filter: Réinitialiser les filtres no_result_reset_filter: Réinitialiser les filtres
procedure_closed: Cette démarche a été clôturée, vous ne pourrez pas redéposer de dossier à partir du lien de la démarche, contactez votre administration pour plus dinformation. procedure_closed: Cette démarche a été clôturée, vous ne pourrez pas redéposer de dossier à partir du lien de la démarche, contactez votre administration pour plus dinformation.

View file

@ -331,7 +331,6 @@ Rails.application.routes.draw do
collection do collection do
get 'transferer', to: 'dossiers#transferer_all' get 'transferer', to: 'dossiers#transferer_all'
get 'recherche'
resources :transfers, only: [:create, :update, :destroy] resources :transfers, only: [:create, :update, :destroy]
end end
end end

View file

@ -164,16 +164,18 @@ describe 'Invitations' do
visit dossiers_path visit dossiers_path
end end
it "can search by id and it redirects to the dossier page" do it "can search by id and it displays the dossier" do
page.find_by_id('q').set(dossier.id) page.find_by_id('q').set(dossier.id)
find('.fr-search-bar .fr-btn').click find('.fr-search-bar .fr-btn').click
expect(current_path).to eq(dossier_path(dossier)) expect(current_path).to eq(dossiers_path)
expect(page).to have_link(dossier.procedure.libelle)
end end
it "can search something inside the dossier and it redirects to the dossier page" do it "can search something inside the dossier and it displays the dossier" do
page.find_by_id('q').set(dossier_2.champs_public.first.value) page.find_by_id('q').set(dossier_2.champs_public.first.value)
find('.fr-search-bar .fr-btn').click find('.fr-search-bar .fr-btn').click
expect(current_path).to eq(dossier_path(dossier_2)) expect(current_path).to eq(dossiers_path)
expect(page).to have_link(dossier.procedure.libelle)
end end
end end
end end

View file

@ -190,7 +190,7 @@ describe 'user access to the list of their dossiers', js: true, retry: 3 do
end end
end end
describe "recherche" do describe "user search bar" do
context "when the dossier does not exist" do context "when the dossier does not exist" do
before do before do
page.find_by_id('q').set(10000000) page.find_by_id('q').set(10000000)
@ -199,7 +199,9 @@ describe 'user access to the list of their dossiers', js: true, retry: 3 do
it "shows an error message on the dossiers page" do it "shows an error message on the dossiers page" do
expect(current_path).to eq(dossiers_path) expect(current_path).to eq(dossiers_path)
expect(page).to have_content("Vous navez pas de dossiers contenant « 10000000 ».") expect(page).to have_content("Résultat de la recherche pour « 10000000 »")
expect(page).to have_content("Aucun dossier")
expect(page).to have_content("ne correspond aux termes recherchés")
end end
end end
@ -213,7 +215,10 @@ describe 'user access to the list of their dossiers', js: true, retry: 3 do
it "shows an error message on the dossiers page" do it "shows an error message on the dossiers page" do
expect(current_path).to eq(dossiers_path) expect(current_path).to eq(dossiers_path)
expect(page).to have_content("Vous navez pas de dossiers contenant « #{dossier_other_user.id} ».") expect(page).to have_content("Résultat de la recherche pour « #{dossier_other_user.id} »")
expect(page).to have_content("Aucun dossier")
expect(page).to have_content("ne correspond aux termes recherchés")
expect(page).to have_content("Réinitialiser la recherche")
end end
end end
@ -223,8 +228,11 @@ describe 'user access to the list of their dossiers', js: true, retry: 3 do
find('.fr-search-bar .fr-btn').click find('.fr-search-bar .fr-btn').click
end end
it "redirects to the dossier page" do it "appears in the result list" do
expect(current_path).to eq(dossier_path(dossier_en_construction)) expect(current_path).to eq(dossiers_path)
expect(page).to have_content("Résultat de la recherche pour « #{dossier_en_construction.id} »")
expect(page).not_to have_css('.tabs')
expect(page).to have_content(dossier_en_construction.id)
end end
end end
@ -233,25 +241,32 @@ describe 'user access to the list of their dossiers', js: true, retry: 3 do
page.find_by_id('q').set(dossier_en_construction.champs_public.first.value) page.find_by_id('q').set(dossier_en_construction.champs_public.first.value)
end end
context 'when it only matches one dossier' do context 'when it matches multiple dossiers' do
before do
find('.fr-search-bar .fr-btn').click
end
it "redirects to the dossier page" do
expect(current_path).to eq(dossier_path(dossier_en_construction))
end
end
context 'when it matches multiple dossier' do
let!(:dossier_with_champs) { create(:dossier, :with_populated_champs, :en_construction, user: user) } let!(:dossier_with_champs) { create(:dossier, :with_populated_champs, :en_construction, user: user) }
before do before do
find('.fr-search-bar .fr-btn').click find('.fr-search-bar .fr-btn').click
end end
it "redirects to the search results" do it "appears in the result list" do
expect(current_path).to eq(recherche_dossiers_path) expect(current_path).to eq(dossiers_path)
expect(page).to have_content(dossier_en_construction.id) expect(page).to have_link(dossier_en_construction.procedure.libelle)
expect(page).to have_content(dossier_with_champs.id) expect(page).to have_link(dossier_with_champs.procedure.libelle)
expect(page).to have_text("2 sur 2 dossiers")
end
it "can be filtered by procedure and display the result - one item" do
select dossier_en_construction.procedure.libelle, from: 'procedure_id'
expect(page).to have_link(dossier_en_construction.procedure.libelle)
expect(page).not_to have_link(dossier_with_champs.procedure.libelle)
expect(page).to have_text("1 dossier")
end
it "can be filtered by procedure and display the result - no item" do
select dossier_brouillon.procedure.libelle, from: 'procedure_id'
expect(page).not_to have_link(dossier_en_construction.id)
expect(page).not_to have_link(dossier_with_champs.id)
expect(page).to have_content("Résultat de la recherche pour « #{dossier_en_construction.champs_public.first.value} » et pour la procédure « #{dossier_brouillon.procedure.libelle} » ")
expect(page).to have_text("Aucun dossier")
end end
end end
end end