feat: Comment edit/delete, auto-link URLs, markdown tables, mobile fixes

- Add comment edit/delete functionality (owner or admin can modify)
- Add edit/delete buttons to comments in TicketView
- Create update_comment.php and delete_comment.php API endpoints
- Add updateComment() and deleteComment() methods to CommentModel
- Show "(edited)" indicator on modified comments
- Add migration script for updated_at column

- Auto-link URLs in plain text comments (non-markdown)
- Add markdown table support with proper HTML rendering
- Preserve code blocks during markdown parsing

- Fix mobile UI elements showing on desktop (add display:none defaults)
- Add mobile styles for CreateTicketView form elements
- Stack status-priority-row on mobile devices

- Update cache busters to v20260124e
- Update Claude.md and README.md documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-24 16:59:29 -05:00
parent 7ecb593c0f
commit 98db586bcf
14 changed files with 977 additions and 20 deletions

View File

@@ -1213,3 +1213,195 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
});
// ========================================
// Comment Edit/Delete Functions
// ========================================
/**
* Edit a comment
*/
function editComment(commentId) {
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
if (!commentDiv) return;
const textDiv = document.getElementById(`comment-text-${commentId}`);
const rawTextarea = document.getElementById(`comment-raw-${commentId}`);
if (!textDiv || !rawTextarea) return;
// Check if already in edit mode
if (commentDiv.classList.contains('editing')) {
cancelEditComment(commentId);
return;
}
// Get original text and markdown setting
const originalText = rawTextarea.value;
const markdownEnabled = commentDiv.dataset.markdownEnabled === '1';
// Create edit form
const editForm = document.createElement('div');
editForm.className = 'comment-edit-form';
editForm.id = `comment-edit-form-${commentId}`;
editForm.innerHTML = `
<textarea id="comment-edit-textarea-${commentId}" class="comment-edit-textarea">${escapeHtml(originalText)}</textarea>
<div class="comment-edit-controls">
<label class="markdown-toggle-small">
<input type="checkbox" id="comment-edit-markdown-${commentId}" ${markdownEnabled ? 'checked' : ''}>
Markdown
</label>
<div class="comment-edit-buttons">
<button type="button" class="btn btn-small" onclick="saveEditComment(${commentId})">Save</button>
<button type="button" class="btn btn-secondary btn-small" onclick="cancelEditComment(${commentId})">Cancel</button>
</div>
</div>
`;
// Hide original text, show edit form
textDiv.style.display = 'none';
textDiv.after(editForm);
commentDiv.classList.add('editing');
// Focus the textarea
document.getElementById(`comment-edit-textarea-${commentId}`).focus();
}
/**
* Save edited comment
*/
function saveEditComment(commentId) {
const textarea = document.getElementById(`comment-edit-textarea-${commentId}`);
const markdownCheckbox = document.getElementById(`comment-edit-markdown-${commentId}`);
if (!textarea) return;
const newText = textarea.value.trim();
if (!newText) {
showToast('Comment cannot be empty', 'error');
return;
}
const markdownEnabled = markdownCheckbox ? markdownCheckbox.checked : false;
// Send update request
fetch('/api/update_comment.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify({
comment_id: commentId,
comment_text: newText,
markdown_enabled: markdownEnabled
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the comment display
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
const textDiv = document.getElementById(`comment-text-${commentId}`);
const rawTextarea = document.getElementById(`comment-raw-${commentId}`);
const editForm = document.getElementById(`comment-edit-form-${commentId}`);
// Update raw text storage
rawTextarea.value = newText;
// Update markdown attribute
commentDiv.dataset.markdownEnabled = markdownEnabled ? '1' : '0';
// Update displayed text
if (markdownEnabled) {
textDiv.setAttribute('data-markdown', '');
textDiv.textContent = newText;
// Re-render markdown
if (typeof parseMarkdown === 'function') {
textDiv.innerHTML = parseMarkdown(newText);
}
} else {
textDiv.removeAttribute('data-markdown');
// Convert newlines to <br> and highlight mentions
let displayText = escapeHtml(newText).replace(/\n/g, '<br>');
displayText = highlightMentions(displayText);
// Auto-link URLs
if (typeof autoLinkUrls === 'function') {
displayText = autoLinkUrls(displayText);
}
textDiv.innerHTML = displayText;
}
// Remove edit form and show text
if (editForm) editForm.remove();
textDiv.style.display = '';
commentDiv.classList.remove('editing');
showToast('Comment updated successfully', 'success');
} else {
showToast(data.error || 'Failed to update comment', 'error');
}
})
.catch(error => {
console.error('Error updating comment:', error);
showToast('Failed to update comment', 'error');
});
}
/**
* Cancel editing a comment
*/
function cancelEditComment(commentId) {
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
const textDiv = document.getElementById(`comment-text-${commentId}`);
const editForm = document.getElementById(`comment-edit-form-${commentId}`);
if (editForm) editForm.remove();
if (textDiv) textDiv.style.display = '';
if (commentDiv) commentDiv.classList.remove('editing');
}
/**
* Delete a comment
*/
function deleteComment(commentId) {
if (!confirm('Are you sure you want to delete this comment? This cannot be undone.')) {
return;
}
fetch('/api/delete_comment.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.CSRF_TOKEN
},
body: JSON.stringify({
comment_id: commentId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the comment from the DOM
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
if (commentDiv) {
commentDiv.style.transition = 'opacity 0.3s, transform 0.3s';
commentDiv.style.opacity = '0';
commentDiv.style.transform = 'translateX(-20px)';
setTimeout(() => commentDiv.remove(), 300);
}
showToast('Comment deleted successfully', 'success');
} else {
showToast(data.error || 'Failed to delete comment', 'error');
}
})
.catch(error => {
console.error('Error deleting comment:', error);
showToast('Failed to delete comment', 'error');
});
}
// Expose functions globally
window.editComment = editComment;
window.saveEditComment = saveEditComment;
window.cancelEditComment = cancelEditComment;
window.deleteComment = deleteComment;