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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user