Merge pull request #7266 from tchak/refactor-poll-controller

turbo poll controller
This commit is contained in:
Paul Chavard 2022-05-17 16:25:36 +02:00 committed by GitHub
commit 185f2acdea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 421 additions and 217 deletions

View file

@ -0,0 +1,92 @@
# Display a widget for uploading, editing and deleting a file attachment
class Attachment::EditComponent < ApplicationComponent
def initialize(form:, attached_file:, accept: nil, template: nil, user_can_destroy: false, direct_upload: true)
@form = form
@attached_file = attached_file
@accept = accept
@template = template
@user_can_destroy = user_can_destroy
@direct_upload = direct_upload
end
attr_reader :template, :form
def self.text(form, file)
new(form: form, attached_file: file, user_can_destroy: true)
end
def self.image(form, file, direct_upload = true)
new(form: form,
attached_file: file,
accept: 'image/png, image/jpg, image/jpeg',
user_can_destroy: true,
direct_upload: direct_upload)
end
def user_can_destroy?
@user_can_destroy
end
def attachment
@attached_file.attachment
end
def attachment_path
helpers.attachment_path attachment.id, { signed_id: attachment.blob.signed_id }
end
def attachment_id
@attachment_id ||= attachment ? attachment.id : SecureRandom.uuid
end
def attachment_input_class
"attachment-input-#{attachment_id}"
end
def persisted?
attachment&.persisted?
end
def champ
@form.object.is_a?(Champ) ? @form.object : nil
end
def file_field_options
{
class: "attachment-input #{attachment_input_class} #{'hidden' if persisted?}",
accept: @accept,
direct_upload: @direct_upload,
id: champ&.input_id,
aria: { describedby: champ&.describedby_id },
data: { auto_attach_url: helpers.auto_attach_url(form, form.object) }
}
end
def file_field_name
@attached_file.name
end
def remove_button_options
{
role: 'button',
class: 'button small danger',
data: { turbo_method: :delete }
}
end
def retry_button_options
{
type: 'button',
class: 'button attachment-error-retry',
data: { input_target: ".#{attachment_input_class}", action: 'autosave#onClickRetryButton' }
}
end
def replace_button_options
{
type: 'button',
class: 'button small',
data: { toggle_target: ".#{attachment_input_class}" }
}
end
end

View file

@ -0,0 +1,2 @@
---
en:

View file

@ -0,0 +1,2 @@
---
fr:

View file

@ -0,0 +1,27 @@
.attachment
- if template&.attached?
%p.mb-1
Veuillez télécharger, remplir et joindre
= link_to('le modèle suivant', url_for(template), target: '_blank', rel: 'noopener')
- 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)
.attachment-action
= button_tag('Remplacer', **replace_button_options)
.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
= form.file_field(file_field_name, **file_field_options)

View file

@ -0,0 +1,27 @@
class Attachment::ShowComponent < ApplicationComponent
def initialize(attachment:, user_can_upload: false)
@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? })
end
def poll_controller_options
{
controller: 'turbo-poll',
turbo_poll_url_value: attachment_path
}
end
end

View file

@ -0,0 +1,2 @@
---
en:

View file

@ -0,0 +1,2 @@
---
fr:

View file

