refactor(piece_justificative): UX follows mockups
This commit is contained in:
parent
84ca01bdf7
commit
b13c5e56f6
39 changed files with 373 additions and 272 deletions
|
@ -1,53 +1,23 @@
|
|||
@import "colors";
|
||||
@import "constants";
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
margin-bottom: $default-spacer;
|
||||
}
|
||||
|
||||
.attachment-action {
|
||||
margin-right: $default-spacer;
|
||||
|
||||
.button {
|
||||
text-transform: lowercase;
|
||||
background-image: none; // remove DSFR underline, TODO: switch to DSFR download links https://github.com/betagouv/demarches-simplifiees.fr/issues/7883
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.attachment-error {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
margin-bottom: $default-padding;
|
||||
padding: $default-padding;
|
||||
background: $background-red;
|
||||
position: relative;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
&::before {
|
||||
box-shadow: inset 2px 0 0 0 var(--border-plain-error);
|
||||
height: 2rem; // height of button
|
||||
content: "";
|
||||
left: -0.75rem;
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
.attachment-filename {
|
||||
color: var(--text-default-error);
|
||||
}
|
||||
|
||||
.fr-error-text {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-error-message {
|
||||
display: inline-block;
|
||||
margin-right: $default-padding;
|
||||
color: $medium-red;
|
||||
}
|
||||
|
||||
.attachment-error-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.attachment-error-retry {
|
||||
white-space: nowrap;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-input.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "constants";
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
|
||||
|
@ -49,3 +51,7 @@
|
|||
.flex-no-shrink {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flex-gap-2 {
|
||||
gap: 2 * $default-spacer; // scss-lint:disable PropertySpelling
|
||||
}
|
||||
|
|
|
@ -386,6 +386,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.editable-champ-titre_identite { // scss-lint:disable SelectorFormat
|
||||
margin-bottom: 2 * $default-padding;
|
||||
}
|
||||
|
||||
.cnaf-inputs,
|
||||
.dgfip-inputs,
|
||||
.pole-emploi-inputs,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@import "constants";
|
||||
|
||||
.rich-text:not(.piece_justificative) {
|
||||
.rich-text:not(.piece_justificative):not(.titre_identite) {
|
||||
i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@ class ApplicationComponent < ViewComponent::Base
|
|||
include ViewComponent::Translatable
|
||||
include FlipperHelper
|
||||
|
||||
# Takes a Hash of { class_name: boolean }.
|
||||
# Returns truthy class names in an array. Array can be passed as-it in rails helpers,
|
||||
# and is still manipulable if needed.
|
||||
def class_names(class_names)
|
||||
class_names.to_a.filter_map { |(class_name, flag)| class_name if flag }.join(' ')
|
||||
class_names.filter { _2 }.keys
|
||||
end
|
||||
|
||||
def current_user
|
||||
|
|
|
@ -1,57 +1,68 @@
|
|||
# Display a widget for uploading, editing and deleting a file attachment
|
||||
class Attachment::EditComponent < ApplicationComponent
|
||||
attr_reader :template, :form, :attachment
|
||||
|
||||
delegate :persisted?, to: :attachment, allow_nil: true
|
||||
attr_reader :champ
|
||||
attr_reader :attachment
|
||||
attr_reader :as_multiple
|
||||
alias as_multiple? as_multiple
|
||||
|
||||
EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze
|
||||
|
||||
def initialize(form:, attached_file:, user_can_destroy: false, direct_upload: true, id: nil, index: 0, **kwargs)
|
||||
@form = form
|
||||
def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, id: nil, index: 0, as_multiple: false, **kwargs)
|
||||
@champ = champ
|
||||
@auto_attach_url = auto_attach_url
|
||||
@attached_file = attached_file
|
||||
|
||||
# attachment passed by kwarg because we don't want a default (nil) value.
|
||||
@attachment = if kwargs.key?(:attachment)
|
||||
kwargs[: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
|
||||
|
||||
@user_can_destroy = user_can_destroy
|
||||
fail ArgumentError, "Unknown kwarg #{kwargs.keys.join(', ')}" unless kwargs.empty?
|
||||
|
||||
@direct_upload = direct_upload
|
||||
@id = id
|
||||
@index = index
|
||||
@as_multiple = as_multiple
|
||||
end
|
||||
|
||||
def object_name
|
||||
@object.class.name.underscore
|
||||
end
|
||||
|
||||
def first?
|
||||
@index.zero?
|
||||
end
|
||||
|
||||
def max_file_size
|
||||
return if file_size_validator.nil?
|
||||
|
||||
file_size_validator.options[:less_than]
|
||||
end
|
||||
|
||||
def user_can_destroy?
|
||||
@user_can_destroy
|
||||
end
|
||||
|
||||
def attachment_path
|
||||
helpers.attachment_path attachment.id, { signed_id: attachment.blob.signed_id }
|
||||
end
|
||||
|
||||
def attachment_id
|
||||
@attachment_id ||= (attachment&.id || SecureRandom.uuid)
|
||||
end
|
||||
|
||||
def attachment_input_class
|
||||
"attachment-input-#{attachment_id}"
|
||||
def attachment_path(**args)
|
||||
helpers.attachment_path attachment.id, args.merge(signed_id: attachment.blob.signed_id)
|
||||
end
|
||||
|
||||
def champ
|
||||
@form.object.is_a?(Champ) ? @form.object : nil
|
||||
def destroy_attachment_path
|
||||
attachment_path(champ_id: champ&.id)
|
||||
end
|
||||
|
||||
def attachment_input_class
|
||||
"attachment-input-#{attachment_id}"
|
||||
end
|
||||
|
||||
def file_field_options
|
||||
track_issue_with_missing_validators if missing_validators?
|
||||
{
|
||||
class: "fr-text--sm attachment-input #{attachment_input_class} #{'hidden' if persisted?}",
|
||||
class: "fr-upload attachment-input #{attachment_input_class} #{persisted? ? 'hidden' : ''}",
|
||||
direct_upload: @direct_upload,
|
||||
id: input_id(@id),
|
||||
aria: { describedby: champ&.describedby_id },
|
||||
|
@ -61,10 +72,76 @@ class Attachment::EditComponent < ApplicationComponent
|
|||
}.merge(has_content_type_validator? ? { accept: accept_content_type } : {})
|
||||
end
|
||||
|
||||
def auto_attach_url
|
||||
helpers.auto_attach_url(form.object)
|
||||
def in_progress?
|
||||
return false if attachment.nil?
|
||||
return true if attachment.virus_scanner.pending?
|
||||
return true if attachment.watermark_pending?
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def progress_bar_label
|
||||
case
|
||||
when attachment.virus_scanner.pending?
|
||||
"Analyse antivirus en cours…"
|
||||
when attachment.watermark_pending?
|
||||
"Traitement en cours…"
|
||||
end
|
||||
end
|
||||
|
||||
def poll_controller_options
|
||||
{
|
||||
controller: 'turbo-poll',
|
||||
turbo_poll_url_value: poll_url
|
||||
}
|
||||
end
|
||||
|
||||
def poll_url
|
||||
if champ.present?
|
||||
auto_attach_url
|
||||
else
|
||||
attachment_path(user_can_edit: true, auto_attach_url: @auto_attach_url)
|
||||
end
|
||||
end
|
||||
|
||||
def file_field_name
|
||||
@attached_file.name
|
||||
end
|
||||
|
||||
def remove_button_options
|
||||
{
|
||||
role: 'button',
|
||||
data: { turbo: "true", turbo_method: :delete }
|
||||
}
|
||||
end
|
||||
|
||||
def retry_button_options
|
||||
{
|
||||
type: 'button',
|
||||
class: 'fr-btn fr-btn--sm fr-btn--tertiary fr-mt-1w fr-icon-refresh-line fr-btn--icon-left attachment-error-retry',
|
||||
data: { input_target: ".#{attachment_input_class}", action: 'autosave#onClickRetryButton' }
|
||||
}
|
||||
end
|
||||
|
||||
def persisted?
|
||||
!!attachment&.persisted?
|
||||
end
|
||||
|
||||
def error?
|
||||
attachment.virus_scanner_error?
|
||||
end
|
||||
|
||||
def error_message
|
||||
case
|
||||
when attachment.virus_scanner.infected?
|
||||
"Virus détecté, merci d’envoyer un autre fichier."
|
||||
when attachment.virus_scanner.corrupt?
|
||||
"Le fichier est corrompu, merci d’envoyer un autre fichier."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def input_id(given_id)
|
||||
return given_id if given_id.present?
|
||||
|
||||
|
@ -77,24 +154,12 @@ class Attachment::EditComponent < ApplicationComponent
|
|||
file_field_name
|
||||
end
|
||||
|
||||
def file_field_name
|
||||
@attached_file.name
|
||||
end
|
||||
def auto_attach_url
|
||||
return @auto_attach_url if @auto_attach_url.present?
|
||||
|
||||
def remove_button_options
|
||||
{
|
||||
role: 'button',
|
||||
class: 'button small danger',
|
||||
data: { turbo_method: :delete }
|
||||
}
|
||||
end
|
||||
return helpers.auto_attach_url(@champ) if @champ.present?
|
||||
|
||||
def retry_button_options
|
||||
{
|
||||
type: 'button',
|
||||
class: 'button attachment-error-retry',
|
||||
data: { input_target: ".#{attachment_input_class}", action: 'autosave#onClickRetryButton' }
|
||||
}
|
||||
fail ArgumentError, "You must pass `auto_attach_url` when not using attachment for a Champ"
|
||||
end
|
||||
|
||||
def file_size_validator
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
.fr-mb-2w.attachment
|
||||
.fr-mb-2w.attachment.fr-upload-group{ id: attachment ? dom_id(attachment, :edit) : nil }
|
||||
- if persisted?
|
||||
.attachment-actions{ id: dom_id(attachment, :actions) }
|
||||
.attachment-action
|
||||
= render Attachment::ShowComponent.new(attachment: attachment, user_can_upload: true)
|
||||
- if user_can_destroy?
|
||||
.attachment-action{ "data-turbo": "true" }
|
||||
= link_to('Supprimer', attachment_path, **remove_button_options)
|
||||
%div{ id: dom_id(attachment, :persisted_row) }
|
||||
.flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) }
|
||||
= link_to('Supprimer', destroy_attachment_path, **remove_button_options, class: "fr-btn fr-btn--tertiary fr-btn--sm fr-icon-delete-line", title: "Supprimer le fichier #{attachment.filename}")
|
||||
|
||||
.attachment-error.hidden
|
||||
.attachment-error-message
|
||||
%p.attachment-error-title
|
||||
Une erreur s’est produite pendant l’envoi du fichier.
|
||||
%p.attachment-error-description
|
||||
Une erreur inconnue s'est produite pendant l'envoi du fichier
|
||||
= button_tag(**retry_button_options) do
|
||||
%span.icon.retry
|
||||
Ré-essayer
|
||||
.fr-py-1v
|
||||
%span.attachment-filename= attachment.filename.to_s
|
||||
- if in_progress?
|
||||
%p.fr-badge.fr-badge--info.fr-badge--sm.fr-badge--no-icon.fr-ml-1w
|
||||
= progress_bar_label
|
||||
|
||||
- if !persisted?
|
||||
%p.fr-text--sm.fr-text-mention--grey
|
||||
= t('.max_file_size', max_file_size: number_to_human_size(max_file_size))
|
||||
- if error?
|
||||
%p.fr-error-text= error_message
|
||||
|
||||
- elsif first?
|
||||
%p.fr-text--sm.fr-text-mention--grey.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
|
||||
Formats supportés :
|
||||
= allowed_formats.join(', ')
|
||||
|
||||
%p= form.file_field(file_field_name, **file_field_options)
|
||||
|
||||
- if !as_multiple?
|
||||
= file_field(champ, file_field_name, **file_field_options)
|
||||
|
||||
- if in_progress?
|
||||
%div{ data: poll_controller_options }
|
||||
- if attachment.created_at < 30.seconds.ago
|
||||
.fr-mt-2w
|
||||
= render partial: "attachments/pending_refresh", url: attachment_path
|
||||
|
||||
.attachment-error.hidden
|
||||
%p.fr-error-text Une erreur s’est produite pendant l’envoi du fichier.
|
||||
= button_tag(**retry_button_options) do
|
||||
Ré-essayer
|
||||
|
||||
|
|
|
@ -22,19 +22,43 @@ class Attachment::MultipleComponent < ApplicationComponent
|
|||
@attachments = attached_file.attachments || []
|
||||
end
|
||||
|
||||
def champ
|
||||
form.object
|
||||
end
|
||||
|
||||
def each_attachment(&block)
|
||||
@attachments.each_with_index(&block)
|
||||
end
|
||||
|
||||
def can_attach_next?
|
||||
return false if @attachments.empty?
|
||||
return false if !@attachments.last.persisted?
|
||||
|
||||
@attachments.count < @max
|
||||
end
|
||||
|
||||
def stimulus_controller_name
|
||||
"attachment-multiple"
|
||||
def empty_component_id
|
||||
"attachment-multiple-empty-#{form.object.id}"
|
||||
end
|
||||
|
||||
def in_progress?
|
||||
@attachments.any? do
|
||||
attachment_in_progress?(_1)
|
||||
end
|
||||
end
|
||||
|
||||
def in_progress_long?
|
||||
@attachments.any? do
|
||||
attachment_in_progress?(_1) && _1.created_at < 30.seconds.ago
|
||||
end
|
||||
end
|
||||
|
||||
def poll_controller_options
|
||||
{
|
||||
controller: 'turbo-poll',
|
||||
turbo_poll_url_value: auto_attach_url
|
||||
}
|
||||
end
|
||||
|
||||
def auto_attach_url
|
||||
helpers.auto_attach_url(form.object)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -42,4 +66,8 @@ class Attachment::MultipleComponent < ApplicationComponent
|
|||
def attachments
|
||||
@attachments
|
||||
end
|
||||
|
||||
def attachment_in_progress?(attachment)
|
||||
attachment.virus_scanner.pending? || attachment.watermark_pending?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
.fr-mb-4w{ data: { controller: stimulus_controller_name }}
|
||||
.fr-mb-4w.attachment-multiple
|
||||
= template
|
||||
|
||||
- each_attachment do |attachment, index|
|
||||
%div{id: dom_id(attachment)}
|
||||
= render Attachment::EditComponent.new(form:, attached_file:, attachment:, user_can_destroy:, direct_upload:, id:, index:)
|
||||
%div{ id: dom_id(attachment) }
|
||||
= render Attachment::EditComponent.new(champ:, attached_file:, attachment:, direct_upload:, id:, index:, as_multiple: true)
|
||||
|
||||
%div{class: [attachments_empty? ? nil : "hidden"], data: { "#{stimulus_controller_name}-target": "empty" }}
|
||||
= render Attachment::EditComponent.new(form:, attached_file:, attachment: nil, user_can_destroy:, direct_upload:, id:, index: attachments_count)
|
||||
|
||||
- if can_attach_next?
|
||||
%button.fr-btn.fr-btn--tertiary.fr-btn--sm{ data: { "#{stimulus_controller_name}-target": "buttonAdd", action: "click->attachment-multiple#add" }} Ajouter un fichier
|
||||
%div{ id: empty_component_id, class: class_names("hidden": !can_attach_next?) }
|
||||
= render Attachment::EditComponent.new(champ:, attached_file:, attachment: nil, direct_upload:, id:, index: attachments_count)
|
||||
|
||||
// single poll and refresh message for all attachments
|
||||
- if in_progress?
|
||||
%div{ data: poll_controller_options }
|
||||
- if in_progress_long?
|
||||
= render "attachments/pending_refresh", url: auto_attach_url
|
||||
|
|
|
@ -1,27 +1,24 @@
|
|||
class Attachment::ShowComponent < ApplicationComponent
|
||||
def initialize(attachment:, user_can_upload: false)
|
||||
def initialize(attachment:)
|
||||
@attachment = attachment
|
||||
@user_can_upload = user_can_upload
|
||||
end
|
||||
|
||||
attr_reader :attachment
|
||||
|
||||
def user_can_upload?
|
||||
@user_can_upload
|
||||
end
|
||||
|
||||
def should_display_link?
|
||||
(attachment.virus_scanner.safe? || !attachment.virus_scanner.started?) && !attachment.watermark_pending?
|
||||
end
|
||||
|
||||
def attachment_path
|
||||
helpers.attachment_path(attachment.id, { signed_id: attachment.blob.signed_id, user_can_upload: user_can_upload? })
|
||||
def error_message
|
||||
case
|
||||
when attachment.virus_scanner.infected?
|
||||
"Virus détecté, le téléchargement est bloqué."
|
||||
when attachment.virus_scanner.corrupt?
|
||||
"Le fichier est corrompu, le téléchargement est bloqué."
|
||||
end
|
||||
end
|
||||
|
||||
def poll_controller_options
|
||||
{
|
||||
controller: 'turbo-poll',
|
||||
turbo_poll_url_value: attachment_path
|
||||
}
|
||||
def error?
|
||||
attachment.virus_scanner_error?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,34 +1,12 @@
|
|||
.attachment-link.fr-download{ id: dom_id(attachment, :show) }
|
||||
%p
|
||||
- if should_display_link?
|
||||
= link_to url_for(attachment.blob), download: "", class: "fr-download__link", title: "Télécharger la pièce jointe" do
|
||||
= attachment.filename.to_s
|
||||
%span.fr-download__detail
|
||||
= helpers.download_details(attachment)
|
||||
|
||||
.attachment-link{ id: dom_id(attachment, :show), class: class_names("attachment-error": error?, "fr-mb-2w": !should_display_link?) }
|
||||
- if should_display_link?
|
||||
= render Dsfr::DownloadComponent.new(attachment: attachment) do |c|
|
||||
- if !attachment.virus_scanner.started?
|
||||
(ce fichier n’a pas été analysé par notre antivirus, téléchargez-le avec précaution)
|
||||
- c.right do
|
||||
(ce fichier n’a pas été analysé par notre antivirus, téléchargez-le avec précaution)
|
||||
|
||||
- else
|
||||
%span{ data: poll_controller_options }
|
||||
= attachment.filename.to_s
|
||||
%span.fr-text-mention--grey
|
||||
- if attachment.virus_scanner.pending?
|
||||
(analyse antivirus en cours
|
||||
= link_to "rafraichir", attachment_path, data: { action: 'turbo-poll#refresh' }
|
||||
)
|
||||
- elsif attachment.watermark_pending?
|
||||
(traitement de la pièce en cours
|
||||
= link_to "rafraichir", attachment_path, data: { action: 'turbo-poll#refresh' }
|
||||
)
|
||||
- elsif attachment.virus_scanner.infected?
|
||||
- if user_can_upload?
|
||||
(virus détecté, merci d’envoyer un autre fichier)
|
||||
- else
|
||||
(virus détecté, le téléchargement de ce fichier est bloqué)
|
||||
- elsif attachment.virus_scanner.corrupt?
|
||||
- if user_can_upload?
|
||||
(le fichier est corrompu, merci d’envoyer un autre fichier)
|
||||
- else
|
||||
(le fichier est corrompu, le téléchargement est bloqué)
|
||||
- else
|
||||
.attachment-filename.fr-mb-1w= attachment.filename.to_s
|
||||
|
||||
- if error?
|
||||
%p.fr-error-text= error_message
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
%div{ class: callout_class }
|
||||
%h3.fr-callout__title= title
|
||||
- if title.present?
|
||||
%h3.fr-callout__title= title
|
||||
%p.fr-callout__text= body
|
||||
= bottom
|
||||
|
|
16
app/components/dsfr/download_component.rb
Normal file
16
app/components/dsfr/download_component.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class Dsfr::DownloadComponent < ApplicationComponent
|
||||
renders_one :right
|
||||
|
||||
attr_reader :attachment
|
||||
attr_reader :html_class
|
||||
attr_reader :name
|
||||
|
||||
def initialize(attachment:, name: nil)
|
||||
@attachment = attachment
|
||||
@name = name || attachment.filename.to_s
|
||||
end
|
||||
|
||||
def title
|
||||
"Télécharger le fichier #{attachment.filename}"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
.fr-download
|
||||
%p
|
||||
= link_to url_for(attachment.blob), download: "", class: "fr-download__link", title: title do
|
||||
= name
|
||||
%span.fr-download__detail
|
||||
= helpers.download_details(attachment)
|
||||
|
||||
= right
|
|
@ -18,7 +18,10 @@ class EditableChamp::EditableChampComponent < ApplicationComponent
|
|||
|
||||
def html_options
|
||||
{
|
||||
class: "editable-champ-#{@champ.type_champ} #{'hidden' if !@champ.visible?}",
|
||||
class: class_names(
|
||||
"editable-champ-#{@champ.type_champ}": true,
|
||||
"hidden": !@champ.visible?
|
||||
),
|
||||
id: @champ.input_group_id,
|
||||
data: { controller: stimulus_controller, block: @champ.block? }
|
||||
}
|
||||
|
|
|
@ -3,13 +3,10 @@
|
|||
= render Attachment::MultipleComponent.new(form: @form, attached_file: @champ.piece_justificative_file, user_can_destroy:, max:) do |c|
|
||||
- if @champ.type_de_champ.piece_justificative_template&.attached?
|
||||
- c.with_template do
|
||||
%p
|
||||
Veuillez télécharger, remplir et joindre
|
||||
= link_to(url_for(@champ.type_de_champ.piece_justificative_template), download: "", class: "fr-link fr-link--icon-right fr-icon-download-line") do
|
||||
le modèle suivant
|
||||
|
||||
= render Dsfr::DownloadComponent.new(attachment: @champ.type_de_champ.piece_justificative_template, name: "Modèle à télécharger") do |c|
|
||||
- if helpers.administrateur_signed_in?
|
||||
%span.fr-ml-2w.fr-text--xs.fr-text-mention--grey.visible-on-previous-hover
|
||||
%span.fr-text-action-high--blue-france.fr-icon-questionnaire-line{ "aria-hidden": "true" }
|
||||
= t('shared.ephemeral_link')
|
||||
- c.with_right do
|
||||
%span.fr-ml-2w.fr-text--xs.fr-text-mention--grey.visible-on-previous-hover
|
||||
%span.fr-text-action-high--blue-france.fr-icon-questionnaire-line{ "aria-hidden": "true" }
|
||||
= t('shared.ephemeral_link')
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
- user_can_destroy = !@champ.mandatory? || @champ.dossier.brouillon?
|
||||
= render Attachment::EditComponent.new(form: @form, attached_file: @champ.piece_justificative_file, attachment: @champ.piece_justificative_file[0], user_can_destroy: user_can_destroy)
|
||||
= render Attachment::EditComponent.new(champ: @form.object, attached_file: @champ.piece_justificative_file, attachment: @champ.piece_justificative_file[0])
|
||||
|
|
|
@ -74,11 +74,10 @@ class TypesDeChampEditor::ChampComponent < ApplicationComponent
|
|||
end
|
||||
end
|
||||
|
||||
def piece_justificative_options(form)
|
||||
def piece_justificative_template_options
|
||||
{
|
||||
form: form,
|
||||
attached_file: type_de_champ.piece_justificative_template,
|
||||
user_can_destroy: true,
|
||||
auto_attach_url: helpers.auto_attach_url(type_de_champ),
|
||||
id: dom_id(type_de_champ, :piece_justificative_template)
|
||||
}
|
||||
end
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
- if type_de_champ.piece_justificative?
|
||||
.cell
|
||||
= form.label :piece_justificative_template, "Modèle", for: dom_id(type_de_champ, :piece_justificative_template)
|
||||
= render Attachment::EditComponent.new(**piece_justificative_options(form))
|
||||
= render Attachment::EditComponent.new(**piece_justificative_template_options)
|
||||
|
||||
- if type_de_champ.titre_identite?
|
||||
%p Dans le cadre de la RGPD, le titre d’identité sera supprimé lors de l’acceptation du dossier
|
||||
|
|
|
@ -375,4 +375,8 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cast_bool(value)
|
||||
ActiveRecord::Type::Boolean.new.deserialize(value)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,9 @@ class AttachmentsController < ApplicationController
|
|||
|
||||
def show
|
||||
@attachment = @blob.attachments.find(params[:id])
|
||||
@user_can_upload = params[:user_can_upload]
|
||||
|
||||
@user_can_edit = cast_bool(params[:user_can_edit])
|
||||
@auto_attach_url = params[:auto_attach_url]
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
|
@ -17,6 +19,8 @@ class AttachmentsController < ApplicationController
|
|||
@attachment.purge_later
|
||||
flash.notice = 'La pièce jointe a bien été supprimée.'
|
||||
|
||||
@champ_id = params[:champ_id]
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html { redirect_back(fallback_location: root_url) }
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
class Champs::PieceJustificativeController < ApplicationController
|
||||
before_action :authenticate_logged_user!
|
||||
before_action :set_champ
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.turbo_stream
|
||||
format.html { redirect_back(fallback_location: root_url) }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if attach_piece_justificative_or_retry
|
||||
|
@ -11,9 +19,11 @@ class Champs::PieceJustificativeController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def attach_piece_justificative
|
||||
def set_champ
|
||||
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||
end
|
||||
|
||||
def attach_piece_justificative
|
||||
@champ.piece_justificative_file.attach(params[:blob_signed_id])
|
||||
save_succeed = @champ.save
|
||||
@champ.dossier.update(last_champ_updated_at: Time.zone.now.utc) if save_succeed
|
||||
|
|
|
@ -9,7 +9,7 @@ module ChampHelper
|
|||
|
||||
def auto_attach_url(object)
|
||||
if object.is_a?(Champ)
|
||||
champs_piece_justificative_url(object.id)
|
||||
champs_attach_piece_justificative_url(object.id)
|
||||
elsif object.is_a?(TypeDeChamp)
|
||||
piece_justificative_template_admin_procedure_type_de_champ_url(stable_id: object.stable_id, procedure_id: object.procedure.id)
|
||||
end
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import {} from '@hotwired/stimulus';
|
||||
import { show, hide } from '~/shared/utils';
|
||||
import { ApplicationController } from './application_controller';
|
||||
|
||||
type AttachementDestroyedEvent = CustomEvent<{ target_id: string }>;
|
||||
|
||||
export class AttachmentMultipleController extends ApplicationController {
|
||||
static targets = ['buttonAdd', 'empty'];
|
||||
|
||||
declare readonly emptyTarget: HTMLDivElement;
|
||||
declare readonly buttonAddTarget: HTMLButtonElement;
|
||||
|
||||
connect() {
|
||||
this.onGlobal('attachment:destroyed', (event: AttachementDestroyedEvent) =>
|
||||
this.onAttachmentDestroy(event)
|
||||
);
|
||||
}
|
||||
|
||||
add(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
hide(this.buttonAddTarget);
|
||||
|
||||
show(this.emptyTarget);
|
||||
|
||||
const inputFile = this.emptyTarget.querySelector(
|
||||
'input[type=file]'
|
||||
) as HTMLInputElement;
|
||||
|
||||
inputFile.click();
|
||||
}
|
||||
|
||||
onAttachmentDestroy(event: AttachementDestroyedEvent) {
|
||||
const { detail } = event;
|
||||
|
||||
const attachmentWrapper = document.getElementById(detail.target_id);
|
||||
|
||||
// Remove this attachment row when there is at least another attachment.
|
||||
if (attachmentWrapper && this.attachmentsCount() > 1) {
|
||||
attachmentWrapper.parentNode?.removeChild(attachmentWrapper);
|
||||
} else {
|
||||
hide(this.buttonAddTarget);
|
||||
}
|
||||
}
|
||||
|
||||
attachmentsCount() {
|
||||
// Don't count the hidden "empty" attachment
|
||||
return this.element.querySelectorAll('.attachment-input').length - 1;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import {
|
|||
|
||||
type ErrorMessage = {
|
||||
title: string;
|
||||
description: string;
|
||||
retry: boolean;
|
||||
};
|
||||
|
||||
|
@ -69,6 +68,8 @@ export class AutoUpload {
|
|||
|
||||
const message = this.messageFromError(error);
|
||||
this.displayErrorMessage(message);
|
||||
|
||||
this.#input.classList.toggle('fr-text-default--error', true);
|
||||
}
|
||||
|
||||
private done() {
|
||||
|
@ -81,20 +82,19 @@ export class AutoUpload {
|
|||
|
||||
if (error.failureReason == FAILURE_CONNECTIVITY) {
|
||||
return {
|
||||
title: 'Le fichier n’a pas pu être envoyé.',
|
||||
description: 'Vérifiez votre connexion à Internet, puis ré-essayez.',
|
||||
title:
|
||||
'Le fichier n’a pas pu être envoyé. Vérifiez votre connexion à Internet, puis ré-essayez.',
|
||||
retry: true
|
||||
};
|
||||
} else if (error.code == ERROR_CODE_READ) {
|
||||
return {
|
||||
title: 'Nous n’arrivons pas à lire ce fichier sur votre appareil.',
|
||||
description: 'Essayez à nouveau, ou sélectionnez un autre fichier.',
|
||||
title:
|
||||
'Nous n’arrivons pas à lire ce fichier sur votre appareil. Essayez à nouveau, ou sélectionnez un autre fichier.',
|
||||
retry: false
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
title: 'Le fichier n’a pas pu être envoyé.',
|
||||
description: message,
|
||||
title: message,
|
||||
retry: !!canRetry
|
||||
};
|
||||
}
|
||||
|
@ -105,7 +105,6 @@ export class AutoUpload {
|
|||
if (errorElement) {
|
||||
show(errorElement);
|
||||
this.errorTitleElement.textContent = message.title || '';
|
||||
this.errorDescriptionElement.textContent = message.description || '';
|
||||
toggle(this.errorRetryButton, message.retry);
|
||||
}
|
||||
}
|
||||
|
@ -124,21 +123,12 @@ export class AutoUpload {
|
|||
}
|
||||
|
||||
get errorTitleElement() {
|
||||
const element = this.errorElement?.querySelector<HTMLElement>(
|
||||
'.attachment-error-title'
|
||||
);
|
||||
const element =
|
||||
this.errorElement?.querySelector<HTMLElement>('.fr-error-text');
|
||||
invariant(element, 'Could not find the error title element.');
|
||||
return element;
|
||||
}
|
||||
|
||||
get errorDescriptionElement() {
|
||||
const element = this.errorElement?.querySelector<HTMLElement>(
|
||||
'.attachment-error-description'
|
||||
);
|
||||
invariant(element, 'Could not find the error description element.');
|
||||
return element;
|
||||
}
|
||||
|
||||
get errorRetryButton() {
|
||||
const element = this.errorElement?.querySelector<HTMLButtonElement>(
|
||||
'.attachment-error-retry'
|
||||
|
|
|
@ -19,6 +19,7 @@ class VirusScannerJob < ApplicationJob
|
|||
if blob.virus_scanner.done? then return end
|
||||
|
||||
metadata = extract_metadata_via_virus_scanner(blob)
|
||||
|
||||
VirusScannerJob.merge_and_update_metadata(blob, metadata)
|
||||
end
|
||||
|
||||
|
|
|
@ -13,6 +13,13 @@ module BlobVirusScannerConcern
|
|||
VirusScannerJob.perform_later(self)
|
||||
end
|
||||
|
||||
def virus_scanner_error?
|
||||
return true if virus_scanner.infected?
|
||||
return true if virus_scanner.corrupt?
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pending
|
||||
|
|
6
app/views/attachments/_pending_refresh.html.haml
Normal file
6
app/views/attachments/_pending_refresh.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
= render Dsfr::CalloutComponent.new(title: nil) do |c|
|
||||
- c.with_body do
|
||||
L’analyse antivirus de votre ou de vos pièces jointes prend plus de temps que prévu.
|
||||
- c.with_bottom do
|
||||
= button_tag "Rafraîchir le traitement", type: "button", class: "fr-btn", data: { action: 'click->turbo-poll#refresh' }
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
= turbo_stream.remove dom_id(@attachment, :actions)
|
||||
= turbo_stream.dispatch "attachment:destroyed", { target_id: dom_id(@attachment) }
|
||||
= turbo_stream.remove dom_id(@attachment, :persisted_row)
|
||||
|
||||
- if @champ_id
|
||||
= turbo_stream.show "attachment-multiple-empty-#{@champ_id}"
|
||||
|
||||
= turbo_stream.show_all ".attachment-input-#{@attachment.id}"
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
= turbo_stream.replace dom_id(@attachment, :show) do
|
||||
= render Attachment::ShowComponent.new(attachment: @attachment, user_can_upload: @user_can_upload)
|
||||
= turbo_stream.replace dom_id(@attachment, :edit) do
|
||||
- if @user_can_edit
|
||||
= render Attachment::EditComponent.new(attachment: @attachment, attached_file: @attachment.record.public_send(@attachment.name), auto_attach_url: @auto_attach_url)
|
||||
- else
|
||||
= render Attachment::ShowComponent.new(attachment: @attachment)
|
||||
|
|
|
@ -2,6 +2,5 @@
|
|||
= turbo_stream.morph @champ.input_group_id do
|
||||
= render EditableChamp::EditableChampComponent.new champ: @champ, form: form
|
||||
|
||||
|
||||
- @champ.piece_justificative_file.attachments.each do |attachment|
|
||||
= turbo_stream.focus_all "button[data-toggle-target=\".attachment-input-#{attachment.id}\"]"
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
= form_for @avis, url: instructeur_avis_path(@avis.procedure, @avis), html: { class: 'form' } do |f|
|
||||
= f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: @avis.piece_justificative_file, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(object: f.object, attached_file: @avis.piece_justificative_file, user_can_destroy: true)
|
||||
|
||||
.flex.justify-between.align-baseline
|
||||
%p.confidentiel.flex
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true
|
||||
%p.tab-title Ajouter une pièce jointe
|
||||
.form-group
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: avis.introduction_file, user_can_destroy: true)
|
||||
= render Attachment::EditComponent.new(object: f.object, attached_file: avis.introduction_file, user_can_destroy: true)
|
||||
|
||||
- if linked_dossiers.present?
|
||||
= f.check_box :invite_linked_dossiers, {}, true, false
|
||||
|
|
|
@ -348,3 +348,39 @@
|
|||
Des marges verticales ont ici été rajoutées.
|
||||
|
||||
|
||||
|
||||
.container
|
||||
%h1.fr-mt-4w Attachment::EditComponent
|
||||
%span.fr-hint-text Note: direct upload, suppression ne marchent pas comme attendu ici.
|
||||
|
||||
- champ = @dossier.champs_public.first
|
||||
- tdc = @dossier.champs_public.find { _1.type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) }.type_de_champ
|
||||
|
||||
- if attachment = ActiveStorage::Attachment.last
|
||||
- attachment.update(created_at: 1.second.ago)
|
||||
%h3 New attachment
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: champ.piece_justificative_file, attachment: nil)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: champ.piece_justificative_file, attachment:)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment, antivirus in progress
|
||||
- attachment.blob.metadata[:virus_scan_result] = ActiveStorage::VirusScanner::PENDING
|
||||
- attachment.created_at = Time.zone.now
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: Champ.new.piece_justificative_file, attachment:)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment, antivirus in progress since long time
|
||||
- attachment.blob.metadata[:virus_scan_result] = ActiveStorage::VirusScanner::PENDING
|
||||
- attachment.created_at = 2.minutes.ago
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: Champ.new.piece_justificative_file, attachment:)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment, error
|
||||
- attachment.blob.metadata[:virus_scan_result] = ActiveStorage::VirusScanner::INFECTED
|
||||
= render Attachment::EditComponent.new(champ:, attached_file: Champ.new.piece_justificative_file, attachment:)
|
||||
|
||||
%h3.fr-mt-4w New attachment not on Champ
|
||||
= render Attachment::EditComponent.new(auto_attach_url: "/some-auto-attach-path", attached_file: tdc.piece_justificative_template, attachment: nil)
|
||||
|
||||
%h3.fr-mt-4w Existing attachment not on Champ
|
||||
= render Attachment::EditComponent.new(auto_attach_url: "/some-auto-attach-path", attached_file: tdc.piece_justificative_template, attachment: attachment.reload)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
- disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false
|
||||
%div
|
||||
- if !disable_piece_jointe
|
||||
= render Attachment::EditComponent.new(form: f, attached_file: commentaire.piece_jointe)
|
||||
= render Attachment::EditComponent.new(object: f.object, attached_file: commentaire.piece_jointe)
|
||||
|
||||
%div
|
||||
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'button primary send', data: { disable: true }
|
||||
|
|
|
@ -166,7 +166,8 @@ Rails.application.routes.draw do
|
|||
patch ':champ_id/carte/features/:id', to: 'carte#update'
|
||||
delete ':champ_id/carte/features/:id', to: 'carte#destroy'
|
||||
|
||||
put ':champ_id/piece_justificative', to: 'piece_justificative#update', as: :piece_justificative
|
||||
get ':champ_id/piece_justificative', to: 'piece_justificative#show', as: :piece_justificative
|
||||
put ':champ_id/piece_justificative', to: 'piece_justificative#update', as: :attach_piece_justificative
|
||||
end
|
||||
|
||||
resources :attachments, only: [:show, :destroy]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
describe EditableChamp::PieceJustificativeComponent, type: :component do
|
||||
let(:champ) { build(:champ_piece_justificative, dossier: create(:dossier)) }
|
||||
let(:champ) { create(:champ_piece_justificative, dossier: create(:dossier)) }
|
||||
let(:component) {
|
||||
described_class.new(form: instance_double(ActionView::Helpers::FormBuilder, object: champ.dossier, file_field: "<input type=\"file\" />"), champ:)
|
||||
described_class.new(form: instance_double(ActionView::Helpers::FormBuilder), champ:)
|
||||
}
|
||||
|
||||
let(:subject) {
|
||||
|
@ -17,14 +17,14 @@ describe EditableChamp::PieceJustificativeComponent, type: :component do
|
|||
end
|
||||
|
||||
it 'renders a link to template' do
|
||||
expect(subject).to have_link('le modèle suivant')
|
||||
expect(subject).to have_link('Modèle à télécharger')
|
||||
expect(subject).not_to have_text("éphémère")
|
||||
end
|
||||
|
||||
context 'as an administrator' do
|
||||
let(:profil) { :administrateur }
|
||||
it 'warn about ephemeral template url' do
|
||||
expect(subject).to have_link('le modèle suivant')
|
||||
expect(subject).to have_link('Modèle à télécharger')
|
||||
expect(subject).to have_text("éphémère")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -93,7 +93,7 @@ describe 'The user' do
|
|||
check_selected_value('communes', with: 'Ambléon (01300)')
|
||||
expect(page).to have_field('dossier_link', with: '123')
|
||||
expect(page).to have_text('file.pdf')
|
||||
expect(page).to have_text('analyse antivirus en cours')
|
||||
expect(page).to have_text('Analyse antivirus en cours')
|
||||
end
|
||||
|
||||
let(:procedure_with_repetition) do
|
||||
|
@ -184,7 +184,7 @@ describe 'The user' do
|
|||
find_field('Pièce justificative 2').attach_file(Rails.root + 'spec/fixtures/files/RIB.pdf')
|
||||
|
||||
# Expect the files to be uploaded immediately
|
||||
expect(page).to have_text('analyse antivirus en cours', count: 2, wait: 5)
|
||||
expect(page).to have_text('Analyse antivirus en cours', count: 2, wait: 5)
|
||||
expect(page).to have_text('file.pdf')
|
||||
expect(page).to have_text('RIB.pdf')
|
||||
|
||||
|
@ -206,7 +206,7 @@ describe 'The user' do
|
|||
# Test invalid file type
|
||||
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/invalid_file_format.json')
|
||||
expect(page).to have_no_text('La pièce justificative n’est pas d’un type accepté')
|
||||
expect(page).to have_text('analyse antivirus en cours', count: 1, wait: 5)
|
||||
expect(page).to have_text('Analyse antivirus en cours', count: 1, wait: 5)
|
||||
end
|
||||
|
||||
scenario 'retry on transcient upload error', js: true do
|
||||
|
@ -216,10 +216,10 @@ describe 'The user' do
|
|||
# Test auto-upload failure
|
||||
# Make the subsequent auto-upload request fail
|
||||
allow_any_instance_of(Champs::PieceJustificativeController).to receive(:update) do |instance|
|
||||
instance.render json: { errors: ['Error'] }, status: :bad_request
|
||||
instance.render json: { errors: ["Une erreur est survenue"] }, status: :bad_request
|
||||
end
|
||||
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/file.pdf')
|
||||
expect(page).to have_css('p', text: 'Le fichier n’a pas pu être envoyé', visible: :visible, wait: 5)
|
||||
expect(page).to have_css('p', text: "Une erreur est survenue", visible: :visible, wait: 5)
|
||||
expect(page).to have_button('Ré-essayer', visible: true)
|
||||
expect(page).to have_button('Déposer le dossier', disabled: false)
|
||||
|
||||
|
@ -227,7 +227,7 @@ describe 'The user' do
|
|||
|
||||
# Test that retrying after a failure works
|
||||
click_on('Ré-essayer', visible: true, wait: 5)
|
||||
expect(page).to have_text('analyse antivirus en cours', wait: 5)
|
||||
expect(page).to have_text('Analyse antivirus en cours', wait: 5)
|
||||
expect(page).to have_text('file.pdf')
|
||||
expect(page).to have_button('Déposer le dossier', disabled: false)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ describe 'shared/attachment/_show.html.haml', type: :view do
|
|||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(champ.piece_justificative_file[0].filename.to_s)
|
||||
expect(subject).not_to have_link(champ.piece_justificative_file[0].filename.to_s)
|
||||
expect(subject).to have_text('analyse antivirus en cours')
|
||||
expect(subject).to have_text('Analyse antivirus en cours')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,7 +34,7 @@ describe 'shared/attachment/_show.html.haml', type: :view do
|
|||
expect(champ.piece_justificative_file.attachments[0].watermark_pending?).to be_truthy
|
||||
expect(subject).to have_text(champ.piece_justificative_file[0].filename.to_s)
|
||||
expect(subject).not_to have_link(champ.piece_justificative_file[0].filename.to_s)
|
||||
expect(subject).to have_text('traitement de la pièce en cours')
|
||||
expect(subject).to have_text('Traitement en cours')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -52,7 +52,7 @@ describe 'shared/attachment/_show.html.haml', type: :view do
|
|||
it 'displays the filename, but doesn’t allow to download the file' do
|
||||
expect(subject).to have_text(champ.piece_justificative_file[0].filename.to_s)
|
||||
expect(subject).not_to have_link(champ.piece_justificative_file[0].filename.to_s)
|
||||
expect(subject).to have_text('virus détecté')
|
||||
expect(subject).to have_text('Virus détecté')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue