Merge pull request #10976 from demarches-simplifiees/feat-admin-can-modify-labels

ETQ instructeur, je veux pouvoir apposer un label à un dossier (part 2)
This commit is contained in:
Lisa Durand 2024-11-05 08:39:40 +00:00 committed by GitHub
commit c462425db2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 823 additions and 55 deletions

View file

@ -37,7 +37,7 @@
} }
.text-right { .text-right {
text-align: right; text-align: right !important;
} }
.text-sm { .text-sm {

View file

@ -1,34 +0,0 @@
@import "colors";
@import "constants";
.badge {
padding: 0 5px;
font-size: 14px;
font-weight: bold;
text-align: center;
white-space: nowrap;
border-radius: 100px;
background-color: rgba(0, 0, 0, 0.08);
vertical-align: top;
&.baseline {
vertical-align: baseline;
}
&.warning {
background-color: $orange;
color: #FFFFFF;
}
}
.badge-group {
display: flex;
.fr-badge {
margin-right: $default-spacer;
}
.fr-badge:last-child {
margin-right: 0;
}
}

View file

@ -262,3 +262,13 @@ button.fr-tag-bug {
.fr-badge--lowercase { .fr-badge--lowercase {
text-transform: lowercase; text-transform: lowercase;
} }
// We don't want badge to split in two lines
.fr-tag {
white-space: nowrap;
}
// We remove the line height because it creates unharmonized spaces - most of all in table
.fr-tags-group > li {
line-height: inherit;
}

View file

@ -48,6 +48,10 @@
width: 450px; width: 450px;
} }
.dropdown-label.dropdown-content {
min-width: 390px;
}
.print-menu { .print-menu {
display: none; display: none;
position: absolute; position: absolute;

View file

@ -0,0 +1,30 @@
@import "colors";
@import "constants";
$colors: "green-tilleul-verveine",
"green-bourgeon",
"green-emeraude",
"green-menthe",
"blue-ecume",
"purple-glycine",
"pink-macaron",
"yellow-tournesol",
"brown-cafe-creme",
"beige-gris-galet";
@each $color in $colors {
.fr-tag--#{$color},
a.fr-tag--#{$color},
button.fr-tag--#{$color},
input[type=button].fr-tag--#{$color},
input[type=image].fr-tag--#{$color},
input[type=reset].fr-tag--#{$color},
input[type=submit].fr-tag--#{$color} {
--idle: transparent;
--hover: var(--background-action-low-#{$color}-hover);
--active: var(--background-action-low-#{$color}-active);
background-color: var(--background-action-low-#{$color});
color: var(--text-action-high-#{$color});
}
}

View file

@ -53,6 +53,10 @@ class Instructeurs::ColumnFilterValueComponent < ApplicationComponent
[_1.label, _1.id] [_1.label, _1.id]
end end
end end
elsif column.table == 'dossier_labels'
Procedure.find(procedure_id).labels.filter_map do
[_1.name, _1.id]
end
else else
find_type_de_champ(column.column).options_for_select(column) find_type_de_champ(column.column).options_for_select(column)
end end

View file

@ -59,6 +59,8 @@ class Instructeurs::FilterButtonsComponent < ApplicationComponent
elsif column.groupe_instructeur? elsif column.groupe_instructeur?
current_instructeur.groupe_instructeurs current_instructeur.groupe_instructeurs
.find { _1.id == filter.to_i }&.label || filter .find { _1.id == filter.to_i }&.label || filter
elsif column.dossier_labels?
Label.find(filter)&.name || filter
elsif column.type == :date elsif column.type == :date
helpers.try_parse_format_date(filter) helpers.try_parse_format_date(filter)
else else

View file

@ -9,6 +9,6 @@
= link_to expert_all_avis_path, class: 'fr-nav__link', aria: aria_current_for(:avis) do = link_to expert_all_avis_path, class: 'fr-nav__link', aria: aria_current_for(:avis) do
= Avis.model_name.human(count: 10) = Avis.model_name.human(count: 10)
- if helpers.current_expert.avis_summary[:unanswered] > 0 - if helpers.current_expert.avis_summary[:unanswered] > 0
%span.badge.warning= helpers.current_expert.avis_summary[:unanswered] %span.fr-badge.fr-badge--new.fr-badge--no-icon= helpers.current_expert.avis_summary[:unanswered]
= render MainNavigation::AnnouncesLinkComponent.new = render MainNavigation::AnnouncesLinkComponent.new

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class Procedure::Card::LabelsComponent < ApplicationComponent
def initialize(procedure:)
@procedure = procedure
end
end

View file

@ -0,0 +1,3 @@
---
fr:
title: Labels

View file

@ -0,0 +1,17 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3
= link_to [:admin, @procedure, :labels], class: 'fr-tile fr-enlarge-link' do
.fr-tile__body.flex.column.align-center.justify-between
- if @procedure.labels.present?
%p.fr-badge.fr-badge--info
Configuré
%div
.line-count.fr-my-1w
%p.fr-tag= @procedure.labels.size
- else
%p.fr-badge
Non configuré
%h3.fr-h6
= t('.title')
%p.fr-tile-subtitle Gérer les labels utilisables par les instructeurs
%p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
module Administrateurs
class LabelsController < AdministrateurController
before_action :retrieve_procedure
before_action :retrieve_label, only: [:edit, :update, :destroy]
before_action :set_colors_collection, only: [:edit, :new, :create, :update]
def index
@labels = @procedure.labels
end
def edit
end
def new
@label = Label.new
end
def create
@label = @procedure.labels.build(label_params)
if @label.save
flash.notice = 'Le label a bien été créé'
redirect_to [:admin, @procedure, :labels]
else
flash.alert = @label.errors.full_messages
render :new
end
end
def update
if @label.update(label_params)
flash.notice = 'Le label a bien été modifié'
redirect_to [:admin, @procedure, :labels]
else
flash.alert = @label.errors.full_messages
render :edit
end
end
def destroy
@label.destroy!
flash.notice = 'Le label a bien été supprimé'
redirect_to [:admin, @procedure, :labels]
end
private
def label_params
params.require(:label).permit(:name, :color)
end
def retrieve_label
@label = @procedure.labels.find(params[:id])
end
def set_colors_collection
@colors_collection = Label.colors.keys
end
end
end

View file

@ -108,6 +108,7 @@ module Administrateurs
flash.now.alert = @procedure.errors.full_messages flash.now.alert = @procedure.errors.full_messages
render 'new' render 'new'
else else
@procedure.create_generic_labels
flash.notice = 'Démarche enregistrée.' flash.notice = 'Démarche enregistrée.'
current_administrateur.instructeur.assign_to_procedure(@procedure) current_administrateur.instructeur.assign_to_procedure(@procedure)

View file

@ -63,6 +63,19 @@ module Instructeurs
end end
end end
def dossier_labels
labels = params[:label_id]&.map(&:to_i) || []
@dossier = dossier
labels.each { |params_label| DossierLabel.find_or_create_by(dossier_id: @dossier.id, label_id: params_label) }
all_labels = DossierLabel.where(dossier_id: @dossier.id).pluck(:label_id)
(all_labels - labels).each { DossierLabel.find_by(dossier_id: @dossier.id, label_id: _1).destroy }
render :change_state
end
def messagerie def messagerie
@commentaire = Commentaire.new @commentaire = Commentaire.new
@messagerie_seen_at = current_instructeur.follows.find_by(dossier: dossier)&.messagerie_seen_at @messagerie_seen_at = current_instructeur.follows.find_by(dossier: dossier)&.messagerie_seen_at

View file

@ -118,6 +118,21 @@ module DossierHelper
tag.span(Dossier.human_attribute_name("pending_correction.resolved"), class: ['fr-badge fr-badge--sm fr-badge--success super', html_class], role: 'status') tag.span(Dossier.human_attribute_name("pending_correction.resolved"), class: ['fr-badge fr-badge--sm fr-badge--success super', html_class], role: 'status')
end end
def tags_label(tags)
if tags.count > 1
tag.ul(class: 'fr-tags-group') do
safe_join(tags.map { |t| tag.li(tag_label(t[1], t[2])) })
end
else
tag = tags.first
tag_label(tag[1], tag[2])
end
end
def tag_label(name, color)
tag.span(name, class: "fr-tag fr-tag--sm fr-tag--#{Label.class_name(color)}")
end
def demandeur_dossier(dossier) def demandeur_dossier(dossier)
if dossier.procedure.for_individual? && dossier.for_tiers? if dossier.procedure.for_individual? && dossier.for_tiers?
return t('shared.dossiers.beneficiaire', mandataire: dossier.mandataire_full_name, beneficiaire: "#{dossier&.individual&.prenom} #{dossier&.individual&.nom}") return t('shared.dossiers.beneficiaire', mandataire: dossier.mandataire_full_name, beneficiaire: "#{dossier&.individual&.prenom} #{dossier&.individual&.nom}")

View file

@ -40,6 +40,7 @@ class Column
def notifications? = [table, column] == ['notifications', 'notifications'] def notifications? = [table, column] == ['notifications', 'notifications']
def dossier_state? = [table, column] == ['self', 'state'] def dossier_state? = [table, column] == ['self', 'state']
def groupe_instructeur? = [table, column] == ['groupe_instructeur', 'id'] def groupe_instructeur? = [table, column] == ['groupe_instructeur', 'id']
def dossier_labels? = [table, column] == ['dossier_labels', 'label_id']
def type_de_champ? = table == TYPE_DE_CHAMP_TABLE def type_de_champ? = table == TYPE_DE_CHAMP_TABLE
def self.find(h_id) def self.find(h_id)

View file

@ -91,6 +91,8 @@ module ColumnsConcern
def user_france_connected_column = Columns::DossierColumn.new(procedure_id: id, table: 'self', column: 'user_from_france_connect?', filterable: false, displayable: false) def user_france_connected_column = Columns::DossierColumn.new(procedure_id: id, table: 'self', column: 'user_from_france_connect?', filterable: false, displayable: false)
def dossier_labels_column = Columns::DossierColumn.new(procedure_id: id, table: 'dossier_labels', column: 'label_id', type: :enum)
def procedure_chorus_columns def procedure_chorus_columns
['domaine_fonctionnel', 'referentiel_prog', 'centre_de_cout'] ['domaine_fonctionnel', 'referentiel_prog', 'centre_de_cout']
.map { |column| Columns::DossierColumn.new(procedure_id: id, table: 'procedure', column:, displayable: false, filterable: false) } .map { |column| Columns::DossierColumn.new(procedure_id: id, table: 'procedure', column:, displayable: false, filterable: false) }
@ -127,7 +129,8 @@ module ColumnsConcern
followers_instructeurs_email_column, followers_instructeurs_email_column,
groupe_instructeurs_id_column, groupe_instructeurs_id_column,
Columns::DossierColumn.new(procedure_id: id, table: 'avis', column: 'question_answer', filterable: false), Columns::DossierColumn.new(procedure_id: id, table: 'avis', column: 'question_answer', filterable: false),
user_france_connected_column user_france_connected_column,
dossier_labels_column
] ]
end end

View file

@ -132,6 +132,8 @@ class Dossier < ApplicationRecord
belongs_to :transfer, class_name: 'DossierTransfer', foreign_key: 'dossier_transfer_id', optional: true, inverse_of: :dossiers belongs_to :transfer, class_name: 'DossierTransfer', foreign_key: 'dossier_transfer_id', optional: true, inverse_of: :dossiers
has_many :transfer_logs, class_name: 'DossierTransferLog', dependent: :destroy has_many :transfer_logs, class_name: 'DossierTransferLog', dependent: :destroy
has_many :dossier_labels, dependent: :destroy
has_many :labels, through: :dossier_labels
after_destroy_commit :log_destroy after_destroy_commit :log_destroy

View file

@ -0,0 +1,6 @@
# frozen_string_literal: true
class DossierLabel < ApplicationRecord
belongs_to :dossier
belongs_to :label
end

35
app/models/label.rb Normal file
View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
class Label < ApplicationRecord
belongs_to :procedure
has_many :dossier_labels, dependent: :destroy
NAME_MAX_LENGTH = 30
GENERIC_LABELS = [
{ name: 'À examiner', color: 'purple_glycine' },
{ name: 'À relancer', color: 'green_tilleul_verveine' },
{ name: 'Complet', color: 'green_emeraude' },
{ name: 'À signer', color: 'blue_ecume' },
{ name: 'Urgent', color: 'pink_macaron' }
]
enum color: {
green_tilleul_verveine: "green-tilleul-verveine",
green_bourgeon: "green-bourgeon",
green_emeraude: "green-emeraude",
green_menthe: "green-menthe",
blue_ecume: "blue-ecume",
purple_glycine: "purple-glycine",
pink_macaron: "pink-macaron",
yellow_tournesol: "yellow-tournesol",
brown_cafe_creme: "brown-cafe-creme",
beige_gris_galet: "beige-gris-galet"
}
validates :name, :color, presence: true
validates :name, length: { maximum: NAME_MAX_LENGTH }
def self.class_name(color)
Label.colors.fetch(color.underscore)
end
end

View file

@ -60,6 +60,7 @@ class Procedure < ApplicationRecord
has_and_belongs_to_many :procedure_tags has_and_belongs_to_many :procedure_tags
has_many :bulk_messages, dependent: :destroy has_many :bulk_messages, dependent: :destroy
has_many :labels, dependent: :destroy
def active_dossier_submitted_message def active_dossier_submitted_message
published_dossier_submitted_message || draft_dossier_submitted_message published_dossier_submitted_message || draft_dossier_submitted_message
@ -528,6 +529,7 @@ class Procedure < ApplicationRecord
procedure.closing_notification_en_cours = false procedure.closing_notification_en_cours = false
procedure.template = false procedure.template = false
procedure.monavis_embed = nil procedure.monavis_embed = nil
procedure.labels = labels.map(&:dup)
if !procedure.valid? if !procedure.valid?
procedure.errors.attribute_names.each do |attribute| procedure.errors.attribute_names.each do |attribute|
@ -935,6 +937,12 @@ class Procedure < ApplicationRecord
end end
end end
def create_generic_labels
Label::GENERIC_LABELS.each do |label|
Label.create(name: label[:name], color: label[:color], procedure_id: self.id)
end
end
def stable_ids_used_by_routing_rules def stable_ids_used_by_routing_rules
@stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact @stable_ids_used_by_routing_rules ||= groupe_instructeurs.flat_map { _1.routing_rule&.sources }.compact
end end

View file

@ -56,6 +56,11 @@ class DossierFilterService
.order("#{sanitized_column(table, column)} #{order}") .order("#{sanitized_column(table, column)} #{order}")
.pluck(:id) .pluck(:id)
.uniq .uniq
when 'dossier_labels'
dossiers.includes(table)
.order("#{self.class.sanitized_column(table, column)} #{order}")
.pluck(:id)
.uniq
when 'self', 'user', 'individual', 'etablissement', 'groupe_instructeur' when 'self', 'user', 'individual', 'etablissement', 'groupe_instructeur'
(table == 'self' ? dossiers : dossiers.includes(table)) (table == 'self' ? dossiers : dossiers.includes(table))
.order("#{sanitized_column(table, column)} #{order}") .order("#{sanitized_column(table, column)} #{order}")
@ -122,6 +127,11 @@ class DossierFilterService
dossiers dossiers
.includes(table) .includes(table)
.filter_ilike(table, column, values) # ilike or where column == 'value' are both valid, we opted for ilike .filter_ilike(table, column, values) # ilike or where column == 'value' are both valid, we opted for ilike
when 'dossier_labels'
assert_supported_column(table, column)
dossiers
.joins(:dossier_labels)
.where(dossier_labels: { label_id: values })
when 'groupe_instructeur' when 'groupe_instructeur'
assert_supported_column(table, column) assert_supported_column(table, column)

View file

@ -123,6 +123,18 @@ class DossierProjectionService
fields[0][:id_value_h] = id_value_h fields[0][:id_value_h] = id_value_h
when 'dossier_labels'
columns = fields.map { _1[COLUMN].to_sym }
id_value_h =
DossierLabel
.includes(:label)
.where(dossier_id: dossiers_ids)
.pluck('dossier_id, labels.name, labels.color')
.group_by { |dossier_id, _| dossier_id }
fields[0][:id_value_h] = id_value_h.transform_values { |v| { value: v, type: :label } }
when 'procedure' when 'procedure'
Dossier Dossier
.joins(:procedure) .joins(:procedure)

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Maintenance
class BackfillLabelsForProceduresTask < MaintenanceTasks::Task
# Cette tâche permet de créer un jeu de labels génériques pour les anciennes procédures
# Plus d'informations sur l'implémentation des labels ici : https://github.com/demarches-simplifiees/demarches-simplifiees.fr/issues/9787
# 2024-10-15
include RunnableOnDeployConcern
run_on_first_deploy
def collection
Procedure
.includes(:labels)
.where(labels: { id: nil })
end
def process(procedure)
Label::GENERIC_LABELS.each do |label|
Label.create(name: label[:name], color: label[:color], procedure_id: procedure.id)
end
end
end
end

View file

@ -0,0 +1,15 @@
= form_with model: label, url: [:admin, @procedure, @label], local: true do |f|
= render Dsfr::InputComponent.new(form: f, attribute: :name, input_type: :text_field, opts: { maxlength: Label::NAME_MAX_LENGTH})
%fieldset.fr-fieldset
%legend.fr-fieldset__legend.fr-fieldset__legend--regular
= t('activerecord.attributes.label.color')
= asterisk
- @colors_collection.each do |color|
.fr-fieldset__element.fr-fieldset__element--inline
.fr-radio-group
= f.radio_button :color, color, checked: (label.color == color)
= f.label :color, t("activerecord.attributes.label/color.#{color}"), value: color, class: "fr-label fr-tag fr-tag--sm fr-tag--#{Label.class_name(color)}"
= render Procedure::FixedFooterComponent.new(procedure: @procedure, form: f)

View file

@ -0,0 +1,20 @@
- content_for :title, "Modifier le label"
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['gestion des labels', [:admin, @procedure, :labels]],
['Modifier le label']] }
.fr-container
.fr-mb-3w
= link_to "Liste de tous les labels",
[:admin, @procedure, :labels],
class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
%h1.fr-h2
Modifier le label
= render partial: 'form',
locals: { label: @label, procedure_id: @procedure.id }

View file

@ -0,0 +1,43 @@
- content_for :title, "Labels"
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Labels']] }
.fr-container
%h1.fr-h2 Labels
= link_to "Nouveau label",
[:new, :admin, @procedure, :label],
class: "fr-btn fr-btn--primary fr-btn--icon-left fr-icon-add-circle-line mb-3"
- if @procedure.labels.present?
.fr-table.fr-table--layout-fixed.fr-table--bordered
%table
%caption Liste des labels
%thead
%tr
%th{ scope: "col" }
Nom
%th.change{ scope: "col" }
Actions
%tbody
- @labels.each do |label|
%tr
%td
= tag_label(label.name, label.color)
%td.change
= link_to 'Modifier',
[:edit, :admin, @procedure, label],
class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-pencil-line'
= link_to 'Supprimer',
[:admin, @procedure, label],
method: :delete,
data: { confirm: "Confirmez vous la suppression de #{label.name}" },
class: 'fr-btn fr-btn--sm fr-btn--secondary fr-btn--icon-left fr-icon-delete-line fr-ml-1w'
= render Procedure::FixedFooterComponent.new(procedure: @procedure)

View file

@ -0,0 +1,20 @@
- content_for :title, "Nouveau label"
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['gestion des labels', [:admin, @procedure, :labels]],
['Nouveau label']] }
.fr-container
.fr-mb-3w
= link_to "Liste de tous les labels",
[:admin, @procedure, :labels],
class: "fr-link fr-icon-arrow-left-line fr-link--icon-left"
%h1.fr-h2
Créer un nouveau label
= render partial: 'form',
locals: { label: @label, procedure_id: @procedure.id }

View file

@ -98,3 +98,4 @@
= render Procedure::Card::DossierSubmittedMessageComponent.new(procedure: @procedure) = render Procedure::Card::DossierSubmittedMessageComponent.new(procedure: @procedure)
= render Procedure::Card::ChorusComponent.new(procedure: @procedure) = render Procedure::Card::ChorusComponent.new(procedure: @procedure)
= render Procedure::Card::AccuseLectureComponent.new(procedure: @procedure) = render Procedure::Card::AccuseLectureComponent.new(procedure: @procedure)
= render Procedure::Card::LabelsComponent.new(procedure: @procedure)

View file

@ -1,11 +1,11 @@
#header-top.fr-container #header-top.fr-container
.flex.fr-mb-3w .flex
%div %div
%h1.fr-h3.fr-mb-1w %h1.fr-h3.fr-mb-1w
= "Dossier nº #{dossier.id}" = "Dossier nº #{dossier.id}"
= link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link" = link_to dossier.procedure.libelle.truncate_words(10), instructeur_procedure_path(dossier.procedure), title: dossier.procedure.libelle, class: "fr-link"
.fr-mt-2w.badge-group .fr-mt-2w.fr-badge-group
= procedure_badge(dossier.procedure) = procedure_badge(dossier.procedure)
= status_badge(dossier.state) = status_badge(dossier.state)
@ -16,7 +16,6 @@
= render Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: dossier, procedure: dossier.procedure, with_label: true) = render Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: dossier, procedure: dossier.procedure, with_label: true)
.header-actions.fr-ml-auto .header-actions.fr-ml-auto
= render partial: 'instructeurs/dossiers/header_actions', locals: { dossier: } = render partial: 'instructeurs/dossiers/header_actions', locals: { dossier: }
= render partial: 'instructeurs/dossiers/print_and_export_actions', locals: { dossier: } = render partial: 'instructeurs/dossiers/print_and_export_actions', locals: { dossier: }
@ -26,3 +25,30 @@
- if dossier.user_deleted? - if dossier.user_deleted?
%p.fr-mb-1w %p.fr-mb-1w
%small Lusager a supprimé son compte. Vous pouvez archiver puis supprimer le dossier. %small Lusager a supprimé son compte. Vous pouvez archiver puis supprimer le dossier.
- if dossier.procedure.labels.present?
.fr-mb-3w
- if dossier.labels.present?
- dossier.labels.each do |label|
= tag_label(label.name, label.color)
= render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm fr-btn--tertiary-no-outline fr-pl-1v']}, menu_options: { class: ['dropdown-label left-aligned'] }) do |menu|
- if dossier.labels.empty?
- menu.with_button_inner_html do
Ajouter un label
- menu.with_form do
= form_with(url: dossier_labels_instructeur_dossier_path(dossier_id: dossier.id, procedure_id: dossier.procedure.id), method: :post, class: 'fr-p-3w', data: { controller: 'autosubmit', turbo: 'true' }) do |f|
%fieldset.fr-fieldset.fr-mt-2w.fr-mb-0
= f.collection_check_boxes :label_id, dossier.procedure.labels, :id, :name, include_hidden: false do |b|
.fr-fieldset__element
.fr-checkbox-group.fr-checkbox-group--sm.fr-mb-1w
= b.check_box(checked: DossierLabel.find_by(dossier_id: dossier.id, label_id: b.value).present? )
= b.label(class: "fr-label fr-tag fr-tag--sm fr-tag--#{Label.colors.fetch(b.object.color)}") { b.text }
%hr
%p.fr-text--sm.fr-text-mention--grey.fr-mb-0
%b Besoin d'autres labels ?
%br
Contactez les
= link_to 'administrateurs de la démarche', administrateurs_instructeur_procedure_path(dossier.procedure), class: 'fr-link fr-link--sm', **external_link_attributes

