feat: add Add Sessions panel JS logic with immediate-commit checkboxes
This commit is contained in:
@@ -1969,6 +1969,260 @@ function _executeKill(name, remoteId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Add Sessions Panel ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the Add Sessions panel for the active user view.
|
||||||
|
* Only available for user views (not "All" or "Hidden").
|
||||||
|
*/
|
||||||
|
function openAddSessionsPanel() {
|
||||||
|
if (_activeView === 'all' || _activeView === 'hidden') return;
|
||||||
|
|
||||||
|
var panel = $('add-sessions-panel');
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
// Update title
|
||||||
|
var titleEl = $('add-sessions-title');
|
||||||
|
if (titleEl) titleEl.textContent = 'Add Sessions to “' + _activeView + '”';
|
||||||
|
|
||||||
|
renderAddSessionsList();
|
||||||
|
panel.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
var backdrop = $('add-sessions-backdrop');
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.onclick = closeAddSessionsPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
var closeBtn = $('add-sessions-close');
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.onclick = closeAddSessionsPanel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the Add Sessions panel.
|
||||||
|
*/
|
||||||
|
function closeAddSessionsPanel() {
|
||||||
|
var panel = $('add-sessions-panel');
|
||||||
|
if (panel) panel.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the session list inside the Add Sessions panel.
|
||||||
|
* Shows all sessions NOT currently in the active view.
|
||||||
|
* Hidden sessions are shown dimmed with a "hidden" badge and disclosure text.
|
||||||
|
* Grouped by device, alphabetical within each group.
|
||||||
|
* Immediate-commit checkboxes — each change fires a PATCH immediately.
|
||||||
|
*/
|
||||||
|
function renderAddSessionsList() {
|
||||||
|
var listEl = $('add-sessions-list');
|
||||||
|
var emptyEl = $('add-sessions-empty');
|
||||||
|
if (!listEl) return;
|
||||||
|
|
||||||
|
var views = (_serverSettings && _serverSettings.views) || [];
|
||||||
|
var hidden = (_serverSettings && _serverSettings.hidden_sessions) || [];
|
||||||
|
|
||||||
|
// Find the active view's session list
|
||||||
|
var activeViewObj = null;
|
||||||
|
for (var i = 0; i < views.length; i++) {
|
||||||
|
if (views[i].name === _activeView) {
|
||||||
|
activeViewObj = views[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!activeViewObj) { listEl.innerHTML = ''; return; }
|
||||||
|
var viewSessions = activeViewObj.sessions || [];
|
||||||
|
|
||||||
|
// Get all real sessions (not status entries), excluding those already in the view
|
||||||
|
var allSessions = (_currentSessions || []).filter(function(s) {
|
||||||
|
return !s.status;
|
||||||
|
});
|
||||||
|
|
||||||
|
var notInView = allSessions.filter(function(s) {
|
||||||
|
var key = s.sessionKey || s.name;
|
||||||
|
return viewSessions.indexOf(key) === -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notInView.length === 0) {
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
if (emptyEl) emptyEl.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
|
|
||||||
|
// Sort: group by deviceName, alphabetical within each group
|
||||||
|
notInView.sort(function(a, b) {
|
||||||
|
var da = (_getDeviceDisplayName(a) || '').toLowerCase();
|
||||||
|
var db = (_getDeviceDisplayName(b) || '').toLowerCase();
|
||||||
|
if (da !== db) return da < db ? -1 : 1;
|
||||||
|
var na = (a.name || '').toLowerCase();
|
||||||
|
var nb = (b.name || '').toLowerCase();
|
||||||
|
return na < nb ? -1 : na > nb ? 1 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
for (var j = 0; j < notInView.length; j++) {
|
||||||
|
var s = notInView[j];
|
||||||
|
var key = s.sessionKey || s.name;
|
||||||
|
var isHidden = hidden.indexOf(key) !== -1 || hidden.indexOf(s.name) !== -1;
|
||||||
|
var escapedName = escapeHtml(s.name || '');
|
||||||
|
var deviceName = escapeHtml(_getDeviceDisplayName(s) || '');
|
||||||
|
|
||||||
|
html += '<label class="add-sessions-item' + (isHidden ? ' add-sessions-item--hidden' : '') + '">';
|
||||||
|
html += '<input type="checkbox" class="add-sessions-item__checkbox" data-session-key="' + escapeHtml(key) + '"' + (isHidden ? ' data-is-hidden="1"' : '') + ' />';
|
||||||
|
html += '<span class="add-sessions-item__name">' + escapedName + '</span>';
|
||||||
|
if (deviceName) html += '<span class="add-sessions-item__device">' + deviceName + '</span>';
|
||||||
|
if (isHidden) html += '<span class="add-sessions-item__badge">hidden</span>';
|
||||||
|
html += '</label>';
|
||||||
|
if (isHidden) {
|
||||||
|
html += '<div class="add-sessions-item__disclosure">This will make it visible again.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listEl.innerHTML = html;
|
||||||
|
|
||||||
|
// Delegated change handler for immediate-commit checkboxes
|
||||||
|
// Each checkbox change fires api('PATCH', '/api/settings') immediately (no batch Done).
|
||||||
|
listEl.onchange = function(e) {
|
||||||
|
var cb = e.target.closest('.add-sessions-item__checkbox');
|
||||||
|
if (!cb) return;
|
||||||
|
var sessionKey = cb.dataset.sessionKey;
|
||||||
|
var isHiddenSession = cb.dataset.isHidden === '1';
|
||||||
|
|
||||||
|
if (cb.checked) {
|
||||||
|
var views = (_serverSettings && _serverSettings.views) || [];
|
||||||
|
var updatedViews = JSON.parse(JSON.stringify(views));
|
||||||
|
for (var vi = 0; vi < updatedViews.length; vi++) {
|
||||||
|
if (updatedViews[vi].name === _activeView) {
|
||||||
|
var vs = updatedViews[vi].sessions || [];
|
||||||
|
if (vs.indexOf(sessionKey) === -1) vs.push(sessionKey);
|
||||||
|
updatedViews[vi].sessions = vs;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var patch = { views: updatedViews };
|
||||||
|
if (isHiddenSession) {
|
||||||
|
var hiddenList = (_serverSettings && _serverSettings.hidden_sessions) || [];
|
||||||
|
var hi = hiddenList.indexOf(sessionKey);
|
||||||
|
if (hi !== -1) {
|
||||||
|
var updatedHidden = hiddenList.slice();
|
||||||
|
updatedHidden.splice(hi, 1);
|
||||||
|
patch.hidden_sessions = updatedHidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api('PATCH', '/api/settings', patch)
|
||||||
|
.then(function() {
|
||||||
|
if (_serverSettings) {
|
||||||
|
_serverSettings.views = updatedViews;
|
||||||
|
if (patch.hidden_sessions) _serverSettings.hidden_sessions = patch.hidden_sessions;
|
||||||
|
}
|
||||||
|
renderAddSessionsList();
|
||||||
|
renderGrid(_currentSessions || []);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
showToast('Couldn’t save — try again');
|
||||||
|
if (cb) cb.checked = false;
|
||||||
|
console.warn('[renderAddSessionsList] PATCH failed:', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Should not normally happen (unchecking means removing), but handle gracefully
|
||||||
|
cb.checked = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show/hide disclosure on hover for hidden items
|
||||||
|
listEl.onmouseover = function(e) {
|
||||||
|
var item = e.target.closest('.add-sessions-item--hidden');
|
||||||
|
if (item) {
|
||||||
|
var disc = item.nextElementSibling;
|
||||||
|
if (disc && disc.classList.contains('add-sessions-item__disclosure')) {
|
||||||
|
disc.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
listEl.onmouseout = function(e) {
|
||||||
|
var item = e.target.closest('.add-sessions-item--hidden');
|
||||||
|
if (item) {
|
||||||
|
var disc = item.nextElementSibling;
|
||||||
|
if (disc && disc.classList.contains('add-sessions-item__disclosure')) {
|
||||||
|
disc.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human-readable display name for the device a session belongs to.
|
||||||
|
* Priority: friendly name → hostname → truncated device_id → empty string.
|
||||||
|
* @param {object} session
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function _getDeviceDisplayName(session) {
|
||||||
|
if (!session) return '';
|
||||||
|
if (session.device_name) return session.device_name;
|
||||||
|
if (session.deviceName) return session.deviceName;
|
||||||
|
if (session.hostname) return session.hostname;
|
||||||
|
if (session.device_id) return session.device_id.slice(0, 8);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a session to the active user view via PATCH.
|
||||||
|
* If the session is hidden, also unhide it.
|
||||||
|
* On error: show toast, revert checkbox.
|
||||||
|
* @param {string} sessionKey
|
||||||
|
* @param {boolean} unhideFirst
|
||||||
|
* @param {HTMLInputElement} checkbox
|
||||||
|
*/
|
||||||
|
function _addSessionToActiveView(sessionKey, unhideFirst, checkbox) {
|
||||||
|
var views = (_serverSettings && _serverSettings.views) || [];
|
||||||
|
var updatedViews = JSON.parse(JSON.stringify(views));
|
||||||
|
|
||||||
|
// Find active view and add session
|
||||||
|
for (var i = 0; i < updatedViews.length; i++) {
|
||||||
|
if (updatedViews[i].name === _activeView) {
|
||||||
|
var sessions = updatedViews[i].sessions || [];
|
||||||
|
if (sessions.indexOf(sessionKey) === -1) {
|
||||||
|
sessions.push(sessionKey);
|
||||||
|
}
|
||||||
|
updatedViews[i].sessions = sessions;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var patch = { views: updatedViews };
|
||||||
|
|
||||||
|
if (unhideFirst) {
|
||||||
|
var hidden = (_serverSettings && _serverSettings.hidden_sessions) || [];
|
||||||
|
var hiddenIdx = hidden.indexOf(sessionKey);
|
||||||
|
if (hiddenIdx !== -1) {
|
||||||
|
var updatedHidden = hidden.slice();
|
||||||
|
updatedHidden.splice(hiddenIdx, 1);
|
||||||
|
patch.hidden_sessions = updatedHidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api('PATCH', '/api/settings', patch)
|
||||||
|
.then(function() {
|
||||||
|
if (_serverSettings) {
|
||||||
|
_serverSettings.views = updatedViews;
|
||||||
|
if (patch.hidden_sessions) _serverSettings.hidden_sessions = patch.hidden_sessions;
|
||||||
|
}
|
||||||
|
// Re-render the list (session is now in the view, so it disappears from the list)
|
||||||
|
renderAddSessionsList();
|
||||||
|
// Refresh the grid behind the panel
|
||||||
|
renderGrid(_currentSessions || []);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
showToast('Couldn’t save — try again');
|
||||||
|
if (checkbox) checkbox.checked = false;
|
||||||
|
console.warn('[_addSessionToActiveView] PATCH failed:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── Notification permission ────────────────────────────────────────────────
|
// ─── Notification permission ────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3758,6 +4012,10 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
createNewSession,
|
createNewSession,
|
||||||
// Kill session
|
// Kill session
|
||||||
killSession,
|
killSession,
|
||||||
|
// Add Sessions panel
|
||||||
|
openAddSessionsPanel,
|
||||||
|
closeAddSessionsPanel,
|
||||||
|
renderAddSessionsList,
|
||||||
// Flyout menu
|
// Flyout menu
|
||||||
openFlyoutMenu,
|
openFlyoutMenu,
|
||||||
closeFlyoutMenu,
|
closeFlyoutMenu,
|
||||||
|
|||||||
@@ -3882,3 +3882,45 @@ def test_do_kill_inline_shows_confirmation_buttons() -> None:
|
|||||||
assert "Yes" in fn_body and "No" in fn_body, (
|
assert "Yes" in fn_body and "No" in fn_body, (
|
||||||
"_doKillSessionInline must show 'Kill? [Yes] [No]' inline"
|
"_doKillSessionInline must show 'Kill? [Yes] [No]' inline"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Add Sessions panel JS logic (task-9)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_add_sessions_panel_function_exists() -> None:
|
||||||
|
"""app.js must define an openAddSessionsPanel function."""
|
||||||
|
assert "function openAddSessionsPanel" in _JS, (
|
||||||
|
"app.js must contain an openAddSessionsPanel function"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_close_add_sessions_panel_function_exists() -> None:
|
||||||
|
"""app.js must define a closeAddSessionsPanel function."""
|
||||||
|
assert "function closeAddSessionsPanel" in _JS, (
|
||||||
|
"app.js must contain a closeAddSessionsPanel function"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_add_sessions_list_function_exists() -> None:
|
||||||
|
"""app.js must define a renderAddSessionsList function."""
|
||||||
|
assert "function renderAddSessionsList" in _JS, (
|
||||||
|
"app.js must contain a renderAddSessionsList function"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_sessions_uses_immediate_commit() -> None:
|
||||||
|
"""renderAddSessionsList must PATCH immediately on checkbox change (no batch Done)."""
|
||||||
|
fn_body = _JS.split("function renderAddSessionsList")[1].split("\nfunction ")[0]
|
||||||
|
assert "PATCH" in fn_body or "api(" in fn_body, (
|
||||||
|
"renderAddSessionsList must fire PATCH on each checkbox change (immediate commit)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_sessions_shows_device_name() -> None:
|
||||||
|
"""renderAddSessionsList must show device name next to each session."""
|
||||||
|
fn_body = _JS.split("function renderAddSessionsList")[1].split("\nfunction ")[0]
|
||||||
|
assert "deviceName" in fn_body or "device" in fn_body, (
|
||||||
|
"renderAddSessionsList must show device name for disambiguation"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user