@ -1,12 +1,5 @@
- should_display_link = (attachment.virus_scanner.safe? || !attachment.virus_scanner.started?) && !attachment.watermark_pending?
- user_can_upload = defined?(user_can_upload) ? user_can_upload : false
- if should_display_link
- attachment_check_url = false
- else
- attachment_check_url = attachment_url(attachment.id, { signed_id: attachment.blob.signed_id, user_can_upload: user_can_upload })
.attachment-link{ 'data-attachment-id': attachment.id, 'data-attachment-poll-url': attachment_check_url }
- if should_display_link
.attachment-link{ id: dom_id(attachment, :show) }
- if should_display_link?
= link_to url_for(attachment.blob), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do
%span.icon.attached
= attachment.filename.to_s
@ -14,22 +7,23 @@
(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
- if attachment.virus_scanner.pending?
(analyse antivirus en cours
= link_to "rafraichir", request.path, data: { 'attachment-refresh': true }
= link_to "rafraichir", attachment_path, data: { action: 'turbo-poll#refresh' }
)
- elsif attachment.watermark_pending?
(traitement de la pièce en cours
= link_to "rafraichir", request.path, data: { 'attachment-refresh': true }
= link_to "rafraichir", attachment_path, data: { action: 'turbo-poll#refresh' }
)
- elsif attachment.virus_scanner.infected?
- if user_can_upload
- 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
- if user_can_upload?
(le fichier est corrompu, merci denvoyer un autre fichier)
- else
(le fichier est corrompu, le téléchargement est bloqué)

View file

@ -0,0 +1,49 @@
class Dossiers::ExportComponent < ApplicationComponent
def initialize(procedure:, exports:, statut:, count:)
@procedure = procedure
@exports = exports
@statut = statut
@count = count
end
def exports
helpers.exports_list(@exports, @statut)
end
def download_export_path(export_format:, force_export: false, no_progress_notification: nil)
download_export_instructeur_procedure_path(@procedure,
export_format: export_format,
statut: @statut,
force_export: force_export,
no_progress_notification: no_progress_notification)
end
def refresh_button_options(export)
{
title: t(".everything_short", export_format: ".#{export.format}"),
class: "button small",
style: "padding-right: 2px"
}
end
def ready_link_label(export)
t(".everything_ready_html",
export_time: helpers.time_ago_in_words(export.updated_at),
export_format: ".#{export.format}")
end
def pending_label(export)
t(".everything_pending_html",
export_time: time_ago_in_words(export.created_at),
export_format: ".#{export.format}")
end
def poll_controller_options(export)
{
controller: 'turbo-poll',
turbo_poll_url_value: download_export_path(export_format: export.format, no_progress_notification: true),
turbo_poll_interval_value: 6000,
turbo_poll_max_checks_value: 10
}
end
end

View file

@ -0,0 +1,12 @@
---
en:
everything_csv_html: Ask an export in format .csv<br>(only folders, without repeatable fields)
everything_xlsx_html: Ask an export in format .xlsx
everything_ods_html: Ask an export in format .ods
everything_zip_html: Ask an export in format .zip
everything_short: Ask an export in format%{export_format}
everything_pending_html: Ask an export in format %{export_format} is being generated<br>(ask %{export_time} ago)
everything_ready_html: Download the export in format %{export_format}<br>(generated %{export_time} ago)
download:
one: Download a file
other: Download %{count} files

View file

@ -0,0 +1,12 @@
---
fr:
everything_csv_html: Demander un export au format .csv<br>(uniquement les dossiers, sans les champs répétables)
everything_xlsx_html: Demander un export au format .xlsx
everything_ods_html: Demander un export au format .ods
everything_zip_html: Demander un export au format .zip
everything_short: Demander un export au format %{export_format}
everything_pending_html: Un export au format %{export_format} est en train dêtre généré<br>(demandé il y a %{export_time})
everything_ready_html: Télécharger lexport au format %{export_format}<br>(généré il y a %{export_time})
download:
one: Télécharger un dossier
other: Télécharger %{count} dossiers

View file

@ -0,0 +1,22 @@
%span.dropdown{ data: { controller: 'menu-button' } }
%button.button.dropdown-button{ data: { menu_button_target: 'button' } }
= t(".download", count: @count)
#download-menu.dropdown-content.fade-in-down{ style: 'width: 450px', data: { menu_button_target: 'menu' } }
%ul.dropdown-items{ 'data-turbo': 'true' }
- exports.each do |item|
- export = item[:export]
%li
- if export.nil?
// i18n-tasks-use t('.everything_csv_html')
// i18n-tasks-use t('.everything_xlsx_html')
// i18n-tasks-use t('.everything_ods_html')
// i18n-tasks-use t('.everything_zip_html')
= link_to t(".everything_#{item[:format]}_html"), download_export_path(export_format: item[:format]), data: { turbo_method: :post }
- elsif export.ready?
= link_to ready_link_label(export), export.file.service_url, target: "_blank", rel: "noopener"
- if export.old?
= button_to download_export_path(export_format: export.format, force_export: true), **refresh_button_options(export) do
.icon.retry
- else
%span{ data: poll_controller_options(export) }
= pending_label(export)

View file

@ -18,7 +18,7 @@
- if commentaire.piece_jointe.attached?
.attachment-link
= render partial: "shared/attachment/show", locals: { attachment: commentaire.piece_jointe.attachment }
= render Attachment::ShowComponent.new(attachment: commentaire.piece_jointe.attachment)
- if show_reply_button?
= button_tag type: 'button', class: 'button small message-answer-button', onclick: 'document.querySelector("#commentaire_body").focus()' do

View file

@ -7,15 +7,19 @@ class AttachmentsController < ApplicationController
@user_can_upload = params[:user_can_upload]
respond_to do |format|
format.js
format.turbo_stream
format.html { redirect_back(fallback_location: root_url) }
end
end
def destroy
attachment = @blob.attachments.find(params[:id])
@attachment_id = attachment.id
attachment.purge_later
flash.now.notice = 'La pièce jointe a bien été supprimée.'
@attachment = @blob.attachments.find(params[:id])
@attachment.purge_later
flash.notice = 'La pièce jointe a bien été supprimée.'
respond_to do |format|
format.turbo_stream
format.html { redirect_back(fallback_location: root_url) }
end
end
end

View file

@ -156,7 +156,7 @@ module Instructeurs
if export.ready?
respond_to do |format|
format.js do
format.turbo_stream do
@procedure = procedure
@statut = export_options[:statut]
@dossiers_count = export.count
@ -172,7 +172,7 @@ module Instructeurs
respond_to do |format|
notice_message = "Nous générons cet export. Veuillez revenir dans quelques minutes pour le télécharger."
format.js do
format.turbo_stream do
@procedure = procedure
@statut = export_options[:statut]
@dossiers_count = export.count

View file

@ -1,19 +0,0 @@
module AttachmentUploadHelper
def image_upload_and_render(form, file, direct_upload = nil)
render 'shared/attachment/edit', {
form: form,
attached_file: file,
accept: 'image/png, image/jpg, image/jpeg',
user_can_destroy: true,
direct_upload: direct_upload
}
end
def text_upload_and_render(form, file)
render 'shared/attachment/edit', {
form: form,
attached_file: file,
user_can_destroy: true
}
end
end

View file

@ -1,18 +1,20 @@
import { Application } from '@hotwired/stimulus';
import { ReactController } from './react_controller';
import { TurboEventController } from './turbo_event_controller';
import { GeoAreaController } from './geo_area_controller';
import { TurboInputController } from './turbo_input_controller';
import { AutosaveController } from './autosave_controller';
import { AutosaveStatusController } from './autosave_status_controller';
import { GeoAreaController } from './geo_area_controller';
import { MenuButtonController } from './menu_button_controller';
import { ReactController } from './react_controller';
import { TurboEventController } from './turbo_event_controller';
import { TurboInputController } from './turbo_input_controller';
import { TurboPollController } from './turbo_poll_controller';
const Stimulus = Application.start();
Stimulus.register('autosave-status', AutosaveStatusController);
Stimulus.register('autosave', AutosaveController);
Stimulus.register('geo-area', GeoAreaController);
Stimulus.register('menu-button', MenuButtonController);
Stimulus.register('react', ReactController);
Stimulus.register('turbo-event', TurboEventController);
Stimulus.register('geo-area', GeoAreaController);
Stimulus.register('turbo-input', TurboInputController);
Stimulus.register('autosave', AutosaveController);
Stimulus.register('autosave-status', AutosaveStatusController);
Stimulus.register('menu-button', MenuButtonController);
Stimulus.register('turbo-poll', TurboPollController);

View file

@ -0,0 +1,94 @@
import { httpRequest } from '@utils';
import { ApplicationController } from './application_controller';
const DEFAULT_POLL_INTERVAL = 3000;
const DEFAULT_MAX_CHECKS = 5;
// Periodically check the state of a URL.
//
// Each time the given URL is requested, a turbo-stream is rendered, causing the state to be refreshed.
//
// This is used mainly to refresh attachments during the anti-virus check,
// but also to refresh the state of a pending spreadsheet export.
export class TurboPollController extends ApplicationController {
static values = {
url: String,
maxChecks: { type: Number, default: DEFAULT_MAX_CHECKS },
interval: { type: Number, default: DEFAULT_POLL_INTERVAL }
};
declare readonly urlValue: string;
declare readonly intervalValue: number;
declare readonly maxChecksValue: number;
#timer?: number;
#abortController?: AbortController;
connect(): void {
const state = this.nextState();
if (state) {
this.schedule(state);
}
}
disconnect(): void {
this.cancel();
}
refresh() {
this.cancel();
this.#abortController = new AbortController();
httpRequest(this.urlValue, { signal: this.#abortController.signal })
.turbo()
.catch(() => null);
}
private schedule(state: PollState): void {
this.cancel();
this.#timer = setTimeout(() => {
this.refresh();
}, state.interval);
}
private cancel(): void {
clearTimeout(this.#timer);
this.#abortController?.abort();
this.#abortController = window.AbortController
? new AbortController()
: undefined;
}
private nextState(): PollState | false {
const state = pollers.get(this.urlValue);
if (!state) {
return this.resetState();
}
state.interval *= 1.5;
state.checks += 1;
if (state.checks <= this.maxChecksValue) {
return state;
} else {
this.resetState();
return false;
}
}
private resetState(): PollState {
const state = {
interval: this.intervalValue,
checks: 0
};
pollers.set(this.urlValue, state);
return state;
}
}
type PollState = {
interval: number;
checks: number;
};
// We keep a global state of the pollers. It will be reset on every page change.
const pollers = new Map<string, PollState>();

View file

@ -25,7 +25,7 @@
= tag[:description]
%h3.header-subsection Logo de l'attestation
= image_upload_and_render f, @attestation_template.logo, false
= render Attachment::EditComponent.image(f, @attestation_template.logo, false)
%p.notice
Formats acceptés : JPG / JPEG / PNG.
@ -33,7 +33,7 @@
Dimensions conseillées : au minimum 500 px de largeur ou de hauteur, poids maximum : 0,5 Mo.
%h3.header-subsection Tampon de l'attestation
= image_upload_and_render f, @attestation_template.signature, false
= render Attachment::EditComponent.image(f, @attestation_template.signature, false)
%p.notice
Formats acceptés : JPG / JPEG / PNG.

View file

@ -20,7 +20,7 @@
= f.select :zone_id, grouped_options_for_zone
%h3.header-subsection Logo de la démarche
= image_upload_and_render f, @procedure.logo
= render Attachment::EditComponent.image(f, @procedure.logo)
%h3.header-subsection Conservation des données
= f.label :duree_conservation_dossiers_dans_ds do
@ -55,7 +55,7 @@
= f.text_field :cadre_juridique, class: 'form-control', placeholder: 'https://www.legifrance.gouv.fr/'
= f.label :deliberation, 'Importer le texte'
= text_upload_and_render f, @procedure.deliberation
= render Attachment::EditComponent.text(f, @procedure.deliberation)
%h3.header-subsection
RGPD
@ -73,7 +73,7 @@
%p.notice
Formats acceptés : .doc, .odt, .pdf, .ppt, .pptx
- notice = @procedure.notice
= text_upload_and_render f, @procedure.notice
= render Attachment::EditComponent.text(f, @procedure.notice)
- if !@procedure.locked?
%h3.header-subsection À qui sadresse ma démarche ?

View file

@ -1,3 +0,0 @@
<%= render_flash(timeout: 5000, sticky: true) %>
<%= remove_element(".attachment-actions-#{@attachment_id}") %>
<%= show_element(".attachment-input-#{@attachment_id}") %>

View file

@ -0,0 +1,2 @@
= turbo_stream.remove dom_id(@attachment, :actions)
= turbo_stream.show_all ".attachment-input-#{@attachment.id}"

View file

@ -1,8 +0,0 @@
<%= render_to_element(".attachment-link[data-attachment-id=\"#{@attachment.id}\"]",
partial: 'shared/attachment/show',
outer: true,
locals: { attachment: @attachment, user_can_upload: @user_can_upload }) %>
<% if @attachment.virus_scanner.pending? || @attachment.watermark_pending? %>
<%= fire_event('attachment:update', { url: attachment_url(@attachment.id, { signed_id: @attachment.blob.signed_id, user_can_upload: @user_can_upload }) }.to_json ) %>
<% end %>

View file

@ -0,0 +1,2 @@
= turbo_stream.replace dom_id(@attachment, :show) do
= render Attachment::ShowComponent.new(attachment: @attachment, user_can_upload: @user_can_upload)

View file

@ -12,12 +12,12 @@
%p.introduction= @avis.introduction
- if @avis.introduction_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: @avis.introduction_file.attachment }
= render Attachment::ShowComponent.new(attachment: @avis.introduction_file.attachment)
%br/
= form_for @avis, url: expert_avis_path(@avis.procedure, @avis), html: { class: 'form', data: { persisted_content_id: @avis.id } } do |f|
= f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true, class: 'persisted-input'
= text_upload_and_render f, @avis.piece_justificative_file
= render Attachment::EditComponent.text(f, @avis.piece_justificative_file)
.flex.justify-between.align-baseline
%p.confidentiel.flex

View file

@ -7,7 +7,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
= text_upload_and_render f, avis.introduction_file
= render Attachment::EditComponent.text(f, avis.introduction_file)
- if linked_dossiers.present?
= f.check_box :invite_linked_dossiers, {}, true, false

View file

@ -14,7 +14,7 @@
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: 'persisted-input'
%p.tab-title Ajouter une pièce jointe
.form-group
= text_upload_and_render f, avis.introduction_file
= render Attachment::EditComponent.text(f, avis.introduction_file)
- if linked_dossiers.present?
= f.check_box :invite_linked_dossiers, {}, true, false

View file

@ -33,6 +33,6 @@
%span.waiting
= t('en_attente', scope: 'views.shared.avis')
- if avis.piece_justificative_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: avis.piece_justificative_file.attachment }
= render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment)
.answer-body
= simple_format(avis.answer)