View file

@ -132,12 +132,12 @@
%td %td
- if p.hidden_by_administration_at.present? - if p.hidden_by_administration_at.present?
%span.cell-link %span.cell-link
= column = column.is_a?(Hash) ? tags_label(column[:value]) : column
- if p.hidden_by_user_at.present? - if p.hidden_by_user_at.present?
= "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}" = "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}"
- else - else
%a.cell-link{ href: path } %a.cell-link{ href: path }
= column = column.is_a?(Hash) ? tags_label(column[:value]) : column
= "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}" if p.hidden_by_user_at.present? = "- #{t("views.instructeurs.dossiers.deleted_reason.#{p.hidden_by_reason}")}" if p.hidden_by_user_at.present?
%td.status-col %td.status-col

View file

@ -5,7 +5,7 @@
= dsfr_icon('fr-icon-user-add-fill', :sm, :mr) = dsfr_icon('fr-icon-user-add-fill', :sm, :mr)
- if invites.present? - if invites.present?
= t('views.invites.dropdown.view_invited_people') = t('views.invites.dropdown.view_invited_people')
%span.badge= invites.size %span.fr-badge.fr-ml-1v= invites.size
- else - else
- if dossier.read_only? - if dossier.read_only?
= t('views.invites.dropdown.invite_to_view') = t('views.invites.dropdown.invite_to_view')

