feat: add grouped view mode to renderGrid with device-group-header sections
- Add renderGroupedGrid(sessions, mobile) that groups sessions by deviceName and creates <h3 class="device-group-header"> headers for each group - Rewrite renderGrid to: apply device filter in 'filtered' mode, apply sort order, call renderGroupedGrid when _gridViewMode === 'grouped', render filter bar in 'filtered' mode, bind click handlers with sourceUrl - Add _setGridViewMode(mode) test-only helper - Export renderGroupedGrid and _setGridViewMode from module.exports
This commit is contained in:
+82
-13
@@ -681,39 +681,101 @@ function bindSidebarClickAway() {
|
||||
* Binds click and keydown handlers on each tile.
|
||||
* @param {object[]} sessions
|
||||
*/
|
||||
function renderGrid(sessions) {
|
||||
const grid = $('session-grid');
|
||||
const emptyState = $('empty-state');
|
||||
|
||||
const visible = getVisibleSessions(sessions);
|
||||
/**
|
||||
* Render sessions grouped by device name. Returns HTML string.
|
||||
* @param {object[]} sessions - sorted, visible sessions
|
||||
* @param {boolean} mobile
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderGroupedGrid(sessions, mobile) {
|
||||
// Group by deviceName
|
||||
var groups = {};
|
||||
var groupOrder = [];
|
||||
for (var i = 0; i < sessions.length; i++) {
|
||||
var dn = sessions[i].deviceName || 'Unknown';
|
||||
if (!groups[dn]) {
|
||||
groups[dn] = [];
|
||||
groupOrder.push(dn);
|
||||
}
|
||||
groups[dn].push(sessions[i]);
|
||||
}
|
||||
|
||||
var html = '';
|
||||
for (var g = 0; g < groupOrder.length; g++) {
|
||||
var name = groupOrder[g];
|
||||
html += '<h3 class="device-group-header">' + escapeHtml(name) + '</h3>';
|
||||
var groupSessions = groups[name];
|
||||
for (var j = 0; j < groupSessions.length; j++) {
|
||||
html += buildTileHTML(groupSessions[j], j, mobile);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderGrid(sessions) {
|
||||
var grid = $('session-grid');
|
||||
var emptyState = $('empty-state');
|
||||
var filterBar = $('filter-bar');
|
||||
|
||||
var visible = getVisibleSessions(sessions);
|
||||
|
||||
// In filtered mode, apply device filter
|
||||
if (_gridViewMode === 'filtered' && _activeFilterDevice !== 'all') {
|
||||
visible = visible.filter(function(s) { return s.deviceName === _activeFilterDevice; });
|
||||
}
|
||||
|
||||
if (visible.length === 0) {
|
||||
if (grid) grid.innerHTML = '';
|
||||
if (emptyState) emptyState.classList.remove('hidden');
|
||||
// Show filter bar even when filtered to empty (so user can switch back)
|
||||
if (filterBar) {
|
||||
if (_gridViewMode === 'filtered') {
|
||||
renderFilterBar(filterBar, sessions);
|
||||
} else {
|
||||
filterBar.innerHTML = '';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyState) emptyState.classList.add('hidden');
|
||||
|
||||
// Apply sort order from server settings
|
||||
const sortOrder = _serverSettings && _serverSettings.sort_order;
|
||||
const mobile = isMobile();
|
||||
let ordered;
|
||||
var sortOrder = _serverSettings && _serverSettings.sort_order;
|
||||
var mobile = isMobile();
|
||||
var ordered;
|
||||
if (sortOrder === 'alphabetical') {
|
||||
ordered = visible.slice().sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
||||
ordered = visible.slice().sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); });
|
||||
} else {
|
||||
// 'recent', 'manual', and default use server-provided order; priority sort on mobile
|
||||
ordered = mobile ? sortByPriority(visible) : visible;
|
||||
}
|
||||
const html = ordered.map((session, index) => buildTileHTML(session, index, mobile)).join('');
|
||||
|
||||
var html;
|
||||
if (_gridViewMode === 'grouped') {
|
||||
html = renderGroupedGrid(ordered, mobile);
|
||||
} else {
|
||||
html = ordered.map(function(session, index) { return buildTileHTML(session, index, mobile); }).join('');
|
||||
}
|
||||
|
||||
if (grid) grid.innerHTML = html;
|
||||
|
||||
// Render filter bar
|
||||
if (filterBar) {
|
||||
if (_gridViewMode === 'filtered') {
|
||||
renderFilterBar(filterBar, sessions);
|
||||
} else {
|
||||
filterBar.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Bind interaction handlers on each tile
|
||||
document.querySelectorAll('.session-tile').forEach((tile) => {
|
||||
on(tile, 'click', () => openSession(tile.dataset.session));
|
||||
on(tile, 'keydown', (e) => {
|
||||
document.querySelectorAll('.session-tile').forEach(function(tile) {
|
||||
on(tile, 'click', function() { openSession(tile.dataset.session, { sourceUrl: tile.dataset.sourceUrl }); });
|
||||
on(tile, 'keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
openSession(tile.dataset.session);
|
||||
openSession(tile.dataset.session, { sourceUrl: tile.dataset.sourceUrl });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1791,6 +1853,11 @@ function _getGridViewMode() {
|
||||
return _gridViewMode;
|
||||
}
|
||||
|
||||
/** Test-only: set _gridViewMode directly. */
|
||||
function _setGridViewMode(mode) {
|
||||
_gridViewMode = mode;
|
||||
}
|
||||
|
||||
/** Test-only: get _sources. */
|
||||
function _getSources() {
|
||||
return _sources;
|
||||
@@ -1839,6 +1906,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
toggleSidebar,
|
||||
bindSidebarClickAway,
|
||||
renderGrid,
|
||||
renderGroupedGrid,
|
||||
requestNotificationPermission,
|
||||
handleBellTransitions,
|
||||
sendHeartbeat,
|
||||
@@ -1891,6 +1959,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
_setSources,
|
||||
_setServerSettings,
|
||||
_getGridViewMode,
|
||||
_setGridViewMode,
|
||||
_getSources,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2503,4 +2503,47 @@ test('pollSessions sets unreachable and applies exponential backoff on network e
|
||||
app._setSources([]);
|
||||
});
|
||||
|
||||
// --- renderGrid grouped mode (task-11) ---
|
||||
|
||||
test('renderGrid in grouped mode produces device-group-header elements', () => {
|
||||
const collectedHTML = [];
|
||||
const mockGrid = {
|
||||
get innerHTML() { return collectedHTML[0] || ''; },
|
||||
set innerHTML(v) { collectedHTML[0] = v; },
|
||||
};
|
||||
const mockEmpty = { style: {}, classList: { add: () => {}, remove: () => {} } };
|
||||
const origGetById = globalThis.document.getElementById;
|
||||
const origQSA = globalThis.document.querySelectorAll;
|
||||
globalThis.document.getElementById = (id) => {
|
||||
if (id === 'session-grid') return mockGrid;
|
||||
if (id === 'empty-state') return mockEmpty;
|
||||
return null;
|
||||
};
|
||||
globalThis.document.querySelectorAll = () => [];
|
||||
|
||||
// Set up sessions from two different devices
|
||||
const sessions = [
|
||||
{ name: 'alpha', deviceName: 'Laptop', sourceUrl: 'http://local', sessionKey: 'http://local::alpha', snapshot: '' },
|
||||
{ name: 'beta', deviceName: 'Server', sourceUrl: 'http://remote', sessionKey: 'http://remote::beta', snapshot: '' },
|
||||
];
|
||||
|
||||
app._setGridViewMode('grouped');
|
||||
app.renderGrid(sessions);
|
||||
|
||||
const html = mockGrid.innerHTML;
|
||||
assert.ok(html.includes('device-group-header'), 'grid HTML should contain device-group-header elements');
|
||||
assert.ok(html.includes('Laptop'), 'grid HTML should contain device name "Laptop"');
|
||||
assert.ok(html.includes('Server'), 'grid HTML should contain device name "Server"');
|
||||
|
||||
// Reset state
|
||||
app._setGridViewMode('flat');
|
||||
globalThis.document.getElementById = origGetById;
|
||||
globalThis.document.querySelectorAll = origQSA;
|
||||
});
|
||||
|
||||
test('_setGridViewMode and renderGroupedGrid are exported', () => {
|
||||
assert.strictEqual(typeof app._setGridViewMode, 'function', '_setGridViewMode should be exported');
|
||||
assert.strictEqual(typeof app.renderGroupedGrid, 'function', 'renderGroupedGrid should be exported');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user