Merge pull request #7755 from mfo/US/filter-by-status-and-processed-at

feat(filter): enable filter on dossiers.state, dossiers.processed_at(since), dossiers.en_instruction_at(since)
This commit is contained in:
mfo 2022-09-27 17:26:16 +02:00 committed by GitHub
commit 19eca6b5a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 242 additions and 88 deletions

View file

@ -304,6 +304,12 @@
select {
width: 200px;
display: inline-block;
background-color: $light-grey;
border: 1px solid $border-grey;
}
[disabled] {
display: none;
}
}

View file

@ -0,0 +1,23 @@
class Dossiers::FilterComponent < ApplicationComponent
def initialize(procedure:, procedure_presentation:, statut:, field_id: nil)
@procedure = procedure
@procedure_presentation = procedure_presentation
@statut = statut
@field_id = field_id
end
attr_reader :procedure, :procedure_presentation, :statut, :field_id
def filterable_fields_for_select
procedure_presentation.filterable_fields_options
end
def field_type
return :text if field_id.nil?
procedure_presentation.field_type(field_id)
end
def options_for_select_of_field
I18n.t(procedure_presentation.field_enum(field_id)).map(&:to_a).map(&:reverse)
end
end

View file

@ -0,0 +1,5 @@
en:
column: Column
value: Value
add_filter: Add filter

View file

@ -0,0 +1,5 @@
fr:
column: Colonne
value: Valeur
add_filter: Ajouter le filtre

View file

@ -0,0 +1,13 @@
= form_tag add_filter_instructeur_procedure_url(procedure), method: :post, class: 'dropdown-form large', id: 'filter-component', data: { controller: 'dossier-filter' } do
= label_tag :field, t('.column')
= select_tag :field, options_for_select(filterable_fields_for_select, field_id), include_blank: field_id.nil?, data: {action: "dossier-filter#onChange"}
%br
= label_tag :value, t('.value'), for: 'value'
- if field_type == :enum
= select_tag :value, options_for_select(options_for_select_of_field), id: 'value', name: 'value'
- else
%input#value{ type: field_type, name: :value, maxlength: ProcedurePresentation::FILTERS_VALUE_MAX_LENGTH, disabled: field_id.nil? ? true : false }
= hidden_field_tag :statut, statut
%br
= submit_tag t('.add_filter'), class: 'button'

View file

@ -127,10 +127,20 @@ module Instructeurs
end
def add_filter
respond_to do |format|
format.html do
procedure_presentation.add_filter(statut, params[:field], params[:value])
redirect_back(fallback_location: instructeur_procedure_url(procedure))
end
format.turbo_stream do
@statut = statut
@procedure = procedure
@procedure_presentation = procedure_presentation
@field = params[:field]
end
end
end
def remove_filter
procedure_presentation.remove_filter(statut, params[:field], params[:value])

View file

@ -0,0 +1,13 @@
import { httpRequest } from '@utils';
import { ApplicationController } from './application_controller';
export class DossierFilterController extends ApplicationController {
onChange() {
const element = this.element as HTMLFormElement;
httpRequest(element.action, {
method: element.getAttribute('method') ?? '',
body: new FormData(element)
}).turbo();
}
}

View file

