Merge pull request #10281 from demarches-simplifiees/gallery

ETQ instructeur et usager je peux voir les pièces jointes dans une galerie
This commit is contained in:
Eric Leroy-Terquem 2024-04-22 09:22:58 +00:00 committed by GitHub
commit 51f9fa2f90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 257 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,91 @@
.gallery {
a {
background-image: none;
}
.champ-libelle {
margin-top: 0.5rem;
}
img {
height: 200px;
width: 200px;
object-fit: cover;
}
.thumbnail {
position: relative;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--border-default-grey);
background-color: var(--border-default-grey);
img {
position: relative;
z-index: 1;
}
.fr-btn {
position: absolute;
z-index: 10;
background-color: var(--background-default-grey);
color: var(--text-active-blue-france);
border: 1px solid var(--border-active-blue-france);
padding: 0.25rem 0.75rem;
&:hover {
background-color: var(--hover-tint);
}
&:active {
background-color: var(--active-tint);
}
}
}
}
.gallery-pieces-jointes {
display: flex;
flex-wrap: wrap;
.gallery-item {
margin: 0 2rem 1.5rem 0;
}
img {
height: 200px;
width: 200px;
}
}
.gallery-demande {
img {
height: 150px;
width: 150px;
}
.fr-download {
margin-bottom: 0.5rem;
}
.thumbnail {
width: fit-content;
margin-bottom: 1rem;
}
}
.lg-has-iframe {
width: 80% !important;
margin-top: 50px;
}
.lg-icon {
--hover-tint: none;
--active-tint: none;
}
.lg-sub-html {
background-image: linear-gradient(180deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1)) !important;
padding: 30px 40px 0 40px !important;
}

View file

