fix: deep audit — wire TDS v1.2 components, fix kanban/tabs/bulk/avatar
- ticket.js: fix showTab() early return preventing attachments/deps from loading - ticket.js: fix performStatusChange() overwriting lt-status-* classes - dashboard.js: fix updateSelectionCount() using is-visible instead of style.display - dashboard.js: fix populateKanbanCards() to use #kanban-col-* IDs (TDS v1.2) - dashboard.js: fix setViewMode() removing references to old non-TDS elements - dashboard.js: remove mobile-bottom-nav injection (no CSS existed for it) - dashboard.css: add full lt-kanban-card component styles with priority accents - dashboard.css: add mobile sidebar overlay, filter toggle, ticket preview popup CSS - DashboardView.php: replace priority badges with lt-chip component - TicketView.php: add lt-avatar with initials to comment author display - ApiKeysView.php: enhance API usage section with lt-code-block component + curl example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+105
-3
@@ -204,6 +204,108 @@ kbd {
|
||||
/* ── lt-msg variants ─────────────────────────────────────────── */
|
||||
.lt-mb-md { margin-bottom: 1rem; }
|
||||
|
||||
/* ── Kanban cards ────────────────────────────────────────────── */
|
||||
.lt-kanban-card {
|
||||
padding: 0.6rem 0.75rem;
|
||||
border: 1px solid rgba(0, 255, 65, 0.2);
|
||||
margin-bottom: 0.4rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.lt-kanban-card:hover,
|
||||
.lt-kanban-card:focus-visible {
|
||||
border-color: var(--lt-text-primary, #00ff41);
|
||||
background: rgba(0, 255, 65, 0.04);
|
||||
outline: none;
|
||||
}
|
||||
.lt-kanban-card--p1 { border-left: 2px solid var(--lt-danger, #ff4d4d); }
|
||||
.lt-kanban-card--p2 { border-left: 2px solid var(--lt-amber, #ffb000); }
|
||||
.lt-kanban-card--p3 { border-left: 2px solid var(--lt-cyan, #00ffff); }
|
||||
.lt-kanban-card--p4 { border-left: 2px solid rgba(0, 255, 65, 0.4); }
|
||||
.lt-kanban-card--p5 { border-left: 2px solid rgba(0, 255, 65, 0.2); }
|
||||
|
||||
.lt-kanban-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.lt-kanban-card-title {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
.lt-kanban-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.lt-kanban-assignee {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(0, 255, 65, 0.35);
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Mobile sidebar overlay ──────────────────────────────────── */
|
||||
.mobile-sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 199;
|
||||
}
|
||||
.mobile-sidebar-overlay.active { display: block; }
|
||||
|
||||
/* ── Mobile filter toggle button ─────────────────────────────── */
|
||||
.mobile-filter-toggle {
|
||||
display: none;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(0, 255, 65, 0.3);
|
||||
color: var(--lt-text-primary, #00ff41);
|
||||
font-family: inherit;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ── Ticket preview popup ────────────────────────────────────── */
|
||||
.ticket-preview-popup {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: var(--lt-surface, #0a0e14);
|
||||
border: 1px solid rgba(0, 255, 65, 0.4);
|
||||
padding: 0.75rem;
|
||||
min-width: 280px;
|
||||
max-width: 360px;
|
||||
font-size: 0.75rem;
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
.ticket-preview-popup .preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.ticket-preview-popup .preview-id { color: var(--lt-cyan, #00ffff); font-weight: 700; }
|
||||
.ticket-preview-popup .preview-title { font-weight: 600; margin-bottom: 0.4rem; }
|
||||
.ticket-preview-popup .preview-meta { opacity: 0.7; display: flex; flex-direction: column; gap: 0.1rem; }
|
||||
.ticket-preview-popup .preview-footer { margin-top: 0.4rem; opacity: 0.5; font-size: 0.65rem; }
|
||||
|
||||
/* ── Responsive ──────────────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.lt-page-header {
|
||||
@@ -214,10 +316,10 @@ kbd {
|
||||
.lt-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.mobile-filter-toggle { display: block; }
|
||||
.lt-sidebar.mobile-open { transform: translateX(0); }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.lt-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.lt-stats-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
+115
-91
@@ -72,31 +72,6 @@ function initMobileSidebar() {
|
||||
sidebar.insertBefore(closeBtn, sidebar.firstChild);
|
||||
}
|
||||
|
||||
// Create mobile bottom navigation
|
||||
if (!document.getElementById('mobileBottomNav')) {
|
||||
const nav = document.createElement('nav');
|
||||
nav.id = 'mobileBottomNav';
|
||||
nav.className = 'mobile-bottom-nav';
|
||||
nav.innerHTML = `
|
||||
<a href="/">
|
||||
<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>
|
||||
</button>
|
||||
<a href="/ticket/create">
|
||||
<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">CFG</span>
|
||||
</button>
|
||||
`;
|
||||
document.body.appendChild(nav);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore sidebar state on page load
|
||||
@@ -143,7 +118,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const action = target.dataset.action;
|
||||
switch (action) {
|
||||
// Bulk operations
|
||||
// Navigation
|
||||
case 'navigate':
|
||||
if (target.dataset.url) window.location.href = target.dataset.url;
|
||||
break;
|
||||
case 'view-ticket':
|
||||
if (target.dataset.ticketId) window.location.href = '/ticket/' + target.dataset.ticketId;
|
||||
break;
|
||||
// Bulk action triggers (show modals)
|
||||
case 'bulk-status':
|
||||
if (typeof showBulkStatusModal === 'function') showBulkStatusModal();
|
||||
break;
|
||||
case 'bulk-assign':
|
||||
if (typeof showBulkAssignModal === 'function') showBulkAssignModal();
|
||||
break;
|
||||
case 'bulk-priority':
|
||||
if (typeof showBulkPriorityModal === 'function') showBulkPriorityModal();
|
||||
break;
|
||||
case 'bulk-delete':
|
||||
if (typeof bulkDelete === 'function') bulkDelete();
|
||||
break;
|
||||
case 'clear-selection':
|
||||
clearSelection();
|
||||
break;
|
||||
// Bulk operation perform actions
|
||||
case 'perform-bulk-assign':
|
||||
performBulkAssign();
|
||||
break;
|
||||
@@ -168,26 +166,63 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
case 'close-bulk-delete-modal':
|
||||
closeBulkDeleteModal();
|
||||
break;
|
||||
// Checkbox selection
|
||||
case 'toggle-select-all':
|
||||
toggleSelectAll();
|
||||
break;
|
||||
case 'update-selection':
|
||||
updateSelectionCount();
|
||||
break;
|
||||
case 'toggle-row-checkbox':
|
||||
toggleRowCheckbox(e, target);
|
||||
break;
|
||||
// Quick actions
|
||||
case 'quick-status':
|
||||
quickStatusChange(target.dataset.ticketId, target.dataset.status);
|
||||
break;
|
||||
case 'perform-quick-status':
|
||||
performQuickStatusChange(target.dataset.ticketId);
|
||||
break;
|
||||
case 'close-quick-status-modal':
|
||||
closeQuickStatusModal();
|
||||
break;
|
||||
case 'quick-assign':
|
||||
quickAssign(target.dataset.ticketId);
|
||||
break;
|
||||
case 'perform-quick-assign':
|
||||
performQuickAssign(target.dataset.ticketId);
|
||||
break;
|
||||
case 'close-quick-assign-modal':
|
||||
closeQuickAssignModal();
|
||||
break;
|
||||
// View mode toggle
|
||||
case 'set-view-mode':
|
||||
if (target.dataset.mode === 'card') populateKanbanCards();
|
||||
break;
|
||||
// Settings
|
||||
case 'open-settings':
|
||||
case 'open-settings-modal':
|
||||
if (typeof openSettingsModal === 'function') openSettingsModal();
|
||||
break;
|
||||
// Refresh
|
||||
case 'manual-refresh':
|
||||
window.location.reload();
|
||||
break;
|
||||
// Export
|
||||
case 'toggle-export-menu':
|
||||
toggleExportMenu(e);
|
||||
break;
|
||||
case 'export-tickets':
|
||||
exportSelectedTickets(target.dataset.format);
|
||||
break;
|
||||
// Advanced search
|
||||
case 'open-advanced-search':
|
||||
if (typeof openAdvancedSearch === 'function') openAdvancedSearch();
|
||||
break;
|
||||
// Mobile navigation
|
||||
case 'open-mobile-sidebar':
|
||||
if (typeof openMobileSidebar === 'function') openMobileSidebar();
|
||||
break;
|
||||
case 'open-settings-modal':
|
||||
if (typeof openSettingsModal === 'function') openSettingsModal();
|
||||
break;
|
||||
// Filter badge actions
|
||||
case 'remove-filter':
|
||||
removeFilter(target.dataset.filterType, target.dataset.filterValue);
|
||||
@@ -682,14 +717,14 @@ function updateSelectionCount() {
|
||||
const exportDropdown = document.getElementById('exportDropdown');
|
||||
const exportCount = document.getElementById('exportCount');
|
||||
|
||||
if (toolbar && countDisplay) {
|
||||
toolbar.classList.toggle('is-visible', count > 0);
|
||||
if (count > 0) countDisplay.textContent = count;
|
||||
if (toolbar) {
|
||||
toolbar.style.display = count > 0 ? 'flex' : 'none';
|
||||
if (count > 0 && countDisplay) countDisplay.textContent = count;
|
||||
}
|
||||
|
||||
// Show/hide export dropdown based on selection
|
||||
if (exportDropdown) {
|
||||
exportDropdown.classList.toggle('is-visible', count > 0);
|
||||
exportDropdown.style.display = count > 0 ? 'inline-flex' : 'none';
|
||||
if (count > 0 && exportCount) exportCount.textContent = count;
|
||||
}
|
||||
}
|
||||
@@ -1278,27 +1313,10 @@ function performQuickAssign(ticketId) {
|
||||
* Set the view mode (table or card)
|
||||
*/
|
||||
function setViewMode(mode) {
|
||||
const tableView = document.querySelector('.ascii-frame-outer');
|
||||
const cardView = document.getElementById('cardView');
|
||||
const tableBtn = document.getElementById('tableViewBtn');
|
||||
const cardBtn = document.getElementById('cardViewBtn');
|
||||
|
||||
if (!tableView || !cardView) return;
|
||||
|
||||
// TDS v1.2 uses lt.tabs for show/hide; we just need to populate kanban cards
|
||||
if (mode === 'card') {
|
||||
tableView.classList.add('is-hidden');
|
||||
cardView.classList.remove('is-hidden');
|
||||
tableBtn.classList.remove('active');
|
||||
cardBtn.classList.add('active');
|
||||
populateKanbanCards();
|
||||
} else {
|
||||
tableView.classList.remove('is-hidden');
|
||||
cardView.classList.add('is-hidden');
|
||||
tableBtn.classList.add('active');
|
||||
cardBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
// Store preference
|
||||
localStorage.setItem('ticketViewMode', mode);
|
||||
}
|
||||
|
||||
@@ -1306,18 +1324,17 @@ function setViewMode(mode) {
|
||||
* Populate Kanban cards from table data
|
||||
*/
|
||||
function populateKanbanCards() {
|
||||
const rows = document.querySelectorAll('tbody tr');
|
||||
const rows = document.querySelectorAll('#tickets-table tbody tr');
|
||||
// TDS v1.2 kanban columns use id="kanban-col-{slug}" with .kanban-cards child
|
||||
const columns = {
|
||||
'Open': document.querySelector('.kanban-column[data-status="Open"] .kanban-cards'),
|
||||
'Pending': document.querySelector('.kanban-column[data-status="Pending"] .kanban-cards'),
|
||||
'In Progress': document.querySelector('.kanban-column[data-status="In Progress"] .kanban-cards'),
|
||||
'Closed': document.querySelector('.kanban-column[data-status="Closed"] .kanban-cards')
|
||||
'Open': document.getElementById('kanban-col-open'),
|
||||
'Pending': document.getElementById('kanban-col-pending'),
|
||||
'In Progress': document.getElementById('kanban-col-inprogress'),
|
||||
'Closed': document.getElementById('kanban-col-closed'),
|
||||
};
|
||||
|
||||
// Clear existing cards
|
||||
Object.values(columns).forEach(col => {
|
||||
if (col) col.innerHTML = '';
|
||||
});
|
||||
Object.values(columns).forEach(col => { if (col) col.innerHTML = ''; });
|
||||
|
||||
const counts = { 'Open': 0, 'Pending': 0, 'In Progress': 0, 'Closed': 0 };
|
||||
const isAdmin = document.getElementById('selectAllCheckbox') !== null;
|
||||
@@ -1325,53 +1342,60 @@ function populateKanbanCards() {
|
||||
|
||||
rows.forEach(row => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
if (cells.length < 6) return; // Skip empty rows
|
||||
if (cells.length < 6) return;
|
||||
|
||||
const ticketId = cells[0 + offset]?.querySelector('.ticket-link')?.textContent.trim() || '';
|
||||
const priority = cells[1 + offset]?.textContent.trim() || '';
|
||||
const title = cells[2 + offset]?.textContent.trim() || '';
|
||||
const category = cells[3 + offset]?.textContent.trim() || '';
|
||||
const status = cells[5 + offset]?.textContent.trim() || '';
|
||||
const ticketId = cells[0 + offset]?.querySelector('.ticket-link')?.textContent.trim() || '';
|
||||
const priorityEl = cells[1 + offset]?.querySelector('[class*="lt-p"]');
|
||||
const priority = priorityEl ? priorityEl.textContent.trim().replace('P','') : cells[1 + offset]?.textContent.trim() || '4';
|
||||
const title = cells[2 + offset]?.textContent.trim() || '';
|
||||
const category = cells[3 + offset]?.textContent.trim() || '';
|
||||
const statusEl = cells[5 + offset]?.querySelector('.lt-status');
|
||||
const status = statusEl ? statusEl.textContent.trim() : cells[5 + offset]?.textContent.trim() || '';
|
||||
const assignedTo = cells[7 + offset]?.textContent.trim() || 'Unassigned';
|
||||
|
||||
// Get initials for assignee
|
||||
const initials = assignedTo === 'Unassigned' ? '?' :
|
||||
assignedTo.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
const initials = assignedTo === 'Unassigned' ? '?'
|
||||
: assignedTo.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
|
||||
const column = columns[status];
|
||||
if (column) {
|
||||
counts[status]++;
|
||||
if (!column || !ticketId) return;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = `kanban-card priority-${priority}`;
|
||||
card.onclick = () => window.location.href = `/ticket/${ticketId}`;
|
||||
card.innerHTML = `
|
||||
<div class="card-header">
|
||||
<span class="card-id">#${lt.escHtml(ticketId)}</span>
|
||||
<span class="lt-priority lt-p${priority}"></span>
|
||||
</div>
|
||||
<div class="card-title">${lt.escHtml(title)}</div>
|
||||
<div class="card-footer">
|
||||
<span class="card-category">${lt.escHtml(category)}</span>
|
||||
<span class="card-assignee" title="${lt.escHtml(assignedTo)}">${lt.escHtml(initials)}</span>
|
||||
</div>
|
||||
`;
|
||||
column.appendChild(card);
|
||||
}
|
||||
counts[status] = (counts[status] || 0) + 1;
|
||||
|
||||
const pNum = parseInt(priority, 10) || 4;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'lt-kanban-card lt-kanban-card--p' + pNum;
|
||||
card.setAttribute('role', 'button');
|
||||
card.setAttribute('tabindex', '0');
|
||||
card.onclick = () => window.location.href = '/ticket/' + encodeURIComponent(ticketId);
|
||||
card.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') card.click(); };
|
||||
card.innerHTML =
|
||||
'<div class="lt-kanban-card-header">' +
|
||||
'<span class="lt-text-xs lt-text-cyan">#' + lt.escHtml(ticketId) + '</span>' +
|
||||
'<span class="lt-p' + pNum + ' lt-text-xs">P' + pNum + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="lt-kanban-card-title">' + lt.escHtml(title) + '</div>' +
|
||||
'<div class="lt-kanban-card-footer">' +
|
||||
'<span class="lt-text-xs lt-text-muted">' + lt.escHtml(category) + '</span>' +
|
||||
'<span class="lt-kanban-assignee lt-text-xs" title="' + lt.escHtml(assignedTo) + '">' + lt.escHtml(initials) + '</span>' +
|
||||
'</div>';
|
||||
|
||||
column.appendChild(card);
|
||||
});
|
||||
|
||||
// Update column counts
|
||||
Object.keys(counts).forEach(status => {
|
||||
const header = document.querySelector(`.kanban-column[data-status="${status}"] .column-count`);
|
||||
if (header) header.textContent = counts[status];
|
||||
document.querySelectorAll('.column-count[data-status]').forEach(el => {
|
||||
const s = el.dataset.status;
|
||||
el.textContent = '(' + (counts[s] || 0) + ')';
|
||||
});
|
||||
}
|
||||
|
||||
// Restore view mode on page load
|
||||
// Restore view mode on page load — click the kanban tab button to trigger lt.tabs
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const savedMode = localStorage.getItem('ticketViewMode');
|
||||
if (savedMode === 'card') {
|
||||
setViewMode('card');
|
||||
const cardBtn = document.getElementById('cardViewBtn');
|
||||
if (cardBtn) cardBtn.click();
|
||||
else populateKanbanCards();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+5
-37
@@ -410,9 +410,9 @@ function performStatusChange(statusSelect, selectedOption, newStatus) {
|
||||
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, status: newStatus })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Update the dropdown to show new status as current
|
||||
const newClass = 'status-' + newStatus.toLowerCase().replace(/ /g, '-');
|
||||
statusSelect.className = 'editable status-select ' + newClass;
|
||||
// Update the dropdown to show new status as current (preserve TDS v1.2 classes)
|
||||
const newClass = 'lt-status-' + newStatus.toLowerCase().replace(/ /g, '-');
|
||||
statusSelect.className = 'lt-select lt-select-sm lt-status-select ' + newClass;
|
||||
|
||||
// Update the selected option text to show as current
|
||||
selectedOption.text = newStatus + ' (current)';
|
||||
@@ -440,43 +440,11 @@ function performStatusChange(statusSelect, selectedOption, newStatus) {
|
||||
}
|
||||
|
||||
function showTab(tabName) {
|
||||
// Hide all tab contents
|
||||
const descriptionTab = document.getElementById('description-tab');
|
||||
const commentsTab = document.getElementById('comments-tab');
|
||||
const attachmentsTab = document.getElementById('attachments-tab');
|
||||
const dependenciesTab = document.getElementById('dependencies-tab');
|
||||
const activityTab = document.getElementById('activity-tab');
|
||||
|
||||
if (!descriptionTab || !commentsTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide all tabs
|
||||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
// Remove active class and aria-selected from all buttons
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
btn.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
// Show selected tab and activate its button
|
||||
const tabEl = document.getElementById(`${tabName}-tab`);
|
||||
if (tabEl) tabEl.classList.add('active');
|
||||
const activeBtn = document.querySelector(`.tab-btn[data-tab="${tabName}"]`);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.add('active');
|
||||
activeBtn.setAttribute('aria-selected', 'true');
|
||||
}
|
||||
|
||||
// Load attachments when tab is shown
|
||||
// Load content for tabs that require it (TDS v1.2 handles the actual show/hide via lt.tabs)
|
||||
if (tabName === 'attachments') {
|
||||
loadAttachments();
|
||||
initializeUploadZone();
|
||||
}
|
||||
|
||||
// Load dependencies when tab is shown
|
||||
if (tabName === 'dependencies') {
|
||||
} else if (tabName === 'dependencies') {
|
||||
loadDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +423,8 @@ include __DIR__ . '/layout_header.php';
|
||||
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
|
||||
</td>
|
||||
<td data-label="Priority">
|
||||
<span class="lt-p<?= $pNum ?>">P<?= $pNum ?></span>
|
||||
<?php $chipClass = match($pNum) { 1 => 'lt-chip-critical', 2 => 'lt-chip-warn', 3 => 'lt-chip-info', default => 'lt-chip-ok' }; ?>
|
||||
<span class="lt-chip <?= $chipClass ?>">P<?= $pNum ?></span>
|
||||
</td>
|
||||
<td data-label="Title"><?= htmlspecialchars($row['title']) ?></td>
|
||||
<td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
|
||||
|
||||
@@ -370,6 +370,11 @@ include __DIR__ . '/layout_header.php';
|
||||
$threadClass = $parentId ? 'comment-reply' : 'comment-root';
|
||||
$dateStr = date('M d, Y H:i', strtotime($comment['created_at']));
|
||||
$editedIndicator = !empty($comment['updated_at']) ? ' <span class="comment-edited lt-text-xs lt-text-muted">(edited)</span>' : '';
|
||||
// Avatar initials + color
|
||||
$words = array_filter(explode(' ', $displayName));
|
||||
$initials = strtoupper(implode('', array_map(fn($w) => $w[0], array_slice($words, 0, 2))));
|
||||
$avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
|
||||
$avatarColor = $avatarColors[abs(crc32($displayName)) % count($avatarColors)];
|
||||
?>
|
||||
<div class="comment <?= $depthClass ?> <?= $threadClass ?>"
|
||||
data-comment-id="<?= $commentId ?>"
|
||||
@@ -379,6 +384,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<?php if ($parentId): ?><div class="thread-line" aria-hidden="true"></div><?php endif ?>
|
||||
<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 <?= $avatarColor ?>" aria-hidden="true"><?= htmlspecialchars($initials) ?></div>
|
||||
<span class="comment-user lt-text-amber"><?= htmlspecialchars($displayName) ?></span>
|
||||
<span class="comment-date lt-text-xs lt-text-muted">
|
||||
<span class="ts-cell"
|
||||
|
||||
@@ -115,8 +115,26 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<div class="lt-section-header">API Usage</div>
|
||||
<div class="lt-section-body">
|
||||
<p class="lt-text-sm lt-text-muted">Include the API key in your requests using the Authorization header:</p>
|
||||
<pre class="lt-text-xs lt-text-cyan" style="border:1px solid rgba(0,255,65,0.2);padding:0.5rem;overflow-x:auto"><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
||||
<p class="lt-text-xs lt-text-muted">API keys provide programmatic access to create and manage tickets. Keep keys secure and rotate them regularly.</p>
|
||||
<div class="lt-code-block">
|
||||
<div class="lt-code-header">
|
||||
<span class="lt-code-lang">HTTP HEADER</span>
|
||||
<button type="button" class="lt-code-copy lt-btn-sm"
|
||||
data-copy="Authorization: Bearer YOUR_API_KEY"
|
||||
data-copy-toast>COPY</button>
|
||||
</div>
|
||||
<pre><code>Authorization: Bearer YOUR_API_KEY</code></pre>
|
||||
</div>
|
||||
<p class="lt-text-xs lt-text-muted" style="margin-top:0.5rem">
|
||||
Example — create a ticket via cURL:<br>
|
||||
</p>
|
||||
<div class="lt-code-block">
|
||||
<div class="lt-code-header"><span class="lt-code-lang">CURL</span></div>
|
||||
<pre><code>curl -X POST https://your-instance/api/create_ticket.php \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"My ticket","category":"General","type":"Issue","priority":3}'</code></pre>
|
||||
</div>
|
||||
<p class="lt-text-xs lt-text-muted" style="margin-top:0.5rem">API keys provide programmatic access to create and manage tickets. Keep keys secure and rotate them regularly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,8 +143,13 @@ document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
switch (target.getAttribute('data-action')) {
|
||||
case 'copy-api-key': copyApiKey(); break;
|
||||
case 'revoke-key': revokeKey(target.getAttribute('data-id')); break;
|
||||
case 'copy-api-key': copyApiKey(); break;
|
||||
case 'revoke-key': revokeKey(target.getAttribute('data-id')); break;
|
||||
case 'copy-header-example':
|
||||
navigator.clipboard.writeText('Authorization: Bearer YOUR_API_KEY')
|
||||
.then(function() { lt.toast.success('Copied!'); })
|
||||
.catch(function() { lt.toast.error('Copy failed'); });
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user