View file

@ -12,12 +12,12 @@
%p.introduction= @avis.introduction
- if @avis.introduction_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: @avis.introduction_file.attachment }
= render Attachment::ShowComponent.new(attachment: @avis.introduction_file.attachment)
%br/
= 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
= text_upload_and_render f, @avis.piece_justificative_file
= render Attachment::EditComponent.text(f, @avis.piece_justificative_file)
.flex.justify-between.align-baseline
%p.confidentiel.flex

View file

@ -1,23 +0,0 @@
%span.dropdown{ data: { controller: 'menu-button' } }
%button.button.dropdown-button{ data: { menu_button_target: 'button' } }
= t(".download", count: count)
#download-menu.dropdown-content.fade-in-down{ style: 'width: 450px', data: { menu_button_target: 'menu' } }
%ul.dropdown-items
- exports_list(exports, statut).each do |item|
- format = item[:format]
- export = item[:export]
%li
- if export.nil?
// i18n-tasks-use t('.everything_csv_html')
// i18n-tasks-use t('.everything_xlsx_html')
// i18n-tasks-use t('.everything_ods_html')
// i18n-tasks-use t('.everything_zip_html')
= link_to t(".everything_#{format}_html"), download_export_instructeur_procedure_path(procedure, statut: statut, export_format: format), remote: true
- elsif export.ready?
= link_to t(".everything_ready_html", export_time: time_ago_in_words(export.updated_at), export_format: ".#{format}"), export.file.service_url, target: "_blank", rel: "noopener"
- if export.old?
= button_to download_export_instructeur_procedure_path(procedure, export_format: format, statut: statut, force_export: true), class: "button small", style: "padding-right: 2px", title: t(".everything_short", export_format: ".#{format}"), remote: true, method: :get, params: { export_format: format, statut: statut, force_export: true } do
.icon.retry
- else
%span{ 'data-export-poll-url': download_export_instructeur_procedure_path(procedure, export_format: format, statut: statut, no_progress_notification: true) }
= t(".everything_pending_html", export_time: time_ago_in_words(export.created_at), export_format: ".#{format}")

View file

@ -1,24 +0,0 @@
<% if @can_download_dossiers %>
<% if @statut.present? %>
<%= render_to_element('.dossiers-export', partial: "dossiers_export", locals: { procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count }) %>
<% else %>
<%= render_to_element('.procedure-actions', partial: "download_dossiers", locals: { procedure: @procedure, exports: @exports }) %>
<% end %>
<% end %>
<% @exports.values.each do |exports| %>
<% if @statut.present? %>
<% export = exports[:statut][@statut] %>
<% if export && !export.ready? %>
<%= fire_event('export:update', { url: download_export_instructeur_procedure_path(@procedure, export_format: export.format, statut: export.statut, no_progress_notification: true) }.to_json) %>
<% end %>
<% else %>
<% exports[:time_span_type].values.each do |export| %>
<% if !export.ready? %>
<%= fire_event('export:update', { url: download_export_instructeur_procedure_path(@procedure, export_format: export.format, time_span_type: export.time_span_type, no_progress_notification: true) }.to_json) %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= render_flash %>

View file

@ -0,0 +1,3 @@
- if @can_download_dossiers
= turbo_stream.update_all '.dossiers-export' do
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count)

View file

@ -26,7 +26,7 @@
%p= message.body
.answer.flex.align-start
- if message.piece_jointe.present?
= render partial: 'shared/attachment/show', locals: { attachment: message.piece_jointe.attachment }
= render Attachment::ShowComponent.new(attachment: message.piece_jointe.attachment)
- else
.page-title.center
%h2 Il n'y a aucun dossier en brouillon dans vos groupes instructeurs

View file

@ -63,7 +63,7 @@
= render partial: "dossiers_filter", locals: { procedure: @procedure, procedure_presentation: @procedure_presentation, current_filters: @current_filters, statut: @statut, displayed_fields_options: @displayed_fields_options }
- if @dossiers_count > 0
.dossiers-export
= render partial: "dossiers_export", locals: { procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count }
= render Dossiers::ExportComponent.new(procedure: @procedure, exports: @exports, statut: @statut, count: @dossiers_count)
%table.table.dossiers-table.hoverable
%thead

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, class: "persisted-input"
%p.tab-title Ajouter une pièce jointe
.form-group
= text_upload_and_render f, avis.introduction_file
= render Attachment::EditComponent.text(f, avis.introduction_file)
- if linked_dossiers.present?
= f.check_box :invite_linked_dossiers, {}, true, false

View file

@ -41,11 +41,11 @@
|
= link_to(t('revoke', scope: 'helpers.label'), revoquer_instructeur_avis_path(avis.procedure, avis), data: { confirm: t('revoke', scope: 'helpers.confirmation', email: avis.expert.email) }, method: :patch)
- if avis.introduction_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: avis.introduction_file.attachment }
= render Attachment::ShowComponent.new(attachment: avis.introduction_file.attachment)
.answer-body.mb-3
%p #{t('views.instructeurs.avis.introduction_file_explaination')} #{avis.claimant.email}
- if avis.piece_justificative_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: avis.piece_justificative_file.attachment }
= render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment)
.answer-body
= simple_format(avis.answer)