View file

@ -153,11 +153,6 @@
%span.label.refused .label.refused %span.label.refused .label.refused
%span.label.without-continuation .label.without-continuation %span.label.without-continuation .label.without-continuation
%h1 Badges
%span.badge 1
%span.badge.warning 1
%h1 Cards %h1 Cards
.card .card

View file

@ -3,5 +3,5 @@
%span.notifications{ 'aria-label': 'notifications' } %span.notifications{ 'aria-label': 'notifications' }
= link_to(url, 'aria-selected': active ? true : nil, class: 'fr-tabs__tab', role: 'tab' ) do = link_to(url, 'aria-selected': active ? true : nil, class: 'fr-tabs__tab', role: 'tab' ) do
- if badge.present? - if badge.present?
%span.badge.fr-mr-1w= badge %span.fr-badge.fr-badge--blue-ecume.fr-mr-1w= badge
= label = label

View file

@ -0,0 +1,17 @@
fr:
activerecord:
attributes:
label:
color: Couleur
name: Nom
label/color: &color
green_tilleul_verveine: 'tilleul'
green_bourgeon: 'bourgeon'
green_emeraude: 'émeraude'
green_menthe: 'menthe'
blue_ecume: 'écume'
purple_glycine: 'glycine'
pink_macaron: 'macaron'
yellow_tournesol: 'tournesol'
brown_cafe_creme: 'café'
beige_gris_galet: 'galet'

View file

@ -81,3 +81,5 @@ en:
association_date_creation: 'Association date de création' association_date_creation: 'Association date de création'
association_date_declaration: 'Association date de déclaration' association_date_declaration: 'Association date de déclaration'
association_date_publication: 'Association date de publication' association_date_publication: 'Association date de publication'
dossier_labels:
label_id: Labels

View file

@ -85,3 +85,5 @@ fr:
association_date_creation: 'Association date de création' association_date_creation: 'Association date de création'
association_date_declaration: 'Association date de déclaration' association_date_declaration: 'Association date de déclaration'
association_date_publication: 'Association date de publication' association_date_publication: 'Association date de publication'
dossier_labels:
label_id: Labels

View file

@ -512,6 +512,7 @@ Rails.application.routes.draw do
resources :commentaires, only: [:destroy] resources :commentaires, only: [:destroy]
post 'repousser-expiration' => 'dossiers#extend_conservation' post 'repousser-expiration' => 'dossiers#extend_conservation'
post 'repousser-expiration-and-restore' => 'dossiers#extend_conservation_and_restore' post 'repousser-expiration-and-restore' => 'dossiers#extend_conservation_and_restore'
post 'dossier_labels' => 'dossiers#dossier_labels'
get 'geo_data' get 'geo_data'
get 'apercu_attestation' get 'apercu_attestation'
get 'bilans_bdf' get 'bilans_bdf'
@ -707,6 +708,8 @@ Rails.application.routes.draw do
get 'preview', on: :member get 'preview', on: :member
end end
resources :labels, controller: 'labels'
resource :attestation_template, only: [:show, :edit, :update, :create] do resource :attestation_template, only: [:show, :edit, :update, :create] do
get 'preview', on: :member get 'preview', on: :member
end end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateLabels < ActiveRecord::Migration[7.0]
def change
create_table :labels do |t|
t.string :name
t.string :color
t.references :procedure, null: false, foreign_key: true
t.timestamps
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateDossierLabels < ActiveRecord::Migration[7.0]
def change
create_table :dossier_labels do |t|
t.references :dossier, null: false, foreign_key: true
t.references :label, null: false, foreign_key: true
t.timestamps
end
end
end

View file

@ -416,6 +416,15 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do
t.index ["resolved_at"], name: "index_dossier_corrections_on_resolved_at", where: "((resolved_at IS NULL) OR (resolved_at IS NOT NULL))" t.index ["resolved_at"], name: "index_dossier_corrections_on_resolved_at", where: "((resolved_at IS NULL) OR (resolved_at IS NOT NULL))"
end end
create_table "dossier_labels", force: :cascade do |t|
t.datetime "created_at", null: false
t.bigint "dossier_id", null: false
t.bigint "label_id", null: false
t.datetime "updated_at", null: false
t.index ["dossier_id"], name: "index_dossier_labels_on_dossier_id"
t.index ["label_id"], name: "index_dossier_labels_on_label_id"
end
create_table "dossier_operation_logs", force: :cascade do |t| create_table "dossier_operation_logs", force: :cascade do |t|
t.boolean "automatic_operation", default: false, null: false t.boolean "automatic_operation", default: false, null: false
t.bigint "bill_signature_id" t.bigint "bill_signature_id"
@ -820,6 +829,15 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do
t.index ["email", "dossier_id"], name: "index_invites_on_email_and_dossier_id", unique: true t.index ["email", "dossier_id"], name: "index_invites_on_email_and_dossier_id", unique: true
end end
create_table "labels", force: :cascade do |t|
t.string "color"
t.datetime "created_at", null: false
t.string "name"
t.bigint "procedure_id", null: false
t.datetime "updated_at", null: false
t.index ["procedure_id"], name: "index_labels_on_procedure_id"
end
create_table "maintenance_tasks_runs", force: :cascade do |t| create_table "maintenance_tasks_runs", force: :cascade do |t|
t.text "arguments" t.text "arguments"
t.text "backtrace" t.text "backtrace"
@ -1280,6 +1298,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do
add_foreign_key "dossier_batch_operations", "dossiers" add_foreign_key "dossier_batch_operations", "dossiers"
add_foreign_key "dossier_corrections", "commentaires" add_foreign_key "dossier_corrections", "commentaires"
add_foreign_key "dossier_corrections", "dossiers" add_foreign_key "dossier_corrections", "dossiers"
add_foreign_key "dossier_labels", "dossiers"
add_foreign_key "dossier_labels", "labels"
add_foreign_key "dossier_operation_logs", "bill_signatures" add_foreign_key "dossier_operation_logs", "bill_signatures"
add_foreign_key "dossier_transfer_logs", "dossiers" add_foreign_key "dossier_transfer_logs", "dossiers"
add_foreign_key "dossiers", "batch_operations" add_foreign_key "dossiers", "batch_operations"
@ -1300,6 +1320,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_14_084333) do
add_foreign_key "groupe_instructeurs", "procedures" add_foreign_key "groupe_instructeurs", "procedures"
add_foreign_key "initiated_mails", "procedures" add_foreign_key "initiated_mails", "procedures"
add_foreign_key "instructeurs", "users" add_foreign_key "instructeurs", "users"
add_foreign_key "labels", "procedures"
add_foreign_key "merge_logs", "users" add_foreign_key "merge_logs", "users"
add_foreign_key "procedure_presentations", "assign_tos" add_foreign_key "procedure_presentations", "assign_tos"
add_foreign_key "procedure_revision_types_de_champ", "procedure_revision_types_de_champ", column: "parent_id" add_foreign_key "procedure_revision_types_de_champ", "procedure_revision_types_de_champ", column: "parent_id"

View file

@ -68,7 +68,7 @@ describe MainNavigation::InstructeurExpertNavigationComponent, type: :component
it 'renders a link to expert all avis with current page class' do it 'renders a link to expert all avis with current page class' do
expect(subject).to have_link('Avis', href: component.helpers.expert_all_avis_path) expect(subject).to have_link('Avis', href: component.helpers.expert_all_avis_path)
expect(subject).to have_selector('a[aria-current="true"]', text: 'Avis') expect(subject).to have_selector('a[aria-current="true"]', text: 'Avis')
expect(subject).not_to have_selector('span.badge') expect(subject).not_to have_selector('span.fr-badge')
end end
it 'does not have Démarches link' do it 'does not have Démarches link' do
@ -79,7 +79,7 @@ describe MainNavigation::InstructeurExpertNavigationComponent, type: :component
let(:unanswered) { 2 } let(:unanswered) { 2 }
it 'renders an unanswered avis badge for the expert' do it 'renders an unanswered avis badge for the expert' do
expect(subject).to have_selector('span.badge.warning', text: '2') expect(subject).to have_selector('span.fr-badge', text: '2')
end end
end end

View file

@ -0,0 +1,168 @@
# frozen_string_literal: true
describe Administrateurs::LabelsController, type: :controller do
let(:admin) { administrateurs(:default_admin) }
let(:procedure) { create(:procedure, administrateur: admin) }
let(:admin_2) { create(:administrateur) }
let(:procedure_2) { create(:procedure, administrateur: admin_2) }
describe '#index' do
render_views
let!(:label_1) { create(:label, procedure:) }
let!(:label_2) { create(:label, procedure:) }
let!(:label_3) { create(:label, procedure:) }
before do
sign_in(admin.user)
end
subject { get :index, params: { procedure_id: procedure.id } }
it 'displays all procedure labels' do
subject
expect(response.body).to have_link("Nouveau label")
expect(response.body).to have_link("Modifier", count: 3)
expect(response.body).to have_link("Supprimer", count: 3)
end
end
describe '#create' do
before do
sign_in(admin.user)
end
subject { post :create, params: params }
context 'when submitting a new label' do
let(:params) do
{
label: {
name: 'Nouveau label',
color: 'green-bourgeon'
},
procedure_id: procedure.id
}
end
it { expect { subject }.to change { Label.count } .by(1) }
it 'creates a new label' do
subject
expect(flash.alert).to be_nil
expect(flash.notice).to eq('Le label a bien été créé')
expect(Label.last.name).to eq('Nouveau label')
expect(Label.last.color).to eq('green_bourgeon')
expect(procedure.labels.last).to eq(Label.last)
end
end
context 'when submitting an invalid label' do
let(:params) { { label: { name: 'Nouveau label' }, procedure_id: procedure.id } }
it { expect { subject }.not_to change { Label.count } }
it 'does not create a new label' do
subject
expect(flash.alert).to eq(["Le champ « Couleur » doit être rempli"])
expect(response).to render_template(:new)
expect(assigns(:label).name).to eq('Nouveau label')
end
end
context 'when submitting a label for a not own procedure' do
let(:params) do
{
label: {
name: 'Nouveau label',
color: 'green-bourgeon'
},
procedure_id: procedure_2.id
}
end
it { expect { subject }.not_to change { Label.count } }
it 'does not create a new label' do
subject
expect(flash.alert).to eq("Démarche inexistante")
expect(response.status).to eq(404)
end
end
end
describe '#update' do
let!(:label) { create(:label, procedure:) }
let(:label_params) { { name: 'Nouveau nom' } }
let(:params) { { id: label.id, label: label_params, procedure_id: procedure.id } }
before do
sign_in(admin.user)
end
subject { patch :update, params: }
context 'when updating a label' do
it 'updates correctly' do
subject
expect(flash.alert).to be_nil
expect(flash.notice).to eq('Le label a bien été modifié')
expect(label.reload.name).to eq('Nouveau nom')
expect(label.reload.color).to eq('green_bourgeon')
expect(label.reload.updated_at).not_to eq(label.reload.created_at)
expect(response).to redirect_to(admin_procedure_labels_path(procedure_id: procedure.id))
end
end
context 'when updating a service with invalid data' do
let(:label_params) { { name: '' } }
it 'does not update' do
subject
expect(flash.alert).not_to be_nil
expect(response).to render_template(:edit)
expect(label.reload.updated_at).to eq(label.reload.created_at)
end
end
context 'when updating a label for a not own procedure' do
let(:params) { { id: label.id, label: label_params, procedure_id: procedure_2.id } }
it 'does not update' do
subject
expect(label.reload.updated_at).to eq(label.reload.created_at)
end
end
end
describe '#destroy' do
let(:label) { create(:label, procedure:) }
before do
sign_in(admin.user)
end
subject { delete :destroy, params: }
context "when deleting a label" do
let(:params) { { id: label.id, procedure_id: procedure.id } }
it "delete the label" do
subject
expect { label.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(flash.notice).to eq('Le label a bien été supprimé')
expect(response).to redirect_to(admin_procedure_labels_path(procedure_id: procedure.id))
end
end
context 'when deleting a label for a not own procedure' do
let(:params) { { id: label.id, procedure_id: procedure_2.id } }
it 'does not delete' do
subject
expect(flash.alert).to eq("Démarche inexistante")
expect(response.status).to eq(404)
expect { label.reload }.not_to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end

View file

@ -513,6 +513,11 @@ describe Administrateurs::ProceduresController, type: :controller do
expect(response).to redirect_to(champs_admin_procedure_path(Procedure.last)) expect(response).to redirect_to(champs_admin_procedure_path(Procedure.last))
expect(flash[:notice]).to be_present expect(flash[:notice]).to be_present
end end
it "create generic labels" do
expect(subject.labels.size).to eq(5)
expect(subject.labels.first.name).to eq('À examiner')
end
end end
describe "procedure is saved with custom retention period" do describe "procedure is saved with custom retention period" do
@ -657,7 +662,7 @@ describe Administrateurs::ProceduresController, type: :controller do
end end
describe 'PUT #clone' do describe 'PUT #clone' do
let(:procedure) { create(:procedure, :with_notice, :with_deliberation, administrateur: admin) } let(:procedure) { create(:procedure, :with_notice, :with_deliberation, :with_labels, administrateur: admin) }
let(:params) { { procedure_id: procedure.id } } let(:params) { { procedure_id: procedure.id } }
subject { put :clone, params: params } subject { put :clone, params: params }
@ -679,6 +684,10 @@ describe Administrateurs::ProceduresController, type: :controller do
expect(Procedure.last.cloned_from_library).to be_falsey expect(Procedure.last.cloned_from_library).to be_falsey
expect(Procedure.last.notice.attached?).to be_truthy expect(Procedure.last.notice.attached?).to be_truthy
expect(Procedure.last.deliberation.attached?).to be_truthy expect(Procedure.last.deliberation.attached?).to be_truthy
expect(Procedure.last.labels.present?).to be_truthy
expect(Procedure.last.labels.first.procedure_id).to eq(Procedure.last.id)
expect(procedure.labels.first.procedure_id).to eq(procedure.id)
expect(flash[:notice]).to have_content 'Démarche clonée. Pensez à vérifier la présentation et choisir le service à laquelle cette démarche est associée.' expect(flash[:notice]).to have_content 'Démarche clonée. Pensez à vérifier la présentation et choisir le service à laquelle cette démarche est associée.'
end end
@ -700,6 +709,7 @@ describe Administrateurs::ProceduresController, type: :controller do
it 'creates a new procedure and redirect to it' do it 'creates a new procedure and redirect to it' do
expect(response).to redirect_to admin_procedure_path(id: Procedure.last.id) expect(response).to redirect_to admin_procedure_path(id: Procedure.last.id)
expect(Procedure.last.labels.present?).to be_truthy
expect(flash[:notice]).to have_content 'Démarche clonée. Pensez à vérifier la présentation et choisir le service à laquelle cette démarche est associée.' expect(flash[:notice]).to have_content 'Démarche clonée. Pensez à vérifier la présentation et choisir le service à laquelle cette démarche est associée.'
end end
end end

View file

@ -1518,4 +1518,38 @@ describe Instructeurs::DossiersController, type: :controller do
expect([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp, Commentaire]).to include(*assigns(:gallery_attachments).map { _1.record.class }) expect([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp, Commentaire]).to include(*assigns(:gallery_attachments).map { _1.record.class })
end end
end end
describe 'dossier_labels' do
let(:procedure) { create(:procedure, :with_labels, instructeurs: [instructeur]) }
let!(:dossier) { create(:dossier, :en_construction, procedure:) }
context 'it create dossier labels' do
subject { post :dossier_labels, params: { procedure_id: procedure.id, dossier_id: dossier.id, label_id: [Label.first.id] }, format: :turbo_stream }
it 'works' do
subject
dossier.reload
expect(dossier.dossier_labels.count).to eq(1)
expect(subject.body).to include('fr-tag--purple-glycine')
expect(subject.body).not_to include('Ajouter un label')
end
end
context 'it remove dossier labels' do
before do
DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.first.id)
end
subject { post :dossier_labels, params: { procedure_id: procedure.id, dossier_id: dossier.id, label_id: [] }, format: :turbo_stream }
it 'works' do
expect(dossier.dossier_labels.count).to eq(1)
subject
dossier.reload
expect(dossier.dossier_labels.count).to eq(0)
expect(subject.body).to include('Ajouter un label')
end
end
end
end end

