diff --git a/app/components/instructeurs/column_filter_component.rb b/app/components/instructeurs/column_filter_component.rb index 5016b4bc5..f92a3c623 100644 --- a/app/components/instructeurs/column_filter_component.rb +++ b/app/components/instructeurs/column_filter_component.rb @@ -3,45 +3,18 @@ class Instructeurs::ColumnFilterComponent < ApplicationComponent attr_reader :procedure, :procedure_presentation, :statut, :column - def initialize(procedure:, procedure_presentation:, statut:, column: nil) - @procedure = procedure + def initialize(procedure_presentation:, statut:, column: nil) @procedure_presentation = procedure_presentation + @procedure = procedure_presentation.procedure @statut = statut @column = column end - def column_type = column.present? ? column.type : :text - - def html_column_type - case column_type - when :datetime, :date - 'date' - when :integer, :decimal - 'number' - else - 'text' - end - end - - def options_for_select_of_column - if column.scope.present? - I18n.t(column.scope).map(&:to_a).map(&:reverse) - elsif column.table == 'groupe_instructeur' - current_instructeur.groupe_instructeurs.filter_map do - if _1.procedure_id == procedure.id - [_1.label, _1.id] - end - end - else - find_type_de_champ(column.column).options_for_select(column) - end - end - def filter_react_props { selected_key: column.present? ? column.id : '', items: filterable_columns_options, - name: "#{prefix}[id]", + name: "filters[][id]", id: 'search-filter', 'aria-describedby': 'instructeur-filter-combo-label', form: 'filter-component', @@ -56,22 +29,9 @@ class Instructeurs::ColumnFilterComponent < ApplicationComponent def current_filter_tags @procedure_presentation.filters_for(@statut).flat_map do [ - hidden_field_tag("#{prefix}[id]", _1.column.id, id: nil), - hidden_field_tag("#{prefix}[filter]", _1.filter, id: nil) + hidden_field_tag("filters[][id]", _1.column.id, id: nil), + hidden_field_tag("filters[][filter]", _1.filter, id: nil) ] end.reduce(&:concat) end - - def prefix = "#{procedure_presentation.filters_name_for(@statut)}[]" - - private - - def find_type_de_champ(column) - stable_id = column.to_s.split('->').first - TypeDeChamp - .joins(:revision_types_de_champ) - .where(revision_types_de_champ: { revision_id: procedure.revisions }) - .order(created_at: :desc) - .find_by(stable_id:) - end end diff --git a/app/components/instructeurs/column_filter_component/column_filter_component.html.haml b/app/components/instructeurs/column_filter_component/column_filter_component.html.haml index ec9c7eee6..c8b94293f 100644 --- a/app/components/instructeurs/column_filter_component/column_filter_component.html.haml +++ b/app/components/instructeurs/column_filter_component/column_filter_component.html.haml @@ -1,4 +1,7 @@ -= form_tag add_filter_instructeur_procedure_path(procedure), method: :post, class: 'dropdown-form large', id: 'filter-component', data: { turbo: true, controller: 'autosubmit' } do += form_with model: [:instructeur, @procedure_presentation], + class: 'dropdown-form large', + id: 'filter-component', + data: { turbo: true, controller: 'autosubmit' } do = current_filter_tags .fr-select-group @@ -8,28 +11,14 @@ %input.hidden{ type: 'submit', - formaction: update_filter_instructeur_procedure_path(procedure), + formmethod: 'get', + formaction: url_for([:refresh_column_filter, :instructeur, @procedure_presentation]), formnovalidate: 'true', data: { autosubmit_target: 'submitter' } } = label_tag :value, t('.value'), for: 'value', class: 'fr-label' - - if column_type.in?([:enum, :enums, :boolean]) - = select_tag :filter, - options_for_select(options_for_select_of_column), - id: 'value', - name: "#{prefix}[filter]", - class: 'fr-select', - data: { no_autosubmit: true } - - else - %input#value.fr-input{ - type: html_column_type, - name: "#{prefix}[filter]", - maxlength: FilteredColumn::FILTERS_VALUE_MAX_LENGTH, - disabled: column.nil? ? true : false, - data: { no_autosubmit: true }, - required: true - } + = render Instructeurs::ColumnFilterValueComponent.new(column:) = hidden_field_tag :statut, statut = submit_tag t('.add_filter'), class: 'fr-btn fr-btn--secondary fr-mt-2w' diff --git a/app/components/instructeurs/column_filter_value_component.rb b/app/components/instructeurs/column_filter_value_component.rb new file mode 100644 index 000000000..d83a239ea --- /dev/null +++ b/app/components/instructeurs/column_filter_value_component.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class Instructeurs::ColumnFilterValueComponent < ApplicationComponent + attr_reader :column + + def initialize(column:) + @column = column + end + + def column_type = column.present? ? column.type : :text + + def call + if column_type.in?([:enum, :enums, :boolean]) + select_tag :filter, + options_for_select(options_for_select_of_column), + id: 'value', + name: "filters[][filter]", + class: 'fr-select', + data: { no_autosubmit: true } + else + tag.input( + class: 'fr-input', + id: 'value', + type:, + name: "filters[][filter]", + maxlength: FilteredColumn::FILTERS_VALUE_MAX_LENGTH, + disabled: column.nil? ? true : false, + data: { no_autosubmit: true }, + required: true + ) + end + end + + private + + def type + case column_type + when :datetime, :date + 'date' + when :integer, :decimal + 'number' + else + 'text' + end + end + + def options_for_select_of_column + if column.scope.present? + I18n.t(column.scope).map(&:to_a).map(&:reverse) + elsif column.table == 'groupe_instructeur' + current_instructeur.groupe_instructeurs.filter_map do + if _1.procedure_id == procedure_id + [_1.label, _1.id] + end + end + else + find_type_de_champ(column.column).options_for_select(column) + end + end + + def find_type_de_champ(column) + stable_id = column.to_s.split('->').first + TypeDeChamp + .joins(:revision_types_de_champ) + .where(revision_types_de_champ: { revision_id: ProcedureRevision.where(procedure_id:) }) + .order(created_at: :desc) + .find_by(stable_id:) + end + + def procedure_id = @column.h_id[:procedure_id] +end diff --git a/app/components/instructeurs/column_picker_component/column_picker_component.html.haml b/app/components/instructeurs/column_picker_component/column_picker_component.html.haml index a5953d0be..717ac143e 100644 --- a/app/components/instructeurs/column_picker_component/column_picker_component.html.haml +++ b/app/components/instructeurs/column_picker_component/column_picker_component.html.haml @@ -1,5 +1,5 @@ -= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do += form_with model: [:instructeur, @procedure_presentation], class: 'dropdown-form large columns-form' do %react-fragment - = render ReactComponent.new "ComboBox/MultiComboBox", items: @displayable_columns_for_select, selected_keys: @displayable_columns_selected, name: 'values[]', 'aria-label': 'Colonne à afficher', value_separator: false + = render ReactComponent.new "ComboBox/MultiComboBox", items: @displayable_columns_for_select, selected_keys: @displayable_columns_selected, name: 'displayed_columns[]', 'aria-label': 'Colonne à afficher', value_separator: false = submit_tag t('.save'), class: 'fr-btn fr-btn--secondary' diff --git a/app/components/instructeurs/column_table_header_component.rb b/app/components/instructeurs/column_table_header_component.rb index 004ef3138..fa4ff6f90 100644 --- a/app/components/instructeurs/column_table_header_component.rb +++ b/app/components/instructeurs/column_table_header_component.rb @@ -2,7 +2,7 @@ class Instructeurs::ColumnTableHeaderComponent < ApplicationComponent def initialize(procedure_presentation:) - @procedure = procedure_presentation.procedure + @procedure_presentation = procedure_presentation @columns = procedure_presentation.displayed_fields_for_headers @sorted_column = procedure_presentation.sorted_column end @@ -15,11 +15,16 @@ class Instructeurs::ColumnTableHeaderComponent < ApplicationComponent return 'sva-col' if column.column == 'sva_svr_decision_on' end - def update_sort_path(column) + def column_header(column) id = column.id order = opposite_order_for(column) - update_sort_instructeur_procedure_path(@procedure, sorted_column: { id:, order: }) + button_to( + label_and_arrow(column), + [:instructeur, @procedure_presentation], + params: { sorted_column: { id: id, order: order } }, + class: 'fr-text--bold' + ) end def opposite_order_for(column) diff --git a/app/components/instructeurs/column_table_header_component/column_table_header_component.html.haml b/app/components/instructeurs/column_table_header_component/column_table_header_component.html.haml index 1e2857ec6..0a15f2350 100644 --- a/app/components/instructeurs/column_table_header_component/column_table_header_component.html.haml +++ b/app/components/instructeurs/column_table_header_component/column_table_header_component.html.haml @@ -1,3 +1,3 @@ - @columns.each do |column| %th{ aria_sort(column), scope: "col", class: classname(column) } - = link_to label_and_arrow(column), update_sort_path(column) + = column_header(column) diff --git a/app/components/instructeurs/filter_buttons_component.rb b/app/components/instructeurs/filter_buttons_component.rb new file mode 100644 index 000000000..950adee9b --- /dev/null +++ b/app/components/instructeurs/filter_buttons_component.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +class Instructeurs::FilterButtonsComponent < ApplicationComponent + def initialize(filters:, procedure_presentation:, statut:) + @filters = filters + @procedure_presentation = procedure_presentation + @statut = statut + end + + def call + safe_join(filters_by_family, ' et ') + end + + private + + def filters_by_family + @filters + .group_by { _1.column.id } + .values + .map { |group| group.map { |f| filter_form(f) } } + .map { |group| safe_join(group, ' ou ') } + end + + def filter_form(filter) + form_with(model: [:instructeur, @procedure_presentation], class: 'inline') do + safe_join([ + hidden_field_tag('filters[]', ''), # to ensure the filters is not empty + *other_hidden_fields(filter), # other filters to keep + hidden_field_tag('statut', @statut), # collection to set + button_tag(button_content(filter), class: 'fr-tag fr-tag--dismiss fr-my-1w') + ]) + end + end + + def other_hidden_fields(filter) + @filters.reject { _1 == filter }.flat_map do |f| + [ + hidden_field_tag("filters[][id]", f.column.id), + hidden_field_tag("filters[][filter]", f.filter) + ] + end + end + + def button_content(filter) + "#{filter.label.truncate(50)} : #{human_value(filter)}" + end + + def human_value(filter_column) + column, filter = filter_column.column, filter_column.filter + + if column.type_de_champ? + find_type_de_champ(column.column).dynamic_type.filter_to_human(filter) + elsif column.dossier_state? + if filter == 'pending_correction' + Dossier.human_attribute_name("pending_correction.for_instructeur") + else + Dossier.human_attribute_name("state.#{filter}") + end + elsif column.groupe_instructeur? + current_instructeur.groupe_instructeurs + .find { _1.id == filter.to_i }&.label || filter + elsif column.type == :date + helpers.try_parse_format_date(filter) + else + filter + end + end + + def find_type_de_champ(column) + stable_id = column.to_s.split('->').first + + TypeDeChamp + .joins(:revision_types_de_champ) + .where(revision_types_de_champ: { revision_id: @procedure_presentation.procedure.revisions }) + .order(created_at: :desc) + .find_by(stable_id:) + end +end diff --git a/app/controllers/instructeurs/procedure_presentation_controller.rb b/app/controllers/instructeurs/procedure_presentation_controller.rb new file mode 100644 index 000000000..952464188 --- /dev/null +++ b/app/controllers/instructeurs/procedure_presentation_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Instructeurs + class ProcedurePresentationController < InstructeurController + before_action :set_procedure_presentation, only: [:update] + + def update + if !@procedure_presentation.update(procedure_presentation_params) + # complicated way to display inner error messages + flash.alert = @procedure_presentation.errors + .flat_map { _1.detail[:value].flat_map { |c| c.errors.full_messages } } + end + + redirect_back_or_to([:instructeur, procedure]) + end + + def refresh_column_filter + # According to the html, the selected filters is the last one + column = ColumnType.new.cast(params['filters'].last['id']) + component = Instructeurs::ColumnFilterValueComponent.new(column:) + + render turbo_stream: turbo_stream.replace('value', component) + end + + private + + def procedure = @procedure_presentation.procedure + + def procedure_presentation_params + h = params.permit(displayed_columns: [], sorted_column: [:order, :id], filters: [:id, :filter]).to_h + + if params[:statut].present? + filter_name = @procedure_presentation.filters_name_for(params[:statut]) + h[filter_name] = h.delete("filters") # move filters to the right key, ex: tous_filters + end + + # React ComboBox/MultiComboBox return [''] when no value is selected + # We need to remove them + if h[:displayed_columns].present? + h[:displayed_columns] = h[:displayed_columns].reject(&:empty?) + end + + h + end + + def set_procedure_presentation + @procedure_presentation = ProcedurePresentation + .includes(:assign_to) + .find_by!(id: params[:id], assign_to: { instructeur: current_instructeur }) + end + end +end diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 2d9b4b26f..d110d1434 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -132,6 +132,7 @@ module Instructeurs @statut = 'supprime' end + # TODO: to remove because of new procedure_presentation_controller def update_displayed_fields ids = (params['values'].presence || []).reject(&:empty?) @@ -140,12 +141,14 @@ module Instructeurs redirect_back(fallback_location: instructeur_procedure_url(procedure)) end + # TODO: to remove because of new procedure_presentation_controller def update_sort procedure_presentation.update!(sorted_column_params) redirect_back(fallback_location: instructeur_procedure_url(procedure)) end + # TODO: to remove because of new procedure_presentation_controller def add_filter if !procedure_presentation.update(filter_params) # complicated way to display inner error messages @@ -156,6 +159,7 @@ module Instructeurs redirect_back(fallback_location: instructeur_procedure_url(procedure)) end + # TODO: to remove because of new procedure_presentation_controller def update_filter @statut = statut @procedure = procedure @@ -407,10 +411,12 @@ module Instructeurs "exports_#{@procedure.id}_seen_at" end + # TODO: to remove because of new procedure_presentation_controller def sorted_column_params params.permit(sorted_column: [:order, :id]) end + # TODO: to remove because of new procedure_presentation_controller def filter_params keys = [:tous_filters, :a_suivre_filters, :suivis_filters, :traites_filters, :expirant_filters, :archives_filters, :supprimes_filters] h = keys.index_with { [:id, :filter] } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 88a398cd7..3fdc45795 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -113,6 +113,10 @@ module ApplicationHelper datetime.present? ? I18n.l(datetime, format:) : '' end + def try_parse_format_date(date) + date.then { Date.parse(_1) rescue nil }&.then { I18n.l(_1) } + end + def try_format_mois_effectif(etablissement) if etablissement.entreprise_effectif_mois.present? && etablissement.entreprise_effectif_annee.present? [etablissement.entreprise_effectif_mois, etablissement.entreprise_effectif_annee].join('/') diff --git a/app/models/assign_to.rb b/app/models/assign_to.rb index d9f6071c5..cfab77484 100644 --- a/app/models/assign_to.rb +++ b/app/models/assign_to.rb @@ -24,7 +24,9 @@ class AssignTo < ApplicationRecord errors = begin procedure_presentation.errors if procedure_presentation&.invalid? rescue ActiveRecord::RecordNotFound => e - [e.message] + errors = ActiveModel::Errors.new(self) + errors.add(:procedure_presentation, e.message) + errors end if errors.present? diff --git a/app/models/column.rb b/app/models/column.rb index 65497078a..94f9295f8 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -38,8 +38,9 @@ class Column end def notifications? = [table, column] == ['notifications', 'notifications'] - def dossier_state? = [table, column] == ['self', 'state'] + def groupe_instructeur? = [table, column] == ['groupe_instructeur', 'id'] + def type_de_champ? = table == TYPE_DE_CHAMP_TABLE def self.find(h_id) begin diff --git a/app/models/filtered_column.rb b/app/models/filtered_column.rb index cd1e1f774..c861b7338 100644 --- a/app/models/filtered_column.rb +++ b/app/models/filtered_column.rb @@ -33,7 +33,7 @@ class FilteredColumn private def check_filter_max_length - if @filter.present? && @filter.length.to_i > FILTERS_VALUE_MAX_LENGTH + if @filter.present? && @filter.length > FILTERS_VALUE_MAX_LENGTH errors.add( :base, "Le filtre « #{label} » est trop long (maximum: #{FILTERS_VALUE_MAX_LENGTH} caractères)" diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb index c45e6ac20..2df4db930 100644 --- a/app/models/procedure_presentation.rb +++ b/app/models/procedure_presentation.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class ProcedurePresentation < ApplicationRecord - TYPE_DE_CHAMP = 'type_de_champ' - belongs_to :assign_to, optional: false has_many :exports, dependent: :destroy @@ -42,45 +40,4 @@ class ProcedurePresentation < ApplicationRecord columns.concat(procedure.sva_svr_columns) if procedure.sva_svr_enabled? columns end - - def human_value_for_filter(filtered_column) - if filtered_column.column.table == TYPE_DE_CHAMP - find_type_de_champ(filtered_column.column.column).dynamic_type.filter_to_human(filtered_column.filter) - elsif filtered_column.column.column == 'state' - if filtered_column.filter == 'pending_correction' - Dossier.human_attribute_name("pending_correction.for_instructeur") - else - Dossier.human_attribute_name("state.#{filtered_column.filter}") - end - elsif filtered_column.column.table == 'groupe_instructeur' && filtered_column.column.column == 'id' - instructeur.groupe_instructeurs - .find { _1.id == filtered_column.filter.to_i }&.label || filtered_column.filter - else - column = procedure.columns.find { _1.table == filtered_column.column.table && _1.column == filtered_column.column.column } - - if column.type == :date - parsed_date = safe_parse_date(filtered_column.filter) - - return parsed_date.present? ? I18n.l(parsed_date) : nil - end - - filtered_column.filter - end - end - - def safe_parse_date(string) - Date.parse(string) - rescue Date::Error - nil - end - - private - - def find_type_de_champ(column) - TypeDeChamp - .joins(:revision_types_de_champ) - .where(revision_types_de_champ: { revision_id: procedure.revisions }) - .order(created_at: :desc) - .find_by(stable_id: column) - end end diff --git a/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml b/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml index 2fe1a3065..56c08315b 100644 --- a/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml +++ b/app/views/instructeurs/procedures/_dossiers_filter_dropdown.html.haml @@ -3,4 +3,4 @@ = t('views.instructeurs.dossiers.filters.title') - menu.with_form do - = render Instructeurs::ColumnFilterComponent.new(procedure:, procedure_presentation:, statut:) + = render Instructeurs::ColumnFilterComponent.new(procedure_presentation:, statut:) diff --git a/app/views/instructeurs/procedures/_dossiers_filter_tags.html.haml b/app/views/instructeurs/procedures/_dossiers_filter_tags.html.haml deleted file mode 100644 index b8673ed2f..000000000 --- a/app/views/instructeurs/procedures/_dossiers_filter_tags.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- if current_filters.count > 0 - .fr-mb-2w - - current_filters.group_by { |filter| filter.column.table }.each_with_index do |(table, filters), i| - - if i > 0 - = " et " - - filters.each_with_index do |filter, i| - - if i > 0 - = " ou " - = form_tag(add_filter_instructeur_procedure_path(procedure), class: 'inline') do - - prefix = procedure_presentation.filters_name_for(statut) - = hidden_field_tag "#{prefix}[]", '' - - (current_filters - [filter]).each do |f| - = hidden_field_tag "#{prefix}[][id]", f.column.id - = hidden_field_tag "#{prefix}[][filter]", f.filter - - = button_tag "#{filter.column.label.truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}", - class: 'fr-tag fr-tag--dismiss fr-my-1w' diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index a7dd1e0a3..e36560e88 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -70,7 +70,7 @@ = render Dossiers::ExportDropdownComponent.new(procedure: @procedure, export_templates: current_instructeur.export_templates_for(@procedure), statut: @statut, count: @dossiers_count, class_btn: 'fr-btn--tertiary', export_url: method(:download_export_instructeur_procedure_path)) - if @filtered_sorted_paginated_ids.present? || @current_filters.count > 0 - = render partial: "dossiers_filter_tags", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut } + = render Instructeurs::FilterButtonsComponent.new(filters: @current_filters, procedure_presentation: @procedure_presentation, statut: @statut) - batch_operation_component = Dossiers::BatchOperationComponent.new(statut: @statut, procedure: @procedure) diff --git a/app/views/instructeurs/procedures/update_filter.turbo_stream.haml b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml index 790b268bf..efc3ed43b 100644 --- a/app/views/instructeurs/procedures/update_filter.turbo_stream.haml +++ b/app/views/instructeurs/procedures/update_filter.turbo_stream.haml @@ -1,2 +1,2 @@ = turbo_stream.replace 'filter-component' do - = render Instructeurs::ColumnFilterComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation, statut: @statut, column: @column) + = render Instructeurs::ColumnFilterComponent.new(procedure_presentation: @procedure_presentation, statut: @statut, column: @column) diff --git a/config/routes.rb b/config/routes.rb index fc64cd041..d194eb920 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -461,6 +461,12 @@ Rails.application.routes.draw do end end + resources :procedure_presentation, only: [:update] do + member do + get 'refresh_column_filter' + end + end + resources :procedures, only: [:index, :show], param: :procedure_id do member do resources :archives, only: [:index, :create] @@ -482,11 +488,13 @@ Rails.application.routes.draw do end end + # TODO: to remove because of new procedure_presentation_controller patch 'update_displayed_fields' get 'update_sort' => 'procedures#update_sort', as: 'update_sort' post 'add_filter' post 'update_filter' get 'remove_filter' + get 'download_export' post 'download_export' get 'polling_last_export' diff --git a/spec/components/instructeurs/column_filter_component_spec.rb b/spec/components/instructeurs/column_filter_component_spec.rb index b1dc597ae..aa730a13c 100644 --- a/spec/components/instructeurs/column_filter_component_spec.rb +++ b/spec/components/instructeurs/column_filter_component_spec.rb @@ -1,12 +1,17 @@ # frozen_string_literal: true describe Instructeurs::ColumnFilterComponent, type: :component do - let(:component) { described_class.new(procedure:, procedure_presentation:, statut:, column:) } + let(:component) { described_class.new(procedure_presentation:, statut:, column:) } let(:instructeur) { create(:instructeur) } - let(:procedure) { create(:procedure, instructeurs: [instructeur]) } + let(:procedure) { create(:procedure) } let(:procedure_id) { procedure.id } - let(:procedure_presentation) { nil } + let(:procedure_presentation) do + groupe_instructeur = procedure.defaut_groupe_instructeur + assign_to = create(:assign_to, instructeur:, groupe_instructeur:) + assign_to.procedure_presentation_or_default_and_errors.first + end + let(:statut) { nil } let(:column) { nil } @@ -19,33 +24,10 @@ describe Instructeurs::ColumnFilterComponent, type: :component do let(:non_filterable_column) { Column.new(procedure_id:, label: 'depose_since', table: 'self', column: 'depose_since', filterable: false) } let(:mocked_columns) { [filterable_column, non_filterable_column] } - before { allow(procedure).to receive(:columns).and_return(mocked_columns) } + before { allow_any_instance_of(Procedure).to receive(:columns).and_return(mocked_columns) } subject { component.filterable_columns_options } it { is_expected.to eq([[filterable_column.label, filterable_column.id]]) } end - - describe '.options_for_select_of_column' do - subject { component.options_for_select_of_column } - - context "column is groupe_instructeur" do - let(:column) { double("Column", scope: nil, table: 'groupe_instructeur') } - let!(:gi_2) { instructeur.groupe_instructeurs.create(label: 'gi2', procedure:) } - let!(:gi_3) { instructeur.groupe_instructeurs.create(label: 'gi3', procedure: create(:procedure)) } - - it { is_expected.to eq([['défaut', procedure.defaut_groupe_instructeur.id], ['gi2', gi_2.id]]) } - end - - context 'when column is dropdown' do - let(:types_de_champ_public) { [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }] } - let(:procedure) { create(:procedure, :published, types_de_champ_public:) } - let(:drop_down_stable_id) { procedure.active_revision.types_de_champ.first.stable_id } - let(:column) { Column.new(procedure_id:, table: 'type_de_champ', scope: nil, column: drop_down_stable_id) } - - it 'find most recent tdc' do - is_expected.to eq(['Paris', 'Lyon', 'Marseille']) - end - end - end end diff --git a/spec/components/instructeurs/column_filter_value_component_spec.rb b/spec/components/instructeurs/column_filter_value_component_spec.rb new file mode 100644 index 000000000..fd9a1f9a9 --- /dev/null +++ b/spec/components/instructeurs/column_filter_value_component_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +describe Instructeurs::ColumnFilterValueComponent, type: :component do + let(:component) { described_class.new(column:) } + let(:instructeur) { create(:instructeur) } + let(:procedure) { create(:procedure, instructeurs: [instructeur]) } + let(:procedure_id) { procedure.id } + + before do + allow(component).to receive(:current_instructeur).and_return(instructeur) + end + + describe '.options_for_select_of_column' do + subject { component.send(:options_for_select_of_column) } + + context "column is groupe_instructeur" do + let(:column) { double("Column", scope: nil, table: 'groupe_instructeur', h_id: { procedure_id: }) } + let!(:gi_2) { instructeur.groupe_instructeurs.create(label: 'gi2', procedure:) } + let!(:gi_3) { instructeur.groupe_instructeurs.create(label: 'gi3', procedure: create(:procedure)) } + + it { is_expected.to eq([['défaut', procedure.defaut_groupe_instructeur.id], ['gi2', gi_2.id]]) } + end + + context 'when column is dropdown' do + let(:types_de_champ_public) { [{ type: :drop_down_list, libelle: 'Votre ville', options: ['Paris', 'Lyon', 'Marseille'] }] } + let(:procedure) { create(:procedure, :published, types_de_champ_public:) } + let(:drop_down_stable_id) { procedure.active_revision.types_de_champ.first.stable_id } + let(:column) { Column.new(procedure_id:, table: 'type_de_champ', scope: nil, column: drop_down_stable_id) } + + it 'find most recent tdc' do + is_expected.to eq(['Paris', 'Lyon', 'Marseille']) + end + end + end +end diff --git a/spec/components/instructeurs/filter_buttons_component_spec.rb b/spec/components/instructeurs/filter_buttons_component_spec.rb new file mode 100644 index 000000000..e022bc150 --- /dev/null +++ b/spec/components/instructeurs/filter_buttons_component_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +describe Instructeurs::FilterButtonsComponent, type: :component do + let(:component) { described_class.new(filters:, procedure_presentation:, statut:) } + let(:instructeur) { create(:instructeur) } + let(:assign_to) { create(:assign_to, procedure:, instructeur:) } + let(:procedure_presentation) { create(:procedure_presentation, assign_to: assign_to) } + let(:statut) { 'tous' } + let(:filters) { [filter] } + + def to_filter((label, filter)) = FilteredColumn.new(column: procedure.find_column(label: label), filter: filter) + + before { render_inline(component) } + + describe "visible text" do + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :text }]) } + let(:first_type_de_champ) { procedure.active_revision.types_de_champ_public.first } + let(:filter) { to_filter([first_type_de_champ.libelle, "true"]) } + + context 'when type_de_champ text' do + it 'should passthrough value' do + expect(page).to have_text("true") + end + end + + context 'when type_de_champ yes_no' do + let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) } + + it 'should transform value' do + expect(page).to have_text("oui") + end + end + + context 'when filter is state' do + let(:filter) { to_filter(['État du dossier', "en_construction"]) } + + it 'should get i18n value' do + expect(page).to have_text("En construction") + end + end + + context 'when filter is a date' do + let(:filter) { to_filter(['Date de création', "15/06/2023"]) } + + it 'should get formatted value' do + expect(page).to have_text("15/06/2023") + end + end + + context 'when there are multiple filters' do + let(:filters) do + [ + to_filter(['État du dossier', "en_construction"]), + to_filter(['État du dossier', "en_instruction"]), + to_filter(['Date de création', "15/06/2023"]) + ] + end + + it 'should display all filters' do + text = "État du dossier : En construction ou État du dossier : En instruction et Date de création : 15/06/2023" + expect(page).to have_text(text) + end + end + end + + describe "hidden inputs" do + let(:procedure) { create(:procedure) } + + context 'with 2 filters' do + let(:en_construction_filter) { to_filter(['État du dossier', "en_construction"]) } + let(:en_instruction_filter) { to_filter(['État du dossier', "en_instruction"]) } + let(:column_id) { procedure.find_column(label: 'État du dossier').id } + let(:filters) { [en_construction_filter, en_instruction_filter] } + + it 'should have the necessary inputs' do + expect(page).to have_field('statut', with: 'tous', type: 'hidden') + + expect(page.all('form').count).to eq(2) + + del_en_construction = page.all('form').first + expect(del_en_construction).to have_text('En construction') + expect(del_en_construction).to have_field('filters[]', with: '', type: 'hidden') + expect(del_en_construction).to have_field('filters[][id]', with: column_id, type: 'hidden') + expect(del_en_construction).to have_field('filters[][filter]', with: 'en_instruction', type: 'hidden') + end + end + end +end diff --git a/spec/controllers/instructeurs/procedure_presentation_controller_spec.rb b/spec/controllers/instructeurs/procedure_presentation_controller_spec.rb new file mode 100644 index 000000000..ccc3d51cb --- /dev/null +++ b/spec/controllers/instructeurs/procedure_presentation_controller_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +describe Instructeurs::ProcedurePresentationController, type: :controller do + describe '#update' do + subject { patch :update, params: } + + let(:procedure) { create(:procedure) } + let(:instructeur) { create(:instructeur) } + let(:procedure_presentation) do + groupe_instructeur = procedure.defaut_groupe_instructeur + assign_to = create(:assign_to, instructeur:, groupe_instructeur:) + assign_to.procedure_presentation_or_default_and_errors.first + end + let(:state_column) { procedure.dossier_state_column } + + let(:params) { { id: procedure_presentation.id }.merge(presentation_params) } + + context 'nominal case' do + before { sign_in(instructeur.user) } + + let(:presentation_params) do + { + displayed_columns: [state_column.id], + sorted_column: { order: 'asc', id: state_column.id }, + filters: [{ id: state_column.id, filter: 'en_construction' }], + statut: 'tous' + } + end + + it 'updates the procedure_presentation' do + expect(procedure_presentation.displayed_columns).to eq(procedure.default_displayed_columns) + expect(procedure_presentation.sorted_column).to eq(procedure.default_sorted_column) + expect(procedure_presentation.tous_filters).to eq([]) + + subject + expect(response).to redirect_to(instructeur_procedure_url(procedure)) + + procedure_presentation.reload + + expect(procedure_presentation.displayed_columns).to eq([state_column]) + + expect(procedure_presentation.sorted_column.column).to eq(state_column) + expect(procedure_presentation.sorted_column.order).to eq('asc') + + filtered_column = FilteredColumn.new(column: state_column, filter: 'en_construction') + expect(procedure_presentation.tous_filters).to eq([filtered_column]) + end + end + + context 'with a wrong instructeur' do + let(:another_instructeur) { create(:instructeur) } + before { sign_in(another_instructeur.user) } + + let(:presentation_params) { { displayed_columns: [state_column.id] } } + + it 'does not update the procedure_presentation' do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with an empty string in displayed_columns' do + before { sign_in(instructeur.user) } + + let(:presentation_params) { { displayed_columns: [''] } } + + it 'removes the empty string' do + subject + expect(procedure_presentation.reload.displayed_columns).to eq([]) + end + end + + context 'with an error in filters' do + before { sign_in(instructeur.user) } + + let(:presentation_params) do + { filters: [{ id: state_column.id, filter: '' }], statut: 'tous' } + end + + it 'does not update the procedure_presentation' do + subject + + expect(flash.alert).to include(/ne peut pas être vide/) + end + end + end +end diff --git a/spec/models/assign_to_spec.rb b/spec/models/assign_to_spec.rb index 59aaf55dd..8ddad0c57 100644 --- a/spec/models/assign_to_spec.rb +++ b/spec/models/assign_to_spec.rb @@ -45,7 +45,7 @@ describe AssignTo, type: :model do it do expect(procedure_presentation_or_default).to be_persisted expect(procedure_presentation_or_default).to be_valid - expect(errors).to be_present + expect(errors.full_messages).to include(/unable to find procedure 666/) expect(assign_to.procedure_presentation).not_to be(procedure_presentation) end end diff --git a/spec/models/procedure_presentation_spec.rb b/spec/models/procedure_presentation_spec.rb index 821204a3c..36c4197d3 100644 --- a/spec/models/procedure_presentation_spec.rb +++ b/spec/models/procedure_presentation_spec.rb @@ -55,44 +55,6 @@ describe ProcedurePresentation do end end - describe "#human_value_for_filter" do - let(:filtered_column) { to_filter([first_type_de_champ.libelle, "true"]) } - - subject do - procedure_presentation.human_value_for_filter(filtered_column) - end - - context 'when type_de_champ text' do - it 'should passthrough value' do - expect(subject).to eq("true") - end - end - - context 'when type_de_champ yes_no' do - let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) } - - it 'should transform value' do - expect(subject).to eq("oui") - end - end - - context 'when filter is state' do - let(:filtered_column) { to_filter(['État du dossier', "en_construction"]) } - - it 'should get i18n value' do - expect(subject).to eq("En construction") - end - end - - context 'when filter is a date' do - let(:filtered_column) { to_filter(['Date de création', "15/06/2023"]) } - - it 'should get formatted value' do - expect(subject).to eq("15/06/2023") - end - end - end - describe '#update_displayed_fields' do let(:en_construction_column) { procedure.find_column(label: 'Date de passage en construction') } let(:mise_a_jour_column) { procedure.find_column(label: 'Date du dernier évènement') } diff --git a/spec/system/instructeurs/procedure_filters_spec.rb b/spec/system/instructeurs/procedure_filters_spec.rb index 65440b22d..c105775f4 100644 --- a/spec/system/instructeurs/procedure_filters_spec.rb +++ b/spec/system/instructeurs/procedure_filters_spec.rb @@ -19,7 +19,7 @@ describe "procedure filters" do scenario "should display demandeur by default" do within ".dossiers-table" do - expect(page).to have_link("Demandeur") + expect(page).to have_button("Demandeur") expect(page).to have_link(new_unfollow_dossier.user.email) end end @@ -28,7 +28,7 @@ describe "procedure filters" do procedure.update!(sva_svr: SVASVRConfiguration.new(decision: :sva).attributes) visit instructeur_procedure_path(procedure) within ".dossiers-table" do - expect(page).to have_link("Date décision SVA") + expect(page).to have_button("Date décision SVA") expect(page).to have_link(new_unfollow_dossier.user.email) end end @@ -46,7 +46,7 @@ describe "procedure filters" do scenario "should add be able to add created_at column", js: true do add_column("Date de création") within ".dossiers-table" do - expect(page).to have_link("Date de création") + expect(page).to have_button("Date de création") expect(page).to have_link(new_unfollow_dossier.created_at.strftime('%d/%m/%Y')) end end @@ -54,20 +54,20 @@ describe "procedure filters" do scenario "should add be able to add and remove custom type_de_champ column", js: true do add_column(type_de_champ.libelle) within ".dossiers-table" do - expect(page).to have_link(type_de_champ.libelle) + expect(page).to have_button(type_de_champ.libelle) expect(page).to have_link(champ.value) end remove_column(type_de_champ.libelle) within ".dossiers-table" do - expect(page).not_to have_link(type_de_champ.libelle) + expect(page).not_to have_button(type_de_champ.libelle) expect(page).not_to have_link(champ.value) end # Test removal of all customizable fields remove_column("Demandeur") within ".dossiers-table" do - expect(page).not_to have_link("Demandeur") + expect(page).not_to have_button("Demandeur") end end diff --git a/spec/system/instructeurs/procedure_sort_spec.rb b/spec/system/instructeurs/procedure_sort_spec.rb index 166b42105..beb59e8a0 100644 --- a/spec/system/instructeurs/procedure_sort_spec.rb +++ b/spec/system/instructeurs/procedure_sort_spec.rb @@ -22,12 +22,12 @@ describe "procedure sort", js: true do expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier.id.to_s) expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier_2.id.to_s) - find("thead .number-col a").click # sort by id asc + click_on "Nº dossier" # sort by id asc expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier.id.to_s) expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier_2.id.to_s) - find("thead .number-col a").click # reverse order - sort by id desc + click_on "Nº dossier" # reverse order - sort by id desc expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier_2.id.to_s) expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier.id.to_s) @@ -44,12 +44,14 @@ describe "procedure sort", js: true do expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier.id.to_s) expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier_2.id.to_s) - find("thead .sva-col a").click # sort by sva date asc + click_on "Date décision SVA", exact: true # sort by sva date asc + # find("thead .sva-col a").click # sort by sva date asc expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier.id.to_s) expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier_2.id.to_s) - find("thead .sva-col a").click # reverse order - sort by sva date desc + click_on "Date décision SVA ↑", exact: true # reverse order - sort by sva date desc + # find("thead .sva-col a").click # reverse order - sort by sva date desc expect(find(".dossiers-table tbody tr:nth-child(2) .number-col a").text).to eq(followed_dossier_2.id.to_s) expect(find(".dossiers-table tbody tr:nth-child(3) .number-col a").text).to eq(followed_dossier.id.to_s) @@ -74,7 +76,7 @@ describe "procedure sort", js: true do end scenario "should be able to sort back by notification filter after any other sort" do - find("thead .number-col a").click # sort by id asc + click_on "Nº dossier" # sort by id asc expect(page).not_to have_checked_field("Remonter les dossiers avec une notification")