feat(ui): dim hidden sessions in Manage View with CSS badge (Phase 5)

Hidden sessions in the Manage View panel now render at 55% opacity with a "(hidden)" pseudo-element badge driven by CSS.

renderManageViewList uses the isHidden() Phase 1 helper for the conditional class — no inline hidden-checks remain in the rendering path.

4 new tests verify the class is applied/removed correctly across renders.
This commit is contained in:
Brian Krabach
2026-05-17 10:26:26 -07:00
parent fe0a5e10b2
commit a202d827d1
3 changed files with 154 additions and 7 deletions
+6 -6
View File
@@ -2613,7 +2613,6 @@ function renderManageViewList() {
if (!listEl) return; if (!listEl) return;
var views = (_serverSettings && _serverSettings.views) || []; var views = (_serverSettings && _serverSettings.views) || [];
var hidden = (_serverSettings && _serverSettings.hidden_sessions) || [];
// Find the active view's session list // Find the active view's session list
var activeViewObj = null; var activeViewObj = null;
@@ -2665,17 +2664,18 @@ function renderManageViewList() {
var s = sorted[j]; var s = sorted[j];
var key = s.sessionKey || s.name; var key = s.sessionKey || s.name;
var isInView = viewSessions.indexOf(key) !== -1 || viewSessions.indexOf(s.name) !== -1; var isInView = viewSessions.indexOf(key) !== -1 || viewSessions.indexOf(s.name) !== -1;
var isHidden = hidden.indexOf(key) !== -1 || hidden.indexOf(s.name) !== -1; // Phase 5: use the isHidden() helper (Phase 1) — do not inline a hidden check here.
// The manage-view-item--hidden class triggers opacity + CSS ::after "(hidden)" badge.
var sessionIsHidden = isHidden(key, _serverSettings);
var escapedName = escapeHtml(s.name || ''); var escapedName = escapeHtml(s.name || '');
var deviceName = escapeHtml(_getDeviceDisplayName(s) || ''); var deviceName = escapeHtml(_getDeviceDisplayName(s) || '');
html += '<label class="manage-view-item' + (isHidden ? ' manage-view-item--hidden' : '') + '">'; html += '<label class="manage-view-item' + (sessionIsHidden ? ' manage-view-item--hidden' : '') + '">';
html += '<input type="checkbox" class="manage-view-item__checkbox" data-session-key="' + escapeHtml(key) + '"' + (isInView ? ' checked' : '') + (isHidden ? ' data-is-hidden="1"' : '') + ' />'; html += '<input type="checkbox" class="manage-view-item__checkbox" data-session-key="' + escapeHtml(key) + '"' + (isInView ? ' checked' : '') + (sessionIsHidden ? ' data-is-hidden="1"' : '') + ' />';
html += '<span class="manage-view-item__name">' + escapedName + '</span>'; html += '<span class="manage-view-item__name">' + escapedName + '</span>';
if (deviceName) html += '<span class="manage-view-item__device">' + deviceName + '</span>'; if (deviceName) html += '<span class="manage-view-item__device">' + deviceName + '</span>';
if (isHidden) html += '<span class="manage-view-item__badge">hidden</span>';
html += '</label>'; html += '</label>';
if (isHidden) { if (sessionIsHidden) {
html += '<div class="manage-view-item__disclosure">Adding will unhide this session</div>'; html += '<div class="manage-view-item__disclosure">Adding will unhide this session</div>';
} }
} }
+10 -1
View File
@@ -1907,7 +1907,16 @@ body {
} }
.manage-view-item--hidden { .manage-view-item--hidden {
opacity: 0.5; opacity: 0.55;
}
/* Phase 5: "(hidden)" badge via CSS pseudo-element on the name span.
CSS pseudo-elements cannot be read from JSDOM — test coverage verifies
the manage-view-item--hidden class is applied (which triggers this rule). */
.manage-view-item--hidden .manage-view-item__name::after {
content: " (hidden)";
font-size: 0.85em;
opacity: 0.7;
} }
.manage-view-item__checkbox { .manage-view-item__checkbox {
+138
View File
@@ -5349,3 +5349,141 @@ test('removeSessionFromViewOp does not mutate its settings argument', () => {
app.removeSessionFromViewOp(settings, 'Work', 'dev1:a'); app.removeSessionFromViewOp(settings, 'Work', 'dev1:a');
assert.strictEqual(JSON.stringify(settings), before, 'settings must not be mutated'); assert.strictEqual(JSON.stringify(settings), before, 'settings must not be mutated');
}); });
// ─── Phase 5 dim styling ────────────────────────────────────────────────────
//
// renderManageViewList applies manage-view-item--hidden to rows for sessions
// that are in settings.hidden_sessions. The CSS class triggers:
// opacity: 0.55
// .manage-view-item__name::after { content: " (hidden)"; ... }
//
// The ::after pseudo-element is not readable from JSDOM, so tests verify that
// the class is applied (which in a browser triggers the badge via CSS).
// ────────────────────────────────────────────────────────────────────────────
// Helper: mock document.getElementById for renderManageViewList and return
// the fake list element so callers can inspect innerHTML.
function _withManageViewDOM(fn) {
const fakeList = { innerHTML: '', onchange: null };
const fakeSummary = { textContent: '' };
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'manage-view-list') return fakeList;
if (id === 'manage-view-summary') return fakeSummary;
return null;
};
try {
fn(fakeList, fakeSummary);
} finally {
globalThis.document.getElementById = origGetById;
}
}
test('Phase 5: hidden session row gets manage-view-item--hidden class', () => {
// Session dev1:hidden-sess is in the Work view AND in hidden_sessions.
const settings = {
hidden_sessions: ['dev1:hidden-sess'],
views: [{ name: 'Work', sessions: ['dev1:hidden-sess'] }],
};
app._setServerSettings(settings);
app._setCurrentSessions([{ name: 'hidden-sess', sessionKey: 'dev1:hidden-sess' }]);
app._setActiveView('Work');
let renderedHTML = '';
_withManageViewDOM((fakeList) => {
app.renderManageViewList();
renderedHTML = fakeList.innerHTML;
});
app._setServerSettings(null);
app._setCurrentSessions([]);
assert.ok(
renderedHTML.includes('manage-view-item--hidden'),
'hidden session label must have manage-view-item--hidden class; got: ' + renderedHTML
);
});
test('Phase 5: non-hidden session row does NOT get manage-view-item--hidden class', () => {
// Session dev1:visible-sess is in the Work view and NOT hidden.
const settings = {
hidden_sessions: [],
views: [{ name: 'Work', sessions: ['dev1:visible-sess'] }],
};
app._setServerSettings(settings);
app._setCurrentSessions([{ name: 'visible-sess', sessionKey: 'dev1:visible-sess' }]);
app._setActiveView('Work');
let renderedHTML = '';
_withManageViewDOM((fakeList) => {
app.renderManageViewList();
renderedHTML = fakeList.innerHTML;
});
app._setServerSettings(null);
app._setCurrentSessions([]);
assert.ok(
!renderedHTML.includes('manage-view-item--hidden'),
'visible session label must NOT have manage-view-item--hidden class; got: ' + renderedHTML
);
});
test('Phase 5: dim class is absent after session is unhidden (re-render)', () => {
// Render once with hidden, once without — verify the class toggles.
const sessionKey = 'dev1:toggled-sess';
const session = [{ name: 'toggled-sess', sessionKey }];
// First render: session is hidden.
app._setServerSettings({
hidden_sessions: [sessionKey],
views: [{ name: 'Work', sessions: [sessionKey] }],
});
app._setCurrentSessions(session);
app._setActiveView('Work');
let htmlWhenHidden = '';
_withManageViewDOM((fakeList) => {
app.renderManageViewList();
htmlWhenHidden = fakeList.innerHTML;
});
// Second render: unhide the session (empty hidden_sessions).
app._setServerSettings({
hidden_sessions: [],
views: [{ name: 'Work', sessions: [sessionKey] }],
});
let htmlWhenVisible = '';
_withManageViewDOM((fakeList) => {
app.renderManageViewList();
htmlWhenVisible = fakeList.innerHTML;
});
app._setServerSettings(null);
app._setCurrentSessions([]);
assert.ok(
htmlWhenHidden.includes('manage-view-item--hidden'),
'row must be dimmed when session is hidden'
);
assert.ok(
!htmlWhenVisible.includes('manage-view-item--hidden'),
'row must NOT be dimmed after session is unhidden'
);
});
test('Phase 5: isHidden() helper (not inline check) drives the dim class', () => {
// Verify that isHidden() correctly reports hidden state, confirming the
// helper is the source of truth used by renderManageViewList.
// (The CSS ::after "(hidden)" badge cannot be verified from JSDOM — see
// comment at top of Phase 5 section. Class presence is sufficient.)
const settings = { hidden_sessions: ['dev1:sess-a'] };
assert.strictEqual(app.isHidden('dev1:sess-a', settings), true,
'isHidden must return true for a key in hidden_sessions');
assert.strictEqual(app.isHidden('dev1:sess-b', settings), false,
'isHidden must return false for a key not in hidden_sessions');
assert.strictEqual(app.isHidden('dev1:sess-a', null), false,
'isHidden must return false when settings is null');
});