View file

@ -637,6 +637,38 @@ describe Instructeurs::ProceduresController, type: :controller do
it { expect(assigns(:last_export)).to eq(nil) } it { expect(assigns(:last_export)).to eq(nil) }
end end
end end
context 'dossier labels' do
let(:procedure) { create(:procedure, :with_labels, instructeurs: [instructeur]) }
let!(:dossier) { create(:dossier, :en_construction, procedure:, groupe_instructeur: gi_2) }
let!(:dossier_2) { create(:dossier, :en_construction, procedure:, groupe_instructeur: gi_2) }
let(:statut) { 'tous' }
let(:label_id) { procedure.find_column(label: 'Labels') }
let!(:procedure_presentation) do
ProcedurePresentation.create!(assign_to: AssignTo.first)
end
render_views
before do
DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.first.id)
DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.second.id)
DossierLabel.create(dossier_id: dossier_2.id, label_id: dossier.procedure.labels.last.id)
procedure_presentation.update(displayed_columns: [
label_id.id
])
subject
end
it 'displays correctly labels in instructeur table' do
expect(response.body).to include("Labels")
expect(response.body).to have_selector('ul.fr-tags-group li span.fr-tag', text: 'À examiner')
expect(response.body).to have_selector('ul.fr-tags-group li span.fr-tag', text: 'À relancer')
expect(response.body).not_to have_selector('ul li span.fr-tag', text: 'Urgent')
expect(response.body).to have_selector('span.fr-tag', text: 'Urgent')
end
end
end end
end end

9
spec/factories/label.rb Normal file
View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :label do
name { 'Un label' }
color { 'green-bourgeon' }
association :procedure
end
end

View file

@ -291,6 +291,12 @@ FactoryBot.define do
trait :accuse_lecture do trait :accuse_lecture do
accuse_lecture { true } accuse_lecture { true }
end end
trait :with_labels do
after(:create) do |procedure, _evaluator|
procedure.create_generic_labels
end
end
end end
end end

View file