@ -159,6 +159,14 @@
}
}
&-eye {
&:before,
&:after {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z'%3E%3C/path%3E%3C/svg%3E");
}
}
&-file-copy-line {
&:before,
&:after {

View file

@ -1,10 +1,11 @@
class Attachment::ShowComponent < ApplicationComponent
def initialize(attachment:, new_tab: false)
def initialize(attachment:, new_tab: false, truncate: false)
@attachment = attachment
@new_tab = new_tab
@truncate = truncate
end
attr_reader :attachment, :new_tab
attr_reader :attachment, :new_tab, :truncate
def should_display_link?
(attachment.virus_scanner.safe? || !attachment.virus_scanner.started?) && !attachment.watermark_pending?

View file

@ -1,6 +1,6 @@
%div{ id: dom_id(attachment, :show), class: class_names("attachment-error": error?, "fr-mb-2w": !should_display_link?) }
- if should_display_link?
= render Dsfr::DownloadComponent.new(attachment: attachment, virus_not_analyzed: !attachment.virus_scanner.started?, new_tab: new_tab)
= render Dsfr::DownloadComponent.new(attachment: attachment, virus_not_analyzed: !attachment.virus_scanner.started?, new_tab: new_tab, truncate: truncate)
- else
.attachment-filename.fr-mb-1w.fr-mr-1w= attachment.filename.to_s

View file

@ -5,14 +5,16 @@ class Dsfr::DownloadComponent < ApplicationComponent
attr_reader :ephemeral_link
attr_reader :virus_not_analyzed
attr_reader :new_tab
attr_reader :truncate
def initialize(attachment:, name: nil, url: nil, ephemeral_link: false, virus_not_analyzed: false, new_tab: false)
def initialize(attachment:, name: nil, url: nil, ephemeral_link: false, virus_not_analyzed: false, new_tab: false, truncate: false)
@attachment = attachment
@name = name || attachment.filename.to_s
@url = url
@ephemeral_link = ephemeral_link
@virus_not_analyzed = virus_not_analyzed
@new_tab = new_tab
@truncate = truncate
end
def title

View file

@ -1,7 +1,7 @@
.fr-download
%p
= link_to url, {class: "fr-download__link", title: title}.merge(new_tab ? { target: '_blank' } : { download: '' }) do
= name
= truncate ? name.truncate(20) : name
%span.fr-download__detail
= helpers.download_details(attachment)
- if ephemeral_link

View file

@ -358,6 +358,13 @@ module Instructeurs
redirect_to instructeur_procedure_path(procedure)
end
def pieces_jointes
@dossier = current_instructeur.dossiers.find(params[:dossier_id])
@champs_with_pieces_jointes = @dossier
.champs
.filter { _1.class.in?([Champs::PieceJustificativeChamp, Champs::TitreIdentiteChamp]) }
end
private
def dossier_scope

View file

@ -0,0 +1,36 @@
import { Controller } from '@hotwired/stimulus';
import lightGallery from 'lightgallery';
import { LightGallery } from 'lightgallery/lightgallery';
import lgThumbnail from 'lightgallery/plugins/thumbnail';
import lgZoom from 'lightgallery/plugins/zoom';
import lgRotate from 'lightgallery/plugins/rotate';
import 'lightgallery/css/lightgallery-bundle.css';
export default class extends Controller {
lightGallery?: LightGallery;
connect(): void {
const options = {
plugins: [lgZoom, lgThumbnail, lgRotate],
flipVertical: false,
flipHorizontal: false,
animateThumb: false,
zoomFromOrigin: false,
allowMediaOverlap: true,
toggleThumb: true,
selector: '.gallery-link'
};
this.lightGallery = lightGallery(this.element as HTMLElement, options);
const downloadIcon = document.querySelector('.lg-download');
if (downloadIcon != null) {
downloadIcon.removeAttribute('target');
}
}
disconnect(): void {
this.lightGallery?.destroy();
}
}

View file

@ -7,6 +7,10 @@
instructeur_dossier_path(dossier.procedure, dossier),
notification: notifications_summary[:demande])
- if dossier.revision.types_de_champ.any?(&:piece_justificative?)
= dynamic_tab_item(t('views.instructeurs.dossiers.tab_steps.attachments'),
pieces_jointes_instructeur_dossier_path(dossier.procedure, dossier))
= dynamic_tab_item(t('views.instructeurs.dossiers.tab_steps.private_annotations'),
annotations_privees_instructeur_dossier_path(dossier.procedure, dossier),
notification: notifications_summary[:annotations_privees])

View file

@ -0,0 +1,40 @@
- content_for(:title, "Pièces jointes")
= render partial: "header", locals: { dossier: @dossier }
.fr-container
- if @champs_with_pieces_jointes.map(&:piece_justificative_file).flatten.none?
.empty-text
Ce dossier ne contient pas de pièces jointes
- else
.gallery.gallery-pieces-jointes{ "data-controller": "lightbox" }
- @champs_with_pieces_jointes.each do |champ|
- champ.piece_justificative_file.each do |attachment|
.gallery-item
- blob = attachment.blob
- if blob.content_type.in?(AUTHORIZED_PDF_TYPES)
= link_to blob.url, id: blob.id, data: { iframe: true, src: blob.url }, class: 'gallery-link', type: blob.content_type, title: "#{champ.libelle} -- #{blob.filename}" do
.thumbnail
= image_tag("pdf-placeholder.png")
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
Visualiser
.champ-libelle
= champ.libelle.truncate(25)
= render Attachment::ShowComponent.new(attachment: attachment, truncate: true)
- elsif blob.content_type.in?(AUTHORIZED_IMAGE_TYPES)
= link_to image_url(blob.url), title: "#{champ.libelle} -- #{blob.filename}", data: { src: blob.url }, class: 'gallery-link' do
.thumbnail
= image_tag(blob.url)
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
Visualiser
.champ-libelle
= champ.libelle.truncate(25)
= render Attachment::ShowComponent.new(attachment: attachment, truncate: true)
- else
.thumbnail
= image_tag('apercu-indisponible.png')
.champ-libelle
= champ.libelle.truncate(25)
= render Attachment::ShowComponent.new(attachment: attachment, truncate: true)

View file

@ -1,4 +1,19 @@
.fr-downloads-group
%ul
- champ.piece_justificative_file.attachments.each do |attachment|
%li= render Attachment::ShowComponent.new(attachment:, new_tab: true)
- champ.piece_justificative_file.attachments.each do |attachment|
%ul
%li= render Attachment::ShowComponent.new(attachment:, new_tab: true, truncate: true)
.gallery-item
- blob = attachment.blob
- if blob.content_type.in?(AUTHORIZED_PDF_TYPES)
= link_to blob.url, id: blob.id, data: { iframe: true, src: blob.url }, class: 'gallery-link', type: blob.content_type, title: "#{champ.libelle} -- #{blob.filename}" do
.thumbnail
= image_tag("pdf-placeholder.png")
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
= 'Visualiser'
- elsif blob.content_type.in?(AUTHORIZED_IMAGE_TYPES)
= link_to image_url(blob.url), title: "#{champ.libelle} -- #{blob.filename}", data: { src: blob.url }, class: 'gallery-link' do
.thumbnail
= image_tag(blob.url)
.fr-btn.fr-btn--tertiary.fr-btn--icon-left.fr-icon-eye{ role: :button }
= 'Visualiser'

View file

@ -2,7 +2,7 @@
- content_for(:notice_info) do
= render partial: "shared/dossiers/france_connect_informations_notice", locals: { user_information: dossier.user.france_connect_informations.first }
.fr-container.counter-start-header-section.dossier-show{ class: class_names("dossier-show-instructeur" => profile =="instructeur") }
.fr-container.counter-start-header-section.dossier-show.gallery.gallery-demande{ class: class_names("dossier-show-instructeur" => profile =="instructeur"), "data-controller": "lightbox" }
.fr-grid-row.fr-grid-row--center
.fr-col-12.fr-col-xl-8
- if profile == 'instructeur' && dossier.termine_and_accuse_lecture?

View file

@ -1,15 +1,25 @@
AUTHORIZED_CONTENT_TYPES = [
# multimedia
AUTHORIZED_PDF_TYPES = [
'application/pdf', # text x 4628654
'application/x-pdf', # text x 30
'image/pdf', # text x 23
'text/pdf' # text x 12
]
AUTHORIZED_IMAGE_TYPES = [
'image/jpeg', # multimedia x 1467465
'image/png', # multimedia x 126662
'image/tiff', # multimedia x 3985
'image/bmp', # multimedia x 3656
'video/mp4', # multimedia x 2075
'image/webp', # multimedia x 529
'video/quicktime', # multimedia x 486
'image/gif', # multimedia x 463
'image/vnd.dwg' # multimedia x 137 auto desk
]
AUTHORIZED_CONTENT_TYPES = AUTHORIZED_IMAGE_TYPES + AUTHORIZED_PDF_TYPES + [
# multimedia
'video/mp4', # multimedia x 2075
'video/quicktime', # multimedia x 486
'video/3gpp', # multimedia x 216
'image/vnd.dwg', # multimedia x 137 auto desk
'audio/mpeg', # multimedia x 26
'video/x-ms-wm', # multimedia x 15 video microsoft ?
'audio/mp4', # audio .mp4, .m4a
@ -45,7 +55,6 @@ AUTHORIZED_CONTENT_TYPES = [
'text/xml', # program x 10
# text / sheet / presentation
'application/pdf', # text x 4628654
'application/vnd.ms-excel', # text x 166674
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', # text x 103879
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # text x 86336
@ -69,18 +78,15 @@ AUTHORIZED_CONTENT_TYPES = [
'application/vnd.ms-word.document.macroenabled.12', # text x 61
'application/vnd.openxmlformats-officedocument.spreadsheetml.template', # text x 59
'application/vnd.openxmlformats-officedocument.presentationml.slideshow', # text x 32
'application/x-pdf', # text x 30
'application/kswps', # inconnu x 26 , text ?
'application/x-iwork-numbers-sffnumbers', # text x 25
'text/rtf', # text x 25
'image/pdf', # text x 23
'application/vnd.ms-xpsdocument', # text x 23
'application/vnd.ms-excel.sheet.binary.macroenabled.12', # text x 21
'application/vnd.ms-powerpoint.presentation.macroenabled.12', # text x 15
'application/x-msword', # text x 15
'application/vnd.oasis.opendocument.spreadsheet-template', # text x 14
'application/vnd.oasis.opendocument.text-master', # text x 12
'text/pdf', # text x 12
'application/x-abiword', # text x 11
'application/x-iwork-keynote-sffnumbers', # text x 11
'application/x-iwork-keynote-sffkey', # text x 10

View file

@ -379,6 +379,7 @@ en:
messaging: Messaging
involved_persons: Involved persons
reaffectation: reassignment
attachments: Attachments
tab_explainations:
a_suivre: No instructor is assigned to follow up on these files. Be the first !
suivis: The folders that are in this tab are only those that you follow. You can exchange with the requester until you can accept them, refuse them or classify them without follow-up.

View file

@ -382,6 +382,7 @@ fr:
messaging: Messagerie
involved_persons: Personnes impliquées
reaffectation: Réaffectation
attachments: Pièces jointes
tab_explainations:
a_suivre: Aucun instructeur nest affecté au suivi de ces dossiers. Soyez le premier !
suivis: Les dossiers qui sont dans cet onglet sont uniquement ceux que vous suivez. Vous pouvez échanger avec le demandeur jusquà pouvoir les accepter, les refuser ou les classer sans suite.

View file

@ -502,6 +502,7 @@ Rails.application.routes.draw do
get 'print' => 'dossiers#print'
get 'telecharger_pjs' => 'dossiers#telecharger_pjs'
get 'reaffectation'
get 'pieces_jointes'
post 'reaffecter'
end
end

View file

@ -54,6 +54,7 @@
"highcharts": "^10.3.3",
"intersection-observer": "^0.12.2",
"is-hotkey": "^0.2.0",
"lightgallery": "^2.7.2",
"maplibre-gl": "^1.15.2",
"match-sorter": "^6.3.4",
"patch-package": "^7.0.0",

View file

@ -1372,4 +1372,29 @@ describe Instructeurs::DossiersController, type: :controller do
it { expect(subject).to have_http_status(:ok) }
end
describe '#pieces_jointes' do
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :piece_justificative }], instructeurs:) }
let(:dossier) { create(:dossier, :en_construction, :with_populated_champs, procedure: procedure) }
before do
dossier.champs.first.piece_justificative_file.attach(
io: StringIO.new("image file"),
filename: "image.jpeg",
content_type: "image/jpeg",
# we don't want to run virus scanner on this file
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
)
get :pieces_jointes, params: {
procedure_id: procedure.id,
dossier_id: dossier.id
}
end
it do
expect(response.body).to include('Télécharger le fichier toto.txt')
expect(response.body).to include('Télécharger le fichier image.jpeg')
expect(response.body).to include('Visualiser')
end
end
end