Merge pull request #7266 from tchak/refactor-poll-controller
turbo poll controller
This commit is contained in:
commit
185f2acdea
51 changed files with 421 additions and 217 deletions
92
app/components/attachment/edit_component.rb
Normal file
92
app/components/attachment/edit_component.rb
Normal 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
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
en:
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
fr:
|
|
@ -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 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
|
||||
|
||||
= form.file_field(file_field_name, **file_field_options)
|
27
app/components/attachment/show_component.rb
Normal file
27
app/components/attachment/show_component.rb
Normal 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
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
en:
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
fr:
|
|
@ -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 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
|
||||
- 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 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
|
||||
- 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é)
|
49
app/components/dossiers/export_component.rb
Normal file
49
app/components/dossiers/export_component.rb
Normal 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
|
|
@ -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
|
|
@ -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 l’export 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
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
|
|
94
app/javascript/controllers/turbo_poll_controller.ts
Normal file
94
app/javascript/controllers/turbo_poll_controller.ts
Normal 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>();
|
|
@ -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.
|
||||
|
|
|
@ -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 s’adresse ma démarche ?
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<%= render_flash(timeout: 5000, sticky: true) %>
|
||||
<%= remove_element(".attachment-actions-#{@attachment_id}") %>
|
||||
<%= show_element(".attachment-input-#{@attachment_id}") %>
|
2
app/views/attachments/destroy.turbo_stream.haml
Normal file
2
app/views/attachments/destroy.turbo_stream.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
= turbo_stream.remove dom_id(@attachment, :actions)
|
||||
= turbo_stream.show_all ".attachment-input-#{@attachment.id}"
|
|
@ -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 %>
|
2
app/views/attachments/show.turbo_stream.haml
Normal file
2
app/views/attachments/show.turbo_stream.haml
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
|
@ -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 %>
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 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 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) }
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}."
|
||||
|
|
|
@ -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 l’export 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}."
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue