From 1e9b7fbbb6ed80a5a7c259e65f2c407af5ae4c66 Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Tue, 13 Feb 2024 07:03:42 +0000 Subject: [PATCH 01/15] components --- app/components/attachment/edit_component.rb | 48 +++++++++++-------- .../attachment/multiple_component.rb | 6 +-- .../attachment/multiple_component_spec.rb | 8 ++++ 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index 05ddb7b18..2c3ef9a9d 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -2,6 +2,7 @@ class Attachment::EditComponent < ApplicationComponent attr_reader :champ attr_reader :attachment + attr_reader :attachments attr_reader :user_can_destroy alias user_can_destroy? user_can_destroy attr_reader :as_multiple @@ -9,24 +10,23 @@ class Attachment::EditComponent < ApplicationComponent EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze - def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, **kwargs) - @as_multiple = as_multiple - @attached_file = attached_file - @auto_attach_url = auto_attach_url + def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, user_can_replace: false, attachments: [], **kwargs) @champ = champ + @attached_file = attached_file @direct_upload = direct_upload @index = index @view_as = view_as @user_can_destroy = user_can_destroy + @user_can_replace = user_can_replace + @as_multiple = as_multiple - # attachment passed by kwarg because we don't want a default (nil) value. - @attachment = if kwargs.key?(:attachment) - kwargs.delete(:attachment) - elsif attached_file.respond_to?(:attachment) - attached_file.attachment - else - fail ArgumentError, "You must pass an `attachment` kwarg when not using as single attachment like in #{attached_file.name}. Set it to nil for a new attachment." - end + # Adaptation pour la gestion des pièces jointes multiples + @attachments = attachments.presence || (kwargs.key?(:attachment) ? [kwargs.delete(:attachment)] : []) + @attachments << attached_file.attachment if attached_file.respond_to?(:attachment) && @attachments.empty? + @attachments.compact! + + # Utilisation du premier attachement comme référence pour la rétrocompatibilité + @attachment = @attachments.first # When parent form has nested attributes, pass the form builder object_name # to correctly infer the input attribute name. @@ -63,7 +63,7 @@ class Attachment::EditComponent < ApplicationComponent def file_field_options track_issue_with_missing_validators if missing_validators? - { + options = { class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?), direct_upload: @direct_upload, id: input_id, @@ -71,8 +71,13 @@ class Attachment::EditComponent < ApplicationComponent data: { auto_attach_url:, turbo_force: :server - }.merge(has_file_size_validator? ? { max_file_size: } : {}) - }.merge(has_content_type_validator? ? { accept: accept_content_type } : {}) + }.merge(has_file_size_validator? ? { max_file_size: max_file_size } : {}) + } + + options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {}) + options[:multiple] = true if as_multiple? + + options end def poll_url @@ -90,7 +95,8 @@ class Attachment::EditComponent < ApplicationComponent end def field_name(object_name = nil, method_name = nil, *method_names, multiple: false, index: nil) - helpers.field_name(@form_object_name || ActiveModel::Naming.param_key(@attached_file.record), attribute_name) + field_name = @form_object_name || ActiveModel::Naming.param_key(@attached_file.record) + "#{field_name}[#{attribute_name}]#{'[]' if as_multiple?}" end def attribute_name @@ -126,24 +132,24 @@ class Attachment::EditComponent < ApplicationComponent !!attachment&.persisted? end - def downloadable? + def downloadable?(attachment) return false unless @view_as == :download - viewable? + viewable?(attachment) end - def viewable? + def viewable?(attachment) return false if attachment.virus_scanner_error? return false if attachment.watermark_pending? true end - def error? + def error?(attachment) attachment.virus_scanner_error? end - def error_message + def error_message(attachment) case when attachment.virus_scanner.infected? t(".errors.virus_infected") diff --git a/app/components/attachment/multiple_component.rb b/app/components/attachment/multiple_component.rb index b5900cd13..80d2fd941 100644 --- a/app/components/attachment/multiple_component.rb +++ b/app/components/attachment/multiple_component.rb @@ -15,7 +15,7 @@ class Attachment::MultipleComponent < ApplicationComponent delegate :count, :empty?, to: :attachments, prefix: true - def initialize(champ:, attached_file:, form_object_name: nil, view_as: :link, user_can_destroy: true, max: nil) + def initialize(champ: nil, attached_file:, form_object_name: nil, view_as: :link, user_can_destroy: true, user_can_replace: false, max: nil) @champ = champ @attached_file = attached_file @form_object_name = form_object_name @@ -35,11 +35,11 @@ class Attachment::MultipleComponent < ApplicationComponent end def empty_component_id - "attachment-multiple-empty-#{champ.public_id}" + champ.present? ? "attachment-multiple-empty-#{champ.public_id}" : "attachment-multiple-empty-generic" end def auto_attach_url - helpers.auto_attach_url(champ) + champ.present? ? helpers.auto_attach_url(champ) : '#' end alias poll_url auto_attach_url diff --git a/spec/components/attachment/multiple_component_spec.rb b/spec/components/attachment/multiple_component_spec.rb index e5f72fbe7..9f4d02549 100644 --- a/spec/components/attachment/multiple_component_spec.rb +++ b/spec/components/attachment/multiple_component_spec.rb @@ -93,6 +93,14 @@ RSpec.describe Attachment::MultipleComponent, type: :component do end end + context 'when user can replace' do + let(:kwargs) { { user_can_replace: true } } + + before do + attach_to_champ(attached_file, champ) + end + end + def attach_to_champ(attached_file, champ) attached_file.attach( io: StringIO.new("x" * 2), From bec9af90e8e8dc4482c6fa1aaa468bb257e6e845 Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Tue, 13 Feb 2024 07:04:07 +0000 Subject: [PATCH 02/15] models --- app/components/attachment/edit_component.rb | 2 +- app/models/champs/piece_justificative_champ.rb | 4 ++++ app/models/champs/titre_identite_champ.rb | 4 ++++ app/models/commentaire.rb | 10 ++-------- app/models/dossier.rb | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index 2c3ef9a9d..e096591ef 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -19,7 +19,7 @@ class Attachment::EditComponent < ApplicationComponent @user_can_destroy = user_can_destroy @user_can_replace = user_can_replace @as_multiple = as_multiple - + @auto_attach_url = auto_attach_url # Adaptation pour la gestion des pièces jointes multiples @attachments = attachments.presence || (kwargs.key?(:attachment) ? [kwargs.delete(:attachment)] : []) @attachments << attached_file.attachment if attached_file.respond_to?(:attachment) && @attachments.empty? diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb index 8d9435d17..bca75c292 100644 --- a/app/models/champs/piece_justificative_champ.rb +++ b/app/models/champs/piece_justificative_champ.rb @@ -30,4 +30,8 @@ class Champs::PieceJustificativeChamp < Champ def blank? piece_justificative_file.blank? end + + def allow_multiple_attachments? + false + end end diff --git a/app/models/champs/titre_identite_champ.rb b/app/models/champs/titre_identite_champ.rb index 05b67e802..88983fa6e 100644 --- a/app/models/champs/titre_identite_champ.rb +++ b/app/models/champs/titre_identite_champ.rb @@ -25,4 +25,8 @@ class Champs::TitreIdentiteChamp < Champ def blank? piece_justificative_file.blank? end + + def allow_multiple_attachments? + false + end end diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index cab033348..8c72c29bd 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -7,7 +7,7 @@ class Commentaire < ApplicationRecord validate :messagerie_available?, on: :create, unless: -> { dossier.brouillon? } - has_one_attached :piece_jointe + has_many_attached :piece_jointe validates :body, presence: { message: "ne peut être vide" }, unless: :discarded? @@ -67,12 +67,6 @@ class Commentaire < ApplicationRecord sent_by?(connected_user) && (sent_by_instructeur? || sent_by_expert?) && !discarded? end - def file_url - if piece_jointe.attached? && piece_jointe.virus_scanner.safe? - Rails.application.routes.url_helpers.url_for(piece_jointe) - end - end - def soft_delete! transaction do discard! @@ -80,7 +74,7 @@ class Commentaire < ApplicationRecord update! body: '' end - piece_jointe.purge_later if piece_jointe.attached? + piece_jointe.each(&:purge_later) if piece_jointe.attached? end def flagged_pending_correction? diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 334d883a7..3212a4f80 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -54,7 +54,7 @@ class Dossier < ApplicationRecord has_many :prefilled_champs_public, -> { root.public_only.prefilled }, class_name: 'Champ', inverse_of: false has_many :commentaires, inverse_of: :dossier, dependent: :destroy - has_many :preloaded_commentaires, -> { includes(:dossier_correction, piece_jointe_attachment: :blob) }, class_name: 'Commentaire', inverse_of: :dossier + has_many :preloaded_commentaires, -> { includes(:dossier_correction, piece_jointe_attachments: :blob) }, class_name: 'Commentaire', inverse_of: :dossier has_many :invites, dependent: :destroy has_many :follows, -> { active }, inverse_of: :dossier @@ -294,7 +294,7 @@ class Dossier < ApplicationRecord scope :for_api, -> { with_champs .with_annotations - .includes(commentaires: { piece_jointe_attachment: :blob }, + .includes(commentaires: { piece_jointe_attachments: :blob }, justificatif_motivation_attachment: :blob, attestation: [], avis: { piece_justificative_file_attachment: :blob }, From 7092583b0a953f758a168c5ba433fd54a9667628 Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Tue, 13 Feb 2024 07:04:27 +0000 Subject: [PATCH 03/15] layout --- .../edit_component/edit_component.html.haml | 72 ++++++++++++------- .../multiple_component.html.haml | 4 +- .../message_component.html.haml | 4 +- .../shared/dossiers/messages/_form.html.haml | 12 ++-- config/locales/views/shared/fr.yml | 1 + 5 files changed, 57 insertions(+), 36 deletions(-) diff --git a/app/components/attachment/edit_component/edit_component.html.haml b/app/components/attachment/edit_component/edit_component.html.haml index 5845e8dad..57b60297d 100644 --- a/app/components/attachment/edit_component/edit_component.html.haml +++ b/app/components/attachment/edit_component/edit_component.html.haml @@ -1,39 +1,59 @@ -.attachment.fr-upload-group{ { id: attachment ? dom_id(attachment, :edit) : nil, class: class_names("fr-mb-1w": !(as_multiple? && downloadable?)) }.compact } - - if persisted? - %div{ id: dom_id(attachment, :persisted_row) } - .flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) } - - if user_can_destroy? - = render NestedForms::OwnedButtonComponent.new(formaction: destroy_attachment_path, http_method: :delete, opt: {class: "fr-btn fr-btn--tertiary fr-btn--sm fr-icon-delete-line", title: t(".delete_file", filename: attachment.filename)}) do - = t('.delete') +.attachment.fr-upload-group{ id: (attachment ? dom_id(attachment, :edit) : nil), class: class_names("fr-mb-1w": !(as_multiple? && attachments.any?(&:persisted?))) } + - if as_multiple? + - attachments.each do |attachment| + - if attachment.persisted? + %div{ id: dom_id(attachment, :persisted_row) } + .flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) } + - if user_can_destroy? + = render NestedForms::OwnedButtonComponent.new(formaction: destroy_attachment_path, http_method: :delete, opt: {class: "fr-btn fr-btn--tertiary fr-btn--sm fr-icon-delete-line", title: t(".delete_file", filename: attachment.filename)}) do + = t('.delete') - - if downloadable? - = render Dsfr::DownloadComponent.new(attachment:) - - else - .fr-py-1v - %span.attachment-filename.fr-mr-1w= link_to_if(viewable?, attachment.filename.to_s, helpers.url_for(attachment.blob), title: t(".open_file", filename: attachment.filename), **helpers.external_link_attributes) + - if downloadable?(attachment) + = render Dsfr::DownloadComponent.new(attachment: attachment) + - else + .fr-py-1v + %span.attachment-filename.fr-mr-1w= link_to_if(viewable?(attachment), attachment.filename.to_s, helpers.url_for(attachment.blob), title: t(".open_file", filename: attachment.filename), **helpers.external_link_attributes) - = render Attachment::ProgressComponent.new(attachment: attachment, ignore_antivirus: true) + = render Attachment::ProgressComponent.new(attachment: attachment, ignore_antivirus: true) - - if error? - %p.fr-error-text= error_message + - if error?(attachment) + %p.fr-error-text= error_message(attachment) + - else + - if persisted? + %div{ id: dom_id(attachment, :persisted_row) } + .flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) } + - if user_can_destroy? + = render NestedForms::OwnedButtonComponent.new(formaction: destroy_attachment_path, http_method: :delete, opt: {class: "fr-btn fr-btn--tertiary fr-btn--sm fr-icon-delete-line", title: t(".delete_file", filename: attachment.filename)}) do + = t('.delete') - - elsif first? + - if downloadable?(attachment) + = render Dsfr::DownloadComponent.new(attachment:) + - else + .fr-py-1v + %span.attachment-filename.fr-mr-1w= link_to_if(viewable?(attachment), attachment.filename.to_s, helpers.url_for(attachment.blob), title: t(".open_file", filename: attachment.filename), **helpers.external_link_attributes) + + = render Attachment::ProgressComponent.new(attachment: attachment, ignore_antivirus: true) + + - if error?(attachment) + %p.fr-error-text= error_message(attachment) + + - if first? && !persisted? %p.fr-hint-text.fr-mb-1w - if max_file_size.present? = t('.max_file_size', max_file_size: number_to_human_size(max_file_size)) - if allowed_formats.present? = t('.allowed_formats', formats: allowed_formats.join(', ')) - - - if !as_multiple? + - if !persisted? || champ.present? && champ.titre_identite? = file_field(champ, field_name, **file_field_options) - - if persisted? - - Attachment::PendingPollComponent.new(attachment: attachment, poll_url:, context: poll_context).then do |component| - .fr-mt-2w - = render component + - attachments.filter(&:persisted?).each do |attachment| + - if attachment.persisted? + - Attachment::PendingPollComponent.new(attachment: attachment, poll_url: poll_url, context: poll_context).then do |component| + .fr-mt-2w + = render component - .attachment-upload-error.hidden - %p.fr-error-text= t('.errors.uploading') - = button_tag(**retry_button_options) do - = t(".retry") + .attachment-upload-error.hidden + %p.fr-error-text= t('.errors.uploading') + = button_tag(**retry_button_options) do + = t(".retry") diff --git a/app/components/attachment/multiple_component/multiple_component.html.haml b/app/components/attachment/multiple_component/multiple_component.html.haml index 545387758..b5a64b044 100644 --- a/app/components/attachment/multiple_component/multiple_component.html.haml +++ b/app/components/attachment/multiple_component/multiple_component.html.haml @@ -5,10 +5,10 @@ %ul.fr-my-1v - each_attachment do |attachment, index| %li{ id: dom_id(attachment) } - = render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, as_multiple: true, view_as:, user_can_destroy:, form_object_name:) + = render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, view_as:, user_can_destroy:, form_object_name:) %div{ id: empty_component_id, class: class_names("hidden": !can_attach_next?), data: { turbo_force: :server } } - = render Attachment::EditComponent.new(champ:, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:) + = render Attachment::EditComponent.new(champ:, as_multiple: champ.present? ? champ.allow_multiple_attachments? : true, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:) // single poll and refresh message for all attachments = render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context) diff --git a/app/components/dossiers/message_component/message_component.html.haml b/app/components/dossiers/message_component/message_component.html.haml index 1f944d151..4631c5d0d 100644 --- a/app/components/dossiers/message_component/message_component.html.haml +++ b/app/components/dossiers/message_component/message_component.html.haml @@ -27,7 +27,9 @@ - if groupe_gestionnaire.nil? && commentaire.piece_jointe.attached? .fr-ml-2w - = render Attachment::ShowComponent.new(attachment: commentaire.piece_jointe.attachment, new_tab: true) + - commentaire.piece_jointe.each do |attachment| + = render Attachment::ShowComponent.new(attachment: attachment, new_tab: true) + - if show_reply_button? = button_tag type: 'button', class: 'fr-btn fr-btn--sm fr-btn--secondary fr-icon-arrow-go-back-line fr-btn--icon-left', onclick: 'document.querySelector("#commentaire_body").focus()' do diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml index f36c6c434..0c2bf2ecb 100644 --- a/app/views/shared/dossiers/messages/_form.html.haml +++ b/app/views/shared/dossiers/messages/_form.html.haml @@ -8,12 +8,10 @@ %p.mandatory-explanation= t('asterisk_html', scope: [:utils]) = render Dsfr::InputComponent.new(form: f, attribute: :body, input_type: :text_area, opts: { rows: 5, placeholder: placeholder, title: placeholder, class: 'fr-input message-textarea'}) - - if local_assigns.has_key?(:dossier) - .fr-mt-3w{ data: { controller: "file-input-reset" } } - = render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe) - %button.hidden.fr-btn.fr-btn--tertiary-no-outline.fr-btn--icon-left.fr-icon-delete-line{ data: { 'file-input-reset-target': 'reset', action: 'file-input-reset#reset' } } - = t('views.shared.messages.remove_file') + .fr-mt-3w{ data: { controller: "file-input-reset", delete_label: t('views.shared.messages.remove_file') } } + = render Attachment::MultipleComponent.new(attached_file: commentaire.piece_jointe) + %ul{ data: { 'file-input-reset-target': 'fileList' } } - .fr-mt-3w - = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true } + .fr-mt-3w + = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true } diff --git a/config/locales/views/shared/fr.yml b/config/locales/views/shared/fr.yml index a8af2562b..803f7c726 100644 --- a/config/locales/views/shared/fr.yml +++ b/config/locales/views/shared/fr.yml @@ -23,3 +23,4 @@ fr: signin: 'Se connecter' messages: remove_file: 'Supprimer le fichier' + remove_all: "Supprimer tous les fichiers" From 981e7ff2445137979f8625bec0d6849d0a943712 Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Tue, 13 Feb 2024 07:04:51 +0000 Subject: [PATCH 04/15] javascript --- .../file_input_reset_controller.ts | 90 ++++++++++++++----- 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/app/javascript/controllers/file_input_reset_controller.ts b/app/javascript/controllers/file_input_reset_controller.ts index cbce6ab6b..0320c54b1 100644 --- a/app/javascript/controllers/file_input_reset_controller.ts +++ b/app/javascript/controllers/file_input_reset_controller.ts @@ -1,37 +1,81 @@ import { ApplicationController } from './application_controller'; -import { hide, show } from '@utils'; - export class FileInputResetController extends ApplicationController { - static targets = ['reset']; - - declare readonly resetTarget: HTMLElement; + static targets = ['fileList']; + declare fileListTarget: HTMLElement; connect() { - this.on('change', (event) => { - if (event.target == this.fileInput) { - this.showResetButton(); + super.connect(); + this.updateFileList(); + this.element.addEventListener('change', (event) => { + if ( + event.target instanceof HTMLInputElement && + event.target.type === 'file' + ) { + this.updateFileList(); } }); } - reset(event: Event) { - event.preventDefault(); - this.fileInput.value = ''; - hide(this.resetTarget); + updateFileList() { + const files = this.fileInput?.files ?? []; + this.fileListTarget.innerHTML = ''; + + const deleteLabel = + this.element.getAttribute('data-delete-label') || 'Delete'; + + Array.from(files).forEach((file, index) => { + const container = document.createElement('li'); + container.style.display = 'flex'; + container.style.alignItems = 'center'; + + const deleteButton = this.createDeleteButton(deleteLabel, index); + container.appendChild(deleteButton); + + const listItem = document.createElement('div'); + listItem.textContent = file.name; + listItem.style.marginLeft = '8px'; + + container.appendChild(listItem); + this.fileListTarget.appendChild(container); + }); } - showResetButton() { - show(this.resetTarget); + createDeleteButton(deleteLabel: string, index: number) { + const button = document.createElement('button'); + button.textContent = deleteLabel; + button.classList.add( + 'fr-btn', + 'fr-btn--tertiary', + 'fr-btn--sm', + 'fr-icon-delete-line' + ); + + button.addEventListener('click', (event) => { + event.preventDefault(); + this.removeFile(index); + }); + + return button; } - private get fileInput() { - const inputs = - this.element.querySelectorAll('input[type="file"]'); - if (inputs.length == 0) { - throw new Error('No file input found'); - } else if (inputs.length > 1) { - throw new Error('Multiple file inputs found'); - } - return inputs[0]; + removeFile(index: number) { + const files = this.fileInput?.files; + if (!files) return; + + const dataTransfer = new DataTransfer(); + Array.from(files).forEach((file, i) => { + if (index !== i) { + dataTransfer.items.add(file); + } + }); + + if (this.fileInput) this.fileInput.files = dataTransfer.files; + this.updateFileList(); + } + + private get fileInput(): HTMLInputElement | null { + return this.element.querySelector( + 'input[type="file"]' + ) as HTMLInputElement | null; } } From b69d8249c5b45910e9f42e1a7c7479af0b1bef0f Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Tue, 13 Feb 2024 07:09:58 +0000 Subject: [PATCH 05/15] controller --- app/controllers/experts/avis_controller.rb | 2 +- app/controllers/instructeurs/dossiers_controller.rb | 3 ++- app/controllers/users/dossiers_controller.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/experts/avis_controller.rb b/app/controllers/experts/avis_controller.rb index a66dec92f..14999d869 100644 --- a/app/controllers/experts/avis_controller.rb +++ b/app/controllers/experts/avis_controller.rb @@ -234,7 +234,7 @@ module Experts end def commentaire_params - params.require(:commentaire).permit(:body, :piece_jointe) + params.require(:commentaire).permit(:body, piece_jointe: []) end end end diff --git a/app/controllers/instructeurs/dossiers_controller.rb b/app/controllers/instructeurs/dossiers_controller.rb index 212a6c8b0..8063e0438 100644 --- a/app/controllers/instructeurs/dossiers_controller.rb +++ b/app/controllers/instructeurs/dossiers_controller.rb @@ -256,6 +256,7 @@ module Instructeurs flash.notice = "Message envoyé" redirect_to messagerie_instructeur_dossier_path(procedure, dossier) else + @commentaire.piece_jointe.purge.reload flash.alert = @commentaire.errors.full_messages render :messagerie end @@ -393,7 +394,7 @@ module Instructeurs end def commentaire_params - params.require(:commentaire).permit(:body, :piece_jointe) + params.require(:commentaire).permit(:body, piece_jointe: []) end def champs_private_params diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 06dd6d742..92f4d00a8 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -621,7 +621,7 @@ module Users end def commentaire_params - params.require(:commentaire).permit(:body, :piece_jointe) + params.require(:commentaire).permit(:body, piece_jointe: []) end end end From bf622eb3ed3fadfdee7941dfe3353bad52bd6007 Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Tue, 13 Feb 2024 07:10:22 +0000 Subject: [PATCH 06/15] service and serializer --- app/serializers/commentaire_serializer.rb | 6 +----- app/services/pieces_justificatives_service.rb | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/serializers/commentaire_serializer.rb b/app/serializers/commentaire_serializer.rb index 3cc79c8a2..2941765e2 100644 --- a/app/serializers/commentaire_serializer.rb +++ b/app/serializers/commentaire_serializer.rb @@ -2,13 +2,9 @@ class CommentaireSerializer < ActiveModel::Serializer attributes :email, :body, :created_at, - :attachment + :piece_jointe_attachments def created_at object.created_at&.in_time_zone('UTC') end - - def attachment - object.file_url - end end diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 6c5648597..2fece0c0b 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -161,7 +161,7 @@ class PiecesJustificativesService def pjs_for_commentaires(dossiers) commentaire_id_dossier_id = Commentaire - .joins(:piece_jointe_attachment) + .joins(:piece_jointe_attachments) .where(dossier: dossiers) .pluck(:id, :dossier_id) .to_h From be056a125803032f7b29c40c59709d09bd164434 Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Tue, 13 Feb 2024 11:06:41 +0000 Subject: [PATCH 07/15] tasks --- app/lib/recovery/exporter.rb | 2 +- lib/tasks/recovery.rake | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/recovery/exporter.rb b/app/lib/recovery/exporter.rb index 4781896f1..f414df28a 100644 --- a/app/lib/recovery/exporter.rb +++ b/app/lib/recovery/exporter.rb @@ -10,7 +10,7 @@ module Recovery :invites, :traitements, :transfer_logs, - commentaires: { piece_jointe_attachment: :blob }, + commentaires: { piece_jointe_attachments: :blob }, avis: { introduction_file_attachment: :blob, piece_justificative_file_attachment: :blob }, dossier_operation_logs: { serialized_attachment: :blob }, attestation: { pdf_attachment: :blob }, diff --git a/lib/tasks/recovery.rake b/lib/tasks/recovery.rake index 58221765a..c56586f0f 100644 --- a/lib/tasks/recovery.rake +++ b/lib/tasks/recovery.rake @@ -48,7 +48,7 @@ namespace :recovery do rake_puts "Will export #{dossier_ids}" dossiers_with_data = Dossier.where(id: dossier_ids) - .preload(commentaires: { piece_jointe_attachment: :blob }, + .preload(commentaires: { pieces_jointes_attachments: :blob }, avis: { introduction_file_attachment: :blob, piece_justificative_file_attachment: :blob }, dossier_operation_logs: { serialized_attachment: :blob }, attestation: { pdf_attachment: :blob }, @@ -67,7 +67,7 @@ namespace :recovery do blob_keys_for_dossier += dossier.commentaires.flat_map do |commentaire| commentaire_blob_key = [] if commentaire.piece_jointe.attached? - commentaire_blob_key += [commentaire.piece_jointe_attachment.blob.key] + commentaire_blob_key += [commentaire.piece_jointe_attachments.blob.key] end commentaire_blob_key end From 548806780130da18eb15d682ccb5c80369667fdf Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Wed, 21 Feb 2024 02:01:10 +0000 Subject: [PATCH 08/15] api --- app/graphql/types/message_type.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/graphql/types/message_type.rb b/app/graphql/types/message_type.rb index 665c2dd0b..70622647b 100644 --- a/app/graphql/types/message_type.rb +++ b/app/graphql/types/message_type.rb @@ -5,10 +5,10 @@ module Types field :body, String, null: false field :created_at, GraphQL::Types::ISO8601DateTime, null: false field :attachment, Types::File, null: true, deprecation_reason: "Utilisez le champ `attachments` à la place.", extensions: [ - { Extensions::Attachment => { attachment: :piece_jointe } } + { Extensions::Attachment => { attachments: :piece_jointe, as: :single } } ] field :attachments, [Types::File], null: false, extensions: [ - { Extensions::Attachment => { attachment: :piece_jointe, as: :multiple } } + { Extensions::Attachment => { attachments: :piece_jointe } } ] field :correction, CorrectionType, null: true From 2612b0a2d1182358b82a97a6beafc55801d77c46 Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Wed, 21 Feb 2024 02:01:24 +0000 Subject: [PATCH 09/15] tests --- .../attachment/edit_component_spec.rb | 4 ++-- .../api/v2/graphql_controller_spec.rb | 22 +++++++++++++++---- .../experts/avis_controller_spec.rb | 4 ++-- spec/factories/commentaire.rb | 9 ++++++-- spec/services/commentaire_service_spec.rb | 15 ++++++++++--- .../pieces_justificatives_service_spec.rb | 2 +- spec/system/users/en_construction_spec.rb | 2 +- 7 files changed, 43 insertions(+), 15 deletions(-) diff --git a/spec/components/attachment/edit_component_spec.rb b/spec/components/attachment/edit_component_spec.rb index 3ce2e9535..c9298f9ff 100644 --- a/spec/components/attachment/edit_component_spec.rb +++ b/spec/components/attachment/edit_component_spec.rb @@ -66,8 +66,8 @@ RSpec.describe Attachment::EditComponent, type: :component do ) end - it 'does not render an empty file' do # (is is rendered by MultipleComponent) - expect(subject).not_to have_selector('input[type=file]') + it 'does render an empty file' do # (is is rendered by MultipleComponent) + expect(subject).to have_selector('input[type=file]') end it 'renders max size for first index' do diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index 4f1aef223..4805df781 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -432,6 +432,12 @@ describe API::V2::GraphqlController do byteSize contentType } + attachments { + filename + checksum + byteSize + contentType + } } avis { expert { @@ -519,11 +525,19 @@ describe API::V2::GraphqlController do { body: commentaire.body, attachment: { - filename: commentaire.piece_jointe.filename.to_s, - contentType: commentaire.piece_jointe.content_type, - checksum: commentaire.piece_jointe.checksum, - byteSize: commentaire.piece_jointe.byte_size + filename: commentaire.piece_jointe.first.filename.to_s, + contentType: commentaire.piece_jointe.first.content_type, + checksum: commentaire.piece_jointe.first.checksum, + byteSize: commentaire.piece_jointe.first.byte_size }, + attachments: commentaire.piece_jointe.map do |pj| + { + filename: pj.filename.to_s, + contentType: pj.content_type, + checksum: pj.checksum, + byteSize: pj.byte_size + } + end, email: commentaire.email } end diff --git a/spec/controllers/experts/avis_controller_spec.rb b/spec/controllers/experts/avis_controller_spec.rb index 5d5786ddf..7a4029eed 100644 --- a/spec/controllers/experts/avis_controller_spec.rb +++ b/spec/controllers/experts/avis_controller_spec.rb @@ -321,7 +321,7 @@ describe Experts::AvisController, type: :controller do let(:now) { Time.zone.parse("14/07/1789") } let(:avis) { avis_without_answer } - subject { post :create_commentaire, params: { id: avis.id, procedure_id:, commentaire: { body: 'commentaire body', piece_jointe: file } } } + subject { post :create_commentaire, params: { id: avis.id, procedure_id:, commentaire: { body: 'commentaire body', piece_jointe: [file] } } } before do allow(ClamavService).to receive(:safe_file?).and_return(scan_result) @@ -343,7 +343,7 @@ describe Experts::AvisController, type: :controller do it do expect { subject }.to change(Commentaire, :count).by(1) - expect(Commentaire.last.piece_jointe.filename).to eq("piece_justificative_0.pdf") + expect(Commentaire.last.piece_jointe.first.filename).to eq("piece_justificative_0.pdf") end end diff --git a/spec/factories/commentaire.rb b/spec/factories/commentaire.rb index 395859b11..13aaf74a7 100644 --- a/spec/factories/commentaire.rb +++ b/spec/factories/commentaire.rb @@ -2,11 +2,16 @@ FactoryBot.define do factory :commentaire do association :dossier, :en_construction email { generate(:user_email) } - body { 'plop' } trait :with_file do - piece_jointe { Rack::Test::UploadedFile.new('spec/fixtures/files/logo_test_procedure.png', 'image/png') } + after(:build) do |commentaire| + commentaire.piece_jointe.attach( + io: File.open('spec/fixtures/files/logo_test_procedure.png'), + filename: 'logo_test_procedure.png', + content_type: 'image/png' + ) + end end end end diff --git a/spec/services/commentaire_service_spec.rb b/spec/services/commentaire_service_spec.rb index a84bb8500..e3441b10f 100644 --- a/spec/services/commentaire_service_spec.rb +++ b/spec/services/commentaire_service_spec.rb @@ -26,11 +26,20 @@ describe CommentaireService do end end - context 'when it has a file' do - let(:file) { fixture_file_upload('spec/fixtures/files/piece_justificative_0.pdf', 'application/pdf') } + context 'when it has multiple files' do + let(:files) do + [ + fixture_file_upload('spec/fixtures/files/piece_justificative_0.pdf', 'application/pdf') + ] + end - it 'attaches the file' do + before do + commentaire.piece_jointe.attach(files) + end + + it 'attaches the files' do expect(commentaire.piece_jointe.attached?).to be_truthy + expect(commentaire.piece_jointe.count).to eq(1) end end end diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index eaf7a919d..7a212912e 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -69,7 +69,7 @@ describe PiecesJustificativesService do attach_file(witness_commentaire.piece_jointe) end - it { expect(subject).to match_array(dossier.commentaires.first.piece_jointe.attachment) } + it { expect(subject).to match_array(dossier.commentaires.first.piece_jointe.attachments) } end context 'with a pj not safe on a commentaire' do diff --git a/spec/system/users/en_construction_spec.rb b/spec/system/users/en_construction_spec.rb index 0444caba9..f0fd43acf 100644 --- a/spec/system/users/en_construction_spec.rb +++ b/spec/system/users/en_construction_spec.rb @@ -31,7 +31,7 @@ describe "Dossier en_construction" do click_on "Supprimer le fichier toto.txt" - input_selector = "#attachment-multiple-empty-#{champ.public_id} " + input_selector = "#attachment-multiple-empty-#{champ.public_id}" expect(page).to have_selector(input_selector) find(input_selector).attach_file(Rails.root.join('spec/fixtures/files/file.pdf')) From 46bec10aa2c7a0d3f05d71eb6bff90f955f58e1e Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Wed, 21 Feb 2024 21:53:14 +0000 Subject: [PATCH 10/15] lib --- app/lib/recovery/importer.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/lib/recovery/importer.rb b/app/lib/recovery/importer.rb index 0edc4b6a7..836917498 100644 --- a/app/lib/recovery/importer.rb +++ b/app/lib/recovery/importer.rb @@ -112,9 +112,12 @@ module Recovery end end - def import(pj) - ActiveStorage::Blob.insert(pj.blob.attributes) - ActiveStorage::Attachment.insert(pj.attributes) + def import(pjs) + attachments = pjs.respond_to?(:each) ? pjs : [pjs] + attachments.each do |pj| + ActiveStorage::Blob.insert(pj.blob.attributes) + ActiveStorage::Attachment.insert(pj.attributes) + end end end end From 5374100866ff894262d8acb982ab676ad6b14b6d Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Mon, 15 Apr 2024 12:38:50 +0200 Subject: [PATCH 11/15] fix(messagerie): fix submit for gestionnaires --- app/views/shared/dossiers/messages/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml index 0c2bf2ecb..eb0c3ecd3 100644 --- a/app/views/shared/dossiers/messages/_form.html.haml +++ b/app/views/shared/dossiers/messages/_form.html.haml @@ -13,5 +13,5 @@ = render Attachment::MultipleComponent.new(attached_file: commentaire.piece_jointe) %ul{ data: { 'file-input-reset-target': 'fileList' } } - .fr-mt-3w - = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true } + .fr-mt-3w + = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true } From 3b7b18ef90e5bf8a737f169f79a7a5f704175874 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Mon, 15 Apr 2024 12:17:19 +0200 Subject: [PATCH 12/15] style(pj-messagerie): same spacing as in PJ champ --- app/assets/stylesheets/attachment.scss | 9 ++++----- .../controllers/file_input_reset_controller.ts | 4 +--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/attachment.scss b/app/assets/stylesheets/attachment.scss index 3688742e7..e8b829486 100644 --- a/app/assets/stylesheets/attachment.scss +++ b/app/assets/stylesheets/attachment.scss @@ -49,9 +49,8 @@ } } -.attachment-multiple.fr-downloads-group.destroyable { - ul { - list-style-type: none; - padding-inline-start: 0; - } +.attachment-multiple.fr-downloads-group.destroyable ul, +ul[data-file-input-reset-target='fileList'] { + list-style-type: none; + padding-inline-start: 0; } diff --git a/app/javascript/controllers/file_input_reset_controller.ts b/app/javascript/controllers/file_input_reset_controller.ts index 0320c54b1..5f4070917 100644 --- a/app/javascript/controllers/file_input_reset_controller.ts +++ b/app/javascript/controllers/file_input_reset_controller.ts @@ -25,15 +25,13 @@ export class FileInputResetController extends ApplicationController { Array.from(files).forEach((file, index) => { const container = document.createElement('li'); - container.style.display = 'flex'; - container.style.alignItems = 'center'; + container.classList.add('flex', 'flex-gap-2', 'fr-mb-1w'); const deleteButton = this.createDeleteButton(deleteLabel, index); container.appendChild(deleteButton); const listItem = document.createElement('div'); listItem.textContent = file.name; - listItem.style.marginLeft = '8px'; container.appendChild(listItem); this.fileListTarget.appendChild(container); From 6748551240dc3fda83685de91218b6812daa6615 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Mon, 15 Apr 2024 12:31:20 +0200 Subject: [PATCH 13/15] chore(messagerie): add missing label on comment form --- app/views/shared/dossiers/messages/_form.html.haml | 9 ++++++--- config/locales/models/commentaire/en.yml | 6 ++++++ config/locales/models/commentaire/fr.yml | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 config/locales/models/commentaire/en.yml diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml index eb0c3ecd3..bd0cfe98e 100644 --- a/app/views/shared/dossiers/messages/_form.html.haml +++ b/app/views/shared/dossiers/messages/_form.html.haml @@ -8,10 +8,13 @@ %p.mandatory-explanation= t('asterisk_html', scope: [:utils]) = render Dsfr::InputComponent.new(form: f, attribute: :body, input_type: :text_area, opts: { rows: 5, placeholder: placeholder, title: placeholder, class: 'fr-input message-textarea'}) + - if local_assigns.has_key?(:dossier) - .fr-mt-3w{ data: { controller: "file-input-reset", delete_label: t('views.shared.messages.remove_file') } } - = render Attachment::MultipleComponent.new(attached_file: commentaire.piece_jointe) - %ul{ data: { 'file-input-reset-target': 'fileList' } } + .fr-mt-3w.fr-input-group + = f.label :piece_jointe, class: "fr-label" + %div{ data: { controller: "file-input-reset", delete_label: t('views.shared.messages.remove_file') } } + = render Attachment::MultipleComponent.new(attached_file: commentaire.piece_jointe) + %ul{ data: { 'file-input-reset-target': 'fileList' } } .fr-mt-3w = f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true } diff --git a/config/locales/models/commentaire/en.yml b/config/locales/models/commentaire/en.yml new file mode 100644 index 000000000..a97b7e0e7 --- /dev/null +++ b/config/locales/models/commentaire/en.yml @@ -0,0 +1,6 @@ +en: + activerecord: + attributes: + commentaire: + body: 'Your message' + piece_jointe: "Attachment" diff --git a/config/locales/models/commentaire/fr.yml b/config/locales/models/commentaire/fr.yml index 71ed7e25e..a4c17be96 100644 --- a/config/locales/models/commentaire/fr.yml +++ b/config/locales/models/commentaire/fr.yml @@ -3,4 +3,4 @@ fr: attributes: commentaire: body: 'Votre message' - file: fichier + piece_jointe: "Pièce jointe" From 9e3bf50e612c8c3fba81dd96d79421ccd2c6353e Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Mon, 15 Apr 2024 12:34:22 +0200 Subject: [PATCH 14/15] chore(messagerie): mention multiple files are possible --- app/components/attachment/edit_component/edit_component.en.yml | 1 + app/components/attachment/edit_component/edit_component.fr.yml | 1 + .../attachment/edit_component/edit_component.html.haml | 2 ++ 3 files changed, 4 insertions(+) diff --git a/app/components/attachment/edit_component/edit_component.en.yml b/app/components/attachment/edit_component/edit_component.en.yml index b5ae8544f..09d8cd176 100644 --- a/app/components/attachment/edit_component/edit_component.en.yml +++ b/app/components/attachment/edit_component/edit_component.en.yml @@ -5,6 +5,7 @@ en: retry: Retry delete: Delete delete_file: Delete file %{filename} + multiple_files: Multiple files possible. replace: Replace replace_file: Replace file %{filename} open_file: Open file %{filename} diff --git a/app/components/attachment/edit_component/edit_component.fr.yml b/app/components/attachment/edit_component/edit_component.fr.yml index d4e5b6811..67e2dd519 100644 --- a/app/components/attachment/edit_component/edit_component.fr.yml +++ b/app/components/attachment/edit_component/edit_component.fr.yml @@ -5,6 +5,7 @@ fr: retry: Réessayer delete: Supprimer delete_file: Supprimer le fichier %{filename} + multiple_files: Plusieurs fichiers possibles. replace: Remplacer replace_file: Remplacer le fichier %{filename} open_file: Ouvrir le fichier %{filename} diff --git a/app/components/attachment/edit_component/edit_component.html.haml b/app/components/attachment/edit_component/edit_component.html.haml index 57b60297d..45d8ceafc 100644 --- a/app/components/attachment/edit_component/edit_component.html.haml +++ b/app/components/attachment/edit_component/edit_component.html.haml @@ -43,6 +43,8 @@ = t('.max_file_size', max_file_size: number_to_human_size(max_file_size)) - if allowed_formats.present? = t('.allowed_formats', formats: allowed_formats.join(', ')) + - if as_multiple? + = t('.multiple_files') - if !persisted? || champ.present? && champ.titre_identite? = file_field(champ, field_name, **file_field_options) From 0dd4bafdfd890ea8e4dbfb3e1db8277466dee0f5 Mon Sep 17 00:00:00 2001 From: Colin Darie Date: Thu, 25 Apr 2024 12:11:48 +0200 Subject: [PATCH 15/15] refactor(pj): more readable as_multiple logic --- .../multiple_component/multiple_component.html.haml | 2 +- app/models/champs/piece_justificative_champ.rb | 4 ---- app/models/champs/titre_identite_champ.rb | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/components/attachment/multiple_component/multiple_component.html.haml b/app/components/attachment/multiple_component/multiple_component.html.haml index b5a64b044..74abdf23c 100644 --- a/app/components/attachment/multiple_component/multiple_component.html.haml +++ b/app/components/attachment/multiple_component/multiple_component.html.haml @@ -8,7 +8,7 @@ = render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, view_as:, user_can_destroy:, form_object_name:) %div{ id: empty_component_id, class: class_names("hidden": !can_attach_next?), data: { turbo_force: :server } } - = render Attachment::EditComponent.new(champ:, as_multiple: champ.present? ? champ.allow_multiple_attachments? : true, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:) + = render Attachment::EditComponent.new(champ:, as_multiple: champ.nil?, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:) // single poll and refresh message for all attachments = render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context) diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb index bca75c292..8d9435d17 100644 --- a/app/models/champs/piece_justificative_champ.rb +++ b/app/models/champs/piece_justificative_champ.rb @@ -30,8 +30,4 @@ class Champs::PieceJustificativeChamp < Champ def blank? piece_justificative_file.blank? end - - def allow_multiple_attachments? - false - end end diff --git a/app/models/champs/titre_identite_champ.rb b/app/models/champs/titre_identite_champ.rb index 88983fa6e..05b67e802 100644 --- a/app/models/champs/titre_identite_champ.rb +++ b/app/models/champs/titre_identite_champ.rb @@ -25,8 +25,4 @@ class Champs::TitreIdentiteChamp < Champ def blank? piece_justificative_file.blank? end - - def allow_multiple_attachments? - false - end end