View file

@ -1,6 +1,6 @@
- if flash.any?
= turbo_stream.replace 'flash_messages', partial: 'layouts/flash_messages'
= turbo_stream.hide 'flash_messages', delay: 10000
= turbo_stream.hide 'flash_messages', delay: 30000
- flash.clear
= yield

View file

@ -1,43 +0,0 @@
-# Display a widget for uploading, editing and deleting a file attachment
- attachment = attached_file.attachment
- attachment_id = attachment ? attachment.id : SecureRandom.uuid
- persisted = attachment && attachment.persisted?
- accept = defined?(accept) ? accept : nil
- user_can_destroy = defined?(user_can_destroy) ? user_can_destroy : false
- direct_upload = direct_upload != nil ? false : true
- champ = form.object.is_a?(Champ) ? form.object : nil
.attachment
- if defined?(template) && template.attached?
%p.mb-1
Veuillez télécharger, remplir et joindre
= link_to('le modèle suivant', url_for(template), target: '_blank', rel: 'noopener')
- if persisted
.attachment-actions{ class: "attachment-actions-#{attachment_id}" }
.attachment-action
= render partial: "shared/attachment/show", locals: { attachment: attachment, user_can_upload: true }
- if user_can_destroy
.attachment-action
= link_to 'Supprimer', attachment_url(attachment.id, { signed_id: attachment.blob.signed_id }), remote: true, method: :delete, class: 'button small danger', data: { disable: true }, role: 'button'
.attachment-action
= button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': ".attachment-input-#{attachment_id}" }
.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 type: 'button', class: 'button attachment-error-retry', data: { 'input-target': ".attachment-input-#{attachment_id}", action: 'autosave#onClickRetryButton' } do
%span.icon.retry
Ré-essayer
= form.file_field attached_file.name,
class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
accept: accept,
direct_upload: direct_upload,
id: champ&.input_id,
aria: { describedby: champ&.describedby_id },
data: { 'auto-attach-url': auto_attach_url(form, form.object) }

