diff --git a/app/assets/stylesheets/attachment.scss b/app/assets/stylesheets/attachment.scss index 2584c972e..3688742e7 100644 --- a/app/assets/stylesheets/attachment.scss +++ b/app/assets/stylesheets/attachment.scss @@ -25,7 +25,8 @@ } } -.attachment-multiple:not(.fr-downloads-group) { +.attachment-multiple:not(.fr-downloads-group), +.attachment-multiple.fr-downloads-group[data-controller=replace-attachment] { ul { list-style-type: none; padding-inline-start: 0; @@ -42,6 +43,10 @@ position: relative; top: -1rem; } + + .fr-btn { + align-self: flex-start; + } } .attachment-multiple.fr-downloads-group.destroyable { diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb index 79238f284..3cd253b22 100644 --- a/app/components/attachment/edit_component.rb +++ b/app/components/attachment/edit_component.rb @@ -111,13 +111,14 @@ class Attachment::EditComponent < ApplicationComponent { type: 'button', data: { - action: "click->replace-attachment#open" - } + action: "click->replace-attachment#open", + auto_attach_url: auto_attach_url + }.compact } end def replace_controller_attributes - return {} unless user_can_replace? || as_multiple? + return {} if !persisted? || !user_can_replace? || as_multiple? { "data-controller": 'replace-attachment' diff --git a/app/components/attachment/edit_component/edit_component.html.haml b/app/components/attachment/edit_component/edit_component.html.haml index 183cf1550..a3466a466 100644 --- a/app/components/attachment/edit_component/edit_component.html.haml +++ b/app/components/attachment/edit_component/edit_component.html.haml @@ -7,10 +7,10 @@ - elsif user_can_replace? = button_tag t('.replace'), **replace_button_options, class: "fr-btn fr-btn--tertiary fr-btn--sm", title: "Remplacer le fichier #{attachment.filename}" - .fr-py-1v - - if downloadable? - = render Dsfr::DownloadComponent.new(attachment:) - - else + - if downloadable? + = render Dsfr::DownloadComponent.new(attachment:) + - else + .fr-py-1v %span.attachment-filename.fr-mr-1w= link_to_if(viewable?, attachment.filename.to_s, helpers.url_for(attachment.blob), title: "Ouvrir le fichier #{attachment.filename.to_s}", **helpers.external_link_attributes) = render Attachment::ProgressComponent.new(attachment: attachment) diff --git a/app/components/attachment/multiple_component.rb b/app/components/attachment/multiple_component.rb index c0229bf90..2a1baa3d7 100644 --- a/app/components/attachment/multiple_component.rb +++ b/app/components/attachment/multiple_component.rb @@ -12,15 +12,18 @@ class Attachment::MultipleComponent < ApplicationComponent attr_reader :view_as attr_reader :user_can_destroy alias user_can_destroy? user_can_destroy + attr_reader :user_can_replace + alias user_can_replace? user_can_replace delegate :count, :empty?, to: :attachments, prefix: true - def initialize(champ:, attached_file:, form_object_name: nil, view_as: :link, user_can_destroy: true, max: nil) + def initialize(champ:, attached_file:, form_object_name: nil, view_as: :link, user_can_destroy: true, user_can_replace: false, max: nil) @champ = champ @attached_file = attached_file @form_object_name = form_object_name @view_as = view_as @user_can_destroy = user_can_destroy + @user_can_replace = user_can_replace @max = max || DEFAULT_MAX_ATTACHMENTS @attachments = attached_file.attachments || [] @@ -48,4 +51,12 @@ class Attachment::MultipleComponent < ApplicationComponent nil end + + def replace_controller_attributes + return {} unless user_can_replace? + + { + "data-controller": 'replace-attachment' + } + end end diff --git a/app/components/attachment/multiple_component/multiple_component.html.haml b/app/components/attachment/multiple_component/multiple_component.html.haml index b7a9036e8..ecca7269f 100644 --- a/app/components/attachment/multiple_component/multiple_component.html.haml +++ b/app/components/attachment/multiple_component/multiple_component.html.haml @@ -1,13 +1,13 @@ -.fr-mb-4w.attachment-multiple{ class: class_names("fr-downloads-group": view_as == :download, "destroyable": user_can_destroy?) } +.fr-mb-4w.attachment-multiple{ class: class_names("fr-downloads-group": view_as == :download, "destroyable": user_can_destroy?), **replace_controller_attributes } = template %ul - each_attachment do |attachment, index| %li{ id: dom_id(attachment) } - = render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, as_multiple: true, view_as:, user_can_destroy:, form_object_name:) + = render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, as_multiple: true, view_as:, user_can_destroy:, user_can_replace:, form_object_name:) %div{ id: empty_component_id, class: class_names("hidden": !can_attach_next?) } - = render Attachment::EditComponent.new(champ:, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:) + = render Attachment::EditComponent.new(champ:, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, user_can_replace:, form_object_name:) // single poll and refresh message for all attachments = render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context) diff --git a/app/javascript/controllers/replace_attachment_controller.ts b/app/javascript/controllers/replace_attachment_controller.ts index 701df521d..07dda54f4 100644 --- a/app/javascript/controllers/replace_attachment_controller.ts +++ b/app/javascript/controllers/replace_attachment_controller.ts @@ -6,8 +6,25 @@ export class ReplaceAttachmentController extends ApplicationController { declare readonly inputTarget: HTMLInputElement; - open() { + open(event: Event) { show(this.inputTarget); this.inputTarget.click(); // opens input prompt + + const target = event.currentTarget as HTMLButtonElement; + + if (target.dataset.autoAttachUrl) { + // set the auto attach url specific to this button to replace the related attachment + this.inputTarget.dataset.originalAutoAttachUrl = + this.inputTarget.dataset.autoAttachUrl; + + this.inputTarget.dataset.autoAttachUrl = target.dataset.autoAttachUrl; + + // reset autoAttachUrl which would add an attachment + // when replace is not finalized + this.inputTarget.addEventListener('cancel', () => { + this.inputTarget.dataset.autoAttachUrl = + this.inputTarget.dataset.originalAutoAttachUrl; + }); + } } } diff --git a/spec/components/attachment/multiple_component_spec.rb b/spec/components/attachment/multiple_component_spec.rb index b34ab17cb..2ac7396be 100644 --- a/spec/components/attachment/multiple_component_spec.rb +++ b/spec/components/attachment/multiple_component_spec.rb @@ -38,13 +38,7 @@ RSpec.describe Attachment::MultipleComponent, type: :component do context 'when there is an attachment' do before do - attached_file.attach( - io: StringIO.new("x" * 2), - filename: "me.jpg", - content_type: "image/jpeg", - metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } - ) - champ.save! + attach_to_champ(attached_file, champ) end it 'renders the filenames' do @@ -98,4 +92,26 @@ RSpec.describe Attachment::MultipleComponent, type: :component do expect(subject).to have_selector('[data-controller=turbo-poll]') end end + + context 'when user can replace' do + let(:kwargs) { { user_can_replace: true } } + + before do + attach_to_champ(attached_file, champ) + end + + it 'setup controller' do + expect(subject).to have_selector('[data-controller=replace-attachment]').once + end + end + + def attach_to_champ(attached_file, champ) + attached_file.attach( + io: StringIO.new("x" * 2), + filename: "me.jpg", + content_type: "image/jpeg", + metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE } + ) + champ.save! + end end diff --git a/spec/system/users/en_construction_spec.rb b/spec/system/users/en_construction_spec.rb new file mode 100644 index 000000000..51d3ded7c --- /dev/null +++ b/spec/system/users/en_construction_spec.rb @@ -0,0 +1,77 @@ +describe "Dossier en_construction" do + let(:user) { create(:user) } + let(:procedure) { create(:simple_procedure, :with_piece_justificative, :with_titre_identite) } + let(:dossier) { create(:dossier, :en_construction, :with_individual, :with_populated_champs, user:, procedure:) } + + let(:tdc) { + procedure.active_revision.types_de_champ_public.find { _1.type_champ == "piece_justificative" } + } + + let(:champ) { + dossier.champs_public.find { _1.type_de_champ_id == tdc.id } + } + + scenario 'delete a non mandatory piece justificative', js: true do + visit_dossier(dossier) + + expect(page).not_to have_button("Remplacer") + click_on "Supprimer le fichier toto.txt" + + expect(page).not_to have_text("toto.txt") + end + + context "with a mandatory piece justificative" do + before do + tdc.update_attribute(:mandatory, true) + end + + scenario 'remplace a mandatory piece justificative', js: true do + visit_dossier(dossier) + + click_on "Remplacer le fichier toto.txt" + + input_selector = "#attachment-multiple-empty-#{champ.id}" + expect(page).to have_selector(input_selector) + find(input_selector).attach_file(Rails.root.join('spec/fixtures/files/file.pdf')) + + expect(page).to have_text("file.pdf") + expect(page).not_to have_text("toto.txt") + end + end + + context "with a mandatory titre identite" do + let(:tdc) { + procedure.active_revision.types_de_champ_public.find { _1.type_champ == "titre_identite" } + } + + before do + tdc.update_attribute(:mandatory, true) + end + + scenario 'remplace a mandatory titre identite', js: true do + visit_dossier(dossier) + + click_on "Remplacer le fichier toto.png" + + input_selector = ".attachment-input-#{champ.piece_justificative_file.attachments.first.id}" + expect(page).to have_selector(input_selector) + find(input_selector).attach_file(Rails.root.join('spec/fixtures/files/file.pdf')) + + expect(page).to have_text("file.pdf") + expect(page).not_to have_text("toto.png") + end + end + + private + + def visit_dossier(dossier) + visit modifier_dossier_path(dossier) + + expect(page).to have_current_path(new_user_session_path) + fill_in 'user_email', with: user.email + fill_in 'user_password', with: user.password + click_on 'Se connecter' + + expect(page).to have_current_path(modifier_dossier_path(dossier)) + end +end