CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
Frontend (web/mail-search): Anhang-Vorschau fuer PDF (Canvas-Rendering via pdfjs-dist, gleiches Muster wie DOC-05s web/pdf-viewer/PdfViewer.tsx -- kein aequivalentes PdfCanvasPreview im Repo gefunden, daher PdfViewer als Vorlage uebernommen statt neu erfunden) und Bilder (<img> auf einem authentifiziert geladenen Blob). Die Vorschauart haengt ausschliesslich vom serverseitig per Magic-Bytes bestimmten contentType ab, nie von einem Dateinamen (den es an der Schnittstelle gar nicht gibt); nicht unterstuetzte Typen werden nur als Hinweistext angezeigt, nie eingebettet. Kein iframe/<object>/dangerouslySetInnerHTML -- durch automatisierten Sicherheitsreview-Test abgesichert. Neue lib/http.ts (authentifizierter Zugriff ueber @nexarch/shl, identisch zu web/pdf-viewer/lib/http.ts) und lib/attachments.ts (Anhangsliste/ -inhalt der Mail-API). Pruefprotokoll in mail/docs/UX-03-PRUEFPROTOKOLL.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
1.6 KiB
TypeScript
55 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
// UX-03: Bild-Anhang-Vorschau. <img> dekodiert Bilddaten nativ im
|
|
// Browser (kein Skript-/HTML-Rendering von Fremdinhalt wie bei einem
|
|
// iframe) -- fuer Bilder reicht das, anders als bei PDF ist hier kein
|
|
// Canvas-Zwang nötig (Akzeptanzkriterium 3 betrifft ausdrücklich nur
|
|
// die PDF-Vorschau).
|
|
import { useEffect, useState } from "react";
|
|
import { apiFetch } from "../../../lib/http";
|
|
|
|
export interface ImagePreviewProps {
|
|
contentUrl: string;
|
|
alt: string;
|
|
}
|
|
|
|
export function ImagePreview({ contentUrl, alt }: ImagePreviewProps) {
|
|
const [src, setSrc] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
let objectUrl: string | null = null;
|
|
setError(null);
|
|
setSrc(null);
|
|
|
|
apiFetch(contentUrl)
|
|
.then((res) => {
|
|
if (!res.ok) throw new Error(`Bild konnte nicht geladen werden (${res.status}).`);
|
|
return res.blob();
|
|
})
|
|
.then((blob) => {
|
|
if (cancelled) return;
|
|
objectUrl = URL.createObjectURL(blob);
|
|
setSrc(objectUrl);
|
|
})
|
|
.catch((err: unknown) => {
|
|
if (cancelled) return;
|
|
setError(err instanceof Error ? err.message : "Bild konnte nicht geladen werden.");
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
};
|
|
}, [contentUrl]);
|
|
|
|
if (error) {
|
|
return <p role="alert">{error}</p>;
|
|
}
|
|
if (!src) {
|
|
return <p>Bild wird geladen…</p>;
|
|
}
|
|
return <img src={src} alt={alt} style={{ maxWidth: "100%" }} />;
|
|
}
|