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;
|
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) {
|
function renderGrid(sessions) {
|
||||||
var grid = $('session-grid');
|
var grid = $('session-grid');
|
||||||
var emptyState = $('empty-state');
|
var emptyState = $('empty-state');
|
||||||
@@ -1794,6 +1824,17 @@ function bindStaticEventListeners() {
|
|||||||
if (el) el.value = NEW_SESSION_DEFAULT_TEMPLATE;
|
if (el) el.value = NEW_SESSION_DEFAULT_TEMPLATE;
|
||||||
patchServerSetting('new_session_template', 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 ────────────────────────────────────────────────────────
|
// ─── Test-only helpers ────────────────────────────────────────────────────────
|
||||||
@@ -1863,6 +1904,11 @@ function _getSources() {
|
|||||||
return _sources;
|
return _sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Test-only: set _activeFilterDevice directly. */
|
||||||
|
function _setActiveFilterDevice(device) {
|
||||||
|
_activeFilterDevice = device;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initDeviceId();
|
initDeviceId();
|
||||||
applyDisplaySettings(loadDisplaySettings());
|
applyDisplaySettings(loadDisplaySettings());
|
||||||
@@ -1953,6 +1999,8 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
// Multi-source parallel polling
|
// Multi-source parallel polling
|
||||||
tagSessions,
|
tagSessions,
|
||||||
mergeSources,
|
mergeSources,
|
||||||
|
// Filter bar
|
||||||
|
renderFilterBar,
|
||||||
// Test-only helpers
|
// Test-only helpers
|
||||||
_setCurrentSessions,
|
_setCurrentSessions,
|
||||||
_setViewMode,
|
_setViewMode,
|
||||||
@@ -1961,5 +2009,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
_getGridViewMode,
|
_getGridViewMode,
|
||||||
_setGridViewMode,
|
_setGridViewMode,
|
||||||
_getSources,
|
_getSources,
|
||||||
|
_setActiveFilterDevice,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<span id="connection-status"></span>
|
<span id="connection-status"></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div id="filter-bar" class="filter-bar"></div>
|
||||||
<div id="session-grid" class="session-grid" role="list"></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 id="empty-state" class="empty-state hidden">No active tmux sessions</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2546,4 +2546,67 @@ test('_setGridViewMode and renderGroupedGrid are exported', () => {
|
|||||||
assert.strictEqual(typeof app.renderGroupedGrid, 'function', 'renderGroupedGrid should be 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