Fix bracket buttons rendering below text + UI/security improvements

CSS fixes:
- Fix [ ] brackets appearing below button text by replacing display:inline-flex
  with display:inline-block + white-space:nowrap on .btn — removes cross-browser
  flex pseudo-element inconsistency as root cause
- Remove conflicting .btn::before ripple block (position:absolute was overriding
  bracket content positioning)
- Remove overflow:hidden from .btn which was clipping bracket content
- Fix body::after duplicate rule causing GPU layer blink (second position:fixed
  rule re-created compositor layer, overriding display:none suppression)
- Replace all transition:all with scoped property transitions in dashboard.css,
  ticket.css, base.css (prevents full CSS property evaluation on every hover)
- Convert pulse-warning/pulse-critical keyframes from box-shadow to opacity
  animation (GPU-composited, eliminates CPU repaints at 60fps)
- Fix mobile *::before/*::after blanket content:none rule — now targets only
  decorative frame glyphs, preserving button brackets and status indicators
- Remove --terminal-green-dim override that broke .lt-btn hover backgrounds

JS fixes:
- Fix all lt.lt.toast.* double-prefix instances in dashboard.js
- Add null guard before .appendChild() on bulkAssignUser select
- Replace all remaining emoji with terminal bracket notation (dashboard.js,
  ticket.js, markdown.js)
- Migrate all toast.*() shim calls to lt.toast.* across all JS files

View fixes:
- Remove hardcoded [ ] brackets from .btn buttons (CSS now adds them)
- Replace all emoji with terminal bracket notation in all views and admin views
- Add missing CSP nonces to AuditLogView.php and UserActivityView.php script tags
- Bump CSS version strings to ?v=20260319b for cache busting

Security fixes:
- update_ticket.php: add authorization check (non-admins can only edit their own
  or assigned tickets)
- add_comment.php: validate and cast ticket_id to integer with 400 response
- clone_ticket.php: fix unconditional session_start(), add ticket ID validation,
  add internal ticket access check
- bulk_operation.php: add HTTP 401/403 status codes on auth failures
- upload_attachment.php: fix missing $conn arg in AttachmentModel constructor
- assign_ticket.php: add ticket existence check and permission verification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 22:20:43 -04:00
parent dd8833ee2f
commit 27075a62ee
23 changed files with 376 additions and 328 deletions

View File

@@ -148,7 +148,7 @@ async function saveCurrentFilter() {
'My Filter',
async (filterName) => {
if (!filterName || filterName.trim() === '') {
toast.warning('Filter name cannot be empty', 2000);
lt.toast.warning('Filter name cannot be empty', 2000);
return;
}

View File

@@ -58,7 +58,7 @@ function initMobileSidebar() {
const toggleBtn = document.createElement('button');
toggleBtn.id = 'mobileFilterToggle';
toggleBtn.className = 'mobile-filter-toggle';
toggleBtn.innerHTML = ' Filters & Search Options';
toggleBtn.innerHTML = '[ = ] Filters & Search Options';
toggleBtn.onclick = openMobileSidebar;
dashboardMain.insertBefore(toggleBtn, dashboardMain.firstChild);
}
@@ -79,20 +79,20 @@ function initMobileSidebar() {
nav.className = 'mobile-bottom-nav';
nav.innerHTML = `
<a href="/">
<span class="nav-icon">🏠</span>
<span class="nav-label">Home</span>
<span class="nav-icon">[ ~ ]</span>
<span class="nav-label">HOME</span>
</a>
<button type="button" data-action="open-mobile-sidebar">
<span class="nav-icon">🔍</span>
<span class="nav-label">Filter</span>
<span class="nav-icon">[ / ]</span>
<span class="nav-label">FILTER</span>
</button>
<a href="/ticket/create">
<span class="nav-icon"></span>
<span class="nav-label">New</span>
<span class="nav-icon">[ + ]</span>
<span class="nav-label">NEW</span>
</a>
<button type="button" data-action="open-settings-modal">
<span class="nav-icon"></span>
<span class="nav-label">Settings</span>
<span class="nav-icon">[ * ]</span>
<span class="nav-label">CFG</span>
</button>
`;
document.body.appendChild(nav);
@@ -566,11 +566,11 @@ function quickSave() {
}
} else {
toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
lt.toast.error('Error updating ticket: ' + (result.error || 'Unknown error'), 5000);
}
})
.catch(error => {
toast.error('Error updating ticket: ' + error.message, 5000);
lt.toast.error('Error updating ticket: ' + error.message, 5000);
});
}
@@ -671,11 +671,11 @@ function loadTemplate() {
document.getElementById('priority').value = template.default_priority;
}
} else {
toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
lt.toast.error('Failed to load template: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
toast.error('Error loading template: ' + error.message, 4000);
lt.toast.error('Error loading template: ' + error.message, 4000);
});
}
@@ -752,7 +752,7 @@ function bulkClose() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
toast.warning('No tickets selected', 2000);
lt.toast.warning('No tickets selected', 2000);
return;
}
@@ -782,17 +782,17 @@ function performBulkCloseAction(ticketIds) {
.then(data => {
if (data.success) {
if (data.failed > 0) {
toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000);
lt.toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000);
} else {
toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000);
lt.toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000);
}
setTimeout(() => window.location.reload(), 1500);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
}
})
.catch(error => {
toast.error('Bulk close failed: ' + error.message, 5000);
lt.toast.error('Bulk close failed: ' + error.message, 5000);
});
}
@@ -800,7 +800,7 @@ function showBulkAssignModal() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
toast.warning('No tickets selected', 2000);
lt.toast.warning('No tickets selected', 2000);
return;
}
@@ -834,12 +834,14 @@ function showBulkAssignModal() {
.then(data => {
if (data.success && data.users) {
const select = document.getElementById('bulkAssignUser');
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.user_id;
option.textContent = user.display_name || user.username;
select.appendChild(option);
});
if (select) {
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.user_id;
option.textContent = user.display_name || user.username;
select.appendChild(option);
});
}
}
})
.catch(() => lt.toast.error('Error loading users'));
@@ -856,7 +858,7 @@ function performBulkAssign() {
const ticketIds = getSelectedTicketIds();
if (!userId) {
toast.warning('Please select a user', 2000);
lt.toast.warning('Please select a user', 2000);
return;
}
@@ -878,17 +880,17 @@ function performBulkAssign() {
if (data.success) {
closeBulkAssignModal();
if (data.failed > 0) {
toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000);
lt.toast.warning(`Bulk assign: ${data.processed} succeeded, ${data.failed} failed`, 5000);
} else {
toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000);
lt.toast.success(`Successfully assigned ${data.processed} ticket(s)`, 4000);
}
setTimeout(() => window.location.reload(), 1500);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
}
})
.catch(error => {
toast.error('Bulk assign failed: ' + error.message, 5000);
lt.toast.error('Bulk assign failed: ' + error.message, 5000);
});
}
@@ -896,7 +898,7 @@ function showBulkPriorityModal() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
toast.warning('No tickets selected', 2000);
lt.toast.warning('No tickets selected', 2000);
return;
}
@@ -941,7 +943,7 @@ function performBulkPriority() {
const ticketIds = getSelectedTicketIds();
if (!priority) {
toast.warning('Please select a priority', 2000);
lt.toast.warning('Please select a priority', 2000);
return;
}
@@ -963,17 +965,17 @@ function performBulkPriority() {
if (data.success) {
closeBulkPriorityModal();
if (data.failed > 0) {
toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
lt.toast.warning(`Priority update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
} else {
toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000);
lt.toast.success(`Successfully updated priority for ${data.processed} ticket(s)`, 4000);
}
setTimeout(() => window.location.reload(), 1500);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
}
})
.catch(error => {
toast.error('Bulk priority update failed: ' + error.message, 5000);
lt.toast.error('Bulk priority update failed: ' + error.message, 5000);
});
}
@@ -1013,7 +1015,7 @@ function showBulkStatusModal() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
toast.warning('No tickets selected', 2000);
lt.toast.warning('No tickets selected', 2000);
return;
}
@@ -1057,7 +1059,7 @@ function performBulkStatusChange() {
const ticketIds = getSelectedTicketIds();
if (!status) {
toast.warning('Please select a status', 2000);
lt.toast.warning('Please select a status', 2000);
return;
}
@@ -1079,17 +1081,17 @@ function performBulkStatusChange() {
closeBulkStatusModal();
if (data.success) {
if (data.failed > 0) {
toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
lt.toast.warning(`Status update: ${data.processed} succeeded, ${data.failed} failed`, 5000);
} else {
toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000);
lt.toast.success(`Successfully updated status for ${data.processed} ticket(s)`, 4000);
}
setTimeout(() => window.location.reload(), 1500);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
}
})
.catch(error => {
toast.error('Bulk status change failed: ' + error.message, 5000);
lt.toast.error('Bulk status change failed: ' + error.message, 5000);
});
}
@@ -1098,7 +1100,7 @@ function showBulkDeleteModal() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
toast.warning('No tickets selected', 2000);
lt.toast.warning('No tickets selected', 2000);
return;
}
@@ -1106,7 +1108,7 @@ function showBulkDeleteModal() {
<div class="lt-modal-overlay" id="bulkDeleteModal" aria-hidden="true">
<div class="lt-modal">
<div class="lt-modal-header" style="color: var(--status-closed);">
<span class="lt-modal-title">⚠ Delete ${ticketIds.length} Ticket(s)</span>
<span class="lt-modal-title">[ ! ] DELETE ${ticketIds.length} TICKET(S)</span>
<button class="lt-modal-close" data-modal-close>✕</button>
</div>
<div class="lt-modal-body" style="text-align:center;">
@@ -1150,14 +1152,14 @@ function performBulkDelete() {
.then(data => {
closeBulkDeleteModal();
if (data.success) {
toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000);
lt.toast.success(`Successfully deleted ${ticketIds.length} ticket(s)`, 4000);
setTimeout(() => window.location.reload(), 1500);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 5000);
}
})
.catch(error => {
toast.error('Bulk delete failed: ' + error.message, 5000);
lt.toast.error('Bulk delete failed: ' + error.message, 5000);
});
}
@@ -1186,15 +1188,15 @@ function showConfirmModal(title, message, type = 'warning', onConfirm, onCancel
// Icon based on type
const icons = {
warning: '',
error: '✗',
info: ''
warning: '[ ! ]',
error: '[ X ]',
info: '[ i ]',
};
const icon = icons[type] || icons.warning;
// Escape user-provided content to prevent XSS
const safeTitle = escapeHtml(title);
const safeMessage = escapeHtml(message);
const safeTitle = lt.escHtml(title);
const safeMessage = lt.escHtml(message);
const modalHtml = `
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
@@ -1243,9 +1245,9 @@ function showInputModal(title, label, placeholder = '', onSubmit, onCancel = nul
const inputId = modalId + '_input';
// Escape user-provided content to prevent XSS
const safeTitle = escapeHtml(title);
const safeLabel = escapeHtml(label);
const safePlaceholder = escapeHtml(placeholder);
const safeTitle = lt.escHtml(title);
const safeLabel = lt.escHtml(label);
const safePlaceholder = lt.escHtml(placeholder);
const modalHtml = `
<div class="lt-modal-overlay" id="${modalId}" aria-hidden="true">
@@ -1307,8 +1309,8 @@ function quickStatusChange(ticketId, currentStatus) {
<button class="lt-modal-close" data-modal-close>✕</button>
</div>
<div class="lt-modal-body">
<p style="margin-bottom:0.5rem;">Ticket #${escapeHtml(ticketId)}</p>
<p style="margin-bottom:0.5rem;color:var(--terminal-amber);">Current: ${escapeHtml(currentStatus)}</p>
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
<p style="margin-bottom:0.5rem;color:var(--terminal-amber);">Current: ${lt.escHtml(currentStatus)}</p>
<label for="quickStatusSelect">New Status:</label>
<select id="quickStatusSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
${otherStatuses.map(s => `<option value="${s}">${s}</option>`).join('')}
@@ -1351,15 +1353,15 @@ function performQuickStatusChange(ticketId) {
.then(data => {
closeQuickStatusModal();
if (data.success) {
toast.success(`Status updated to ${newStatus}`, 3000);
lt.toast.success(`Status updated to ${newStatus}`, 3000);
setTimeout(() => window.location.reload(), 1000);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
closeQuickStatusModal();
toast.error('Error updating status', 4000);
lt.toast.error('Error updating status', 4000);
});
}
@@ -1375,7 +1377,7 @@ function quickAssign(ticketId) {
<button class="lt-modal-close" data-modal-close>✕</button>
</div>
<div class="lt-modal-body">
<p style="margin-bottom:0.5rem;">Ticket #${escapeHtml(ticketId)}</p>
<p style="margin-bottom:0.5rem;">Ticket #${lt.escHtml(ticketId)}</p>
<label for="quickAssignSelect">Assign to:</label>
<select id="quickAssignSelect" class="lt-select" style="width:100%;margin-top:0.5rem;">
<option value="">Unassigned</option>
@@ -1433,15 +1435,15 @@ function performQuickAssign(ticketId) {
.then(data => {
closeQuickAssignModal();
if (data.success) {
toast.success('Assignment updated', 3000);
lt.toast.success('Assignment updated', 3000);
setTimeout(() => window.location.reload(), 1000);
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
closeQuickAssignModal();
toast.error('Error updating assignment', 4000);
lt.toast.error('Error updating assignment', 4000);
});
}
@@ -1522,13 +1524,13 @@ function populateKanbanCards() {
card.onclick = () => window.location.href = `/ticket/${ticketId}`;
card.innerHTML = `
<div class="card-header">
<span class="card-id">#${escapeHtml(ticketId)}</span>
<span class="card-id">#${lt.escHtml(ticketId)}</span>
<span class="card-priority p${priority}">P${priority}</span>
</div>
<div class="card-title">${escapeHtml(title)}</div>
<div class="card-title">${lt.escHtml(title)}</div>
<div class="card-footer">
<span class="card-category">${escapeHtml(category)}</span>
<span class="card-assignee" title="${escapeHtml(assignedTo)}">${escapeHtml(initials)}</span>
<span class="card-category">${lt.escHtml(category)}</span>
<span class="card-assignee" title="${lt.escHtml(assignedTo)}">${lt.escHtml(initials)}</span>
</div>
`;
column.appendChild(card);
@@ -1615,17 +1617,17 @@ function showTicketPreview(event) {
// Build preview content
currentPreview.innerHTML = `
<div class="preview-header">
<span class="preview-id">#${escapeHtml(ticketId)}</span>
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${escapeHtml(status)}</span>
<span class="preview-id">#${lt.escHtml(ticketId)}</span>
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${lt.escHtml(status)}</span>
</div>
<div class="preview-title">${escapeHtml(title)}</div>
<div class="preview-title">${lt.escHtml(title)}</div>
<div class="preview-meta">
<div><strong>Priority:</strong> P${escapeHtml(priority)}</div>
<div><strong>Category:</strong> ${escapeHtml(category)}</div>
<div><strong>Type:</strong> ${escapeHtml(type)}</div>
<div><strong>Assigned:</strong> ${escapeHtml(assignedTo)}</div>
<div><strong>Priority:</strong> P${lt.escHtml(priority)}</div>
<div><strong>Category:</strong> ${lt.escHtml(category)}</div>
<div><strong>Type:</strong> ${lt.escHtml(type)}</div>
<div><strong>Assigned:</strong> ${lt.escHtml(assignedTo)}</div>
</div>
<div class="preview-footer">Created by ${escapeHtml(createdBy)}</div>
<div class="preview-footer">Created by ${lt.escHtml(createdBy)}</div>
`;
// Position the preview
@@ -1697,7 +1699,7 @@ function exportSelectedTickets(format) {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
toast.warning('No tickets selected', 2000);
lt.toast.warning('No tickets selected', 2000);
return;
}

View File

@@ -364,7 +364,7 @@ function createEditorToolbar(textareaId, containerId) {
<button type="button" data-toolbar-action="list" data-textarea="${textareaId}" title="List">≡</button>
<button type="button" data-toolbar-action="quote" data-textarea="${textareaId}" title="Quote">"</button>
<span class="toolbar-separator"></span>
<button type="button" data-toolbar-action="link" data-textarea="${textareaId}" title="Link">🔗</button>
<button type="button" data-toolbar-action="link" data-textarea="${textareaId}" title="Link">[ @ ]</button>
`;
// Add event delegation for toolbar buttons

View File

@@ -77,7 +77,7 @@ function saveTicket() {
statusDisplay.className = `status-${data.status}`;
statusDisplay.textContent = data.status;
}
toast.success('Ticket updated successfully');
lt.toast.success('Ticket updated successfully');
} else {
}
})
@@ -317,11 +317,11 @@ function handleAssignmentChange() {
.then(response => response.json())
.then(data => {
if (!data.success) {
toast.error('Error updating assignment');
lt.toast.error('Error updating assignment');
}
})
.catch(error => {
toast.error('Error updating assignment: ' + error.message);
lt.toast.error('Error updating assignment: ' + error.message);
});
});
}
@@ -353,7 +353,7 @@ function handleMetadataChanges() {
.then(response => response.json())
.then(data => {
if (!data.success) {
toast.error(`Error updating ${fieldName}`);
lt.toast.error(`Error updating ${fieldName}`);
} else {
// Update window.ticketData
window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue;
@@ -375,7 +375,7 @@ function handleMetadataChanges() {
}
})
.catch(error => {
toast.error(`Error updating ${fieldName}: ` + error.message);
lt.toast.error(`Error updating ${fieldName}: ` + error.message);
});
}
@@ -492,13 +492,13 @@ function performStatusChange(statusSelect, selectedOption, newStatus) {
window.location.reload();
}, 500);
} else {
toast.error('Error updating status: ' + (data.error || 'Unknown error'));
lt.toast.error('Error updating status: ' + (data.error || 'Unknown error'));
// Reset to current status
statusSelect.selectedIndex = 0;
}
})
.catch(error => {
toast.error('Error updating status: ' + error.message);
lt.toast.error('Error updating status: ' + error.message);
// Reset to current status
statusSelect.selectedIndex = 0;
});
@@ -587,10 +587,10 @@ function showDependencyError(message) {
const dependentsList = document.getElementById('dependentsList');
if (dependenciesList) {
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
}
if (dependentsList) {
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${escapeHtml(message)}</p>`;
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
}
}
@@ -618,11 +618,11 @@ function renderDependencies(dependencies) {
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
<div>
<a href="/ticket/${escapeHtml(dep.depends_on_id)}" style="color: var(--terminal-green);">
#${escapeHtml(dep.depends_on_id)}
<a href="/ticket/${lt.escHtml(dep.depends_on_id)}" style="color: var(--terminal-green);">
#${lt.escHtml(dep.depends_on_id)}
</a>
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
</div>
<button data-action="remove-dependency" data-dependency-id="${dep.dependency_id}" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
</div>`;
@@ -653,12 +653,12 @@ function renderDependents(dependents) {
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
<div>
<a href="/ticket/${escapeHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
#${escapeHtml(dep.ticket_id)}
<a href="/ticket/${lt.escHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
#${lt.escHtml(dep.ticket_id)}
</a>
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${escapeHtml(dep.dependency_type)})</span>
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${lt.escHtml(dep.dependency_type)})</span>
</div>
</div>`;
});
@@ -672,7 +672,7 @@ function addDependency() {
const dependencyType = document.getElementById('dependencyType').value;
if (!dependsOnId) {
toast.warning('Please enter a ticket ID', 3000);
lt.toast.warning('Please enter a ticket ID', 3000);
return;
}
@@ -692,15 +692,15 @@ function addDependency() {
.then(response => response.json())
.then(data => {
if (data.success) {
toast.success('Dependency added', 3000);
lt.toast.success('Dependency added', 3000);
document.getElementById('dependencyTicketId').value = '';
loadDependencies();
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
toast.error('Error adding dependency', 4000);
lt.toast.error('Error adding dependency', 4000);
});
}
@@ -723,14 +723,14 @@ function removeDependency(dependencyId) {
.then(response => response.json())
.then(data => {
if (data.success) {
toast.success('Dependency removed', 3000);
lt.toast.success('Dependency removed', 3000);
loadDependencies();
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
toast.error('Error removing dependency', 4000);
lt.toast.error('Error removing dependency', 4000);
});
}
@@ -828,18 +828,18 @@ function handleFileUpload(files) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
if (uploadedCount === totalFiles) {
toast.success(`${totalFiles} file(s) uploaded successfully`, 3000);
lt.toast.success(`${totalFiles} file(s) uploaded successfully`, 3000);
loadAttachments();
resetUploadUI();
}
} else {
toast.error(`Error uploading ${file.name}: ${response.error}`, 4000);
lt.toast.error(`Error uploading ${file.name}: ${response.error}`, 4000);
}
} catch (e) {
toast.error(`Error parsing response for ${file.name}`, 4000);
lt.toast.error(`Error parsing response for ${file.name}`, 4000);
}
} else {
toast.error(`Error uploading ${file.name}: Server error`, 4000);
lt.toast.error(`Error uploading ${file.name}: Server error`, 4000);
}
if (uploadedCount === totalFiles) {
@@ -849,7 +849,7 @@ function handleFileUpload(files) {
xhr.addEventListener('error', () => {
uploadedCount++;
toast.error(`Error uploading ${file.name}: Network error`, 4000);
lt.toast.error(`Error uploading ${file.name}: Network error`, 4000);
if (uploadedCount === totalFiles) {
setTimeout(resetUploadUI, 2000);
}
@@ -914,15 +914,15 @@ function renderAttachments(attachments) {
});
html += `<div class="attachment-item" data-id="${att.attachment_id}">
<div class="attachment-icon">${escapeHtml(att.icon || '📎')}</div>
<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>
<div class="attachment-info">
<div class="attachment-name" title="${escapeHtml(att.original_filename)}">
<div class="attachment-name" title="${lt.escHtml(att.original_filename)}">
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank" style="color: var(--terminal-green);">
${escapeHtml(att.original_filename)}
${lt.escHtml(att.original_filename)}
</a>
</div>
<div class="attachment-meta">
${escapeHtml(att.file_size_formatted || formatFileSize(att.file_size))}${escapeHtml(uploaderName)}${escapeHtml(uploadDate)}
${lt.escHtml(att.file_size_formatted || formatFileSize(att.file_size))}${lt.escHtml(uploaderName)}${lt.escHtml(uploadDate)}
</div>
</div>
<div class="attachment-actions">
@@ -968,14 +968,14 @@ function deleteAttachment(attachmentId) {
.then(response => response.json())
.then(data => {
if (data.success) {
toast.success('Attachment deleted', 3000);
lt.toast.success('Attachment deleted', 3000);
loadAttachments();
} else {
toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
}
})
.catch(error => {
toast.error('Error deleting attachment', 4000);
lt.toast.error('Error deleting attachment', 4000);
});
}
@@ -1118,9 +1118,9 @@ function showMentionSuggestions(query, textarea) {
let html = '';
filtered.forEach((user, index) => {
const isSelected = index === 0 ? 'selected' : '';
html += `<div class="mention-option ${isSelected}" data-username="${escapeHtml(user.username)}" data-action="select-mention">
<span class="mention-username">@${escapeHtml(user.username)}</span>
${user.display_name ? `<span class="mention-displayname">${escapeHtml(user.display_name)}</span>` : ''}
html += `<div class="mention-option ${isSelected}" data-username="${lt.escHtml(user.username)}" data-action="select-mention">
<span class="mention-username">@${lt.escHtml(user.username)}</span>
${user.display_name ? `<span class="mention-displayname">${lt.escHtml(user.display_name)}</span>` : ''}
</div>`;
});
@@ -1252,7 +1252,7 @@ function editComment(commentId) {
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>
<textarea id="comment-edit-textarea-${commentId}" class="comment-edit-textarea">${lt.escHtml(originalText)}</textarea>
<div class="comment-edit-controls">
<label class="markdown-toggle-small">
<input type="checkbox" id="comment-edit-markdown-${commentId}" ${markdownEnabled ? 'checked' : ''}>
@@ -1331,7 +1331,7 @@ function saveEditComment(commentId) {
} else {
textDiv.removeAttribute('data-markdown');
// Convert newlines to <br> and highlight mentions
let displayText = escapeHtml(newText).replace(/\n/g, '<br>');
let displayText = lt.escHtml(newText).replace(/\n/g, '<br>');
displayText = highlightMentions(displayText);
// Auto-link URLs
if (typeof autoLinkUrls === 'function') {
@@ -1543,8 +1543,8 @@ function submitReply(parentCommentId) {
<span class="comment-date">${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="${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">✏️</button>
<button type="button" class="comment-action-btn delete-btn" data-action="delete-comment" data-comment-id="${data.comment_id}" title="Delete">🗑️</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' : ''}>