@ -2,13 +2,18 @@ module DossierFilteringConcern
extend ActiveSupport::Concern
included do
DATE_SINCE_MAPPING = {
'updated_since' => 'updated_at',
'depose_since' => 'depose_at',
'en_construction_since' => 'en_construction_at',
'en_instruction_since' => 'en_instruction_at',
'processed_since' => 'processed_at'
}
scope :filter_by_datetimes, lambda { |column, dates|
if dates.present?
case column
when 'depose_since'
where('dossiers.depose_at >= ?', dates.sort.first)
when 'updated_since'
where('dossiers.updated_at >= ?', dates.sort.first)
when *DATE_SINCE_MAPPING.keys
where("dossiers.#{DATE_SINCE_MAPPING.fetch(column)} >= ?", dates.sort.first)
else
dates
.map { |date| self.where(column => date..(date + 1.day)) }

View file

@ -35,47 +35,60 @@ class ProcedurePresentation < ApplicationRecord
validate :check_allowed_filter_columns
validate :check_filters_max_length
def fields
fields = [
field_hash('self', 'created_at'),
field_hash('self', 'en_construction_at'),
field_hash('self', 'depose_at'),
field_hash('self', 'updated_at'),
field_hash('self', 'depose_since', virtual: true),
field_hash('self', 'updated_since', virtual: true),
field_hash('user', 'email'),
field_hash('followers_instructeurs', 'email'),
field_hash('groupe_instructeur', 'label')
def self_fields
[
field_hash('self', 'created_at', type: :date),
field_hash('self', 'updated_at', type: :date),
field_hash('self', 'depose_at', type: :date),
field_hash('self', 'en_construction_at', type: :date),
field_hash('self', 'en_instruction_at', type: :date),
field_hash('self', 'processed_at', type: :date),
field_hash('self', 'updated_since', type: :date, virtual: true),
field_hash('self', 'depose_since', type: :date, virtual: true),
field_hash('self', 'en_construction_since', type: :date, virtual: true),
field_hash('self', 'en_instruction_since', type: :date, virtual: true),
field_hash('self', 'processed_since', type: :date, virtual: true),
field_hash('self', 'state', type: :enum, scope: 'instructeurs.dossiers.filterable_state')
]
end
def fields
fields = self_fields
fields.push(
field_hash('user', 'email', type: :text),
field_hash('followers_instructeurs', 'email', type: :text),
field_hash('groupe_instructeur', 'label', type: :text)
)
if procedure.for_individual
fields.push(
field_hash("individual", "prenom"),
field_hash("individual", "nom"),
field_hash("individual", "gender")
field_hash("individual", "prenom", type: :text),
field_hash("individual", "nom", type: :text),
field_hash("individual", "gender", type: :text)
)
end
if !procedure.for_individual
fields.push(
field_hash('etablissement', 'entreprise_siren'),
field_hash('etablissement', 'entreprise_forme_juridique'),
field_hash('etablissement', 'entreprise_nom_commercial'),
field_hash('etablissement', 'entreprise_raison_sociale'),
field_hash('etablissement', 'entreprise_siret_siege_social'),
field_hash('etablissement', 'entreprise_date_creation')
field_hash('etablissement', 'entreprise_siren', type: :text),
field_hash('etablissement', 'entreprise_forme_juridique', type: :text),
field_hash('etablissement', 'entreprise_nom_commercial', type: :text),
field_hash('etablissement', 'entreprise_raison_sociale', type: :text),
field_hash('etablissement', 'entreprise_siret_siege_social', type: :text),
field_hash('etablissement', 'entreprise_date_creation', type: :date)
)
fields.push(
field_hash('etablissement', 'siret'),
field_hash('etablissement', 'libelle_naf'),
field_hash('etablissement', 'code_postal')
field_hash('etablissement', 'siret', type: :text),
field_hash('etablissement', 'libelle_naf', type: :text),
field_hash('etablissement', 'code_postal', type: :text)
)
end
fields.concat procedure.types_de_champ_for_procedure_presentation
.pluck(:libelle, :private, :stable_id)
.map { |(libelle, is_private, stable_id)| field_hash(is_private ? TYPE_DE_CHAMP_PRIVATE : TYPE_DE_CHAMP, stable_id.to_s, label: libelle) }
.map { |(libelle, is_private, stable_id)| field_hash(is_private ? TYPE_DE_CHAMP_PRIVATE : TYPE_DE_CHAMP, stable_id.to_s, label: libelle, type: :text) }
fields
end
@ -89,7 +102,9 @@ class ProcedurePresentation < ApplicationRecord
end
def filterable_fields_options
fields.map { |field| [field['label'], field_id(field)] }
fields.map do |field|
[field['label'], field_id(field)]
end
end
def displayed_fields_for_headers
@ -152,14 +167,21 @@ class ProcedurePresentation < ApplicationRecord
end
def filtered_ids(dossiers, statut)
filters[statut].group_by { |filter| filter.values_at(TABLE, COLUMN) } .map do |(table, column), filters|
filters.fetch(statut)
.group_by { |filter| filter.values_at(TABLE, COLUMN) }
.map do |(table, column), filters|
values = filters.pluck('value')
case table
when 'self'
field = self_fields.find { |h| h['column'] == column }
if field['type'] == :date
dates = values
.filter_map { |v| Time.zone.parse(v).beginning_of_day rescue nil }
dossiers.filter_by_datetimes(column, dates)
else
dossiers.where("dossiers.#{column} = ?", values)
end
when TYPE_DE_CHAMP
dossiers.with_type_de_champ(column)
.filter_ilike(:champs, :value, values)
@ -282,6 +304,14 @@ class ProcedurePresentation < ApplicationRecord
slice(:filters, :sort, :displayed_fields)
end
def field_type(field_id)
find_field(*field_id.split(SLASH))['type']
end
def field_enum(field_id)
find_field(*field_id.split(SLASH))['scope']
end
private
def field_id(field)
@ -342,13 +372,15 @@ class ProcedurePresentation < ApplicationRecord
end
end
def field_hash(table, column, label: nil, classname: '', virtual: false)
def field_hash(table, column, label: nil, classname: '', virtual: false, type: :text, scope: '')
{
'label' => label || I18n.t(column, scope: [:activerecord, :attributes, :procedure_presentation, :fields, table]),
TABLE => table,
COLUMN => column,
'classname' => classname,
'virtual' => virtual
'virtual' => virtual,
'type' => type,
'scope' => scope
}
end

View file

@ -2,15 +2,7 @@
%button.button.dropdown-button{ data: { menu_button_target: 'button' } }
= t('views.instructeurs.dossiers.filters.title')
#filter-menu.dropdown-content.left-aligned.fade-in-down{ data: { menu_button_target: 'menu' } }
= form_tag add_filter_instructeur_procedure_path(procedure), method: :post, class: 'dropdown-form large' do
= label_tag :field, t('views.instructeurs.dossiers.filters.column')
= select_tag :field, options_for_select(filterable_fields_for_select)
%br
= label_tag :value, t('views.instructeurs.dossiers.filters.value')
= text_field_tag :value, nil, maxlength: ProcedurePresentation::FILTERS_VALUE_MAX_LENGTH
= hidden_field_tag :statut, statut
%br
= submit_tag t('views.instructeurs.dossiers.filters.add_filter'), class: 'button'
= render Dossiers::FilterComponent.new(procedure: procedure, procedure_presentation: @procedure_presentation, statut: statut)
- current_filters.group_by { |filter| filter['table'] }.each_with_index do |(table, filters), i|
- if i > 0

View file

@ -0,0 +1,2 @@
= turbo_stream.replace 'filter-component' do
= render Dossiers::FilterComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation, statut: @statut, field_id: @field)

View file

@ -106,6 +106,7 @@ ignore_unused:
- 'pluralize.*'
- 'views.pagination.*'
- 'time.formats.default'
- 'instructeurs.dossiers.filterable_state.*'
# - '{devise,kaminari,will_paginate}.*'
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'

View file

@ -174,9 +174,6 @@ en:
deleted_by_administration: "File deleted by administration"
restore: "Restore"
filters:
column: Column
value: Value
add_filter: Add filter
title: Filter
personalize: Personalize
follow_file: Follow-up the file

View file

@ -169,9 +169,6 @@ fr:
deleted_by_administration: "Dossier supprimé par l'administration"
restore: "Restaurer"
filters:
column: Colonne
value: Valeur
add_filter: Ajouter le filtre
title: Filtrer
personalize: Personnaliser
download: Télécharger un dossier

View file

@ -7,11 +7,16 @@ en:
id: File 
state: State
created_at: Created on
en_construction_at: En construction le
depose_at: Submitted on
updated_at: Updated on
depose_since: Submitted since
depose_at: First submission on
en_construction_at: Submitted on
en_instruction_at: En instruction on
processed_at: Done on
depose_since: First Submission since
updated_since: Updated since
en_construction_since: Submitted since
en_instruction_since: Instructed since
processed_since: Finished since
user:
email: Requester
followers_instructeurs:

View file

@ -7,11 +7,16 @@ fr:
id:  dossier
state: Statut
created_at: Créé le
en_construction_at: En construction le
depose_at: Déposé le
updated_at: Mis à jour le
depose_since: Déposé depuis
depose_at: Déposé le
en_construction_at: En construction le
en_instruction_at: En instruction le
processed_at: Terminé le
updated_since: Mis à jour depuis
depose_since: Déposé depuis
en_construction_since: En construction depuis
en_instruction_since: En instruction depuis
processed_since: Terminé depuis
user:
email: Demandeur
followers_instructeurs:

View file

@ -3,3 +3,23 @@ en:
procedure:
archive_pending_html: Archive creation pending<br>(requested %{created_period} ago)
archive_ready_html: Download archive<br>(requested %{generated_period} ago)
dossiers:
decisions_rendues_block:
without_email:
en_construction: The file had been sent at %{processed_at}
en_instruction: The file had been on instruction at %{processed_at}
accepte: The file had been accepted at %{processed_at}
refuse: The file file hadd been refused at %{processed_at}
classe_sans_suite: The file had been filed away at %{processed_at}
with_email:
en_construction: '%{email} sent this file at %{processed_at}'
en_instruction: '%{email} started this file the instruction at %{processed_at}'
accepte: '%{email} accepted this file at %{processed_at}'
refuse: '%{email} refused this file at %{processed_at}'
classe_sans_suite: '%{email} filed away this file at %{processed_at}'
filterable_state:
en_construction: "In progress"
en_instruction: "Processing"
accepte: "Accepted"
refuse: "Refused"
sans_suite: "No further action"

View file

@ -4,8 +4,6 @@ fr:
archive_pending_html: Archive en cours de création<br>(demandée il y a %{created_period})
archive_ready_html: Télécharger larchive<br>(demandée il y a %{generated_period})
dossiers:
extend_conservation:
archived_dossier: "Le dossier sera conservé 1 mois supplémentaire"
decisions_rendues_block:
without_email:
en_construction: Le %{processed_at} ce dossier a été passé en construction
@ -19,3 +17,9 @@ fr:
accepte: Le %{processed_at}, %{email} a accepté ce dossier
refuse: Le %{processed_at}, %{email} a refusé ce dossier
classe_sans_suite: Le %{processed_at}, %{email} a classé ce dossier sans suite
filterable_state:
en_construction: "En construction"
en_instruction: "En instruction"
accepte: "Accepté"
refuse: "Refusé"
sans_suite: "Classé sans suite"

View file

@ -58,28 +58,34 @@ describe ProcedurePresentation do
let(:tdc_private_2) { procedure.types_de_champ_private[1] }
let(:expected) {
[
{ "label" => 'Créé le', "table" => 'self', "column" => 'created_at', 'classname' => '', 'virtual' => false },
{ "label" => 'En construction le', "table" => 'self', "column" => 'en_construction_at', 'classname' => '', 'virtual' => false },
{ "label" => 'Déposé le', "table" => 'self', "column" => 'depose_at', 'classname' => '', 'virtual' => false },
{ "label" => 'Mis à jour le', "table" => 'self', "column" => 'updated_at', 'classname' => '', 'virtual' => false },
{ "label" => "Déposé depuis", "table" => "self", "column" => "depose_since", "classname" => "", 'virtual' => true },
{ "label" => "Mis à jour depuis", "table" => "self", "column" => "updated_since", "classname" => "", 'virtual' => true },
{ "label" => 'Demandeur', "table" => 'user', "column" => 'email', 'classname' => '', 'virtual' => false },
{ "label" => 'Email instructeur', "table" => 'followers_instructeurs', "column" => 'email', 'classname' => '', 'virtual' => false },
{ "label" => 'Groupe instructeur', "table" => 'groupe_instructeur', "column" => 'label', 'classname' => '', 'virtual' => false },
{ "label" => 'SIREN', "table" => 'etablissement', "column" => 'entreprise_siren', 'classname' => '', 'virtual' => false },
{ "label" => 'Forme juridique', "table" => 'etablissement', "column" => 'entreprise_forme_juridique', 'classname' => '', 'virtual' => false },
{ "label" => 'Nom commercial', "table" => 'etablissement', "column" => 'entreprise_nom_commercial', 'classname' => '', 'virtual' => false },
{ "label" => 'Raison sociale', "table" => 'etablissement', "column" => 'entreprise_raison_sociale', 'classname' => '', 'virtual' => false },
{ "label" => 'SIRET siège social', "table" => 'etablissement', "column" => 'entreprise_siret_siege_social', 'classname' => '', 'virtual' => false },
{ "label" => 'Date de création', "table" => 'etablissement', "column" => 'entreprise_date_creation', 'classname' => '', 'virtual' => false },
{ "label" => 'SIRET', "table" => 'etablissement', "column" => 'siret', 'classname' => '', 'virtual' => false },
{ "label" => 'Libellé NAF', "table" => 'etablissement', "column" => 'libelle_naf', 'classname' => '', 'virtual' => false },
{ "label" => 'Code postal', "table" => 'etablissement', "column" => 'code_postal', 'classname' => '', 'virtual' => false },
{ "label" => tdc_1.libelle, "table" => 'type_de_champ', "column" => tdc_1.stable_id.to_s, 'classname' => '', 'virtual' => false },
{ "label" => tdc_2.libelle, "table" => 'type_de_champ', "column" => tdc_2.stable_id.to_s, 'classname' => '', 'virtual' => false },
{ "label" => tdc_private_1.libelle, "table" => 'type_de_champ_private', "column" => tdc_private_1.stable_id.to_s, 'classname' => '', 'virtual' => false },
{ "label" => tdc_private_2.libelle, "table" => 'type_de_champ_private', "column" => tdc_private_2.stable_id.to_s, 'classname' => '', 'virtual' => false }
{ "label" => 'Créé le', "table" => 'self', "column" => 'created_at', 'classname' => '', 'virtual' => false, 'type' => :date, "scope" => '' },
{ "label" => 'Mis à jour le', "table" => 'self', "column" => 'updated_at', 'classname' => '', 'virtual' => false, 'type' => :date, "scope" => '' },
{ "label" => 'Déposé le', "table" => 'self', "column" => 'depose_at', 'classname' => '', 'virtual' => false, 'type' => :date, "scope" => '' },
{ "label" => 'En construction le', "table" => 'self', "column" => 'en_construction_at', 'classname' => '', 'virtual' => false, 'type' => :date, "scope" => '' },
{ "label" => 'En instruction le', "table" => 'self', "column" => 'en_instruction_at', 'classname' => '', 'virtual' => false, 'type' => :date, "scope" => '' },
{ "label" => 'Terminé le', "table" => 'self', "column" => 'processed_at', 'classname' => '', 'virtual' => false, 'type' => :date, "scope" => '' },
{ "label" => "Mis à jour depuis", "table" => "self", "column" => "updated_since", "classname" => "", 'virtual' => true, 'type' => :date, 'scope' => '' },
{ "label" => "Déposé depuis", "table" => "self", "column" => "depose_since", "classname" => "", 'virtual' => true, 'type' => :date, 'scope' => '' },
{ "label" => "En construction depuis", "table" => "self", "column" => "en_construction_since", "classname" => "", 'virtual' => true, 'type' => :date, 'scope' => '' },
{ "label" => "En instruction depuis", "table" => "self", "column" => "en_instruction_since", "classname" => "", 'virtual' => true, 'type' => :date, 'scope' => '' },
{ "label" => "Terminé depuis", "table" => "self", "column" => "processed_since", "classname" => "", 'virtual' => true, 'type' => :date, 'scope' => '' },
{ "label" => "Statut", "table" => "self", "column" => "state", "classname" => "", 'virtual' => false, 'scope' => 'instructeurs.dossiers.filterable_state', 'type' => :enum },
{ "label" => 'Demandeur', "table" => 'user', "column" => 'email', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Email instructeur', "table" => 'followers_instructeurs', "column" => 'email', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Groupe instructeur', "table" => 'groupe_instructeur', "column" => 'label', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'SIREN', "table" => 'etablissement', "column" => 'entreprise_siren', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Forme juridique', "table" => 'etablissement', "column" => 'entreprise_forme_juridique', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Nom commercial', "table" => 'etablissement', "column" => 'entreprise_nom_commercial', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Raison sociale', "table" => 'etablissement', "column" => 'entreprise_raison_sociale', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'SIRET siège social', "table" => 'etablissement', "column" => 'entreprise_siret_siege_social', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Date de création', "table" => 'etablissement', "column" => 'entreprise_date_creation', 'classname' => '', 'virtual' => false, 'type' => :date, "scope" => '' },
{ "label" => 'SIRET', "table" => 'etablissement', "column" => 'siret', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Libellé NAF', "table" => 'etablissement', "column" => 'libelle_naf', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Code postal', "table" => 'etablissement', "column" => 'code_postal', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => tdc_1.libelle, "table" => 'type_de_champ', "column" => tdc_1.stable_id.to_s, 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => tdc_2.libelle, "table" => 'type_de_champ', "column" => tdc_2.stable_id.to_s, 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => tdc_private_1.libelle, "table" => 'type_de_champ_private', "column" => tdc_private_1.stable_id.to_s, 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => tdc_private_2.libelle, "table" => 'type_de_champ_private', "column" => tdc_private_2.stable_id.to_s, 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' }
]
}
@ -96,9 +102,9 @@ describe ProcedurePresentation do
end
context 'when the procedure is for individuals' do
let(:name_field) { { "label" => "Prénom", "table" => "individual", "column" => "prenom", 'classname' => '', 'virtual' => false } }
let(:surname_field) { { "label" => "Nom", "table" => "individual", "column" => "nom", 'classname' => '', 'virtual' => false } }
let(:gender_field) { { "label" => "Civilité", "table" => "individual", "column" => "gender", 'classname' => '', 'virtual' => false } }
let(:name_field) { { "label" => "Prénom", "table" => "individual", "column" => "prenom", 'classname' => '', 'virtual' => false, "type" => :text, "scope" => '' } }
let(:surname_field) { { "label" => "Nom", "table" => "individual", "column" => "nom", 'classname' => '', 'virtual' => false, "type" => :text, "scope" => '' } }
let(:gender_field) { { "label" => "Civilité", "table" => "individual", "column" => "gender", 'classname' => '', 'virtual' => false, "type" => :text, "scope" => '' } }
let(:procedure) { create(:procedure, :for_individual) }
let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) }

