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.
|
* Binds click and keydown handlers on each tile.
|
||||||
* @param {object[]} sessions
|
* @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 (visible.length === 0) {
|
||||||
if (grid) grid.innerHTML = '';
|
if (grid) grid.innerHTML = '';
|
||||||
if (emptyState) emptyState.classList.remove('hidden');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emptyState) emptyState.classList.add('hidden');
|
if (emptyState) emptyState.classList.add('hidden');
|
||||||
|
|
||||||
// Apply sort order from server settings
|
// Apply sort order from server settings
|
||||||
const sortOrder = _serverSettings && _serverSettings.sort_order;
|
var sortOrder = _serverSettings && _serverSettings.sort_order;
|
||||||
const mobile = isMobile();
|
var mobile = isMobile();
|
||||||
let ordered;
|
var ordered;
|
||||||
if (sortOrder === 'alphabetical') {
|
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 {
|
} else {
|
||||||
// 'recent', 'manual', and default use server-provided order; priority sort on mobile
|
// 'recent', 'manual', and default use server-provided order; priority sort on mobile
|
||||||
ordered = mobile ? sortByPriority(visible) : visible;
|
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;
|
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
|
// Bind interaction handlers on each tile
|
||||||
document.querySelectorAll('.session-tile').forEach((tile) => {
|
document.querySelectorAll('.session-tile').forEach(function(tile) {
|
||||||
on(tile, 'click', () => openSession(tile.dataset.session));
|
on(tile, 'click', function() { openSession(tile.dataset.session, { sourceUrl: tile.dataset.sourceUrl }); });
|
||||||
on(tile, 'keydown', (e) => {
|
on(tile, 'keydown', function(e) {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
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;
|
return _gridViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Test-only: set _gridViewMode directly. */
|
||||||
|
function _setGridViewMode(mode) {
|
||||||
|
_gridViewMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
/** Test-only: get _sources. */
|
/** Test-only: get _sources. */
|
||||||
function _getSources() {
|
function _getSources() {
|
||||||
return _sources;
|
return _sources;
|
||||||
@@ -1839,6 +1906,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
bindSidebarClickAway,
|
bindSidebarClickAway,
|
||||||
renderGrid,
|
renderGrid,
|
||||||
|
renderGroupedGrid,
|
||||||
requestNotificationPermission,
|
requestNotificationPermission,
|
||||||
handleBellTransitions,
|
handleBellTransitions,
|
||||||
sendHeartbeat,
|
sendHeartbeat,
|
||||||
@@ -1891,6 +1959,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
_setSources,
|
_setSources,
|
||||||
_setServerSettings,
|
_setServerSettings,
|
||||||
_getGridViewMode,
|
_getGridViewMode,
|
||||||
|
_setGridViewMode,
|
||||||
_getSources,
|
_getSources,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2503,4 +2503,47 @@ test('pollSessions sets unreachable and applies exponential backoff on network e
|
|||||||
app._setSources([]);
|
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