feat(admin/exports): inline preview modal with copy-to-clipboard (#631)

Follow-up to #623. The Lightroom TXT export now shows the filename list
in a modal with a "Copy to clipboard" button instead of triggering a
.txt file download — saves the "open file → select all → copy" dance
admins were doing anyway. CSV export takes the same path (paste straight
into Sheets / Excel).

The modal keeps a "Download as file" button so admins who want the file
(sharing with colleagues, archiving, post-processing tooling) aren't
worse off than before — fully additive.

XMP (ZIP archive) and JSON exports keep their direct download path. A
textarea preview is the wrong UI for a binary archive, and JSON is
structured tool input where the file form is the natural mode.

Implementation:

- ExportPreviewModal — readonly textarea, copy + download buttons,
  monospace font for filename lists, click-to-select-all on the textarea
  for browsers that block clipboard writes (older Safari, hardened
  sandboxes — the catch falls through to a "select and copy manually"
  toast instead of silent failure).
- photosService.exportPhotosAsText — same backend endpoint as
  exportPhotos but resolves the blob.text() and returns
  { content, filename } instead of triggering a download. Preserves
  the existing exportPhotos for the XMP / JSON paths.
- PhotoExportMenu — PREVIEW_FORMATS = ['txt', 'csv']; non-preview
  formats keep the direct-download flow unchanged.
- EN + DE i18n entries.

No backend changes. No new endpoints. No breaking changes for callers
of photosService.exportPhotos.
This commit is contained in:
Paul Nothaft
2026-06-17 23:35:19 +02:00
parent 9d3daa1f93
commit 27b5f7e4b6
6 changed files with 228 additions and 6 deletions
+29
View File
@@ -295,6 +295,35 @@ class PhotosService {
window.URL.revokeObjectURL(url);
}
// Same endpoint as `exportPhotos` but returns the text content + filename
// instead of triggering a download. Used by the ExportPreviewModal (#631)
// for TXT / CSV formats where the admin wants to paste rather than save.
// XMP (ZIP archive) and JSON keep using exportPhotos — a textarea is the
// wrong UI for binary archives and structured tool input.
async exportPhotosAsText(
eventId: number,
options: ExportOptions
): Promise<{ content: string; filename: string }> {
const response = await api.post(
`/admin/photo-export/${eventId}/export`,
options,
{ responseType: 'blob' }
);
const contentDisposition = response.headers['content-disposition'];
let filename = `export_${Date.now()}`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?([^";\n]+)"?/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
const blob = response.data as Blob;
const content = await blob.text();
return { content, filename };
}
async getExportFormats(): Promise<ExportFormat[]> {
const response = await api.get('/admin/photo-export/export-formats');
return response.data.data;