View file

@ -1,5 +1,5 @@
- pj = champ.piece_justificative_file
- if pj.attached?
= render partial: "shared/attachment/show", locals: { attachment: pj.attachment }
= render Attachment::ShowComponent.new(attachment: pj.attachment)
- else
Pièce justificative non fournie

View file

@ -8,4 +8,4 @@
%td.libelle Justificatif :
%td
.action
= render partial: 'shared/attachment/show', locals: { attachment: dossier.justificatif_motivation.attachment }
= render Attachment::ShowComponent.new(attachment: dossier.justificatif_motivation.attachment)

View file

@ -1,4 +1 @@
= render 'shared/attachment/edit',
{ form: form,
attached_file: champ.piece_justificative_file,
template: champ.type_de_champ.piece_justificative_template, user_can_destroy: true }
= render Attachment::EditComponent.new(form: form, attached_file: champ.piece_justificative_file, template: champ.type_de_champ.piece_justificative_template, user_can_destroy: true)

View file

@ -1,4 +1 @@
= render 'shared/attachment/edit',
{ form: form,
attached_file: champ.piece_justificative_file,
user_can_destroy: true }
= render Attachment::EditComponent.new(form: form, attached_file: champ.piece_justificative_file, user_can_destroy: true)

View file

@ -1,2 +1,2 @@
- if dossier.present? && dossier.justificatif_motivation.attached?
= render partial: "shared/attachment/show", locals: { attachment: dossier.justificatif_motivation.attachment }
= render Attachment::ShowComponent.new(attachment: dossier.justificatif_motivation.attachment)

View file

@ -87,5 +87,8 @@ module TPS
config.view_component.show_previews_source = true
config.view_component.default_preview_layout = 'component_preview'
config.view_component.preview_paths << "#{Rails.root}/spec/components/previews"
# see: https://viewcomponent.org/known_issues.html
config.view_component.use_global_output_buffer = true
end
end

View file

@ -9,17 +9,6 @@ en:
archived: archived
dossiers_close_to_expiration: expiring
dossiers_supprimes_recemment: recently deleted
dossiers_export:
everything_csv_html: Ask an export in format .csv<br>(only folders, without repeatable fields)
everything_xlsx_html: Ask an export in format .xlsx
everything_ods_html: Ask an export in format .ods
everything_zip_html: Ask an export in format .zip
everything_short: Ask an export in format%{export_format}
everything_pending_html: Ask an export in format %{export_format} is being generated<br>(ask %{export_time} ago)
everything_ready_html: Download the export in format %{export_format}<br>(generated %{export_time} ago)
download:
one: Download a file
other: Download %{count} files
email_usagers:
contact_users: Contact users (draft)
notice: "You will send a message to %{dossiers_count} whose files are in draft, in the instructor groups : %{groupe_instructeurs}."

View file

