T08a: Frontend DMS + Tags + Permissions UI — 33 tests, tsc clean, vite build pass
- DMS file browser: folder tree + file grid + upload dropzone + search + preview modal - DMS share dialog: user/group share + public share links with password+expiry - DMS bulk actions: bulk move + bulk delete with confirm dialogs - DMS trash view: deleted files list with restore button - Tags: TagPicker on company/contact detail pages (new tabs tab) - Tags: TagCloud + BulkTagDialog for bulk tag assignment - Permissions: share link creation, permission display, copy-link button - API clients: dms.ts, tags.ts, permissions.ts - Routes: /dms, /dms/trash added to router - Sidebar: DMS nav link updated - i18n: de.json + en.json translations for DMS/Tags/Permissions - 33 new tests (5 test files), full regression 276/276 pass - tsc --noEmit: 0 errors, vite build: 252 modules
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* File preview modal for DMS.
|
||||
* Renders PDF files in an iframe via the preview endpoint.
|
||||
* Shows file metadata and download button for non-PDF files.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
|
||||
|
||||
export interface FilePreviewModalProps {
|
||||
open: boolean;
|
||||
file: DmsFile | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
export function FilePreviewModal({ open, file, onClose }: FilePreviewModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && file) {
|
||||
setPreviewUrl(getFilePreviewUrl(file.id));
|
||||
} else {
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
}, [open, file]);
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
const isPdf = file.mime_type === 'application/pdf';
|
||||
const isImage = file.mime_type.startsWith('image/');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={file.name}
|
||||
size="xl"
|
||||
data-testid="file-preview-modal"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<Badge variant="info">{file.mime_type.split('/')[1]?.toUpperCase() || t('dms.icon.other')}</Badge>
|
||||
<span className="text-sm text-secondary-500">{formatFileSize(file.size)}</span>
|
||||
{file.created_at && (
|
||||
<span className="text-sm text-secondary-500">
|
||||
{t('dms.fileModified')}: {new Date(file.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isPdf && previewUrl && (
|
||||
<div className="border border-secondary-200 rounded-lg overflow-hidden" data-testid="pdf-preview">
|
||||
<iframe
|
||||
src={previewUrl} className="w-full h-[60vh]"
|
||||
title={file.name} aria-label={t('dms.preview')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isImage && previewUrl && (
|
||||
<div className="flex justify-center border border-secondary-200 rounded-lg p-4" data-testid="image-preview">
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={file.name}
|
||||
className="max-h-[60vh] object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPdf && !isImage && (
|
||||
<div className="text-center py-12" data-testid="no-preview-available">
|
||||
<svg className="mx-auto h-12 w-12 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<p className="mt-2 text-sm text-secondary-500">{t('dms.preview')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
{previewUrl && (
|
||||
<a
|
||||
href={previewUrl}
|
||||
download={file.name}
|
||||
className="inline-flex items-center px-4 py-2 rounded-md bg-primary-600 text-white text-sm font-medium hover:bg-primary-700 min-h-touch"
|
||||
>
|
||||
{t('dms.download')}
|
||||
</a>
|
||||
)}
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{t('dms.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user