mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r3.3): aperçu réel des documents + ouverture dans le navigateur
Retour de pré-recette : les fichiers téléversés n'avaient ni aperçu ni ouverture. Les images affichent leur vraie vignette (blob authentifié → URL objet révoquée au démontage — un <img src> nu ne porte pas le jeton) ; « Ouvrir » (bouton + clic vignette) affiche PDF et images dans un nouvel onglet, ouvert dans le geste utilisateur pour passer les bloqueurs de pop-up. « Télécharger » inchangé. Vérifié en navigateur réel : vignette chargée, onglets blob (PDF rendu dans la visionneuse), download intact, zéro erreur console. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -217,13 +217,17 @@ export function useDeleteDocument() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Téléchargement authentifié (un lien nu ne porte pas le jeton). */
|
||||
export async function telechargerDocument(id: string, fileName: string): Promise<void> {
|
||||
/** Récupération authentifiée du fichier (un lien nu ne porte pas le jeton). */
|
||||
export async function blobDocument(id: string): Promise<Blob> {
|
||||
const res = await fetch(`/api/documents/${id}/download`, {
|
||||
headers: { Authorization: `Bearer ${getToken()}` },
|
||||
});
|
||||
if (!res.ok) throw new Error('Téléchargement impossible');
|
||||
const blob = await res.blob();
|
||||
return res.blob();
|
||||
}
|
||||
|
||||
export async function telechargerDocument(id: string, fileName: string): Promise<void> {
|
||||
const blob = await blobDocument(id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -232,6 +236,22 @@ export async function telechargerDocument(id: string, fileName: string): Promise
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/** Ouverture dans un nouvel onglet (PDF et images : le navigateur les affiche). */
|
||||
export async function ouvrirDocument(id: string): Promise<void> {
|
||||
// Onglet ouvert dans le geste utilisateur, sinon les bloqueurs de pop-up
|
||||
// refusent un window.open survenant après le fetch.
|
||||
const onglet = window.open('', '_blank');
|
||||
try {
|
||||
const blob = await blobDocument(id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
if (onglet) onglet.location.href = url;
|
||||
else window.open(url, '_blank');
|
||||
} catch (e) {
|
||||
onglet?.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ————— Analytics —————
|
||||
|
||||
export function useAnalyticsSummary(enabled = true) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
DOCUMENT_KIND_LABELS,
|
||||
DOCUMENT_KINDS,
|
||||
@@ -6,7 +6,14 @@ import {
|
||||
type DocumentDto,
|
||||
type DocumentKind,
|
||||
} from '@siop/shared';
|
||||
import { telechargerDocument, useDeleteDocument, useDocuments, useUploadDocument } from '@/api/gestion';
|
||||
import {
|
||||
blobDocument,
|
||||
ouvrirDocument,
|
||||
telechargerDocument,
|
||||
useDeleteDocument,
|
||||
useDocuments,
|
||||
useUploadDocument,
|
||||
} from '@/api/gestion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const CLASSE_TYPE: Partial<Record<DocumentKind, string>> = {
|
||||
@@ -20,10 +27,46 @@ export function tailleLisible(octets: number): string {
|
||||
return `${Math.max(1, Math.round(octets / 1024))} Ko`;
|
||||
}
|
||||
|
||||
/** Aperçu réel des images : le fichier exige le jeton, donc pas de <img src>
|
||||
* direct — on récupère le blob et on affiche une URL objet, révoquée au
|
||||
* démontage. Les PDF gardent leur pictogramme (le navigateur les ouvre). */
|
||||
function ApercuVignette({ doc }: { doc: DocumentDto }) {
|
||||
const estImage = doc.contentType.startsWith('image/');
|
||||
const [url, setUrl] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!estImage) return;
|
||||
let actif = true;
|
||||
let objet: string | undefined;
|
||||
blobDocument(doc.id)
|
||||
.then((blob) => {
|
||||
if (!actif) return;
|
||||
objet = URL.createObjectURL(blob);
|
||||
setUrl(objet);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
actif = false;
|
||||
if (objet) URL.revokeObjectURL(objet);
|
||||
};
|
||||
}, [doc.id, estImage]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="vignette"
|
||||
title={`Ouvrir ${doc.fileName}`}
|
||||
onClick={() => void ouvrirDocument(doc.id)}
|
||||
>
|
||||
{url ? <img src={url} alt={doc.fileName} /> : estImage ? '🖼' : '📄'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function VignetteDoc({ doc, surSuppression }: { doc: DocumentDto; surSuppression?: (id: string) => void }) {
|
||||
return (
|
||||
<div className="doc">
|
||||
<div className="vignette">{doc.contentType.startsWith('image/') ? '🖼' : '📄'}</div>
|
||||
<ApercuVignette doc={doc} />
|
||||
<span className={CLASSE_TYPE[doc.kind] ?? 'type-doc'}>{DOCUMENT_KIND_LABELS[doc.kind]}</span>
|
||||
<b>{doc.fileName}</b>
|
||||
<span className="meta">
|
||||
@@ -34,6 +77,13 @@ export function VignetteDoc({ doc, surSuppression }: { doc: DocumentDto; surSupp
|
||||
{doc.uploadedByName ? ` · ${doc.uploadedByName}` : ''}
|
||||
</span>
|
||||
<span style={{ display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void ouvrirDocument(doc.id)}
|
||||
style={{ color: 'var(--primaire)', fontWeight: 600, fontSize: 12.5 }}
|
||||
>
|
||||
Ouvrir
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void telechargerDocument(doc.id, doc.fileName)}
|
||||
|
||||
@@ -906,7 +906,9 @@ table {
|
||||
height: 84px; border-radius: 8px; background: var(--surface-2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--encre-3); font-size: 24px;
|
||||
width: 100%; padding: 0; border: 0; overflow: hidden; cursor: pointer;
|
||||
}
|
||||
.doc .vignette img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.doc b { font-size: 13px; word-break: break-all; }
|
||||
.doc .meta { color: var(--encre-2); font-size: 11.5px; }
|
||||
.type-doc {
|
||||
|
||||
Reference in New Issue
Block a user