diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb new file mode 100644 index 000000000..5ae74730c --- /dev/null +++ b/app/components/attachment/edit_component.rb @@ -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 diff --git a/app/components/attachment/edit_component/edit_component.en.yml b/app/components/attachment/edit_component/edit_component.en.yml new file mode 100644 index 000000000..1491cafbf --- /dev/null +++ b/app/components/attachment/edit_component/edit_component.en.yml @@ -0,0 +1,2 @@ +--- +en: diff --git a/app/components/attachment/edit_component/edit_component.fr.yml b/app/components/attachment/edit_component/edit_component.fr.yml new file mode 100644 index 000000000..09f6db466 --- /dev/null +++ b/app/components/attachment/edit_component/edit_component.fr.yml @@ -0,0 +1,2 @@ +--- +fr: diff --git a/app/components/attachment/edit_component/edit_component.html.haml b/app/components/attachment/edit_component/edit_component.html.haml new file mode 100644 index 000000000..ef538b7e4 --- /dev/null +++ b/app/components/attachment/edit_component/edit_component.html.haml @@ -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) diff --git a/app/components/attachment/show_component.rb b/app/components/attachment/show_component.rb new file mode 100644 index 000000000..c62894aaa --- /dev/null +++ b/app/components/attachment/show_component.rb @@ -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 diff --git a/app/components/attachment/show_component/show_component.en.yml b/app/components/attachment/show_component/show_component.en.yml new file mode 100644 index 000000000..1491cafbf --- /dev/null +++ b/app/components/attachment/show_component/show_component.en.yml @@ -0,0 +1,2 @@ +--- +en: diff --git a/app/components/attachment/show_component/show_component.fr.yml b/app/components/attachment/show_component/show_component.fr.yml new file mode 100644 index 000000000..09f6db466 --- /dev/null +++ b/app/components/attachment/show_component/show_component.fr.yml @@ -0,0 +1,2 @@ +--- +fr: diff --git a/app/views/shared/attachment/_show.html.haml b/app/components/attachment/show_component/show_component.html.haml similarity index 55% rename from app/views/shared/attachment/_show.html.haml rename to app/components/attachment/show_component/show_component.html.haml index 4437724a7..39925adab 100644 --- a/app/views/shared/attachment/_show.html.haml +++ b/app/components/attachment/show_component/show_component.html.haml @@ -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é) diff --git a/app/components/dossiers/export_component.rb b/app/components/dossiers/export_component.rb new file mode 100644 index 000000000..96e4f5d7a --- /dev/null +++ b/app/components/dossiers/export_component.rb @@ -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 diff --git a/app/components/dossiers/export_component/export_component.en.yml b/app/components/dossiers/export_component/export_component.en.yml new file mode 100644 index 000000000..c548a2b08 --- /dev/null +++ b/app/components/dossiers/export_component/export_component.en.yml @@ -0,0 +1,12 @@ +--- +en: + everything_csv_html: Ask an export in format .csv
(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
(ask %{export_time} ago) + everything_ready_html: Download the export in format %{export_format}
(generated %{export_time} ago) + download: + one: Download a file + other: Download %{count} files diff --git a/app/components/dossiers/export_component/export_component.fr.yml b/app/components/dossiers/export_component/export_component.fr.yml new file mode 100644 index 000000000..7e185ccda --- /dev/null +++ b/app/components/dossiers/export_component/export_component.fr.yml @@ -0,0 +1,12 @@ +--- +fr: + everything_csv_html: Demander un export au format .csv
(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é
(demandé il y a %{export_time}) + everything_ready_html: Télécharger l’export au format %{export_format}
(généré il y a %{export_time}) + download: + one: Télécharger un dossier + other: Télécharger %{count} dossiers diff --git a/app/components/dossiers/export_component/export_component.html.haml b/app/components/dossiers/export_component/export_component.html.haml new file mode 100644 index 000000000..aad58e0c5 --- /dev/null +++ b/app/components/dossiers/export_component/export_component.html.haml @@ -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) diff --git a/app/components/dossiers/message_component/message_component.html.haml b/app/components/dossiers/message_component/message_component.html.haml index c19f78791..622e4d09e 100644 --- a/app/components/dossiers/message_component/message_component.html.haml +++ b/app/components/dossiers/message_component/message_component.html.haml @@ -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 diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index a3a2fc471..be5d3ace2 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -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 diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index cf32e1167..379119141 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -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 diff --git a/app/helpers/attachment_upload_helper.rb b/app/helpers/attachment_upload_helper.rb deleted file mode 100644 index 0edcc7cb9..000000000 --- a/app/helpers/attachment_upload_helper.rb +++ /dev/null @@ -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 diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts index 133d83ea7..f235d0069 100644 --- a/app/javascript/controllers/index.ts +++ b/app/javascript/controllers/index.ts @@ -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); diff --git a/app/javascript/controllers/turbo_poll_controller.ts b/app/javascript/controllers/turbo_poll_controller.ts new file mode 100644 index 000000000..abf249416 --- /dev/null +++ b/app/javascript/controllers/turbo_poll_controller.ts @@ -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(); diff --git a/app/views/administrateurs/attestation_templates/_informations.html.haml b/app/views/administrateurs/attestation_templates/_informations.html.haml index 2b28b1b1a..397d2c57a 100644 --- a/app/views/administrateurs/attestation_templates/_informations.html.haml +++ b/app/views/administrateurs/attestation_templates/_informations.html.haml @@ -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. diff --git a/app/views/administrateurs/procedures/_informations.html.haml b/app/views/administrateurs/procedures/_informations.html.haml index 858673cbd..6d3eda15d 100644 --- a/app/views/administrateurs/procedures/_informations.html.haml +++ b/app/views/administrateurs/procedures/_informations.html.haml @@ -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 ? diff --git a/app/views/attachments/destroy.js.erb b/app/views/attachments/destroy.js.erb deleted file mode 100644 index 0d4b5e5ff..000000000 --- a/app/views/attachments/destroy.js.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= render_flash(timeout: 5000, sticky: true) %> -<%= remove_element(".attachment-actions-#{@attachment_id}") %> -<%= show_element(".attachment-input-#{@attachment_id}") %> diff --git a/app/views/attachments/destroy.turbo_stream.haml b/app/views/attachments/destroy.turbo_stream.haml new file mode 100644 index 000000000..323dd8f03 --- /dev/null +++ b/app/views/attachments/destroy.turbo_stream.haml @@ -0,0 +1,2 @@ += turbo_stream.remove dom_id(@attachment, :actions) += turbo_stream.show_all ".attachment-input-#{@attachment.id}" diff --git a/app/views/attachments/show.js.erb b/app/views/attachments/show.js.erb deleted file mode 100644 index 0a58c0af5..000000000 --- a/app/views/attachments/show.js.erb +++ /dev/null @@ -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 %> diff --git a/app/views/attachments/show.turbo_stream.haml b/app/views/attachments/show.turbo_stream.haml new file mode 100644 index 000000000..dc7cd008f --- /dev/null +++ b/app/views/attachments/show.turbo_stream.haml @@ -0,0 +1,2 @@ += turbo_stream.replace dom_id(@attachment, :show) do + = render Attachment::ShowComponent.new(attachment: @attachment, user_can_upload: @user_can_upload) diff --git a/app/views/experts/avis/instruction.html.haml b/app/views/experts/avis/instruction.html.haml index 3c0673547..9e904d26b 100644 --- a/app/views/experts/avis/instruction.html.haml +++ b/app/views/experts/avis/instruction.html.haml @@ -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 diff --git a/app/views/experts/avis/shared/avis/_form.html.haml b/app/views/experts/avis/shared/avis/_form.html.haml index a1bcf1fac..00df6b934 100644 --- a/app/views/experts/avis/shared/avis/_form.html.haml +++ b/app/views/experts/avis/shared/avis/_form.html.haml @@ -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 diff --git a/app/views/experts/shared/avis/_form.html.haml b/app/views/experts/shared/avis/_form.html.haml index 84aa541ad..1430c83ca 100644 --- a/app/views/experts/shared/avis/_form.html.haml +++ b/app/views/experts/shared/avis/_form.html.haml @@ -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 diff --git a/app/views/experts/shared/avis/_list.html.haml b/app/views/experts/shared/avis/_list.html.haml index f9acf92cd..256c857ee 100644 --- a/app/views/experts/shared/avis/_list.html.haml +++ b/app/views/experts/shared/avis/_list.html.haml @@ -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) diff --git a/app/views/instructeurs/avis/instruction.html.haml b/app/views/instructeurs/avis/instruction.html.haml index 61d03c91b..db68c6ed8 100644 --- a/app/views/instructeurs/avis/instruction.html.haml +++ b/app/views/instructeurs/avis/instruction.html.haml @@ -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 diff --git a/app/views/instructeurs/procedures/_dossiers_export.html.haml b/app/views/instructeurs/procedures/_dossiers_export.html.haml deleted file mode 100644 index cffde1c1a..000000000 --- a/app/views/instructeurs/procedures/_dossiers_export.html.haml +++ /dev/null @@ -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}") diff --git a/app/views/instructeurs/procedures/download_export.js.erb b/app/views/instructeurs/procedures/download_export.js.erb deleted file mode 100644 index ab489f92d..000000000 --- a/app/views/instructeurs/procedures/download_export.js.erb +++ /dev/null @@ -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 %> diff --git a/app/views/instructeurs/procedures/download_export.turbo_stream.haml b/app/views/instructeurs/procedures/download_export.turbo_stream.haml new file mode 100644 index 000000000..103a64e4f --- /dev/null +++ b/app/views/instructeurs/procedures/download_export.turbo_stream.haml @@ -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) diff --git a/app/views/instructeurs/procedures/email_usagers.html.haml b/app/views/instructeurs/procedures/email_usagers.html.haml index 5f0a5be1f..5373d9435 100644 --- a/app/views/instructeurs/procedures/email_usagers.html.haml +++ b/app/views/instructeurs/procedures/email_usagers.html.haml @@ -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 diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index b565fe51e..8c1090f38 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -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 diff --git a/app/views/instructeurs/shared/avis/_form.html.haml b/app/views/instructeurs/shared/avis/_form.html.haml index b82396296..4bd16c19c 100644 --- a/app/views/instructeurs/shared/avis/_form.html.haml +++ b/app/views/instructeurs/shared/avis/_form.html.haml @@ -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 diff --git a/app/views/instructeurs/shared/avis/_list.html.haml b/app/views/instructeurs/shared/avis/_list.html.haml index fb1f6b032..8c1693569 100644 --- a/app/views/instructeurs/shared/avis/_list.html.haml +++ b/app/views/instructeurs/shared/avis/_list.html.haml @@ -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) diff --git a/app/views/layouts/application.turbo_stream.haml b/app/views/layouts/application.turbo_stream.haml index 343964e1f..accaad848 100644 --- a/app/views/layouts/application.turbo_stream.haml +++ b/app/views/layouts/application.turbo_stream.haml @@ -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 diff --git a/app/views/shared/attachment/_edit.html.haml b/app/views/shared/attachment/_edit.html.haml deleted file mode 100644 index 358b1a3a3..000000000 --- a/app/views/shared/attachment/_edit.html.haml +++ /dev/null @@ -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) } diff --git a/app/views/shared/champs/piece_justificative/_show.html.haml b/app/views/shared/champs/piece_justificative/_show.html.haml index a9d7ae19c..c57cf367e 100644 --- a/app/views/shared/champs/piece_justificative/_show.html.haml +++ b/app/views/shared/champs/piece_justificative/_show.html.haml @@ -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 diff --git a/app/views/shared/dossiers/_infos_generales.html.haml b/app/views/shared/dossiers/_infos_generales.html.haml index 36bfc77ed..aa27ea4e6 100644 --- a/app/views/shared/dossiers/_infos_generales.html.haml +++ b/app/views/shared/dossiers/_infos_generales.html.haml @@ -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) diff --git a/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml b/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml index 0fc1f99c8..aa4c655b0 100644 --- a/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml +++ b/app/views/shared/dossiers/editable_champs/_piece_justificative.html.haml @@ -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) diff --git a/app/views/shared/dossiers/editable_champs/_titre_identite.html.haml b/app/views/shared/dossiers/editable_champs/_titre_identite.html.haml index e4e65d20d..1ff8bd6d5 100644 --- a/app/views/shared/dossiers/editable_champs/_titre_identite.html.haml +++ b/app/views/shared/dossiers/editable_champs/_titre_identite.html.haml @@ -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) diff --git a/app/views/users/dossiers/show/_download_justificatif.html.haml b/app/views/users/dossiers/show/_download_justificatif.html.haml index ce596c299..d7ecefab3 100644 --- a/app/views/users/dossiers/show/_download_justificatif.html.haml +++ b/app/views/users/dossiers/show/_download_justificatif.html.haml @@ -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) diff --git a/config/application.rb b/config/application.rb index aaf6aa527..f73c01df2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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 diff --git a/config/locales/views/instructeurs/procedures/en.yml b/config/locales/views/instructeurs/procedures/en.yml index fd70b140f..f8b2fd478 100644 --- a/config/locales/views/instructeurs/procedures/en.yml +++ b/config/locales/views/instructeurs/procedures/en.yml @@ -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
(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
(ask %{export_time} ago) - everything_ready_html: Download the export in format %{export_format}
(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}." diff --git a/config/locales/views/instructeurs/procedures/fr.yml b/config/locales/views/instructeurs/procedures/fr.yml index 7d9f79ed1..8436728af 100644 --- a/config/locales/views/instructeurs/procedures/fr.yml +++ b/config/locales/views/instructeurs/procedures/fr.yml @@ -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
(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é
(demandé il y a %{export_time}) - everything_ready_html: Télécharger l’export au format %{export_format}
(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}." diff --git a/config/routes.rb b/config/routes.rb index 89a87b307..de17165e9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/spec/controllers/attachments_controller_spec.rb b/spec/controllers/attachments_controller_spec.rb index fe10c5f03..8b1f8dff8 100644 --- a/spec/controllers/attachments_controller_spec.rb +++ b/spec/controllers/attachments_controller_spec.rb @@ -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 diff --git a/spec/controllers/instructeurs/procedures_controller_spec.rb b/spec/controllers/instructeurs/procedures_controller_spec.rb index 57e492fcb..dc1c31eaa 100644 --- a/spec/controllers/instructeurs/procedures_controller_spec.rb +++ b/spec/controllers/instructeurs/procedures_controller_spec.rb @@ -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 diff --git a/spec/views/shared/attachment/_show.html.haml_spec.rb b/spec/views/shared/attachment/_show.html.haml_spec.rb index b9737c8c4..6bd7b4586 100644 --- a/spec/views/shared/attachment/_show.html.haml_spec.rb +++ b/spec/views/shared/attachment/_show.html.haml_spec.rb @@ -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 } diff --git a/spec/views/shared/attachment/_update.html.haml_spec.rb b/spec/views/shared/attachment/_update.html.haml_spec.rb index 6ec4d7aae..2430cf7cd 100644 --- a/spec/views/shared/attachment/_update.html.haml_spec.rb +++ b/spec/views/shared/attachment/_update.html.haml_spec.rb @@ -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