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:
@@ -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,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,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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
if (IMAGE_VIEWABLE_MIME.has(file.mime_type)) {
|
||||||
|
const blob = await fetchAttachmentBlob(file.id);
|
||||||
|
setViewImageUrl(URL.createObjectURL(blob));
|
||||||
|
} else {
|
||||||
setViewContent(await readAttachmentText(file.id));
|
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>
|
||||||
|
|||||||
@@ -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}` } : {},
|
||||||
|
|||||||
Reference in New Issue
Block a user