feat: version 2.12.0 -- editar y borrar comentarios
Hasta ahora un comentario (de proyecto o actividad) solo se podia crear, nunca tocar. Se agrega PUT/DELETE /comments/:id, restringido a quien lo escribio o a superadmin/admin (igual criterio que useria para notas/tareas: nunca otro colaborador aunque tenga permiso de escritura en el proyecto). Migracion 021 agrega updated_at a comments para mostrar "Editado" igual que ya hace Notas. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import { Send } from 'lucide-react';
|
||||
import { Send, Pencil, Trash2, Save, X } from 'lucide-react';
|
||||
import { useUi } from '../context/ui-context.jsx';
|
||||
import { useAuth } from '../context/auth-context.jsx';
|
||||
import { useAsync, useAction } from '../hooks/useAsync.js';
|
||||
import { listComments, createComment } from '../services/comments.js';
|
||||
import { listComments, createComment, updateComment, deleteComment } from '../services/comments.js';
|
||||
import { Button } from './Button.jsx';
|
||||
|
||||
const MANAGE_ROLES = ['superadmin', 'admin'];
|
||||
|
||||
export function CommentsPanel({ entityType, entityId, onChanged }) {
|
||||
const { t } = useUi();
|
||||
const { t, openDialog } = useUi();
|
||||
const { user: currentUser } = useAuth();
|
||||
const runAction = useAction();
|
||||
const { data: comments, reload } = useAsync(() => listComments(entityType, entityId), [entityType, entityId]);
|
||||
const [message, setMessage] = useState('');
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editingText, setEditingText] = useState('');
|
||||
|
||||
async function send() {
|
||||
if (!message.trim()) return;
|
||||
@@ -21,6 +27,48 @@ export function CommentsPanel({ entityType, entityId, onChanged }) {
|
||||
});
|
||||
}
|
||||
|
||||
function canManage(c) {
|
||||
return c.user_id === currentUser?.id || MANAGE_ROLES.includes(currentUser?.roleCode);
|
||||
}
|
||||
|
||||
function startEdit(c) {
|
||||
setEditingId(c.id);
|
||||
setEditingText(c.message);
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
setEditingId(null);
|
||||
setEditingText('');
|
||||
}
|
||||
|
||||
async function saveEdit(id) {
|
||||
if (!editingText.trim()) return;
|
||||
await runAction(async () => {
|
||||
await updateComment(id, editingText.trim());
|
||||
cancelEdit();
|
||||
await reload();
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDelete(c) {
|
||||
openDialog({
|
||||
tone: 'warning',
|
||||
title: t('common_confirm_delete_title'),
|
||||
message: t('common_confirm_delete_msg'),
|
||||
actions: [
|
||||
{ label: t('common_cancel'), variant: 'standard' },
|
||||
{
|
||||
label: t('common_delete'), variant: 'danger',
|
||||
onClick: () => runAction(async () => {
|
||||
await deleteComment(c.id);
|
||||
await reload();
|
||||
onChanged?.();
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (!comments) return <div className="text-muted">{t('common_loading')}</div>;
|
||||
|
||||
return (
|
||||
@@ -28,8 +76,32 @@ export function CommentsPanel({ entityType, entityId, onChanged }) {
|
||||
{comments.length === 0 && <div className="text-muted" style={{ marginBottom: 12 }}>{t('common_empty')}</div>}
|
||||
{comments.map((c) => (
|
||||
<div key={c.id} style={{ padding: '8px 0', borderBottom: '1px solid rgb(var(--line) / .5)' }}>
|
||||
<div style={{ fontSize: '.78rem', fontWeight: 700 }}>{c.user_name} <span className="text-muted" style={{ fontWeight: 400 }}>· {c.created_at}</span></div>
|
||||
<div style={{ fontSize: '.82rem', marginTop: 2 }}>{c.message}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ fontSize: '.78rem', fontWeight: 700, flex: 1 }}>
|
||||
{c.user_name} <span className="text-muted" style={{ fontWeight: 400 }}>· {c.created_at}{c.updated_at !== c.created_at ? ` · ${t('notes_updated')}` : ''}</span>
|
||||
</div>
|
||||
{editingId !== c.id && canManage(c) && (
|
||||
<>
|
||||
<button className="icon-btn" title={t('common_edit')} onClick={() => startEdit(c)}><Pencil size={14} /></button>
|
||||
<button className="icon-btn" title={t('common_delete')} onClick={() => confirmDelete(c)}><Trash2 size={14} /></button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{editingId === c.id ? (
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<input
|
||||
className="input" value={editingText} autoFocus
|
||||
onChange={(e) => setEditingText(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') saveEdit(c.id); if (e.key === 'Escape') cancelEdit(); }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
|
||||
<Button variant="accent" size="sm" icon={Save} onClick={() => saveEdit(c.id)}>{t('common_save')}</Button>
|
||||
<Button variant="standard" size="sm" icon={X} onClick={cancelEdit}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: '.82rem', marginTop: 2 }}>{c.message}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
|
||||
@@ -2,3 +2,5 @@ import { api } from './api.js';
|
||||
|
||||
export const listComments = (entityType, entityId) => api.get(`/comments?entityType=${entityType}&entityId=${entityId}`);
|
||||
export const createComment = (entityType, entityId, message) => api.post('/comments', { entityType, entityId, message });
|
||||
export const updateComment = (id, message) => api.put(`/comments/${id}`, { message });
|
||||
export const deleteComment = (id) => api.delete(`/comments/${id}`);
|
||||
|
||||
Reference in New Issue
Block a user