Fix comment avatar, activity log labels, and ticket update permissions
- add_comment.php: include user_id in response for avatar rendering - ticket.js: add buildCommentElement() helper that matches server-rendered comment structure (avatar, edit/delete buttons, textarea); use it in addComment() and submitReply() so new comments show the avatar immediately - AuditLogModel: logCommentCreate uses action_type='comment' not 'create' - TicketView: formatAction handles entity_type='comment' with action_type='create' for existing DB records; prevents "created this ticket" showing for comments - update_ticket.php: remove owner/assignee restriction so any authenticated team member can update ticket status and fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+98
-53
@@ -182,6 +182,81 @@ function toggleEditMode() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute avatar color class from display name (mirrors PHP crc32 % 4 logic)
|
||||
*/
|
||||
function avatarColorClass(displayName) {
|
||||
var colors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
|
||||
var h = 0;
|
||||
for (var i = 0; i < displayName.length; i++) {
|
||||
h = ((h << 5) - h + displayName.charCodeAt(i)) | 0;
|
||||
}
|
||||
return colors[Math.abs(h) % 4];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a comment/reply DOM element matching the server-rendered structure
|
||||
*/
|
||||
function buildCommentElement(opts) {
|
||||
// opts: { commentId, userId, displayName, createdAt, commentText, isMarkdown,
|
||||
// depth, parentId, canModify }
|
||||
var depth = opts.depth || 0;
|
||||
var depthClass = 'thread-depth-' + Math.min(depth, 3);
|
||||
var threadClass = opts.parentId ? 'comment-reply' : 'comment-root';
|
||||
|
||||
var words = (opts.displayName || '').trim().split(/\s+/).filter(Boolean);
|
||||
var initials = words.slice(0, 2).map(function(w) { return w[0].toUpperCase(); }).join('');
|
||||
var color = avatarColorClass(opts.displayName || '');
|
||||
var avatarImg = opts.userId > 0
|
||||
? '<img src="/api/user_avatar.php?user_id=' + opts.userId + '" alt="" class="lt-avatar-img">'
|
||||
: '';
|
||||
|
||||
var threadLine = opts.parentId ? '<div class="thread-line" aria-hidden="true"></div>' : '';
|
||||
|
||||
var replyBtn = depth < 3
|
||||
? '<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm comment-action-btn reply-btn"' +
|
||||
' data-action="reply-comment" data-comment-id="' + opts.commentId + '"' +
|
||||
' data-user="' + lt.escHtml(opts.displayName) + '" aria-label="Reply to comment">Reply</button>'
|
||||
: '';
|
||||
var modBtns = opts.canModify !== false
|
||||
? '<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm comment-action-btn edit-btn"' +
|
||||
' data-action="edit-comment" data-comment-id="' + opts.commentId + '" aria-label="Edit comment">Edit</button>' +
|
||||
'<button type="button" class="lt-btn lt-btn-danger lt-btn-sm comment-action-btn delete-btn"' +
|
||||
' data-action="delete-comment" data-comment-id="' + opts.commentId + '" aria-label="Delete comment">Del</button>'
|
||||
: '';
|
||||
|
||||
var div = document.createElement('div');
|
||||
div.className = 'comment ' + depthClass + ' ' + threadClass + ' animate-fadein';
|
||||
div.dataset.commentId = opts.commentId;
|
||||
div.dataset.markdownEnabled = opts.isMarkdown ? '1' : '0';
|
||||
div.dataset.threadDepth = depth;
|
||||
div.dataset.parentId = opts.parentId || '';
|
||||
|
||||
div.innerHTML =
|
||||
threadLine +
|
||||
'<div class="comment-content">' +
|
||||
'<div class="comment-header lt-flex lt-flex-gap-sm lt-flex-align-center">' +
|
||||
'<div class="lt-avatar lt-avatar--xs ' + color + '" aria-hidden="true">' +
|
||||
avatarImg +
|
||||
'<span class="lt-avatar-initials">' + lt.escHtml(initials) + '</span>' +
|
||||
'</div>' +
|
||||
'<span class="comment-user lt-text-amber">' + lt.escHtml(opts.displayName) + '</span>' +
|
||||
'<span class="comment-date lt-text-xs lt-text-muted">' + lt.escHtml(opts.createdAt) + '</span>' +
|
||||
'<div class="comment-actions lt-btn-group">' + replyBtn + modBtns + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="comment-text" id="comment-text-' + opts.commentId + '"' +
|
||||
(opts.isMarkdown ? ' data-markdown data-rendered="1"' : '') + '>' +
|
||||
opts.commentText +
|
||||
'</div>' +
|
||||
'<textarea class="lt-input lt-textarea comment-edit-raw is-hidden"' +
|
||||
' id="comment-raw-' + opts.commentId + '" aria-hidden="true">' +
|
||||
lt.escHtml(opts.rawText) +
|
||||
'</textarea>' +
|
||||
'</div>';
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
function addComment() {
|
||||
const newComment = document.getElementById('newComment');
|
||||
if (!newComment) return;
|
||||
@@ -226,32 +301,19 @@ function addComment() {
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// Add new comment to the list (using safe DOM API to prevent XSS)
|
||||
// Add new comment to the list
|
||||
const commentsList = document.querySelector('.comments-list');
|
||||
|
||||
const commentDiv = document.createElement('div');
|
||||
commentDiv.className = 'comment';
|
||||
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.className = 'comment-header';
|
||||
|
||||
const userSpan = document.createElement('span');
|
||||
userSpan.className = 'comment-user';
|
||||
userSpan.textContent = data.user_name; // Safe - auto-escapes
|
||||
|
||||
const dateSpan = document.createElement('span');
|
||||
dateSpan.className = 'comment-date';
|
||||
dateSpan.textContent = data.created_at; // Safe - auto-escapes
|
||||
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.className = 'comment-text';
|
||||
textDiv.innerHTML = displayText; // displayText already sanitized above
|
||||
|
||||
headerDiv.appendChild(userSpan);
|
||||
headerDiv.appendChild(dateSpan);
|
||||
commentDiv.appendChild(headerDiv);
|
||||
commentDiv.appendChild(textDiv);
|
||||
|
||||
const commentDiv = buildCommentElement({
|
||||
commentId: data.comment_id,
|
||||
userId: data.user_id,
|
||||
displayName: data.user_name,
|
||||
createdAt: data.created_at,
|
||||
commentText: displayText,
|
||||
rawText: commentText,
|
||||
isMarkdown: isMarkdownEnabled,
|
||||
depth: 0,
|
||||
parentId: null,
|
||||
});
|
||||
commentsList.insertBefore(commentDiv, commentsList.firstChild);
|
||||
} else {
|
||||
lt.toast.error(data.error || 'Failed to add comment');
|
||||
@@ -1550,34 +1612,17 @@ function submitReply(parentCommentId) {
|
||||
}
|
||||
|
||||
// Create the new reply element
|
||||
const replyDiv = document.createElement('div');
|
||||
replyDiv.className = `comment thread-depth-${newDepth} comment-reply`;
|
||||
replyDiv.dataset.commentId = data.comment_id;
|
||||
replyDiv.dataset.markdownEnabled = isMarkdownEnabled ? '1' : '0';
|
||||
replyDiv.dataset.threadDepth = newDepth;
|
||||
replyDiv.dataset.parentId = parentCommentId;
|
||||
|
||||
replyDiv.innerHTML = `
|
||||
<div class="thread-line"></div>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<span class="comment-user">${lt.escHtml(data.user_name)}</span>
|
||||
<span class="comment-date">${lt.escHtml(data.created_at)}</span>
|
||||
<div class="comment-actions">
|
||||
${newDepth < 3 ? `<button type="button" class="comment-action-btn reply-btn" data-action="reply-comment" data-comment-id="${data.comment_id}" data-user="${lt.escHtml(data.user_name)}" title="Reply">↩</button>` : ''}
|
||||
<button type="button" class="comment-action-btn edit-btn" data-action="edit-comment" data-comment-id="${data.comment_id}" title="Edit">[ EDIT ]</button>
|
||||
<button type="button" class="comment-action-btn delete-btn" data-action="delete-comment" data-comment-id="${data.comment_id}" title="Delete">[ DEL ]</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-text" id="comment-text-${data.comment_id}" ${isMarkdownEnabled ? 'data-markdown' : ''}>
|
||||
${displayText}
|
||||
</div>
|
||||
<textarea class="comment-edit-raw is-hidden" id="comment-raw-${data.comment_id}">${lt.escHtml(commentText)}</textarea>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add animation
|
||||
replyDiv.classList.add('animate-fadein');
|
||||
const replyDiv = buildCommentElement({
|
||||
commentId: data.comment_id,
|
||||
userId: data.user_id,
|
||||
displayName: data.user_name,
|
||||
createdAt: data.created_at,
|
||||
commentText: displayText,
|
||||
rawText: commentText,
|
||||
isMarkdown: isMarkdownEnabled,
|
||||
depth: newDepth,
|
||||
parentId: parentCommentId,
|
||||
});
|
||||
repliesContainer.appendChild(replyDiv);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user