@ -51,6 +51,7 @@ describe ColumnsConcern do
{ label: 'Groupe instructeur', table: 'groupe_instructeur', column: 'id', displayable: true, type: :enum, scope: '', value_column: :value, filterable: true }, { label: 'Groupe instructeur', table: 'groupe_instructeur', column: 'id', displayable: true, type: :enum, scope: '', value_column: :value, filterable: true },
{ label: 'Avis oui/non', table: 'avis', column: 'question_answer', displayable: true, type: :text, scope: '', value_column: :value, filterable: false }, { label: 'Avis oui/non', table: 'avis', column: 'question_answer', displayable: true, type: :text, scope: '', value_column: :value, filterable: false },
{ label: 'France connecté ?', table: 'self', column: 'user_from_france_connect?', displayable: false, type: :text, scope: '', value_column: :value, filterable: false }, { label: 'France connecté ?', table: 'self', column: 'user_from_france_connect?', displayable: false, type: :text, scope: '', value_column: :value, filterable: false },
{ label: "Labels", table: "dossier_labels", column: "label_id", displayable: true, scope: '', value_column: :value, filterable: true },
{ label: 'SIREN', table: 'etablissement', column: 'entreprise_siren', displayable: true, type: :text, scope: '', value_column: :value, filterable: true }, { label: 'SIREN', table: 'etablissement', column: 'entreprise_siren', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
{ label: 'Forme juridique', table: 'etablissement', column: 'entreprise_forme_juridique', displayable: true, type: :text, scope: '', value_column: :value, filterable: true }, { label: 'Forme juridique', table: 'etablissement', column: 'entreprise_forme_juridique', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },
{ label: 'Nom commercial', table: 'etablissement', column: 'entreprise_nom_commercial', displayable: true, type: :text, scope: '', value_column: :value, filterable: true }, { label: 'Nom commercial', table: 'etablissement', column: 'entreprise_nom_commercial', displayable: true, type: :text, scope: '', value_column: :value, filterable: true },

View file

@ -72,7 +72,7 @@ describe 'Inviting an expert:', js: true do
expect(page).to have_text('1 avis à donner') expect(page).to have_text('1 avis à donner')
expect(page).to have_text('0 avis donnés') expect(page).to have_text('0 avis donnés')
expect(page).to have_selector('.badge', text: 1) expect(page).to have_selector('.fr-badge', text: 1)
expect(page).to have_selector('.notifications') expect(page).to have_selector('.notifications')
click_on '1 avis à donner' click_on '1 avis à donner'
@ -93,7 +93,7 @@ describe 'Inviting an expert:', js: true do
expect(page).to have_text('0 avis à donner') expect(page).to have_text('0 avis à donner')
expect(page).to have_text('1 avis donné') expect(page).to have_text('1 avis donné')
expect(page).not_to have_selector('.badge', text: 1) expect(page).not_to have_selector('.fr-badge', text: 1)
expect(page).not_to have_selector('.notifications') expect(page).not_to have_selector('.notifications')
end end

View file

@ -272,6 +272,39 @@ describe 'Instructing a dossier:', js: true do
after { DownloadHelpers.clear_downloads } after { DownloadHelpers.clear_downloads }
end end
context 'An instructeur can add labels' do
let(:procedure) { create(:procedure, :with_labels, :published, instructeurs: [instructeur]) }
scenario 'An instructeur can add and remove labels to a dossier' do
log_in(instructeur.email, password)
visit instructeur_dossier_path(procedure, dossier)
click_on 'Ajouter un label'
check 'À relancer', allow_label_click: true
expect(page).to have_css('.fr-tag', text: "À relancer", count: 2)
expect(dossier.dossier_labels.count).to eq(1)
expect(page).not_to have_text('Ajouter un label')
find('span.dropdown button.dropdown-button').click
expect(page).to have_checked_field('À relancer')
check 'Complet', allow_label_click: true
expect(page).to have_css('.fr-tag', text: "Complet", count: 2)
expect(dossier.dossier_labels.count).to eq(2)
find('span.dropdown button.dropdown-button').click
uncheck 'À relancer', allow_label_click: true
expect(page).to have_unchecked_field('À relancer')
expect(page).to have_checked_field('Complet')
expect(page).to have_css('.fr-tag', text: "À relancer", count: 1)
expect(page).to have_css('.fr-tag', text: "Complet", count: 2)
expect(dossier.dossier_labels.count).to eq(1)
end
end
def log_in(email, password, check_email: true) def log_in(email, password, check_email: true)
visit new_user_session_path visit new_user_session_path
expect(page).to have_current_path(new_user_session_path) expect(page).to have_current_path(new_user_session_path)

View file

@ -2,7 +2,7 @@
describe "procedure filters" do describe "procedure filters" do
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let(:procedure) { create(:procedure, :published, types_de_champ_public:, instructeurs: [instructeur]) } let(:procedure) { create(:procedure, :published, :with_labels, types_de_champ_public:, instructeurs: [instructeur]) }
let(:types_de_champ_public) { [{ type: :text }] } let(:types_de_champ_public) { [{ type: :text }] }
let!(:type_de_champ) { procedure.active_revision.types_de_champ_public.first } let!(:type_de_champ) { procedure.active_revision.types_de_champ_public.first }
let!(:new_unfollow_dossier) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction)) } let!(:new_unfollow_dossier) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction)) }
@ -94,6 +94,7 @@ describe "procedure filters" do
expect(page).to have_link(new_unfollow_dossier_2.user.email) expect(page).to have_link(new_unfollow_dossier_2.user.email)
end end
end end
describe 'with dropdown' do describe 'with dropdown' do
let(:types_de_champ_public) { [{ type: :drop_down_list }] } let(:types_de_champ_public) { [{ type: :drop_down_list }] }
@ -171,6 +172,15 @@ describe "procedure filters" do
end end
end end
describe 'dossier labels' do
scenario "should be able to filter by dossier labels", js: true do
DossierLabel.create!(dossier_id: new_unfollow_dossier.id, label_id: procedure.labels.first.id)
add_filter('Labels', procedure.labels.first.name, type: :enum)
expect(page).to have_link(new_unfollow_dossier.id.to_s)
expect(page).not_to have_link(new_unfollow_dossier_2.id.to_s)
end
end
scenario "should be able to add and remove two filters for the same field", js: true do 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.value)
add_filter(type_de_champ.libelle, champ_2.value) add_filter(type_de_champ.libelle, champ_2.value)

View file

@ -217,4 +217,44 @@ describe 'instructeurs/dossiers/show', type: :view do
expect(subject).to have_selector('a.fr-sidemenu__link', text: 'l1') expect(subject).to have_selector('a.fr-sidemenu__link', text: 'l1')
end end
end end
describe "Dossier labels" do
let(:procedure) { create(:procedure, :with_labels) }
let(:dossier) { create(:dossier, :en_construction, procedure:) }
context "Procedure without labels" do
let(:procedure_without_labels) { create(:procedure) }
let(:dossier) { create(:dossier, :en_construction, procedure: procedure_without_labels) }
it 'does not display button to add label or dropdown' do
expect(subject).not_to have_text("Ajouter un label")
expect(subject).not_to have_text("À examiner")
end
end
context "Dossier without labels" do
it 'displays button with text to add label' do
expect(subject).to have_text("Ajouter un label")
expect(subject).to have_selector("button.dropdown-button")
expect(subject).to have_text("À examiner", count: 1)
within('.dropdown') do
expect(subject).to have_text("À examiner", count: 1)
end
end
end
context "Dossier with labels" do
before do
DossierLabel.create(dossier_id: dossier.id, label_id: dossier.procedure.labels.first.id)
end
it 'displays labels and button without text to add label' do
expect(subject).not_to have_text("Ajouter un label")
expect(subject).to have_selector("button.dropdown-button")
expect(subject).to have_text("À examiner", count: 2)
within('.dropdown') do
expect(subject).to have_text("À examiner", count: 1)
end
end
end
end
end end