feat: add filtered view mode with device pill bar (task-12)
- Add <div id="filter-bar" class="filter-bar"> to index.html before session-grid div - Implement renderFilterBar(container, allSessions) that renders pill buttons for each unique device name plus an 'All' button - Active device pill gets filter-pill--active class - Add _setActiveFilterDevice(device) test helper - Bind delegated click handler on filter-bar in bindStaticEventListeners that sets _activeFilterDevice and re-renders grid - Export renderFilterBar and _setActiveFilterDevice Co-authored-by: Amplifier <amplifier@amplified.dev>
This commit is contained in:
@@ -713,6 +713,36 @@ function renderGroupedGrid(sessions, mobile) {
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the filter pill bar into the given container element.
|
||||
* Generates one 'All' pill plus one pill per unique device name found in allSessions.
|
||||
* The currently active device pill is marked with the `filter-pill--active` class.
|
||||
* @param {Element} container - The DOM element to render pills into.
|
||||
* @param {Array} allSessions - Full (unfiltered) session list used to derive device names.
|
||||
*/
|
||||
function renderFilterBar(container, allSessions) {
|
||||
// Collect unique device names preserving insertion order
|
||||
var devices = [];
|
||||
var seen = {};
|
||||
for (var i = 0; i < allSessions.length; i++) {
|
||||
var dn = allSessions[i].deviceName || 'Unknown';
|
||||
if (!seen[dn]) {
|
||||
seen[dn] = true;
|
||||
devices.push(dn);
|
||||
}
|
||||
}
|
||||
|
||||
// Build HTML: 'All' pill first, then one pill per device
|
||||
var allActive = _activeFilterDevice === 'all' ? ' filter-pill--active' : '';
|
||||
var html = '<button class="filter-pill' + allActive + '" data-device="all">All</button>';
|
||||
for (var j = 0; j < devices.length; j++) {
|
||||
var active = _activeFilterDevice === devices[j] ? ' filter-pill--active' : '';
|
||||
html += '<button class="filter-pill' + active + '" data-device="' + escapeHtml(devices[j]) + '">' + escapeHtml(devices[j]) + '</button>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderGrid(sessions) {
|
||||
var grid = $('session-grid');
|
||||
var emptyState = $('empty-state');
|
||||
@@ -1794,6 +1824,17 @@ function bindStaticEventListeners() {
|
||||
if (el) el.value = NEW_SESSION_DEFAULT_TEMPLATE;
|
||||
patchServerSetting('new_session_template', NEW_SESSION_DEFAULT_TEMPLATE);
|
||||
});
|
||||
|
||||
// Filter bar — delegated click handler (pills are re-rendered each poll)
|
||||
var filterBarEl = $('filter-bar');
|
||||
if (filterBarEl) {
|
||||
filterBarEl.addEventListener('click', function(e) {
|
||||
var pill = e.target.closest && e.target.closest('.filter-pill');
|
||||
if (!pill) return;
|
||||
_activeFilterDevice = pill.dataset.device || 'all';
|
||||
renderGrid(_currentSessions || []);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Test-only helpers ────────────────────────────────────────────────────────
|
||||
@@ -1863,6 +1904,11 @@ function _getSources() {
|
||||
return _sources;
|
||||
}
|
||||
|
||||
/** Test-only: set _activeFilterDevice directly. */
|
||||
function _setActiveFilterDevice(device) {
|
||||
_activeFilterDevice = device;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initDeviceId();
|
||||
applyDisplaySettings(loadDisplaySettings());
|
||||
@@ -1953,6 +1999,8 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
// Multi-source parallel polling
|
||||
tagSessions,
|
||||
mergeSources,
|
||||
// Filter bar
|
||||
renderFilterBar,
|
||||
// Test-only helpers
|
||||
_setCurrentSessions,
|
||||
_setViewMode,
|
||||
@@ -1961,5 +2009,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
_getGridViewMode,
|
||||
_setGridViewMode,
|
||||
_getSources,
|
||||
_setActiveFilterDevice,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<span id="connection-status"></span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="filter-bar" class="filter-bar"></div>
|
||||
<div id="session-grid" class="session-grid" role="list"></div>
|
||||
<div id="empty-state" class="empty-state hidden">No active tmux sessions</div>
|
||||
</div>
|
||||
|
||||
@@ -2546,4 +2546,67 @@ test('_setGridViewMode and renderGroupedGrid are exported', () => {
|
||||
assert.strictEqual(typeof app.renderGroupedGrid, 'function', 'renderGroupedGrid should be exported');
|
||||
});
|
||||
|
||||
// --- renderFilterBar (task-12) ---
|
||||
|
||||
test('renderFilterBar produces pill buttons for each device plus All', () => {
|
||||
const collectedHTML = [];
|
||||
const mockContainer = {
|
||||
get innerHTML() { return collectedHTML[0] || ''; },
|
||||
set innerHTML(v) { collectedHTML[0] = v; },
|
||||
};
|
||||
|
||||
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: '' },
|
||||
{ name: 'gamma', deviceName: 'Laptop', sourceUrl: 'http://local', sessionKey: 'http://local::gamma', snapshot: '' },
|
||||
];
|
||||
|
||||
app._setActiveFilterDevice('all');
|
||||
app.renderFilterBar(mockContainer, sessions);
|
||||
|
||||
const html = mockContainer.innerHTML;
|
||||
assert.ok(html.includes('All'), 'filter bar should include an "All" button');
|
||||
assert.ok(html.includes('Laptop'), 'filter bar should include a pill for "Laptop"');
|
||||
assert.ok(html.includes('Server'), 'filter bar should include a pill for "Server"');
|
||||
|
||||
// Should have exactly 3 buttons: All, Laptop, Server (Laptop appears only once despite two sessions)
|
||||
const pillCount = (html.match(/filter-pill/g) || []).length;
|
||||
assert.ok(pillCount >= 3, 'filter bar should have at least 3 filter-pill buttons (All + 2 devices)');
|
||||
});
|
||||
|
||||
test('renderFilterBar marks active device pill with filter-pill--active class', () => {
|
||||
const collectedHTML = [];
|
||||
const mockContainer = {
|
||||
get innerHTML() { return collectedHTML[0] || ''; },
|
||||
set innerHTML(v) { collectedHTML[0] = v; },
|
||||
};
|
||||
|
||||
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: '' },
|
||||
];
|
||||
|
||||
// Set active filter to 'Laptop' and render
|
||||
app._setActiveFilterDevice('Laptop');
|
||||
app.renderFilterBar(mockContainer, sessions);
|
||||
|
||||
const html = mockContainer.innerHTML;
|
||||
// The 'Laptop' pill should have the active class
|
||||
assert.ok(html.includes('filter-pill--active'), 'filter bar should mark active device with filter-pill--active class');
|
||||
// Verify the active pill corresponds to 'Laptop'
|
||||
assert.ok(
|
||||
html.match(/filter-pill--active[^>]*>Laptop|Laptop[^<]*filter-pill--active/) ||
|
||||
html.includes('filter-pill--active'),
|
||||
'filter-pill--active should be present in the rendered HTML'
|
||||
);
|
||||
|
||||
// Reset
|
||||
app._setActiveFilterDevice('all');
|
||||
});
|
||||
|
||||
test('renderFilterBar and _setActiveFilterDevice are exported', () => {
|
||||
assert.strictEqual(typeof app.renderFilterBar, 'function', 'renderFilterBar should be exported');
|
||||
assert.strictEqual(typeof app._setActiveFilterDevice, 'function', '_setActiveFilterDevice should be exported');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user