feat: sidebar device grouping when multiple sources configured (task-16)

- Replace renderSidebar to group sessions by deviceName when _sources.length > 1
- Render sidebar-device-header h4 elements before each device's sessions
- When single source configured, render flat list with no headers
- Click handlers now pass sourceUrl to openSession
- Add tests: groups with header when multiple sources, flat when single source

🤖 Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
Brian Krabach
2026-03-31 01:16:21 -07:00
parent 88102751c8
commit 70b8df1458
2 changed files with 95 additions and 3 deletions
+24 -3
View File
@@ -561,14 +561,35 @@ function renderSidebar(sessions, currentSession) {
return; return;
} }
list.innerHTML = visible.map((session) => buildSidebarHTML(session, currentSession)).join(''); let html = '';
// Bind click handlers on each sidebar item if (_sources.length > 1) {
// Group sessions by deviceName when multiple sources configured
const groups = new Map();
for (const session of visible) {
const deviceName = session.deviceName || 'Unknown';
if (!groups.has(deviceName)) groups.set(deviceName, []);
groups.get(deviceName).push(session);
}
for (const [deviceName, deviceSessions] of groups) {
html += `<h4 class="sidebar-device-header">${escapeHtml(deviceName)}</h4>`;
html += deviceSessions.map((session) => buildSidebarHTML(session, currentSession)).join('');
}
} else {
// Single source: flat list with no device headers
html = visible.map((session) => buildSidebarHTML(session, currentSession)).join('');
}
list.innerHTML = html;
// Bind click handlers on each sidebar item, passing sourceUrl
if (typeof list.querySelectorAll === 'function') { if (typeof list.querySelectorAll === 'function') {
list.querySelectorAll('.sidebar-item').forEach((item) => { list.querySelectorAll('.sidebar-item').forEach((item) => {
const name = item.dataset.session; const name = item.dataset.session;
const sourceUrl = item.dataset.sourceUrl || '';
on(item, 'click', () => { on(item, 'click', () => {
if (name !== currentSession) openSession(name); if (name !== currentSession) openSession(name, { sourceUrl });
}); });
}); });
} }
+71
View File
@@ -2666,4 +2666,75 @@ test('saveGridViewMode stores to localStorage when scope is local', () => {
app._setGridViewMode('flat'); app._setGridViewMode('flat');
}); });
// --- renderSidebar device grouping (task-16) ---
test('renderSidebar groups sessions by device with sidebar-device-header when multiple sources configured', () => {
let capturedHTML = '';
const mockList = {
get innerHTML() { return capturedHTML; },
set innerHTML(v) { capturedHTML = v; },
querySelectorAll: () => [],
};
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar-list') return mockList;
return null;
};
// Set up multiple sources
app._setSources([
{ url: '', name: 'Laptop', type: 'local', status: 'authenticated', backoffMs: 2000 },
{ url: 'https://remote.example.com', name: 'Server', type: 'remote', status: 'authenticated', backoffMs: 2000 },
]);
app._setViewMode('fullscreen');
const sessions = [
{ name: 'alpha', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::alpha', snapshot: '', bell: { unseen_count: 0 } },
{ name: 'beta', deviceName: 'Server', sourceUrl: 'https://remote.example.com', sessionKey: 'https://remote.example.com::beta', snapshot: '', bell: { unseen_count: 0 } },
];
app.renderSidebar(sessions, null);
assert.ok(capturedHTML.includes('sidebar-device-header'), 'sidebar HTML should contain sidebar-device-header elements when multiple sources');
assert.ok(capturedHTML.includes('Laptop'), 'sidebar HTML should contain device name "Laptop"');
assert.ok(capturedHTML.includes('Server'), 'sidebar HTML should contain device name "Server"');
// Cleanup
app._setSources([]);
globalThis.document.getElementById = origGetById;
app._setViewMode('grid');
});
test('renderSidebar does NOT group when only one source configured', () => {
let capturedHTML = '';
const mockList = {
get innerHTML() { return capturedHTML; },
set innerHTML(v) { capturedHTML = v; },
querySelectorAll: () => [],
};
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar-list') return mockList;
return null;
};
// Set up single source
app._setSources([
{ url: '', name: 'Laptop', type: 'local', status: 'authenticated', backoffMs: 2000 },
]);
app._setViewMode('fullscreen');
const sessions = [
{ name: 'alpha', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::alpha', snapshot: '', bell: { unseen_count: 0 } },
{ name: 'beta', deviceName: 'Laptop', sourceUrl: '', sessionKey: '::beta', snapshot: '', bell: { unseen_count: 0 } },
];
app.renderSidebar(sessions, null);
assert.ok(!capturedHTML.includes('sidebar-device-header'), 'sidebar HTML should NOT contain sidebar-device-header when only one source');
assert.ok(capturedHTML.includes('sidebar-item'), 'sidebar HTML should still contain sidebar-item elements');
// Cleanup
app._setSources([]);
globalThis.document.getElementById = origGetById;
app._setViewMode('grid');
});