fix(web_viewer): keep hops info tip out of sort icon updates

Scope contacts header sort styling/JS to .sort-icon so the path-encoding
(i) tip is not rewritten into a sort chevron.
This commit is contained in:
agessaman
2026-08-06 21:14:26 -07:00
parent 35e3a85c0a
commit bdde4efe00
2 changed files with 79 additions and 18 deletions
+20 -18
View File
@@ -496,23 +496,23 @@
<input type="checkbox" class="form-check-input" id="contacts-select-all" title="Select all on page">
</th>
<th class="sortable" data-sort="username">
Name <i class="fas fa-sort"></i>
Name <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable" data-sort="device_type">
Device Type <i class="fas fa-sort"></i>
Device Type <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable" data-sort="location">
Location <i class="fas fa-sort"></i>
Location <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable" data-sort="distance">
Distance <i class="fas fa-sort"></i>
Distance <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable" data-sort="snr">
SNR <i class="fas fa-sort"></i>
SNR <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable" data-sort="hop_count">
<span class="d-inline-flex align-items-center gap-1">
Hops <i class="fas fa-sort"></i>
Hops <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
<button type="button" class="btn btn-link p-0 border-0 align-baseline text-muted path-encoding-info-btn"
id="contacts-list-multibyte-badge-info"
aria-label="How Multibyte and 1-byte only badges are determined"
@@ -522,16 +522,16 @@
</span>
</th>
<th class="sortable" data-sort="path_bytes">
Bytes/hop <i class="fas fa-sort"></i>
Bytes/hop <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable" data-sort="first_heard">
First Heard <i class="fas fa-sort"></i>
First Heard <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable sort-active" data-sort="last_seen">
Last Heard <i class="fas fa-sort-down"></i>
Last Heard <i class="fas fa-sort-down sort-icon" aria-hidden="true"></i>
</th>
<th class="sortable" data-sort="advert_count">
Adverts <i class="fas fa-sort"></i>
Adverts <i class="fas fa-sort sort-icon" aria-hidden="true"></i>
</th>
<th>Actions</th>
</tr>
@@ -863,9 +863,9 @@ class ModernContactsManager {
}
updateSortIcons() {
// Remove all sort icons
document.querySelectorAll('.sortable i').forEach(icon => {
icon.className = 'fas fa-sort';
// Only touch dedicated sort chevrons — never the hops path-encoding (i) tip.
document.querySelectorAll('.sortable .sort-icon').forEach(icon => {
icon.className = 'fas fa-sort sort-icon';
});
// Remove active class from all headers
@@ -877,9 +877,11 @@ class ModernContactsManager {
const activeHeader = document.querySelector(`[data-sort="${this.sortColumn}"]`);
if (activeHeader) {
activeHeader.classList.add('sort-active');
const icon = activeHeader.querySelector('i');
const icon = activeHeader.querySelector('.sort-icon');
if (icon) {
icon.className = this.sortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down';
icon.className = this.sortDirection === 'asc'
? 'fas fa-sort-up sort-icon'
: 'fas fa-sort-down sort-icon';
}
}
this.syncMobileSortSelect();
@@ -2723,18 +2725,18 @@ document.addEventListener('DOMContentLoaded', () => {
transition: all 0.2s ease;
}
.sortable i {
.sortable .sort-icon {
margin-left: 5px;
opacity: 0.5;
transition: opacity 0.2s ease;
font-size: 0.8em;
}
.sortable:hover i {
.sortable:hover .sort-icon {
opacity: 0.8;
}
.sort-active i {
.sort-active .sort-icon {
opacity: 1;
color: #0d6efd !important;
}
+59
View File
@@ -223,6 +223,65 @@ test('a newer contacts request aborts the stale request', async () => {
assert.equal(signals[1].aborted, false);
});
test('updateSortIcons leaves the hops info tip alone', () => {
const { context, Manager } = loadManagerClass();
const sortIcon = fakeElement({ className: 'fas fa-sort sort-icon' });
const infoIcon = fakeElement({ className: 'fas fa-info-circle' });
const hopsHeader = fakeElement({
classList: {
remove(name) {
this._removed = name;
},
add(name) {
this._added = name;
},
},
querySelector(selector) {
if (selector === '.sort-icon') return sortIcon;
if (selector === 'i') return sortIcon;
return null;
},
querySelectorAll(selector) {
if (selector === '.sort-icon') return [sortIcon];
if (selector === 'i') return [sortIcon, infoIcon];
return [];
},
});
const otherHeader = fakeElement({
classList: {
remove() {},
add() {},
},
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
});
context.document.querySelectorAll = selector => {
if (selector === '.sortable .sort-icon') return [sortIcon];
if (selector === '.sortable i') return [sortIcon, infoIcon];
if (selector === '.sortable') return [hopsHeader, otherHeader];
return [];
};
context.document.querySelector = selector => {
if (selector === '[data-sort="hop_count"]') return hopsHeader;
return null;
};
const manager = bareManager(Manager);
manager.sortColumn = 'hop_count';
manager.sortDirection = 'asc';
manager.syncMobileSortSelect = () => {};
manager.updateSortIcons();
assert.equal(sortIcon.className, 'fas fa-sort-up sort-icon');
assert.equal(infoIcon.className, 'fas fa-info-circle');
assert.equal(hopsHeader.classList._added, 'sort-active');
});
test('search is debounced before reloading the first page', () => {
const { context, elements, Manager } = loadManagerClass();
const requiredIds = [