|
|
|
@@ -2,7 +2,7 @@ import { useRef, useState } from 'react';
|
|
|
|
|
import { Upload, Download, Trash2, Paperclip, Eye } from 'lucide-react';
|
|
|
|
|
import { useUi } from '../context/ui-context.jsx';
|
|
|
|
|
import { useAsync, useAction } from '../hooks/useAsync.js';
|
|
|
|
|
import { listAttachments, uploadAttachment, deleteAttachment, downloadAttachment, readAttachmentText } from '../services/attachments.js';
|
|
|
|
|
import { listAttachments, uploadAttachment, deleteAttachment, downloadAttachment, readAttachmentText, fetchAttachmentBlob } from '../services/attachments.js';
|
|
|
|
|
import { renderMarkdown } from '../utils/renderMarkdown.js';
|
|
|
|
|
import { Button } from './Button.jsx';
|
|
|
|
|
import { DetailPopup } from './DetailPopup.jsx';
|
|
|
|
@@ -11,7 +11,9 @@ import { DetailPopup } from './DetailPopup.jsx';
|
|
|
|
|
import hljsLight from 'highlight.js/styles/vs.css?inline';
|
|
|
|
|
import hljsDark from 'highlight.js/styles/vs2015.css?inline';
|
|
|
|
|
|
|
|
|
|
const VIEWABLE_MIME = new Set(['text/plain', 'text/markdown']);
|
|
|
|
|
const TEXT_VIEWABLE_MIME = new Set(['text/plain', 'text/markdown']);
|
|
|
|
|
const IMAGE_VIEWABLE_MIME = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
|
|
|
|
const VIEWABLE_MIME = new Set([...TEXT_VIEWABLE_MIME, ...IMAGE_VIEWABLE_MIME]);
|
|
|
|
|
|
|
|
|
|
export function AttachmentsPanel({ entityType, entityId, onChanged }) {
|
|
|
|
|
const { t, openDialog, theme } = useUi();
|
|
|
|
@@ -20,14 +22,29 @@ export function AttachmentsPanel({ entityType, entityId, onChanged }) {
|
|
|
|
|
const inputRef = useRef(null);
|
|
|
|
|
const [viewing, setViewing] = useState(null); // adjunto abierto en el popup de lectura
|
|
|
|
|
const [viewContent, setViewContent] = useState('');
|
|
|
|
|
const [viewImageUrl, setViewImageUrl] = useState(null);
|
|
|
|
|
const [viewLoading, setViewLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
function closeView() {
|
|
|
|
|
setViewing(null);
|
|
|
|
|
// Libera la memoria del blob de la imagen -- no toca nada del flujo de texto/markdown.
|
|
|
|
|
if (viewImageUrl) URL.revokeObjectURL(viewImageUrl);
|
|
|
|
|
setViewImageUrl(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openView(file) {
|
|
|
|
|
setViewing(file);
|
|
|
|
|
setViewContent('');
|
|
|
|
|
if (viewImageUrl) URL.revokeObjectURL(viewImageUrl);
|
|
|
|
|
setViewImageUrl(null);
|
|
|
|
|
setViewLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
setViewContent(await readAttachmentText(file.id));
|
|
|
|
|
if (IMAGE_VIEWABLE_MIME.has(file.mime_type)) {
|
|
|
|
|
const blob = await fetchAttachmentBlob(file.id);
|
|
|
|
|
setViewImageUrl(URL.createObjectURL(blob));
|
|
|
|
|
} else {
|
|
|
|
|
setViewContent(await readAttachmentText(file.id));
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
setViewContent(t('document_view_error'));
|
|
|
|
|
} finally {
|
|
|
|
@@ -84,9 +101,13 @@ export function AttachmentsPanel({ entityType, entityId, onChanged }) {
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
<DetailPopup open={!!viewing} title={viewing?.original_name || ''} onClose={() => setViewing(null)} width={720} height={600}>
|
|
|
|
|
<DetailPopup open={!!viewing} title={viewing?.original_name || ''} onClose={closeView} width={720} height={600}>
|
|
|
|
|
{viewLoading ? (
|
|
|
|
|
<div className="text-muted">{t('common_loading')}</div>
|
|
|
|
|
) : viewImageUrl ? (
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
|
|
|
<img src={viewImageUrl} alt={viewing?.original_name} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
|
|
|
|
|
</div>
|
|
|
|
|
) : viewing?.mime_type === 'text/markdown' ? (
|
|
|
|
|
<>
|
|
|
|
|
<style>{theme === 'light' ? hljsLight : hljsDark}</style>
|
|
|
|
|