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-msg variants ─────────────────────────────────────────── */
|
||||||
.lt-mb-md { margin-bottom: 1rem; }
|
.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 ──────────────────────────────────────────────── */
|
/* ── Responsive ──────────────────────────────────────────────── */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.lt-page-header {
|
.lt-page-header {
|
||||||
@@ -214,10 +316,10 @@ kbd {
|
|||||||
.lt-stats-grid {
|
.lt-stats-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
.mobile-filter-toggle { display: block; }
|
||||||
|
.lt-sidebar.mobile-open { transform: translateX(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.lt-stats-grid {
|
.lt-stats-grid { grid-template-columns: 1fr; }
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-86
@@ -72,31 +72,6 @@ function initMobileSidebar() {
|
|||||||
sidebar.insertBefore(closeBtn, sidebar.firstChild);
|
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
|
// Restore sidebar state on page load
|
||||||
@@ -143,7 +118,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
const action = target.dataset.action;
|
const action = target.dataset.action;
|
||||||
switch (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':
|
case 'perform-bulk-assign':
|
||||||
performBulkAssign();
|
performBulkAssign();
|
||||||
break;
|
break;
|
||||||
@@ -168,26 +166,63 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
case 'close-bulk-delete-modal':
|
case 'close-bulk-delete-modal':
|
||||||
closeBulkDeleteModal();
|
closeBulkDeleteModal();
|
||||||
break;
|
break;
|
||||||
|
// Checkbox selection
|
||||||
|
case 'toggle-select-all':
|
||||||
|
toggleSelectAll();
|
||||||
|
break;
|
||||||
|
case 'update-selection':
|
||||||
|
updateSelectionCount();
|
||||||
|
break;
|
||||||
|
case 'toggle-row-checkbox':
|
||||||
|
toggleRowCheckbox(e, target);
|
||||||
|
break;
|
||||||
// Quick actions
|
// Quick actions
|
||||||
|
case 'quick-status':
|
||||||
|
quickStatusChange(target.dataset.ticketId, target.dataset.status);
|
||||||
|
break;
|
||||||
case 'perform-quick-status':
|
case 'perform-quick-status':
|
||||||
performQuickStatusChange(target.dataset.ticketId);
|
performQuickStatusChange(target.dataset.ticketId);
|
||||||
break;
|
break;
|
||||||
case 'close-quick-status-modal':
|
case 'close-quick-status-modal':
|
||||||
closeQuickStatusModal();
|
closeQuickStatusModal();
|
||||||
break;
|
break;
|
||||||
|
case 'quick-assign':
|
||||||
|
quickAssign(target.dataset.ticketId);
|
||||||
|
break;
|
||||||
case 'perform-quick-assign':
|
case 'perform-quick-assign':
|
||||||
performQuickAssign(target.dataset.ticketId);
|
performQuickAssign(target.dataset.ticketId);
|
||||||
break;
|
break;
|
||||||
case 'close-quick-assign-modal':
|
case 'close-quick-assign-modal':
|
||||||
closeQuickAssignModal();
|
closeQuickAssignModal();
|
||||||
break;
|
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
|
// Mobile navigation
|
||||||
case 'open-mobile-sidebar':
|
case 'open-mobile-sidebar':
|
||||||
if (typeof openMobileSidebar === 'function') openMobileSidebar();
|
if (typeof openMobileSidebar === 'function') openMobileSidebar();
|
||||||
break;
|
break;
|
||||||
case 'open-settings-modal':
|
|
||||||
if (typeof openSettingsModal === 'function') openSettingsModal();
|
|
||||||
break;
|
|
||||||
// Filter badge actions
|
// Filter badge actions
|
||||||
case 'remove-filter':
|
case 'remove-filter':
|
||||||
removeFilter(target.dataset.filterType, target.dataset.filterValue);
|
removeFilter(target.dataset.filterType, target.dataset.filterValue);
|
||||||
@@ -682,14 +717,14 @@ function updateSelectionCount() {
|
|||||||
const exportDropdown = document.getElementById('exportDropdown');
|
const exportDropdown = document.getElementById('exportDropdown');
|
||||||
const exportCount = document.getElementById('exportCount');
|
const exportCount = document.getElementById('exportCount');
|
||||||
|
|
||||||
if (toolbar && countDisplay) {
|
if (toolbar) {
|
||||||
toolbar.classList.toggle('is-visible', count > 0);
|
toolbar.style.display = count > 0 ? 'flex' : 'none';
|
||||||
if (count > 0) countDisplay.textContent = count;
|
if (count > 0 && countDisplay) countDisplay.textContent = count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide export dropdown based on selection
|
// Show/hide export dropdown based on selection
|
||||||
if (exportDropdown) {
|
if (exportDropdown) {
|
||||||
exportDropdown.classList.toggle('is-visible', count > 0);
|
exportDropdown.style.display = count > 0 ? 'inline-flex' : 'none';
|
||||||
if (count > 0 && exportCount) exportCount.textContent = count;
|
if (count > 0 && exportCount) exportCount.textContent = count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1278,27 +1313,10 @@ function performQuickAssign(ticketId) {
|
|||||||
* Set the view mode (table or card)
|
* Set the view mode (table or card)
|
||||||
*/
|
*/
|
||||||
function setViewMode(mode) {
|
function setViewMode(mode) {
|
||||||
const tableView = document.querySelector('.ascii-frame-outer');
|
// TDS v1.2 uses lt.tabs for show/hide; we just need to populate kanban cards
|
||||||
const cardView = document.getElementById('cardView');
|
|
||||||
const tableBtn = document.getElementById('tableViewBtn');
|
|
||||||
const cardBtn = document.getElementById('cardViewBtn');
|
|
||||||
|
|
||||||
if (!tableView || !cardView) return;
|
|
||||||
|
|
||||||
if (mode === 'card') {
|
if (mode === 'card') {
|
||||||
tableView.classList.add('is-hidden');
|
|
||||||
cardView.classList.remove('is-hidden');
|
|
||||||
tableBtn.classList.remove('active');
|
|
||||||
cardBtn.classList.add('active');
|
|
||||||
populateKanbanCards();
|
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);
|
localStorage.setItem('ticketViewMode', mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1306,18 +1324,17 @@ function setViewMode(mode) {
|
|||||||
* Populate Kanban cards from table data
|
* Populate Kanban cards from table data
|
||||||
*/
|
*/
|
||||||
function populateKanbanCards() {
|
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 = {
|
const columns = {
|
||||||
'Open': document.querySelector('.kanban-column[data-status="Open"] .kanban-cards'),
|
'Open': document.getElementById('kanban-col-open'),
|
||||||
'Pending': document.querySelector('.kanban-column[data-status="Pending"] .kanban-cards'),
|
'Pending': document.getElementById('kanban-col-pending'),
|
||||||
'In Progress': document.querySelector('.kanban-column[data-status="In Progress"] .kanban-cards'),
|
'In Progress': document.getElementById('kanban-col-inprogress'),
|
||||||
'Closed': document.querySelector('.kanban-column[data-status="Closed"] .kanban-cards')
|
'Closed': document.getElementById('kanban-col-closed'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear existing cards
|
// Clear existing cards
|
||||||
Object.values(columns).forEach(col => {
|
Object.values(columns).forEach(col => { if (col) col.innerHTML = ''; });
|
||||||
if (col) col.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const counts = { 'Open': 0, 'Pending': 0, 'In Progress': 0, 'Closed': 0 };
|
const counts = { 'Open': 0, 'Pending': 0, 'In Progress': 0, 'Closed': 0 };
|
||||||
const isAdmin = document.getElementById('selectAllCheckbox') !== null;
|
const isAdmin = document.getElementById('selectAllCheckbox') !== null;
|
||||||
@@ -1325,53 +1342,60 @@ function populateKanbanCards() {
|
|||||||
|
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const cells = row.querySelectorAll('td');
|
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 ticketId = cells[0 + offset]?.querySelector('.ticket-link')?.textContent.trim() || '';
|
||||||
const priority = cells[1 + offset]?.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 title = cells[2 + offset]?.textContent.trim() || '';
|
||||||
const category = cells[3 + offset]?.textContent.trim() || '';
|
const category = cells[3 + offset]?.textContent.trim() || '';
|
||||||
const status = cells[5 + 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';
|
const assignedTo = cells[7 + offset]?.textContent.trim() || 'Unassigned';
|
||||||
|
|
||||||
// Get initials for assignee
|
const initials = assignedTo === 'Unassigned' ? '?'
|
||||||
const initials = assignedTo === 'Unassigned' ? '?' :
|
: assignedTo.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||||
assignedTo.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
||||||
|
|
||||||
const column = columns[status];
|
const column = columns[status];
|
||||||
if (column) {
|
if (!column || !ticketId) return;
|
||||||
counts[status]++;
|
|
||||||
|
|
||||||
|
counts[status] = (counts[status] || 0) + 1;
|
||||||
|
|
||||||
|
const pNum = parseInt(priority, 10) || 4;
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = `kanban-card priority-${priority}`;
|
card.className = 'lt-kanban-card lt-kanban-card--p' + pNum;
|
||||||
card.onclick = () => window.location.href = `/ticket/${ticketId}`;
|
card.setAttribute('role', 'button');
|
||||||
card.innerHTML = `
|
card.setAttribute('tabindex', '0');
|
||||||
<div class="card-header">
|
card.onclick = () => window.location.href = '/ticket/' + encodeURIComponent(ticketId);
|
||||||
<span class="card-id">#${lt.escHtml(ticketId)}</span>
|
card.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') card.click(); };
|
||||||
<span class="lt-priority lt-p${priority}"></span>
|
card.innerHTML =
|
||||||
</div>
|
'<div class="lt-kanban-card-header">' +
|
||||||
<div class="card-title">${lt.escHtml(title)}</div>
|
'<span class="lt-text-xs lt-text-cyan">#' + lt.escHtml(ticketId) + '</span>' +
|
||||||
<div class="card-footer">
|
'<span class="lt-p' + pNum + ' lt-text-xs">P' + pNum + '</span>' +
|
||||||
<span class="card-category">${lt.escHtml(category)}</span>
|
'</div>' +
|
||||||
<span class="card-assignee" title="${lt.escHtml(assignedTo)}">${lt.escHtml(initials)}</span>
|
'<div class="lt-kanban-card-title">' + lt.escHtml(title) + '</div>' +
|
||||||
</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);
|
column.appendChild(card);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update column counts
|
// Update column counts
|
||||||
Object.keys(counts).forEach(status => {
|
document.querySelectorAll('.column-count[data-status]').forEach(el => {
|
||||||
const header = document.querySelector(`.kanban-column[data-status="${status}"] .column-count`);
|
const s = el.dataset.status;
|
||||||
if (header) header.textContent = counts[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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const savedMode = localStorage.getItem('ticketViewMode');
|
const savedMode = localStorage.getItem('ticketViewMode');
|
||||||
if (savedMode === 'card') {
|
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 })
|
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, status: newStatus })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Update the dropdown to show new status as current
|
// Update the dropdown to show new status as current (preserve TDS v1.2 classes)
|
||||||
const newClass = 'status-' + newStatus.toLowerCase().replace(/ /g, '-');
|
const newClass = 'lt-status-' + newStatus.toLowerCase().replace(/ /g, '-');
|
||||||
statusSelect.className = 'editable status-select ' + newClass;
|
statusSelect.className = 'lt-select lt-select-sm lt-status-select ' + newClass;
|
||||||
|
|
||||||
// Update the selected option text to show as current
|
// Update the selected option text to show as current
|
||||||
selectedOption.text = newStatus + ' (current)';
|
selectedOption.text = newStatus + ' (current)';
|
||||||
@@ -440,43 +440,11 @@ function performStatusChange(statusSelect, selectedOption, newStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showTab(tabName) {
|
function showTab(tabName) {
|
||||||
// Hide all tab contents
|
// Load content for tabs that require it (TDS v1.2 handles the actual show/hide via lt.tabs)
|
||||||
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
|
|
||||||
if (tabName === 'attachments') {
|
if (tabName === 'attachments') {
|
||||||
loadAttachments();
|
loadAttachments();
|
||||||
initializeUploadZone();
|
initializeUploadZone();
|
||||||
}
|
} else if (tabName === 'dependencies') {
|
||||||
|
|
||||||
// Load dependencies when tab is shown
|
|
||||||
if (tabName === 'dependencies') {
|
|
||||||
loadDependencies();
|
loadDependencies();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,7 +423,8 @@ include __DIR__ . '/layout_header.php';
|
|||||||
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
|
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Priority">
|
<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>
|
||||||
<td data-label="Title"><?= htmlspecialchars($row['title']) ?></td>
|
<td data-label="Title"><?= htmlspecialchars($row['title']) ?></td>
|
||||||
<td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></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';
|
$threadClass = $parentId ? 'comment-reply' : 'comment-root';
|
||||||
$dateStr = date('M d, Y H:i', strtotime($comment['created_at']));
|
$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>' : '';
|
$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 ?>"
|
<div class="comment <?= $depthClass ?> <?= $threadClass ?>"
|
||||||
data-comment-id="<?= $commentId ?>"
|
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 ?>
|
<?php if ($parentId): ?><div class="thread-line" aria-hidden="true"></div><?php endif ?>
|
||||||
<div class="comment-content">
|
<div class="comment-content">
|
||||||
<div class="comment-header lt-flex lt-flex-gap-sm lt-flex-align-center">
|
<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-user lt-text-amber"><?= htmlspecialchars($displayName) ?></span>
|
||||||
<span class="comment-date lt-text-xs lt-text-muted">
|
<span class="comment-date lt-text-xs lt-text-muted">
|
||||||
<span class="ts-cell"
|
<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-header">API Usage</div>
|
||||||
<div class="lt-section-body">
|
<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>
|
<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>
|
<div class="lt-code-block">
|
||||||
<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-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,6 +145,11 @@ document.addEventListener('click', function (e) {
|
|||||||
switch (target.getAttribute('data-action')) {
|
switch (target.getAttribute('data-action')) {
|
||||||
case 'copy-api-key': copyApiKey(); break;
|
case 'copy-api-key': copyApiKey(); break;
|
||||||
case 'revoke-key': revokeKey(target.getAttribute('data-id')); 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