View file

@ -77,12 +77,26 @@ describe "procedure filters" do
end
end
scenario "should be able to user custom fiters", js: true do
# use date filter
click_on 'Filtrer'
select "En construction le", from: "Colonne"
find("input#value[type=date]", visible: true)
fill_in "Valeur", with: "10/10/2010"
click_button "Ajouter le filtre"
# use enum filter
click_on 'Filtrer'
select "Statut", from: "Colonne"
find("select#value", visible: false)
select 'En construction', from: "Valeur"
click_button "Ajouter le filtre"
end
scenario "should be able to add and remove two filters for the same field", js: true do
add_filter(type_de_champ.libelle, champ.value)
add_filter(type_de_champ.libelle, champ_2.value)
expect(page).to have_content("#{type_de_champ.libelle} : #{champ.value}")
within ".dossiers-table" do
expect(page).to have_link(new_unfollow_dossier.id.to_s, exact: true)
expect(page).to have_link(new_unfollow_dossier.user.email)
@ -106,7 +120,6 @@ describe "procedure filters" do
within ".dossiers-table" do
expect(page).to have_link(new_unfollow_dossier.id.to_s, exact: true)
expect(page).to have_link(new_unfollow_dossier.user.email)
expect(page).to have_link(new_unfollow_dossier_2.id.to_s, exact: true)
expect(page).to have_link(new_unfollow_dossier_2.user.email)
end