feat: standalone buttons for all plugin pages + window system for modals
Standalone buttons: - Add ExternalLink button to Dms, Calendar, Mail, ContactsList toolbars - Create standalone pages for each (no sidebar, full screen) - Add standalone routes outside ProtectedRoute Window system for modals: - AppointmentModal -> AppointmentEditForm + openWindow() - ComposeModal -> MailComposeForm + openWindow() - FilePreviewModal -> FilePreviewContent + openWindow() - Calendar, Mail, Dms use openWindow() instead of modal state
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* FilePreviewContent — preview content for DMS files.
|
||||
* Extracted from FilePreviewModal for use with the Window system.
|
||||
* 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 { Button } from '@/components/ui/Button';
|
||||
import { formatDateShort } from '@/utils/date';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
|
||||
import { useWindowStore } from '@/store/windowStore';
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
export interface FilePreviewContentProps {
|
||||
file: DmsFile | null;
|
||||
windowId?: string;
|
||||
}
|
||||
|
||||
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 FilePreviewContent({ file, windowId }: FilePreviewContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const closeWindow = useWindowStore((s) => s.closeWindow);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (file) {
|
||||
setPreviewUrl(getFilePreviewUrl(file.id));
|
||||
} else {
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
const isPdf = file.mime_type === 'application/pdf';
|
||||
const isImage = file.mime_type.startsWith('image/');
|
||||
|
||||
const handleClose = () => {
|
||||
if (windowId) closeWindow(windowId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4" data-testid="file-preview-modal">
|
||||
<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_bytes ?? file.size ?? 0)}</span>
|
||||
{file.created_at && (
|
||||
<span className="text-sm text-secondary-500">
|
||||
{t('dms.fileModified')}: {formatDateShort(file.created_at)}
|
||||
</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">
|
||||
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
||||
<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={handleClose}>
|
||||
{t('dms.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilePreviewContent;
|
||||
Reference in New Issue
Block a user