diff --git a/messages/de.json b/messages/de.json index 8395f39a5..b370a245b 100644 --- a/messages/de.json +++ b/messages/de.json @@ -10858,6 +10858,7 @@ "saveError": "Die Änderung konnte nicht gespeichert werden.", "deleteError": "Das Dokument konnte nicht gelöscht werden.", "download": "Herunterladen", + "openInNewTab": "In neuem Tab öffnen", "delete": "Löschen", "titleLabel": "Titel", "titlePlaceholder": "Titel hinzufügen", diff --git a/messages/en.json b/messages/en.json index 164ca09ff..232132002 100644 --- a/messages/en.json +++ b/messages/en.json @@ -10858,6 +10858,7 @@ "saveError": "Couldn't save the change.", "deleteError": "Couldn't delete the document.", "download": "Download", + "openInNewTab": "Open in new tab", "delete": "Delete", "titleLabel": "Title", "titlePlaceholder": "Add a title", diff --git a/messages/es.json b/messages/es.json index 85feaee82..521e07378 100644 --- a/messages/es.json +++ b/messages/es.json @@ -10858,6 +10858,7 @@ "saveError": "No se pudo guardar el cambio.", "deleteError": "No se pudo eliminar el documento.", "download": "Descargar", + "openInNewTab": "Abrir en una pestaña nueva", "delete": "Eliminar", "titleLabel": "Título", "titlePlaceholder": "Añadir un título", diff --git a/messages/fr.json b/messages/fr.json index e84f55fb3..c20ce3479 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -10858,6 +10858,7 @@ "saveError": "Impossible d'enregistrer la modification.", "deleteError": "Impossible de supprimer le document.", "download": "Télécharger", + "openInNewTab": "Ouvrir dans un nouvel onglet", "delete": "Supprimer", "titleLabel": "Titre", "titlePlaceholder": "Ajouter un titre", diff --git a/messages/it.json b/messages/it.json index 6937f4746..292b56dd7 100644 --- a/messages/it.json +++ b/messages/it.json @@ -10858,6 +10858,7 @@ "saveError": "Impossibile salvare la modifica.", "deleteError": "Impossibile eliminare il documento.", "download": "Scarica", + "openInNewTab": "Apri in una nuova scheda", "delete": "Elimina", "titleLabel": "Titolo", "titlePlaceholder": "Aggiungi un titolo", diff --git a/messages/ko.json b/messages/ko.json index ec79c416d..db3908b97 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -10858,6 +10858,7 @@ "saveError": "변경 내용을 저장하지 못했어요.", "deleteError": "문서를 삭제하지 못했어요.", "download": "다운로드", + "openInNewTab": "새 탭에서 열기", "delete": "삭제", "titleLabel": "제목", "titlePlaceholder": "제목 입력", diff --git a/messages/pl.json b/messages/pl.json index 19bdad46a..dba9b65ea 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -10858,6 +10858,7 @@ "saveError": "Nie udało się zapisać zmiany.", "deleteError": "Nie udało się usunąć dokumentu.", "download": "Pobierz", + "openInNewTab": "Otwórz w nowej karcie", "delete": "Usuń", "titleLabel": "Tytuł", "titlePlaceholder": "Dodaj tytuł", diff --git a/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts b/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts index c759ae581..dea3c6a7c 100644 --- a/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts +++ b/src/app/api/documents/inbound/[id]/original/__tests__/route.test.ts @@ -240,6 +240,10 @@ describe("serving-posture header matrix (Class A inline vs Class B attachment)", ["image/heic", "photo.heic"], ["application/xml", "export.xml"], ["application/json", "export.json"], + // Upload refuses markup outright; a row that somehow carries one must + // still never render in-origin, where its script would run. + ["image/svg+xml", "drawing.svg"], + ["text/html", "page.html"], ])( "Class B %s → attachment + octet-stream, never inline", async (mime, name) => { diff --git a/src/components/documents/__tests__/document-detail-download.test.ts b/src/components/documents/__tests__/document-detail-download.test.ts index e3d331e5e..b59f64e1a 100644 --- a/src/components/documents/__tests__/document-detail-download.test.ts +++ b/src/components/documents/__tests__/document-detail-download.test.ts @@ -81,3 +81,41 @@ describe("the detail sheet's original-document transport", () => { ); }); }); + +/** + * "Open in new tab" hands the browser the types it renders itself, so a long + * report gets the browser's zoom and search. It must open the object URL the + * sheet already holds (a bare navigation to the route carries no record-session + * assertion and is refused once the session has been inside a shared record), + * and it must never appear for a type outside the inline serving class. + */ +describe("the detail sheet's open-in-new-tab control", () => { + const control = SHEET.slice( + SHEET.indexOf("function DocumentOpenInTabButton("), + SHEET.indexOf("/** Inline preview for Class A documents"), + ); + + it("exists", () => { + expect(control.length).toBeGreaterThan(0); + expect(SHEET).toMatch(/ { + expect(SHEET).toMatch( + /doc\.servingClass === "inline" \? \(\s*\n\s* { + expect(control).toContain('window.open(readyUrl, "_blank", "noopener")'); + expect(control).not.toContain("/api/documents"); + }); + + it("is disabled until the bytes are in hand, and when they failed", () => { + expect(control).toMatch(/disabled=\{readyUrl === null \|\| unavailable\}/); + expect(SHEET).toMatch( + / { + if (readyUrl) window.open(readyUrl, "_blank", "noopener"); + }} + > + + + {t("documents.detail.openInNewTab")} + + + ); +} + /** Inline preview for Class A documents; skeleton until the blob paints. */ function InlinePreview({ src, @@ -759,8 +797,8 @@ export function DocumentDetailSheet({ doc ? ( // Delete sits bottom-LEFT, quieted to a text button: the // bottom-right slot reads as the primary/confirm action, and a - // destructive control there is a footgun. Share + Download — the - // safe, expected actions — keep the trailing edge. On phones their + // destructive control there is a footgun. Share, open in a new tab and + // Download, the safe, expected actions, keep the trailing edge. On phones their // labels collapse to icon-only (aria-labelled) so three controls // never overflow the sheet at 360 px; the labels return at `sm+`.
@@ -824,6 +862,12 @@ export function DocumentDetailSheet({ )} + {doc.servingClass === "inline" ? ( + + ) : null}