Merge pull request #9986 from demarches-simplifiees/feat/4408
ETQ Usager / Instructeur, je souhaite ajouter plusieurs Pj d'un coup dans la messagerie
This commit is contained in:
commit
e5b6a28e0b
32 changed files with 239 additions and 128 deletions
|
@ -49,9 +49,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-multiple.fr-downloads-group.destroyable {
|
.attachment-multiple.fr-downloads-group.destroyable ul,
|
||||||
ul {
|
ul[data-file-input-reset-target='fileList'] {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
class Attachment::EditComponent < ApplicationComponent
|
class Attachment::EditComponent < ApplicationComponent
|
||||||
attr_reader :champ
|
attr_reader :champ
|
||||||
attr_reader :attachment
|
attr_reader :attachment
|
||||||
|
attr_reader :attachments
|
||||||
attr_reader :user_can_destroy
|
attr_reader :user_can_destroy
|
||||||
alias user_can_destroy? user_can_destroy
|
alias user_can_destroy? user_can_destroy
|
||||||
attr_reader :as_multiple
|
attr_reader :as_multiple
|
||||||
|
@ -9,24 +10,23 @@ class Attachment::EditComponent < ApplicationComponent
|
||||||
|
|
||||||
EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze
|
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)
|
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)
|
||||||
@as_multiple = as_multiple
|
|
||||||
@attached_file = attached_file
|
|
||||||
@auto_attach_url = auto_attach_url
|
|
||||||
@champ = champ
|
@champ = champ
|
||||||
|
@attached_file = attached_file
|
||||||
@direct_upload = direct_upload
|
@direct_upload = direct_upload
|
||||||
@index = index
|
@index = index
|
||||||
@view_as = view_as
|
@view_as = view_as
|
||||||
@user_can_destroy = user_can_destroy
|
@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?
|
||||||
|
@attachments.compact!
|
||||||
|
|
||||||
# attachment passed by kwarg because we don't want a default (nil) value.
|
# Utilisation du premier attachement comme référence pour la rétrocompatibilité
|
||||||
@attachment = if kwargs.key?(:attachment)
|
@attachment = @attachments.first
|
||||||
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
|
|
||||||
|
|
||||||
# When parent form has nested attributes, pass the form builder object_name
|
# When parent form has nested attributes, pass the form builder object_name
|
||||||
# to correctly infer the input attribute name.
|
# to correctly infer the input attribute name.
|
||||||
|
@ -63,7 +63,7 @@ class Attachment::EditComponent < ApplicationComponent
|
||||||
|
|
||||||
def file_field_options
|
def file_field_options
|
||||||
track_issue_with_missing_validators if missing_validators?
|
track_issue_with_missing_validators if missing_validators?
|
||||||
{
|
options = {
|
||||||
class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?),
|
class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?),
|
||||||
direct_upload: @direct_upload,
|
direct_upload: @direct_upload,
|
||||||
id: input_id,
|
id: input_id,
|
||||||
|
@ -71,8 +71,13 @@ class Attachment::EditComponent < ApplicationComponent
|
||||||
data: {
|
data: {
|
||||||
auto_attach_url:,
|
auto_attach_url:,
|
||||||
turbo_force: :server
|
turbo_force: :server
|
||||||
}.merge(has_file_size_validator? ? { max_file_size: } : {})
|
}.merge(has_file_size_validator? ? { max_file_size: max_file_size } : {})
|
||||||
}.merge(has_content_type_validator? ? { accept: accept_content_type } : {})
|
}
|
||||||
|
|
||||||
|
options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {})
|
||||||
|
options[:multiple] = true if as_multiple?
|
||||||
|
|
||||||
|
options
|
||||||
end
|
end
|
||||||
|
|
||||||
def poll_url
|
def poll_url
|
||||||
|
@ -90,7 +95,8 @@ class Attachment::EditComponent < ApplicationComponent
|
||||||
end
|
end
|
||||||
|
|
||||||
def field_name(object_name = nil, method_name = nil, *method_names, multiple: false, index: nil)
|
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
|
end
|
||||||
|
|
||||||
def attribute_name
|
def attribute_name
|
||||||
|
@ -126,24 +132,24 @@ class Attachment::EditComponent < ApplicationComponent
|
||||||
!!attachment&.persisted?
|
!!attachment&.persisted?
|
||||||
end
|
end
|
||||||
|
|
||||||
def downloadable?
|
def downloadable?(attachment)
|
||||||
return false unless @view_as == :download
|
return false unless @view_as == :download
|
||||||
|
|
||||||
viewable?
|
viewable?(attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
def viewable?
|
def viewable?(attachment)
|
||||||
return false if attachment.virus_scanner_error?
|
return false if attachment.virus_scanner_error?
|
||||||
return false if attachment.watermark_pending?
|
return false if attachment.watermark_pending?
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def error?
|
def error?(attachment)
|
||||||
attachment.virus_scanner_error?
|
attachment.virus_scanner_error?
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_message
|
def error_message(attachment)
|
||||||
case
|
case
|
||||||
when attachment.virus_scanner.infected?
|
when attachment.virus_scanner.infected?
|
||||||
t(".errors.virus_infected")
|
t(".errors.virus_infected")
|
||||||
|
|
|
@ -5,6 +5,7 @@ en:
|
||||||
retry: Retry
|
retry: Retry
|
||||||
delete: Delete
|
delete: Delete
|
||||||
delete_file: Delete file %{filename}
|
delete_file: Delete file %{filename}
|
||||||
|
multiple_files: Multiple files possible.
|
||||||
replace: Replace
|
replace: Replace
|
||||||
replace_file: Replace file %{filename}
|
replace_file: Replace file %{filename}
|
||||||
open_file: Open file %{filename}
|
open_file: Open file %{filename}
|
||||||
|
|
|
@ -5,6 +5,7 @@ fr:
|
||||||
retry: Réessayer
|
retry: Réessayer
|
||||||
delete: Supprimer
|
delete: Supprimer
|
||||||
delete_file: Supprimer le fichier %{filename}
|
delete_file: Supprimer le fichier %{filename}
|
||||||
|
multiple_files: Plusieurs fichiers possibles.
|
||||||
replace: Remplacer
|
replace: Remplacer
|
||||||
replace_file: Remplacer le fichier %{filename}
|
replace_file: Remplacer le fichier %{filename}
|
||||||
open_file: Ouvrir le fichier %{filename}
|
open_file: Ouvrir le fichier %{filename}
|
||||||
|
|
|
@ -1,39 +1,61 @@
|
||||||
.attachment.fr-upload-group{ { id: attachment ? dom_id(attachment, :edit) : nil, class: class_names("fr-mb-1w": !(as_multiple? && downloadable?)) }.compact }
|
.attachment.fr-upload-group{ id: (attachment ? dom_id(attachment, :edit) : nil), class: class_names("fr-mb-1w": !(as_multiple? && attachments.any?(&:persisted?))) }
|
||||||
- if persisted?
|
- if as_multiple?
|
||||||
%div{ id: dom_id(attachment, :persisted_row) }
|
- attachments.each do |attachment|
|
||||||
.flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) }
|
- if attachment.persisted?
|
||||||
- if user_can_destroy?
|
%div{ id: dom_id(attachment, :persisted_row) }
|
||||||
= 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
|
.flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) }
|
||||||
= t('.delete')
|
- 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?
|
- if downloadable?(attachment)
|
||||||
= render Dsfr::DownloadComponent.new(attachment:)
|
= render Dsfr::DownloadComponent.new(attachment: attachment)
|
||||||
- else
|
- else
|
||||||
.fr-py-1v
|
.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)
|
%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?
|
- if error?(attachment)
|
||||||
%p.fr-error-text= error_message
|
%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
|
%p.fr-hint-text.fr-mb-1w
|
||||||
- if max_file_size.present?
|
- if max_file_size.present?
|
||||||
= t('.max_file_size', max_file_size: number_to_human_size(max_file_size))
|
= t('.max_file_size', max_file_size: number_to_human_size(max_file_size))
|
||||||
- if allowed_formats.present?
|
- if allowed_formats.present?
|
||||||
= t('.allowed_formats', formats: allowed_formats.join(', '))
|
= t('.allowed_formats', formats: allowed_formats.join(', '))
|
||||||
|
- if as_multiple?
|
||||||
|
= t('.multiple_files')
|
||||||
|
|
||||||
|
- if !persisted? || champ.present? && champ.titre_identite?
|
||||||
- if !as_multiple?
|
|
||||||
= file_field(champ, field_name, **file_field_options)
|
= file_field(champ, field_name, **file_field_options)
|
||||||
|
|
||||||
- if persisted?
|
- attachments.filter(&:persisted?).each do |attachment|
|
||||||
- Attachment::PendingPollComponent.new(attachment: attachment, poll_url:, context: poll_context).then do |component|
|
- if attachment.persisted?
|
||||||
.fr-mt-2w
|
- Attachment::PendingPollComponent.new(attachment: attachment, poll_url: poll_url, context: poll_context).then do |component|
|
||||||
= render component
|
.fr-mt-2w
|
||||||
|
= render component
|
||||||
|
|
||||||
.attachment-upload-error.hidden
|
.attachment-upload-error.hidden
|
||||||
%p.fr-error-text= t('.errors.uploading')
|
%p.fr-error-text= t('.errors.uploading')
|
||||||
= button_tag(**retry_button_options) do
|
= button_tag(**retry_button_options) do
|
||||||
= t(".retry")
|
= t(".retry")
|
||||||
|
|
|
@ -15,7 +15,7 @@ class Attachment::MultipleComponent < ApplicationComponent
|
||||||
|
|
||||||
delegate :count, :empty?, to: :attachments, prefix: true
|
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
|
@champ = champ
|
||||||
@attached_file = attached_file
|
@attached_file = attached_file
|
||||||
@form_object_name = form_object_name
|
@form_object_name = form_object_name
|
||||||
|
@ -35,11 +35,11 @@ class Attachment::MultipleComponent < ApplicationComponent
|
||||||
end
|
end
|
||||||
|
|
||||||
def empty_component_id
|
def empty_component_id
|
||||||
"attachment-multiple-empty-#{champ.public_id}"
|
champ.present? ? "attachment-multiple-empty-#{champ.public_id}" : "attachment-multiple-empty-generic"
|
||||||
end
|
end
|
||||||
|
|
||||||
def auto_attach_url
|
def auto_attach_url
|
||||||
helpers.auto_attach_url(champ)
|
champ.present? ? helpers.auto_attach_url(champ) : '#'
|
||||||
end
|
end
|
||||||
alias poll_url auto_attach_url
|
alias poll_url auto_attach_url
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
%ul.fr-my-1v
|
%ul.fr-my-1v
|
||||||
- each_attachment do |attachment, index|
|
- each_attachment do |attachment, index|
|
||||||
%li{ id: dom_id(attachment) }
|
%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 } }
|
%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.nil?, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:)
|
||||||
|
|
||||||
// single poll and refresh message for all attachments
|
// single poll and refresh message for all attachments
|
||||||
= render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context)
|
= render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context)
|
||||||
|
|
|
@ -27,7 +27,9 @@
|
||||||
|
|
||||||
- if groupe_gestionnaire.nil? && commentaire.piece_jointe.attached?
|
- if groupe_gestionnaire.nil? && commentaire.piece_jointe.attached?
|
||||||
.fr-ml-2w
|
.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?
|
- 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
|
= 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
|
||||||
|
|
|
@ -234,7 +234,7 @@ module Experts
|
||||||
end
|
end
|
||||||
|
|
||||||
def commentaire_params
|
def commentaire_params
|
||||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
params.require(:commentaire).permit(:body, piece_jointe: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -256,6 +256,7 @@ module Instructeurs
|
||||||
flash.notice = "Message envoyé"
|
flash.notice = "Message envoyé"
|
||||||
redirect_to messagerie_instructeur_dossier_path(procedure, dossier)
|
redirect_to messagerie_instructeur_dossier_path(procedure, dossier)
|
||||||
else
|
else
|
||||||
|
@commentaire.piece_jointe.purge.reload
|
||||||
flash.alert = @commentaire.errors.full_messages
|
flash.alert = @commentaire.errors.full_messages
|
||||||
render :messagerie
|
render :messagerie
|
||||||
end
|
end
|
||||||
|
@ -393,7 +394,7 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def commentaire_params
|
def commentaire_params
|
||||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
params.require(:commentaire).permit(:body, piece_jointe: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def champs_private_params
|
def champs_private_params
|
||||||
|
|
|
@ -621,7 +621,7 @@ module Users
|
||||||
end
|
end
|
||||||
|
|
||||||
def commentaire_params
|
def commentaire_params
|
||||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
params.require(:commentaire).permit(:body, piece_jointe: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,10 +5,10 @@ module Types
|
||||||
field :body, String, null: false
|
field :body, String, null: false
|
||||||
field :created_at, GraphQL::Types::ISO8601DateTime, 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: [
|
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: [
|
field :attachments, [Types::File], null: false, extensions: [
|
||||||
{ Extensions::Attachment => { attachment: :piece_jointe, as: :multiple } }
|
{ Extensions::Attachment => { attachments: :piece_jointe } }
|
||||||
]
|
]
|
||||||
field :correction, CorrectionType, null: true
|
field :correction, CorrectionType, null: true
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,79 @@
|
||||||
import { ApplicationController } from './application_controller';
|
import { ApplicationController } from './application_controller';
|
||||||
import { hide, show } from '@utils';
|
|
||||||
|
|
||||||
export class FileInputResetController extends ApplicationController {
|
export class FileInputResetController extends ApplicationController {
|
||||||
static targets = ['reset'];
|
static targets = ['fileList'];
|
||||||
|
declare fileListTarget: HTMLElement;
|
||||||
declare readonly resetTarget: HTMLElement;
|
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.on('change', (event) => {
|
super.connect();
|
||||||
if (event.target == this.fileInput) {
|
this.updateFileList();
|
||||||
this.showResetButton();
|
this.element.addEventListener('change', (event) => {
|
||||||
|
if (
|
||||||
|
event.target instanceof HTMLInputElement &&
|
||||||
|
event.target.type === 'file'
|
||||||
|
) {
|
||||||
|
this.updateFileList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(event: Event) {
|
updateFileList() {
|
||||||
event.preventDefault();
|
const files = this.fileInput?.files ?? [];
|
||||||
this.fileInput.value = '';
|
this.fileListTarget.innerHTML = '';
|
||||||
hide(this.resetTarget);
|
|
||||||
|
const deleteLabel =
|
||||||
|
this.element.getAttribute('data-delete-label') || 'Delete';
|
||||||
|
|
||||||
|
Array.from(files).forEach((file, index) => {
|
||||||
|
const container = document.createElement('li');
|
||||||
|
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;
|
||||||
|
|
||||||
|
container.appendChild(listItem);
|
||||||
|
this.fileListTarget.appendChild(container);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showResetButton() {
|
createDeleteButton(deleteLabel: string, index: number) {
|
||||||
show(this.resetTarget);
|
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() {
|
removeFile(index: number) {
|
||||||
const inputs =
|
const files = this.fileInput?.files;
|
||||||
this.element.querySelectorAll<HTMLInputElement>('input[type="file"]');
|
if (!files) return;
|
||||||
if (inputs.length == 0) {
|
|
||||||
throw new Error('No file input found');
|
const dataTransfer = new DataTransfer();
|
||||||
} else if (inputs.length > 1) {
|
Array.from(files).forEach((file, i) => {
|
||||||
throw new Error('Multiple file inputs found');
|
if (index !== i) {
|
||||||
}
|
dataTransfer.items.add(file);
|
||||||
return inputs[0];
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Recovery
|
||||||
:invites,
|
:invites,
|
||||||
:traitements,
|
:traitements,
|
||||||
:transfer_logs,
|
:transfer_logs,
|
||||||
commentaires: { piece_jointe_attachment: :blob },
|
commentaires: { piece_jointe_attachments: :blob },
|
||||||
avis: { introduction_file_attachment: :blob, piece_justificative_file_attachment: :blob },
|
avis: { introduction_file_attachment: :blob, piece_justificative_file_attachment: :blob },
|
||||||
dossier_operation_logs: { serialized_attachment: :blob },
|
dossier_operation_logs: { serialized_attachment: :blob },
|
||||||
attestation: { pdf_attachment: :blob },
|
attestation: { pdf_attachment: :blob },
|
||||||
|
|
|
@ -112,9 +112,12 @@ module Recovery
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def import(pj)
|
def import(pjs)
|
||||||
ActiveStorage::Blob.insert(pj.blob.attributes)
|
attachments = pjs.respond_to?(:each) ? pjs : [pjs]
|
||||||
ActiveStorage::Attachment.insert(pj.attributes)
|
attachments.each do |pj|
|
||||||
|
ActiveStorage::Blob.insert(pj.blob.attributes)
|
||||||
|
ActiveStorage::Attachment.insert(pj.attributes)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Commentaire < ApplicationRecord
|
||||||
|
|
||||||
validate :messagerie_available?, on: :create, unless: -> { dossier.brouillon? }
|
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?
|
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?
|
sent_by?(connected_user) && (sent_by_instructeur? || sent_by_expert?) && !discarded?
|
||||||
end
|
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!
|
def soft_delete!
|
||||||
transaction do
|
transaction do
|
||||||
discard!
|
discard!
|
||||||
|
@ -80,7 +74,7 @@ class Commentaire < ApplicationRecord
|
||||||
update! body: ''
|
update! body: ''
|
||||||
end
|
end
|
||||||
|
|
||||||
piece_jointe.purge_later if piece_jointe.attached?
|
piece_jointe.each(&:purge_later) if piece_jointe.attached?
|
||||||
end
|
end
|
||||||
|
|
||||||
def flagged_pending_correction?
|
def flagged_pending_correction?
|
||||||
|
|
|
@ -54,7 +54,7 @@ class Dossier < ApplicationRecord
|
||||||
has_many :prefilled_champs_public, -> { root.public_only.prefilled }, class_name: 'Champ', inverse_of: false
|
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 :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 :invites, dependent: :destroy
|
||||||
has_many :follows, -> { active }, inverse_of: :dossier
|
has_many :follows, -> { active }, inverse_of: :dossier
|
||||||
|
@ -294,7 +294,7 @@ class Dossier < ApplicationRecord
|
||||||
scope :for_api, -> {
|
scope :for_api, -> {
|
||||||
with_champs
|
with_champs
|
||||||
.with_annotations
|
.with_annotations
|
||||||
.includes(commentaires: { piece_jointe_attachment: :blob },
|
.includes(commentaires: { piece_jointe_attachments: :blob },
|
||||||
justificatif_motivation_attachment: :blob,
|
justificatif_motivation_attachment: :blob,
|
||||||
attestation: [],
|
attestation: [],
|
||||||
avis: { piece_justificative_file_attachment: :blob },
|
avis: { piece_justificative_file_attachment: :blob },
|
||||||
|
|
|
@ -2,13 +2,9 @@ class CommentaireSerializer < ActiveModel::Serializer
|
||||||
attributes :email,
|
attributes :email,
|
||||||
:body,
|
:body,
|
||||||
:created_at,
|
:created_at,
|
||||||
:attachment
|
:piece_jointe_attachments
|
||||||
|
|
||||||
def created_at
|
def created_at
|
||||||
object.created_at&.in_time_zone('UTC')
|
object.created_at&.in_time_zone('UTC')
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachment
|
|
||||||
object.file_url
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -161,7 +161,7 @@ class PiecesJustificativesService
|
||||||
|
|
||||||
def pjs_for_commentaires(dossiers)
|
def pjs_for_commentaires(dossiers)
|
||||||
commentaire_id_dossier_id = Commentaire
|
commentaire_id_dossier_id = Commentaire
|
||||||
.joins(:piece_jointe_attachment)
|
.joins(:piece_jointe_attachments)
|
||||||
.where(dossier: dossiers)
|
.where(dossier: dossiers)
|
||||||
.pluck(:id, :dossier_id)
|
.pluck(:id, :dossier_id)
|
||||||
.to_h
|
.to_h
|
||||||
|
|
|
@ -10,10 +10,11 @@
|
||||||
= render Dsfr::InputComponent.new(form: f, attribute: :body, input_type: :text_area, opts: { rows: 5, placeholder: placeholder, title: placeholder, class: 'fr-input message-textarea'})
|
= 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)
|
- if local_assigns.has_key?(:dossier)
|
||||||
.fr-mt-3w{ data: { controller: "file-input-reset" } }
|
.fr-mt-3w.fr-input-group
|
||||||
= render Attachment::EditComponent.new(attached_file: commentaire.piece_jointe)
|
= f.label :piece_jointe, class: "fr-label"
|
||||||
%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' } }
|
%div{ data: { controller: "file-input-reset", delete_label: t('views.shared.messages.remove_file') } }
|
||||||
= 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-mt-3w
|
||||||
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true }
|
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true }
|
||||||
|
|
6
config/locales/models/commentaire/en.yml
Normal file
6
config/locales/models/commentaire/en.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
en:
|
||||||
|
activerecord:
|
||||||
|
attributes:
|
||||||
|
commentaire:
|
||||||
|
body: 'Your message'
|
||||||
|
piece_jointe: "Attachment"
|
|
@ -3,4 +3,4 @@ fr:
|
||||||
attributes:
|
attributes:
|
||||||
commentaire:
|
commentaire:
|
||||||
body: 'Votre message'
|
body: 'Votre message'
|
||||||
file: fichier
|
piece_jointe: "Pièce jointe"
|
||||||
|
|
|
@ -23,3 +23,4 @@ fr:
|
||||||
signin: 'Se connecter'
|
signin: 'Se connecter'
|
||||||
messages:
|
messages:
|
||||||
remove_file: 'Supprimer le fichier'
|
remove_file: 'Supprimer le fichier'
|
||||||
|
remove_all: "Supprimer tous les fichiers"
|
||||||
|
|
|
@ -48,7 +48,7 @@ namespace :recovery do
|
||||||
rake_puts "Will export #{dossier_ids}"
|
rake_puts "Will export #{dossier_ids}"
|
||||||
|
|
||||||
dossiers_with_data = Dossier.where(id: 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 },
|
avis: { introduction_file_attachment: :blob, piece_justificative_file_attachment: :blob },
|
||||||
dossier_operation_logs: { serialized_attachment: :blob },
|
dossier_operation_logs: { serialized_attachment: :blob },
|
||||||
attestation: { pdf_attachment: :blob },
|
attestation: { pdf_attachment: :blob },
|
||||||
|
@ -67,7 +67,7 @@ namespace :recovery do
|
||||||
blob_keys_for_dossier += dossier.commentaires.flat_map do |commentaire|
|
blob_keys_for_dossier += dossier.commentaires.flat_map do |commentaire|
|
||||||
commentaire_blob_key = []
|
commentaire_blob_key = []
|
||||||
if commentaire.piece_jointe.attached?
|
if commentaire.piece_jointe.attached?
|
||||||
commentaire_blob_key += [commentaire.piece_jointe_attachment.blob.key]
|
commentaire_blob_key += [commentaire.piece_jointe_attachments.blob.key]
|
||||||
end
|
end
|
||||||
commentaire_blob_key
|
commentaire_blob_key
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,8 +66,8 @@ RSpec.describe Attachment::EditComponent, type: :component do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not render an empty file' do # (is is rendered by MultipleComponent)
|
it 'does render an empty file' do # (is is rendered by MultipleComponent)
|
||||||
expect(subject).not_to have_selector('input[type=file]')
|
expect(subject).to have_selector('input[type=file]')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders max size for first index' do
|
it 'renders max size for first index' do
|
||||||
|
|
|
@ -93,6 +93,14 @@ RSpec.describe Attachment::MultipleComponent, type: :component do
|
||||||
end
|
end
|
||||||
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)
|
def attach_to_champ(attached_file, champ)
|
||||||
attached_file.attach(
|
attached_file.attach(
|
||||||
io: StringIO.new("x" * 2),
|
io: StringIO.new("x" * 2),
|
||||||
|
|
|
@ -432,6 +432,12 @@ describe API::V2::GraphqlController do
|
||||||
byteSize
|
byteSize
|
||||||
contentType
|
contentType
|
||||||
}
|
}
|
||||||
|
attachments {
|
||||||
|
filename
|
||||||
|
checksum
|
||||||
|
byteSize
|
||||||
|
contentType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
avis {
|
avis {
|
||||||
expert {
|
expert {
|
||||||
|
@ -519,11 +525,19 @@ describe API::V2::GraphqlController do
|
||||||
{
|
{
|
||||||
body: commentaire.body,
|
body: commentaire.body,
|
||||||
attachment: {
|
attachment: {
|
||||||
filename: commentaire.piece_jointe.filename.to_s,
|
filename: commentaire.piece_jointe.first.filename.to_s,
|
||||||
contentType: commentaire.piece_jointe.content_type,
|
contentType: commentaire.piece_jointe.first.content_type,
|
||||||
checksum: commentaire.piece_jointe.checksum,
|
checksum: commentaire.piece_jointe.first.checksum,
|
||||||
byteSize: commentaire.piece_jointe.byte_size
|
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
|
email: commentaire.email
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -321,7 +321,7 @@ describe Experts::AvisController, type: :controller do
|
||||||
let(:now) { Time.zone.parse("14/07/1789") }
|
let(:now) { Time.zone.parse("14/07/1789") }
|
||||||
let(:avis) { avis_without_answer }
|
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
|
before do
|
||||||
allow(ClamavService).to receive(:safe_file?).and_return(scan_result)
|
allow(ClamavService).to receive(:safe_file?).and_return(scan_result)
|
||||||
|
@ -343,7 +343,7 @@ describe Experts::AvisController, type: :controller do
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect { subject }.to change(Commentaire, :count).by(1)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,16 @@ FactoryBot.define do
|
||||||
factory :commentaire do
|
factory :commentaire do
|
||||||
association :dossier, :en_construction
|
association :dossier, :en_construction
|
||||||
email { generate(:user_email) }
|
email { generate(:user_email) }
|
||||||
|
|
||||||
body { 'plop' }
|
body { 'plop' }
|
||||||
|
|
||||||
trait :with_file do
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,11 +26,20 @@ describe CommentaireService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when it has a file' do
|
context 'when it has multiple files' do
|
||||||
let(:file) { fixture_file_upload('spec/fixtures/files/piece_justificative_0.pdf', 'application/pdf') }
|
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.attached?).to be_truthy
|
||||||
|
expect(commentaire.piece_jointe.count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,7 +69,7 @@ describe PiecesJustificativesService do
|
||||||
attach_file(witness_commentaire.piece_jointe)
|
attach_file(witness_commentaire.piece_jointe)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'with a pj not safe on a commentaire' do
|
context 'with a pj not safe on a commentaire' do
|
||||||
|
|
|
@ -31,7 +31,7 @@ describe "Dossier en_construction" do
|
||||||
|
|
||||||
click_on "Supprimer le fichier toto.txt"
|
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)
|
expect(page).to have_selector(input_selector)
|
||||||
find(input_selector).attach_file(Rails.root.join('spec/fixtures/files/file.pdf'))
|
find(input_selector).attach_file(Rails.root.join('spec/fixtures/files/file.pdf'))
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue