fix: watcher avatars, dependency TDS styling, asset versions, nav dropdown light theme

- watch_ticket.php GET now returns watcher list (up to 6 users) for avatar group
- TicketView: watcher avatar group rendered next to WATCH button, refreshes on toggle
- Rewrite renderDependencies/renderDependents to use TDS lt-kv-grid/lt-badge/lt-btn classes
- renderDependencies: show lt-alert--warning blocker banner when blocked_by has open tickets
- Fix ALL hardcoded ?v=20260327 asset version strings in CreateTicketView + all admin views
- base.css: fix .lt-nav-dropdown-menu hardcoded background → var(--bg-overlay)
- base.css: add light-theme overrides for nav dropdown menu (background, links, hover)
- ticket.css: add .lt-avatar-group and .lt-avatar--overflow styles for watcher display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:02:30 -04:00
parent c0dfbdbc26
commit fca4896e0d
13 changed files with 162 additions and 56 deletions
+57 -40
View File
@@ -552,75 +552,92 @@ function showDependencyError(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 `<span class="lt-badge ${cls} lt-text-xs">${lt.escHtml(status)}</span>`;
}
function renderDependencies(dependencies) {
const container = document.getElementById('dependenciesList');
if (!container) return;
const typeLabels = {
'blocks': 'Blocks',
'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 = `<div class="lt-alert lt-alert--warning" id="blockerAlert" role="alert" style="margin-bottom:0.75rem">
<span class="lt-alert-icon" aria-hidden="true">[!]</span>
<div class="lt-alert-body">
<div class="lt-alert-title">Blocked</div>
<div class="lt-alert-msg">This ticket is blocked by ${blockers.length} open ticket${blockers.length > 1 ? 's' : ''}:
${blockers.map(b => `<a href="/ticket/${lt.escHtml(b.depends_on_id)}" class="lt-text-cyan">#${lt.escHtml(b.depends_on_id)}</a>`).join(', ')}
</div>
</div>
</div>`;
// Insert blocker alert above the frame if not already there
const panel = document.getElementById('dependencies-panel');
if (panel && !panel.querySelector('#blockerAlert')) {
panel.insertAdjacentHTML('afterbegin', alertHtml);
}
}
let html = '';
let hasAny = false;
for (const [type, items] of Object.entries(dependencies)) {
if (items.length > 0) {
hasAny = true;
html += `<div class="dependency-group">
<h4>${typeLabels[type]}</h4>`;
items.forEach(dep => {
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
html += `<div class="dependency-item">
<div>
<a href="/ticket/${lt.escHtml(dep.depends_on_id)}">
#${lt.escHtml(dep.depends_on_id)}
</a>
<span class="dependency-title">${lt.escHtml(dep.title)}</span>
<span class="status-badge ${statusClass}">${lt.escHtml(dep.status)}</span>
</div>
<button data-action="remove-dependency" data-dependency-id="${lt.escHtml(String(dep.dependency_id))}" class="lt-btn lt-btn-sm">REMOVE</button>
</div>`;
});
html += '</div>';
}
if (!items.length) continue;
hasAny = true;
const label = typeLabels[type] || type;
html += `<div class="lt-kv-row" style="flex-direction:column;align-items:flex-start;gap:0.3rem">
<span class="lt-kv-label lt-text-xs">${lt.escHtml(label)}</span>`;
items.forEach(dep => {
html += `<div class="lt-flex lt-flex-gap-sm lt-flex-align-center" style="width:100%;padding:0.25rem 0;border-bottom:1px solid rgba(0,255,65,0.08)">
<a href="/ticket/${lt.escHtml(dep.depends_on_id)}" class="lt-text-cyan lt-text-xs">
#${lt.escHtml(dep.depends_on_id)}
</a>
<span class="lt-text-sm" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
title="${lt.escHtml(dep.title)}">${lt.escHtml(dep.title)}</span>
${_depStatusBadge(dep.status)}
<button data-action="remove-dependency"
data-dependency-id="${lt.escHtml(String(dep.dependency_id))}"
class="lt-btn lt-btn-ghost lt-btn-sm" aria-label="Remove dependency">&#x2715;</button>
</div>`;
});
html += '</div>';
}
if (!hasAny) {
html = '<p class="lt-text-muted">No dependencies configured.</p>';
}
container.innerHTML = html;
container.innerHTML = hasAny ? `<div class="lt-kv-grid">${html}</div>` : '<p class="lt-text-muted lt-text-sm">No dependencies configured.</p>';
}
function renderDependents(dependents) {
const container = document.getElementById('dependentsList');
if (!container) return;
if (dependents.length === 0) {
container.innerHTML = '<p class="lt-text-muted">No tickets depend on this one.</p>';
if (!dependents.length) {
container.innerHTML = '<p class="lt-text-muted lt-text-sm">No tickets depend on this one.</p>';
return;
}
const relLabels = { 'blocks':'blocks', 'blocked_by':'blocked by', 'relates_to':'relates to', 'duplicates':'duplicates' };
let html = '';
dependents.forEach(dep => {
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
html += `<div class="dependency-item">
<div>
<a href="/ticket/${lt.escHtml(dep.ticket_id)}">
#${lt.escHtml(dep.ticket_id)}
</a>
<span class="dependency-title">${lt.escHtml(dep.title)}</span>
<span class="status-badge ${statusClass}">${lt.escHtml(dep.status)}</span>
<span class="dependency-title lt-text-amber">(${lt.escHtml(dep.dependency_type)})</span>
</div>
const relLabel = relLabels[dep.dependency_type] || dep.dependency_type;
html += `<div class="lt-flex lt-flex-gap-sm lt-flex-align-center" style="padding:0.25rem 0;border-bottom:1px solid rgba(0,255,65,0.08)">
<a href="/ticket/${lt.escHtml(dep.ticket_id)}" class="lt-text-cyan lt-text-xs">#${lt.escHtml(dep.ticket_id)}</a>
<span class="lt-text-xs lt-text-muted">${lt.escHtml(relLabel)}</span>
<span class="lt-text-sm" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
title="${lt.escHtml(dep.title)}">${lt.escHtml(dep.title)}</span>
${_depStatusBadge(dep.status)}
</div>`;
});
container.innerHTML = html;
}