@ -8,18 +8,7 @@ fr:
all: dossiers
archived: archivés
dossiers_close_to_expiration: expirant
dossiers_supprimes_recemment: supprimés
dossiers_export:
everything_csv_html: Demander un export au format .csv<br>(uniquement les dossiers, sans les champs répétables)
everything_xlsx_html: Demander un export au format .xlsx
everything_ods_html: Demander un export au format .ods
everything_zip_html: Demander un export au format .zip
everything_short: Demander un export au format %{export_format}
everything_pending_html: Un export au format %{export_format} est en train dêtre généré<br>(demandé il y a %{export_time})
everything_ready_html: Télécharger lexport au format %{export_format}<br>(généré il y a %{export_time})
download:
one: Télécharger un dossier
other: Télécharger %{count} dossiers
dossiers_supprimes_recemment: supprimés
email_usagers:
contact_users: Contacter les usagers (brouillon)
notice: "Vous allez envoyer un message à %{dossiers_count} dont les dossiers sont en brouillon, dans les groupes instructeurs : %{groupe_instructeurs}."

View file

@ -355,6 +355,7 @@ Rails.application.routes.draw do
post 'add_filter'
get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter'
get 'download_export'
post 'download_export'
get 'stats'
get 'email_notifications'
patch 'update_email_notifications'

View file

@ -8,24 +8,24 @@ describe AttachmentsController, type: :controller do
describe '#show' do
render_views
let(:format) { :js }
let(:format) { :turbo_stream }
subject do
request.headers['HTTP_REFERER'] = dossier_url(dossier)
get :show, params: { id: attachment.id, signed_id: signed_id }, format: format, xhr: (format == :js)
get :show, params: { id: attachment.id, signed_id: signed_id }, format: format
end
context 'when authenticated' do
before { sign_in(user) }
context 'when requesting Javascript' do
let(:format) { :js }
context 'when requesting turbo_stream' do
let(:format) { :turbo_stream }
it { is_expected.to have_http_status(200) }
it 'renders JS that replaces the attachment HTML' do
it 'renders turbo_stream that replaces the attachment HTML' do
subject
expect(response.body).to have_text(".attachment-link[data-attachment-id=\"#{attachment.id}\"]")
expect(response.body).to include(ActionView::RecordIdentifier.dom_id(attachment, :show))
end
end
@ -51,7 +51,7 @@ describe AttachmentsController, type: :controller do
let(:signed_id) { attachment.blob.signed_id }
subject do
delete :destroy, params: { id: attachment.id, signed_id: signed_id }, format: :js
delete :destroy, params: { id: attachment.id, signed_id: signed_id }, format: :turbo_stream
end
context "when authenticated" do

View file

@ -523,15 +523,15 @@ describe Instructeurs::ProceduresController, type: :controller do
end
end
context 'when the js format is used' do
context 'when the turbo_stream format is used' do
before do
post :download_export,
params: { export_format: :csv, procedure_id: procedure.id },
format: :js
format: :turbo_stream
end
it 'responds in the correct format' do
expect(response.media_type).to eq('text/javascript')
expect(response.media_type).to eq('text/vnd.turbo-stream.html')
expect(response).to have_http_status(:ok)
end
end

View file

@ -6,7 +6,7 @@ describe 'shared/attachment/_show.html.haml', type: :view do
champ.piece_justificative_file.blob.update(metadata: champ.piece_justificative_file.blob.metadata.merge(virus_scan_result: virus_scan_result))
end
subject { render 'shared/attachment/show', attachment: champ.piece_justificative_file.attachment }
subject { render Attachment::ShowComponent.new(attachment: champ.piece_justificative_file.attachment) }
context 'when there is no anti-virus scan' do
let(:virus_scan_result) { nil }

View file

@ -5,7 +5,7 @@ describe 'shared/attachment/_update.html.haml', type: :view do
subject do
form_for(champ.dossier) do |form|
view.image_upload_and_render form, attached_file
view.render Attachment::EditComponent.image(form, attached_file)
end
end
@ -53,12 +53,10 @@ describe 'shared/attachment/_update.html.haml', type: :view do
context 'when the user cannot destroy the attachment' do
subject do
form_for(champ.dossier) do |form|
render 'shared/attachment/edit', {
form: form,
render Attachment::EditComponent.new(form: form,
attached_file: attached_file,
accept: 'image/png',
user_can_destroy: user_can_destroy
}
user_can_destroy: user_can_destroy)
end
end