refactor(piece_justificative): UX follows mockups

This commit is contained in:
Colin Darie 2022-11-09 12:33:20 +01:00
parent 84ca01bdf7
commit b13c5e56f6
39 changed files with 373 additions and 272 deletions

View file

@ -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;
}

View file

@ -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
}

View file

@ -386,6 +386,10 @@
}
}
.editable-champ-titre_identite { // scss-lint:disable SelectorFormat
margin-bottom: 2 * $default-padding;
}
.cnaf-inputs,
.dgfip-inputs,
.pole-emploi-inputs,

View file

@ -1,6 +1,6 @@
@import "constants";
.rich-text:not(.piece_justificative) {
.rich-text:not(.piece_justificative):not(.titre_identite) {
i {
font-style: italic;
}

View file

@ -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

View file

@ -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 denvoyer un autre fichier."
when attachment.virus_scanner.corrupt?
"Le fichier est corrompu, merci denvoyer 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

View file

@ -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 sest produite pendant lenvoi 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 sest produite pendant lenvoi du fichier.
= button_tag(**retry_button_options) do
Ré-essayer

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 na pas été analysé par notre antivirus, téléchargez-le avec précaution)
- c.right do
(ce fichier na 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 denvoyer 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 denvoyer 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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? }
}

View file

@ -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')

View file

@ -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])

View file

@ -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

View file

@ -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 didentité sera supprimé lors de lacceptation du dossier

View file

@ -375,4 +375,8 @@ class ApplicationController < ActionController::Base
end
end
end
def cast_bool(value)
ActiveRecord::Type::Boolean.new.deserialize(value)
end
end

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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;
}
}

View file

@ -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 na pas pu être envoyé.',
description: 'Vérifiez votre connexion à Internet, puis ré-essayez.',
title:
'Le fichier na pas pu être envoyé. Vérifiez votre connexion à Internet, puis ré-essayez.',
retry: true
};
} else if (error.code == ERROR_CODE_READ) {
return {
title: 'Nous narrivons pas à lire ce fichier sur votre appareil.',
description: 'Essayez à nouveau, ou sélectionnez un autre fichier.',
title:
'Nous narrivons pas à lire ce fichier sur votre appareil. Essayez à nouveau, ou sélectionnez un autre fichier.',
retry: false
};
} else {
return {
title: 'Le fichier na 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'

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,6 @@
= render Dsfr::CalloutComponent.new(title: nil) do |c|
- c.with_body do
Lanalyse 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' }

View file

@ -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}"

View file

@ -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)

View file

@ -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}\"]"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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 }

View file

@ -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]

View file

@ -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

View file

@ -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 nest pas dun 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 na 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)

View file

@ -23,7 +23,7 @@ describe 'shared/attachment/_show.html.haml', type: :view do
it 'displays the filename, but doesnt 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 doesnt 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