/** * Toggle visibility groups field based on visibility selection */ function toggleVisibilityGroupsEdit() { const visibility = document.getElementById('visibilitySelect')?.value; const groupsField = document.getElementById('visibilityGroupsField'); if (groupsField) { groupsField.classList.toggle('is-hidden', visibility !== 'internal'); } } /** * Get selected visibility groups */ function getSelectedVisibilityGroups() { const checkboxes = document.querySelectorAll('.visibility-group-checkbox:checked'); return Array.from(checkboxes).map(cb => cb.value); } function saveTicket() { const editables = document.querySelectorAll('.editable'); const data = {}; const ticketId = getTicketIdFromUrl(); if (!ticketId) { return; } editables.forEach(field => { if (field.dataset.field) { // For contenteditable divs, use textContent/innerText; for inputs/textareas, use value if (field.hasAttribute('contenteditable')) { data[field.dataset.field] = field.textContent.trim(); } else { data[field.dataset.field] = field.value; } } }); // Get visibility settings const visibilitySelect = document.getElementById('visibilitySelect'); if (visibilitySelect) { data.visibility = visibilitySelect.value; if (data.visibility === 'internal') { data.visibility_groups = getSelectedVisibilityGroups(); } } // Include optimistic lock timestamp so the server can detect concurrent edits if (window.ticketData && window.ticketData.updated_at) { data.expected_updated_at = window.ticketData.updated_at; } // Use the correct API path lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, ...data }) .then(resp => { if (resp.success) { const statusDisplay = document.getElementById('statusDisplay'); if (statusDisplay) { statusDisplay.className = `status-${resp.status}`; statusDisplay.textContent = resp.status; } // Keep local updated_at in sync so the next save uses the right lock key if (resp.updated_at && window.ticketData) { window.ticketData.updated_at = resp.updated_at; } lt.toast.success('Ticket updated successfully'); } else if (resp.conflict) { lt.toast.error('This ticket was modified by someone else while you were editing. Reload to see the latest version.', 8000); } else { lt.toast.error('Error saving ticket: ' + (resp.error || 'Unknown error')); } }) .catch(error => { lt.toast.error('Error saving ticket: ' + error.message); }); } // ── Description read/edit helpers ──────────────────────────────────────────── // Read mode: styled lt-markdown div (full contrast, even on OLED). // Edit mode: raw textarea (enabled for editing). function renderDescriptionView() { var viewDiv = document.getElementById('ticketDescriptionView'); var textarea = document.querySelector('textarea[data-field="description"]'); if (!viewDiv || !textarea) return; var raw = textarea.value || ''; if (!raw.trim()) { viewDiv.innerHTML = '
No description provided.
'; } else { // Ticket descriptions are plain text. CSS white-space:pre-wrap handles // line breaks and multiple spaces (ASCII art) — no${lt.escHtml(message)}
`; } if (dependentsList) { dependentsList.innerHTML = `${lt.escHtml(message)}
`; } } function _depStatusBadge(status) { const slug = (status || '').toLowerCase().replace(/ /g, '-'); const cls = status === 'Closed' ? 'lt-badge-closed' : status === 'Open' ? 'lt-badge-open' : 'lt-badge-sm'; return `${lt.escHtml(status)}`; } function renderDependencies(dependencies) { const container = document.getElementById('dependenciesList'); if (!container) return; const typeLabels = { 'blocks': 'Blocks', 'blocked_by': 'Blocked By', 'relates_to': 'Relates To', 'duplicates': 'Duplicates' }; // Check for open "blocked_by" dependencies — show alert const blockers = (dependencies['blocked_by'] || []).filter(d => d.status !== 'Closed'); const blockerAlert = document.getElementById('blockerAlert'); if (blockers.length > 0) { const alertHtml = `No dependencies configured.
'; } function renderDependents(dependents) { const container = document.getElementById('dependentsList'); if (!container) return; if (!dependents.length) { container.innerHTML = 'No tickets depend on this one.
'; return; } const relLabels = { 'blocks':'blocks', 'blocked_by':'blocked by', 'relates_to':'relates to', 'duplicates':'duplicates' }; let html = ''; dependents.forEach(dep => { const relLabel = relLabels[dep.dependency_type] || dep.dependency_type; html += `Error loading attachments.
'; } else { lt.toast.error('Error loading more attachments'); } }) .catch(error => { if (!append) { container.innerHTML = 'Error loading attachments.
'; } else { lt.toast.error('Error loading more attachments'); } }); } function renderAttachments(attachments, append, hasMore) { const container = document.getElementById('attachmentsList'); if (!container) return; if (!append && attachments.length === 0) { container.innerHTML = 'No files attached to this ticket.
'; return; } let grid = append ? container.querySelector('.attachments-grid') : null; let html = ''; attachments.forEach(att => { const uploaderName = att.display_name || att.username || 'Unknown'; const uploadDateFormatted = new Date(att.uploaded_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); const uploadDate = `${lt.time.ago(att.uploaded_at)}`; const isImage = /^image\//i.test(att.mime_type || ''); const imgUrl = `/api/download_attachment.php?id=${att.attachment_id}&inline=1`; const iconHtml = isImage ? `