feat: version 2.12.1 -- ver imagenes (png/jpg/webp) en Documentos, sin tocar txt/md

Agrega soporte de imagenes al popup "Ver" de Documentos (proyectos y actividades): se
descargan como blob y se muestran con un object URL (revocado al cerrar), separado por
completo del camino de texto/markdown ya existente (readAttachmentText), que queda intacto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-14 12:58:27 -05:00
parent 3257c4f065
commit 971de05773
6 changed files with 34 additions and 8 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog - project.linkedcorp.net # Changelog - project.linkedcorp.net
## [2.12.1] - 2026-10-14
### Added
- Documentos (proyectos y actividades): las imágenes (.png, .jpg, .webp) también se pueden ver
en el popup con el botón "Ver", igual que ya funcionaba con .txt/.md — sin tocar ese flujo.
## [2.12.0] - 2026-10-14 ## [2.12.0] - 2026-10-14
### Added ### Added
- Comentarios (de proyectos y actividades): ahora se pueden editar y borrar, solo quien lo - Comentarios (de proyectos y actividades): ahora se pueden editar y borrar, solo quien lo
+1 -1
View File
@@ -1 +1 @@
2.12.0 2.12.1
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "project-linkedcorp-backend", "name": "project-linkedcorp-backend",
"version": "2.12.0", "version": "2.12.1",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/server.js", "main": "src/server.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "project-linkedcorp-frontend", "name": "project-linkedcorp-frontend",
"private": true, "private": true,
"version": "2.12.0", "version": "2.12.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+25 -4
View File
@@ -2,7 +2,7 @@ import { useRef, useState } from 'react';
import { Upload, Download, Trash2, Paperclip, Eye } from 'lucide-react'; import { Upload, Download, Trash2, Paperclip, Eye } from 'lucide-react';
import { useUi } from '../context/ui-context.jsx'; import { useUi } from '../context/ui-context.jsx';
import { useAsync, useAction } from '../hooks/useAsync.js'; 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 { renderMarkdown } from '../utils/renderMarkdown.js';
import { Button } from './Button.jsx'; import { Button } from './Button.jsx';
import { DetailPopup } from './DetailPopup.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 hljsLight from 'highlight.js/styles/vs.css?inline';
import hljsDark from 'highlight.js/styles/vs2015.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 }) { export function AttachmentsPanel({ entityType, entityId, onChanged }) {
const { t, openDialog, theme } = useUi(); const { t, openDialog, theme } = useUi();
@@ -20,14 +22,29 @@ export function AttachmentsPanel({ entityType, entityId, onChanged }) {
const inputRef = useRef(null); const inputRef = useRef(null);
const [viewing, setViewing] = useState(null); // adjunto abierto en el popup de lectura const [viewing, setViewing] = useState(null); // adjunto abierto en el popup de lectura
const [viewContent, setViewContent] = useState(''); const [viewContent, setViewContent] = useState('');
const [viewImageUrl, setViewImageUrl] = useState(null);
const [viewLoading, setViewLoading] = useState(false); 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) { async function openView(file) {
setViewing(file); setViewing(file);
setViewContent(''); setViewContent('');
if (viewImageUrl) URL.revokeObjectURL(viewImageUrl);
setViewImageUrl(null);
setViewLoading(true); setViewLoading(true);
try { 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 { } catch {
setViewContent(t('document_view_error')); setViewContent(t('document_view_error'));
} finally { } finally {
@@ -84,9 +101,13 @@ export function AttachmentsPanel({ entityType, entityId, onChanged }) {
</div> </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 ? ( {viewLoading ? (
<div className="text-muted">{t('common_loading')}</div> <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' ? ( ) : viewing?.mime_type === 'text/markdown' ? (
<> <>
<style>{theme === 'light' ? hljsLight : hljsDark}</style> <style>{theme === 'light' ? hljsLight : hljsDark}</style>
+1 -1
View File
@@ -12,7 +12,7 @@ export async function uploadAttachment(entityType, entityId, file) {
export const deleteAttachment = (id) => api.delete(`/attachments/${id}`); export const deleteAttachment = (id) => api.delete(`/attachments/${id}`);
async function fetchAttachmentBlob(id) { export async function fetchAttachmentBlob(id) {
const token = localStorage.getItem('plc_token'); const token = localStorage.getItem('plc_token');
const res = await fetch(api.downloadUrl(`/attachments/${id}/download`), { const res = await fetch(api.downloadUrl(`/attachments/${id}/download`), {
headers: token ? { Authorization: `Bearer ${token}` } : {}, headers: token ? { Authorization: `Bearer ${token}` } : {},