feat: UX overhaul — Manage View panel, dropdown refinements, label fixes, badge sizing
This commit is contained in:
+158
-157
@@ -911,10 +911,15 @@ function renderViewDropdown() {
|
||||
var hiddenActive = _activeView === 'hidden' ? ' view-dropdown__item--active' : '';
|
||||
html += '<button class="view-dropdown__item' + hiddenActive + '" role="menuitem" data-view="hidden">Hidden <span class="view-dropdown__count">' + hiddenCount + '</span></button>';
|
||||
|
||||
// — Actions
|
||||
html += '<div class="view-dropdown__separator"></div>';
|
||||
// — Actions (stronger separator)
|
||||
html += '<div class="view-dropdown__separator view-dropdown__separator--strong"></div>';
|
||||
// Only show "Manage [ViewName]\u2026" when a user view is active
|
||||
if (_activeView !== 'all' && _activeView !== 'hidden') {
|
||||
var displayViewName = _activeView.length > 20 ? _activeView.substring(0, 20) + '\u2026' : _activeView;
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="manage-view">Manage \u201c' + escapeHtml(displayViewName) + '\u201d\u2026</button>';
|
||||
}
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="manage-views">Manage All Views\u2026</button>';
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="new-view">+ New View</button>';
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="manage-views">Manage Views\u2026</button>';
|
||||
|
||||
menu.innerHTML = html;
|
||||
|
||||
@@ -1003,10 +1008,15 @@ function renderSidebarViewDropdown() {
|
||||
var hiddenActive = _activeView === 'hidden' ? ' view-dropdown__item--active' : '';
|
||||
html += '<button class="view-dropdown__item' + hiddenActive + '" role="menuitem" data-view="hidden">Hidden <span class="view-dropdown__count">' + hiddenCount + '</span></button>';
|
||||
|
||||
// — Actions: new view + manage views
|
||||
html += '<div class="view-dropdown__separator"></div>';
|
||||
// — Actions (stronger separator)
|
||||
html += '<div class="view-dropdown__separator view-dropdown__separator--strong"></div>';
|
||||
// Only show "Manage [ViewName]…" when a user view is active
|
||||
if (_activeView !== 'all' && _activeView !== 'hidden') {
|
||||
var sbDisplayViewName = _activeView.length > 20 ? _activeView.substring(0, 20) + '…' : _activeView;
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="manage-view">Manage “' + escapeHtml(sbDisplayViewName) + '”…</button>';
|
||||
}
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="manage-views">Manage All Views…</button>';
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="new-view">+ New View</button>';
|
||||
html += '<button class="view-dropdown__item view-dropdown__action" role="menuitem" data-action="manage-views">Manage Views\u2026</button>';
|
||||
|
||||
menu.innerHTML = html;
|
||||
}
|
||||
@@ -1100,7 +1110,7 @@ function showNewViewInput() {
|
||||
.then(function() {
|
||||
if (_serverSettings) _serverSettings.views = updatedViews;
|
||||
switchView(name);
|
||||
openAddSessionsPanel();
|
||||
openManageViewPanel();
|
||||
})
|
||||
.catch(function() {
|
||||
showToast('Failed to create view');
|
||||
@@ -1122,7 +1132,7 @@ function showNewViewInput() {
|
||||
/**
|
||||
* Show an inline text input inside the SIDEBAR view dropdown for creating a new view.
|
||||
* Targets #sidebar-view-dropdown-menu instead of #view-dropdown-menu.
|
||||
* - On Enter: validates, PATCHes /api/settings, calls switchView + openAddSessionsPanel.
|
||||
* - On Enter: validates, PATCHes /api/settings, calls switchView + openManageViewPanel.
|
||||
* - On Escape / blur: closes the sidebar dropdown.
|
||||
*/
|
||||
function showSidebarNewViewInput() {
|
||||
@@ -1185,7 +1195,7 @@ function showSidebarNewViewInput() {
|
||||
if (_serverSettings) _serverSettings.views = updatedViews;
|
||||
closeSidebarDropdown();
|
||||
switchView(name);
|
||||
openAddSessionsPanel();
|
||||
openManageViewPanel();
|
||||
})
|
||||
.catch(function() {
|
||||
showToast('Failed to create view');
|
||||
@@ -1241,7 +1251,7 @@ function renderViewsSettingsTab() {
|
||||
|
||||
if (emptyEl) emptyEl.style.display = 'none';
|
||||
|
||||
// Build the list of view rows
|
||||
// Build the list of view rows (no inline rename — rename is in Manage View panel)
|
||||
listEl.innerHTML = '';
|
||||
views.forEach(function(view, idx) {
|
||||
var viewSessions = view.sessions || [];
|
||||
@@ -1251,11 +1261,10 @@ function renderViewsSettingsTab() {
|
||||
row.className = 'views-settings-row';
|
||||
row.setAttribute('data-view-idx', String(idx));
|
||||
|
||||
// Name span (clickable for rename)
|
||||
// Name span (not clickable for rename — rename is in Manage View panel)
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'views-settings-name';
|
||||
nameSpan.textContent = view.name;
|
||||
nameSpan.title = 'Click to rename';
|
||||
|
||||
// Session count
|
||||
var countSpan = document.createElement('span');
|
||||
@@ -1269,7 +1278,7 @@ function renderViewsSettingsTab() {
|
||||
// Up button
|
||||
var upBtn = document.createElement('button');
|
||||
upBtn.className = 'views-settings-btn';
|
||||
upBtn.textContent = '▲';
|
||||
upBtn.textContent = '\u25b2';
|
||||
upBtn.title = 'Move up';
|
||||
upBtn.setAttribute('data-action', 'move-up');
|
||||
upBtn.setAttribute('data-idx', String(idx));
|
||||
@@ -1278,12 +1287,19 @@ function renderViewsSettingsTab() {
|
||||
// Down button
|
||||
var downBtn = document.createElement('button');
|
||||
downBtn.className = 'views-settings-btn';
|
||||
downBtn.textContent = '▼';
|
||||
downBtn.textContent = '\u25bc';
|
||||
downBtn.title = 'Move down';
|
||||
downBtn.setAttribute('data-action', 'move-down');
|
||||
downBtn.setAttribute('data-idx', String(idx));
|
||||
if (idx === views.length - 1) downBtn.disabled = true;
|
||||
|
||||
// Manage button (opens Manage View panel — close settings first)
|
||||
var manageBtn = document.createElement('button');
|
||||
manageBtn.className = 'views-settings-btn views-settings-btn--manage';
|
||||
manageBtn.textContent = 'Manage';
|
||||
manageBtn.setAttribute('data-action', 'manage');
|
||||
manageBtn.setAttribute('data-idx', String(idx));
|
||||
|
||||
// Delete button
|
||||
var deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'views-settings-btn views-settings-btn--danger';
|
||||
@@ -1293,6 +1309,7 @@ function renderViewsSettingsTab() {
|
||||
|
||||
actionsDiv.appendChild(upBtn);
|
||||
actionsDiv.appendChild(downBtn);
|
||||
actionsDiv.appendChild(manageBtn);
|
||||
actionsDiv.appendChild(deleteBtn);
|
||||
|
||||
row.appendChild(nameSpan);
|
||||
@@ -1301,6 +1318,16 @@ function renderViewsSettingsTab() {
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
|
||||
// Add "+ New View" button at the bottom
|
||||
var newViewRow = document.createElement('div');
|
||||
newViewRow.className = 'views-settings-new-row';
|
||||
var newViewBtn = document.createElement('button');
|
||||
newViewBtn.className = 'views-settings-btn views-settings-btn--new';
|
||||
newViewBtn.textContent = '+ New View';
|
||||
newViewBtn.setAttribute('data-action', 'new-view-in-settings');
|
||||
newViewRow.appendChild(newViewBtn);
|
||||
listEl.appendChild(newViewRow);
|
||||
|
||||
// Delegated click handler on the list
|
||||
listEl.onclick = function(e) {
|
||||
var views = (_serverSettings && _serverSettings.views) || [];
|
||||
@@ -1332,6 +1359,17 @@ function renderViewsSettingsTab() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Manage: close settings, switch to that view, open Manage View panel
|
||||
if (target.getAttribute('data-action') === 'manage') {
|
||||
var idx = parseInt(target.getAttribute('data-idx'), 10);
|
||||
var viewName = views[idx] && views[idx].name;
|
||||
if (!viewName) return;
|
||||
closeSettings();
|
||||
switchView(viewName);
|
||||
openManageViewPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete: show inline confirm
|
||||
if (target.getAttribute('data-action') === 'delete') {
|
||||
var idx = parseInt(target.getAttribute('data-idx'), 10);
|
||||
@@ -1385,69 +1423,34 @@ function renderViewsSettingsTab() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Click on name: inline rename
|
||||
if (target.classList.contains('views-settings-name')) {
|
||||
var row = target.closest('.views-settings-row');
|
||||
if (!row) return;
|
||||
var idx = parseInt(row.getAttribute('data-view-idx'), 10);
|
||||
var currentName = views[idx] && views[idx].name;
|
||||
if (currentName === undefined) return;
|
||||
|
||||
var input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'views-settings-rename-input';
|
||||
input.value = currentName;
|
||||
input.maxLength = 30;
|
||||
target.parentNode.replaceChild(input, target);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
var committed = false;
|
||||
function commitRename() {
|
||||
if (committed) return;
|
||||
var newName = input.value.trim();
|
||||
if (!newName || newName === currentName) {
|
||||
renderViewsSettingsTab();
|
||||
return;
|
||||
}
|
||||
// Validate: not reserved (case-insensitive)
|
||||
// + New View: create a new view and open Manage View panel
|
||||
if (target.getAttribute('data-action') === 'new-view-in-settings') {
|
||||
var newName = prompt('View name:');
|
||||
if (!newName || !newName.trim()) return;
|
||||
newName = newName.trim();
|
||||
if (newName.toLowerCase() === 'all' || newName.toLowerCase() === 'hidden') {
|
||||
showToast('Cannot use reserved name \'' + newName + '\'');
|
||||
renderViewsSettingsTab();
|
||||
return;
|
||||
}
|
||||
// Validate: not duplicate
|
||||
var duplicate = views.find(function(v, i) { return i !== idx && v.name === newName; });
|
||||
if (duplicate) {
|
||||
if (views.find(function(v) { return v.name === newName; })) {
|
||||
showToast('View \'' + newName + '\' already exists');
|
||||
renderViewsSettingsTab();
|
||||
return;
|
||||
}
|
||||
// Update the view name
|
||||
committed = true;
|
||||
var updated = views.map(function(v, i) {
|
||||
return i === idx ? Object.assign({}, v, { name: newName }) : v;
|
||||
});
|
||||
// If this was the active view, update _activeView
|
||||
if (_activeView === currentName) {
|
||||
_activeView = newName;
|
||||
api('PATCH', '/api/state', { active_view: _activeView }).catch(function() {});
|
||||
}
|
||||
_saveViewsAndRerender(updated);
|
||||
}
|
||||
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
commitRename();
|
||||
} else if (e.key === 'Escape') {
|
||||
var updatedViews = views.concat([{ name: newName, sessions: [] }]);
|
||||
api('PATCH', '/api/settings', { views: updatedViews })
|
||||
.then(function() {
|
||||
if (_serverSettings) _serverSettings.views = updatedViews;
|
||||
renderViewsSettingsTab();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
commitRename();
|
||||
renderViewDropdown();
|
||||
// Close settings and open Manage View panel for the new view
|
||||
closeSettings();
|
||||
switchView(newName);
|
||||
openManageViewPanel();
|
||||
})
|
||||
.catch(function() {
|
||||
showToast('Failed to create view');
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1474,8 +1477,6 @@ function switchView(viewName) {
|
||||
sidebarLabel.textContent = viewName;
|
||||
}
|
||||
}
|
||||
// Show/hide the header Add Sessions button
|
||||
updateAddSessionsButton();
|
||||
// Persist active view — fire and forget
|
||||
api('PATCH', '/api/state', { active_view: viewName }).catch(function() {});
|
||||
}
|
||||
@@ -2346,78 +2347,56 @@ function _executeKill(name, remoteId) {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Add Sessions Panel ──────────────────────────────────────────────────────────────────────────
|
||||
// ─── Manage View Panel ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Open the Add Sessions panel for the active user view.
|
||||
* Open the Manage View panel for the active user view.
|
||||
* Only available for user views (not "All" or "Hidden").
|
||||
*/
|
||||
function openAddSessionsPanel() {
|
||||
function openManageViewPanel() {
|
||||
if (_activeView === 'all' || _activeView === 'hidden') return;
|
||||
|
||||
var panel = $('add-sessions-panel');
|
||||
var panel = $('manage-view-panel');
|
||||
if (!panel) return;
|
||||
|
||||
// Update title
|
||||
var titleEl = $('add-sessions-title');
|
||||
if (titleEl) titleEl.textContent = 'Add Sessions to “' + _activeView + '”';
|
||||
// Update title/name
|
||||
var nameEl = $('manage-view-name');
|
||||
if (nameEl) nameEl.textContent = _activeView;
|
||||
|
||||
renderAddSessionsList();
|
||||
renderManageViewList();
|
||||
panel.classList.remove('hidden');
|
||||
|
||||
// Close on backdrop click
|
||||
var backdrop = $('add-sessions-backdrop');
|
||||
var backdrop = $('manage-view-backdrop');
|
||||
if (backdrop) {
|
||||
backdrop.onclick = closeAddSessionsPanel;
|
||||
backdrop.onclick = closeManageViewPanel;
|
||||
}
|
||||
|
||||
// Close button
|
||||
var closeBtn = $('add-sessions-close');
|
||||
// Close button at bottom
|
||||
var closeBtn = $('manage-view-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = closeAddSessionsPanel;
|
||||
closeBtn.onclick = closeManageViewPanel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the Add Sessions panel.
|
||||
* Close the Manage View panel.
|
||||
*/
|
||||
function closeAddSessionsPanel() {
|
||||
var panel = $('add-sessions-panel');
|
||||
function closeManageViewPanel() {
|
||||
var panel = $('manage-view-panel');
|
||||
if (panel) panel.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the header #add-sessions-btn based on the active view.
|
||||
* Visible only when in a user-created view (not 'all' or 'hidden').
|
||||
* Called from switchView() and on initial load.
|
||||
* Render the session list inside the Manage View panel.
|
||||
* Shows ALL sessions: checked = in this view, unchecked = not in this view.
|
||||
* Checked items sorted first, then unchecked. Within each group, alphabetical by device.
|
||||
* Immediate-commit: each checkbox toggle fires PATCH /api/settings immediately.
|
||||
* Hidden sessions: dimmed with "hidden" badge. Static note below for hidden items.
|
||||
*/
|
||||
function updateAddSessionsButton() {
|
||||
var btn = $('add-sessions-btn');
|
||||
if (!btn) return;
|
||||
var isUserView = false;
|
||||
if (_activeView !== 'all' && _activeView !== 'hidden') {
|
||||
var views = (_serverSettings && _serverSettings.views) || [];
|
||||
for (var i = 0; i < views.length; i++) {
|
||||
if (views[i].name === _activeView) { isUserView = true; break; }
|
||||
}
|
||||
}
|
||||
if (isUserView) {
|
||||
btn.classList.remove('hidden');
|
||||
} else {
|
||||
btn.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');
|
||||
function renderManageViewList() {
|
||||
var listEl = $('manage-view-list');
|
||||
var summaryEl = $('manage-view-summary');
|
||||
if (!listEl) return;
|
||||
|
||||
var views = (_serverSettings && _serverSettings.views) || [];
|
||||
@@ -2434,25 +2413,29 @@ function renderAddSessionsList() {
|
||||
if (!activeViewObj) { listEl.innerHTML = ''; return; }
|
||||
var viewSessions = activeViewObj.sessions || [];
|
||||
|
||||
// Get all real sessions (not status entries), excluding those already in the view
|
||||
// Get all real sessions (not status entries)
|
||||
var allSessions = (_currentSessions || []).filter(function(s) {
|
||||
return !s.status;
|
||||
});
|
||||
|
||||
// Update summary line
|
||||
if (summaryEl) {
|
||||
summaryEl.textContent = allSessions.length + ' sessions · ' + viewSessions.length + ' in this view';
|
||||
}
|
||||
|
||||
// Partition into inView (checked first) and notInView
|
||||
var inView = allSessions.filter(function(s) {
|
||||
var key = s.sessionKey || s.name;
|
||||
return viewSessions.indexOf(key) !== -1 || viewSessions.indexOf(s.name) !== -1;
|
||||
});
|
||||
var notInView = allSessions.filter(function(s) {
|
||||
var key = s.sessionKey || s.name;
|
||||
return viewSessions.indexOf(key) === -1;
|
||||
return viewSessions.indexOf(key) === -1 && viewSessions.indexOf(s.name) === -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) {
|
||||
// Sort each group: alphabetical, grouped by device
|
||||
function sortByDeviceAlpha(arr) {
|
||||
return arr.slice().sort(function(a, b) {
|
||||
var da = (_getDeviceDisplayName(a) || '').toLowerCase();
|
||||
var db = (_getDeviceDisplayName(b) || '').toLowerCase();
|
||||
if (da !== db) return da < db ? -1 : 1;
|
||||
@@ -2460,49 +2443,59 @@ function renderAddSessionsList() {
|
||||
var nb = (b.name || '').toLowerCase();
|
||||
return na < nb ? -1 : na > nb ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
var sorted = sortByDeviceAlpha(inView).concat(sortByDeviceAlpha(notInView));
|
||||
|
||||
var html = '';
|
||||
for (var j = 0; j < notInView.length; j++) {
|
||||
var s = notInView[j];
|
||||
for (var j = 0; j < sorted.length; j++) {
|
||||
var s = sorted[j];
|
||||
var key = s.sessionKey || s.name;
|
||||
var isInView = viewSessions.indexOf(key) !== -1 || viewSessions.indexOf(s.name) !== -1;
|
||||
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 class="manage-view-item' + (isHidden ? ' 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 += '<span class="manage-view-item__name">' + escapedName + '</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>';
|
||||
if (isHidden) {
|
||||
html += '<div class="add-sessions-item__disclosure">This will make it visible again.</div>';
|
||||
html += '<div class="manage-view-item__disclosure">Adding will unhide this session</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');
|
||||
var cb = e.target.closest('.manage-view-item__checkbox');
|
||||
if (!cb) return;
|
||||
var sessionKey = cb.dataset.sessionKey;
|
||||
var isChecked = cb.checked;
|
||||
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 (isChecked) {
|
||||
if (vs.indexOf(sessionKey) === -1) vs.push(sessionKey);
|
||||
} else {
|
||||
var pos = vs.indexOf(sessionKey);
|
||||
if (pos !== -1) vs.splice(pos, 1);
|
||||
}
|
||||
updatedViews[vi].sessions = vs;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var patch = { views: updatedViews };
|
||||
if (isHiddenSession) {
|
||||
if (isHiddenSession && isChecked) {
|
||||
var hiddenList = (_serverSettings && _serverSettings.hidden_sessions) || [];
|
||||
var hi = hiddenList.indexOf(sessionKey);
|
||||
if (hi !== -1) {
|
||||
@@ -2511,26 +2504,22 @@ function renderAddSessionsList() {
|
||||
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();
|
||||
renderManageViewList();
|
||||
renderGrid(_currentSessions || []);
|
||||
})
|
||||
.catch(function(err) {
|
||||
showToast('Couldn’t save — try again');
|
||||
if (cb) cb.checked = false;
|
||||
console.warn('[renderAddSessionsList] PATCH failed:', err);
|
||||
if (cb) cb.checked = !isChecked;
|
||||
console.warn('[renderManageViewList] PATCH failed:', err);
|
||||
});
|
||||
} else {
|
||||
// Should not normally happen (unchecking means removing), but handle gracefully
|
||||
cb.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2548,6 +2537,7 @@ function _getDeviceDisplayName(session) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
// ─── Notification permission ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -3895,6 +3885,9 @@ function bindStaticEventListeners() {
|
||||
if (action) {
|
||||
if (action.dataset.action === 'new-view') {
|
||||
showNewViewInput();
|
||||
} else if (action.dataset.action === 'manage-view') {
|
||||
closeViewDropdown();
|
||||
openManageViewPanel();
|
||||
} else if (action.dataset.action === 'manage-views') {
|
||||
closeViewDropdown();
|
||||
openSettings();
|
||||
@@ -3925,6 +3918,10 @@ function bindStaticEventListeners() {
|
||||
if (action) {
|
||||
if (action.dataset.action === 'new-view') {
|
||||
showSidebarNewViewInput();
|
||||
} else if (action.dataset.action === 'manage-view') {
|
||||
sidebarViewMenu.classList.add('hidden');
|
||||
if (sidebarViewTrigger) sidebarViewTrigger.setAttribute('aria-expanded', 'false');
|
||||
openManageViewPanel();
|
||||
} else if (action.dataset.action === 'manage-views') {
|
||||
sidebarViewMenu.classList.add('hidden');
|
||||
if (sidebarViewTrigger) sidebarViewTrigger.setAttribute('aria-expanded', 'false');
|
||||
@@ -3938,9 +3935,6 @@ function bindStaticEventListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
var addSessionsBtn = $('add-sessions-btn');
|
||||
if (addSessionsBtn) on(addSessionsBtn, 'click', openAddSessionsPanel);
|
||||
|
||||
// Click-outside closes the header view dropdown
|
||||
document.addEventListener('click', function(e) {
|
||||
var dropdown = $('view-dropdown-menu');
|
||||
@@ -3958,6 +3952,8 @@ function bindStaticEventListeners() {
|
||||
if (!sidebarDropdown || sidebarDropdown.classList.contains('hidden')) return;
|
||||
var sidebarTrigger = $('sidebar-view-dropdown-trigger');
|
||||
if (sidebarTrigger && sidebarTrigger.contains(e.target)) return;
|
||||
// Don't close if a new-view input was just created (replaceChild removes the click target from DOM)
|
||||
if (sidebarDropdown.querySelector('.view-dropdown__new-input')) return;
|
||||
if (!sidebarDropdown.contains(e.target)) {
|
||||
sidebarDropdown.classList.add('hidden');
|
||||
if (sidebarTrigger) sidebarTrigger.setAttribute('aria-expanded', 'false');
|
||||
@@ -4278,7 +4274,14 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
updatePageTitle();
|
||||
startHeartbeat();
|
||||
bindStaticEventListeners();
|
||||
updateAddSessionsButton();
|
||||
renderViewDropdown();
|
||||
// Update sidebar label after restoreState sets _activeView (Issue 7)
|
||||
var sidebarLabelEl = $('sidebar-view-label');
|
||||
if (sidebarLabelEl) {
|
||||
if (_activeView === 'all') sidebarLabelEl.textContent = 'All Sessions';
|
||||
else if (_activeView === 'hidden') sidebarLabelEl.textContent = 'Hidden';
|
||||
else sidebarLabelEl.textContent = _activeView;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('[init] restoreState failed, retrying in 5s:', err);
|
||||
@@ -4357,10 +4360,10 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
createNewSession,
|
||||
// Kill session
|
||||
killSession,
|
||||
// Add Sessions panel
|
||||
openAddSessionsPanel,
|
||||
closeAddSessionsPanel,
|
||||
renderAddSessionsList,
|
||||
// Manage View panel
|
||||
openManageViewPanel,
|
||||
closeManageViewPanel,
|
||||
renderManageViewList,
|
||||
// Flyout menu
|
||||
openFlyoutMenu,
|
||||
closeFlyoutMenu,
|
||||
@@ -4376,8 +4379,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
renderSidebarViewDropdown,
|
||||
toggleSidebarViewDropdown,
|
||||
showSidebarNewViewInput,
|
||||
// Add Sessions header button
|
||||
updateAddSessionsButton,
|
||||
// Manage Views settings tab
|
||||
renderViewsSettingsTab,
|
||||
_saveViewsAndRerender,
|
||||
|
||||
+14
-11
@@ -27,7 +27,6 @@
|
||||
<div id="view-dropdown-menu" class="view-dropdown__menu hidden" role="menu" aria-label="Switch view"></div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="add-sessions-btn" class="header-btn hidden" aria-label="Add sessions to view">+ Add</button>
|
||||
<button id="new-session-btn" class="header-btn" aria-label="New session">+</button>
|
||||
<button id="view-mode-btn" class="header-btn" aria-label="Toggle view mode" title="View: auto">▦</button>
|
||||
<button id="settings-btn" class="header-btn" aria-label="Settings">⚙</button>
|
||||
@@ -87,16 +86,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- —— Add Sessions panel ——————————————————————————————————————————— -->
|
||||
<div id="add-sessions-panel" class="add-sessions-panel hidden" role="dialog" aria-modal="true" aria-label="Add sessions to view">
|
||||
<div class="add-sessions-panel__backdrop" id="add-sessions-backdrop"></div>
|
||||
<div class="add-sessions-panel__content">
|
||||
<div class="add-sessions-panel__header">
|
||||
<h2 id="add-sessions-title" class="add-sessions-panel__title">Add Sessions</h2>
|
||||
<button id="add-sessions-close" class="add-sessions-panel__close" aria-label="Close">×</button>
|
||||
<!-- —— Manage View panel ———————————————————————————————————————————————————————————————————————————————————— -->
|
||||
<div id="manage-view-panel" class="manage-view-panel hidden" role="dialog" aria-modal="true" aria-label="Manage view">
|
||||
<div class="manage-view-panel__backdrop" id="manage-view-backdrop"></div>
|
||||
<div class="manage-view-panel__content">
|
||||
<div class="manage-view-panel__header">
|
||||
<div class="manage-view-panel__name-row">
|
||||
<h2 id="manage-view-name" class="manage-view-panel__name">View Name</h2>
|
||||
</div>
|
||||
<p id="manage-view-summary" class="manage-view-panel__summary"></p>
|
||||
</div>
|
||||
<div id="manage-view-list" class="manage-view-panel__list"></div>
|
||||
<div class="manage-view-panel__footer">
|
||||
<button id="manage-view-close" class="manage-view-panel__close-btn">Close</button>
|
||||
</div>
|
||||
<div id="add-sessions-list" class="add-sessions-panel__list"></div>
|
||||
<p id="add-sessions-empty" class="add-sessions-panel__empty" style="display:none">All sessions are already in this view.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -228,7 +231,7 @@
|
||||
<button id="setting-template-reset" class="settings-action-btn">Reset to default</button>
|
||||
</div>
|
||||
<div class="settings-field settings-field--column">
|
||||
<label class="settings-label" for="setting-delete-template">Delete session command</label>
|
||||
<label class="settings-label" for="setting-delete-template">Kill session command</label>
|
||||
<textarea id="setting-delete-template" class="settings-textarea" rows="3" placeholder="tmux kill-session -t {name}"></textarea>
|
||||
<span class="settings-helper">{name} is replaced with the session name</span>
|
||||
<button id="setting-delete-template-reset" class="settings-action-btn">Reset to default</button>
|
||||
|
||||
+107
-21
@@ -1687,6 +1687,11 @@ body {
|
||||
background: var(--border-subtle);
|
||||
}
|
||||
|
||||
.view-dropdown__separator--strong {
|
||||
border-color: var(--border);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.view-dropdown__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1731,14 +1736,16 @@ body {
|
||||
============================================================ */
|
||||
|
||||
.device-badge {
|
||||
display: inline-block;
|
||||
font-size: 9px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-dim);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 1px 4px;
|
||||
padding: 3px 6px;
|
||||
line-height: 1.4;
|
||||
min-height: 22px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -1818,7 +1825,7 @@ body {
|
||||
|
||||
/* —— Add Sessions Panel —————————————————————————————————————————————————————————————————————————— */
|
||||
|
||||
.add-sessions-panel {
|
||||
.manage-view-panel {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 250;
|
||||
@@ -1827,13 +1834,13 @@ body {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-sessions-panel__backdrop {
|
||||
.manage-view-panel__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--bg-overlay);
|
||||
}
|
||||
|
||||
.add-sessions-panel__content {
|
||||
.manage-view-panel__content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 440px;
|
||||
@@ -1846,7 +1853,7 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.add-sessions-panel__header {
|
||||
.manage-view-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -1854,14 +1861,14 @@ body {
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.add-sessions-panel__title {
|
||||
.manage-view-panel__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.add-sessions-panel__close {
|
||||
.manage-view-panel__close {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
@@ -1875,25 +1882,25 @@ body {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.add-sessions-panel__close:hover {
|
||||
.manage-view-panel__close:hover {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.add-sessions-panel__list {
|
||||
.manage-view-panel__list {
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.add-sessions-panel__empty {
|
||||
.manage-view-panel__empty {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.add-sessions-item {
|
||||
.manage-view-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
@@ -1901,20 +1908,20 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-sessions-item:hover {
|
||||
.manage-view-item:hover {
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.add-sessions-item--hidden {
|
||||
.manage-view-item--hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.add-sessions-item__checkbox {
|
||||
.manage-view-item__checkbox {
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.add-sessions-item__name {
|
||||
.manage-view-item__name {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
@@ -1924,13 +1931,13 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-sessions-item__device {
|
||||
.manage-view-item__device {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-sessions-item__badge {
|
||||
.manage-view-item__badge {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg);
|
||||
@@ -1939,7 +1946,7 @@ body {
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
.add-sessions-item__disclosure {
|
||||
.manage-view-item__disclosure {
|
||||
width: 100%;
|
||||
padding: 2px 16px 6px 42px;
|
||||
font-size: 11px;
|
||||
@@ -1949,7 +1956,7 @@ body {
|
||||
|
||||
/* Mobile: full-screen sheet for Add Sessions */
|
||||
@media (max-width: 599px) {
|
||||
.add-sessions-panel__content {
|
||||
.manage-view-panel__content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -1958,6 +1965,66 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.manage-view-panel__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.manage-view-panel__name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.manage-view-panel__name:hover::after {
|
||||
content: ' \270E';
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.manage-view-panel__summary {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.manage-view-panel__footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.manage-view-panel__close-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-surface);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.manage-view-panel__close-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.manage-view-panel__name-input {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Responsive overlay sidebar at <960px
|
||||
============================================================ */
|
||||
@@ -2195,6 +2262,25 @@ body {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.views-settings-btn--manage {
|
||||
background: var(--bg-surface);
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.views-settings-btn--manage:hover:not(:disabled) {
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
.views-settings-btn--new {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.views-settings-new-row {
|
||||
padding: 8px 0 4px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.views-settings-confirm {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2214,17 +2214,16 @@ def test_flyout_bottom_sheet_styled() -> None:
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_add_sessions_panel_styled() -> None:
|
||||
"""style.css must contain .add-sessions-panel styles."""
|
||||
def test_manage_view_panel_styled() -> None:
|
||||
"""style.css must contain .manage-view-panel styles (renamed from .add-sessions-panel)."""
|
||||
css = read_css()
|
||||
assert ".add-sessions-panel" in css, "style.css must style .add-sessions-panel"
|
||||
assert ".manage-view-panel" in css, "style.css must style .manage-view-panel"
|
||||
|
||||
|
||||
def test_add_sessions_item_styled() -> None:
|
||||
"""style.css must contain .add-sessions-item styles."""
|
||||
def test_manage_view_item_styled() -> None:
|
||||
"""style.css must contain .manage-view-item styles (renamed from .add-sessions-item)."""
|
||||
css = read_css()
|
||||
assert ".add-sessions-item" in css, "style.css must style .add-sessions-item"
|
||||
assert ".add-sessions-item" in css, "style.css must style .add-sessions-item"
|
||||
assert ".manage-view-item" in css, "style.css must style .manage-view-item"
|
||||
|
||||
|
||||
# ─── Phase 3 COE findings regression tests ──────────────────────────────────
|
||||
@@ -2246,11 +2245,11 @@ def test_disclosure_not_hidden_by_css() -> None:
|
||||
import re as _re
|
||||
|
||||
match = _re.search(
|
||||
r"\.add-sessions-item__disclosure\s*\{([^}]*)\}",
|
||||
r"\.manage-view-item__disclosure\s*\{([^}]*)\}",
|
||||
css,
|
||||
_re.DOTALL,
|
||||
)
|
||||
assert match, ".add-sessions-item__disclosure rule not found in style.css"
|
||||
assert match, ".manage-view-item__disclosure rule not found in style.css"
|
||||
body = match.group(1)
|
||||
assert "display: none" not in body and "display:none" not in body.replace(" ", ""), (
|
||||
".add-sessions-item__disclosure must NOT have display:none — "
|
||||
@@ -2309,3 +2308,57 @@ def test_no_sidebar_delete_css() -> None:
|
||||
".sidebar-delete CSS rules must be removed from style.css — "
|
||||
"the button was removed from buildSidebarHTML(); orphaned CSS is dead code"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# UX Overhaul CSS — Issues 3 and 8
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_manage_view_panel_styles_exist() -> None:
|
||||
""".manage-view-panel CSS rule must exist (renamed from .add-sessions-panel).
|
||||
|
||||
Issue 3: The Add Sessions panel was redesigned as the Manage View panel.
|
||||
All CSS classes must be renamed from add-sessions-* to manage-view-*.
|
||||
"""
|
||||
css = read_css()
|
||||
assert ".manage-view-panel" in css, (
|
||||
".manage-view-panel CSS rule must exist in style.css — "
|
||||
"renamed from .add-sessions-panel (Issue 3)"
|
||||
)
|
||||
assert ".add-sessions-panel" not in css, (
|
||||
".add-sessions-panel CSS rule must be removed — "
|
||||
"renamed to .manage-view-panel (Issue 3)"
|
||||
)
|
||||
|
||||
|
||||
def test_device_badge_height_matches_flyout_button() -> None:
|
||||
""".device-badge must have a height/line-height matching the ⋮ button (~22-24px).
|
||||
|
||||
Issue 8: The device badge was shorter than the ⋮ flyout button (24x24px),
|
||||
making the tile header look uneven. Fix: increase .device-badge dimensions.
|
||||
"""
|
||||
import re
|
||||
|
||||
css = read_css()
|
||||
# Find the BASE .device-badge rule (not a qualified selector like .parent .device-badge)
|
||||
# We look for the standalone rule at the start of a line (or after whitespace)
|
||||
match = re.search(r"(?:^|\n)\.device-badge\s*\{([^}]*)\}", css, re.DOTALL)
|
||||
assert match, ".device-badge CSS rule not found"
|
||||
rule = match.group(1)
|
||||
# Should have a line-height or min-height that results in ~22-24px
|
||||
# Check for either explicit height/min-height or a large enough line-height/padding
|
||||
has_height = (
|
||||
"min-height" in rule
|
||||
or ("line-height" in rule and any(
|
||||
f"line-height: {n}" in rule or f"line-height:{n}" in rule
|
||||
for n in ["1.6", "1.7", "1.8", "1.9", "2", "22px", "23px", "24px"]
|
||||
))
|
||||
or ("padding" in rule and "padding: 2px" in rule)
|
||||
or ("padding: 3px" in rule)
|
||||
or ("padding: 4px" in rule)
|
||||
)
|
||||
assert has_height, (
|
||||
".device-badge must have increased height to match the ~24px ⋮ button — "
|
||||
"add min-height or increase padding/line-height so the badge is ~22-24px tall"
|
||||
)
|
||||
|
||||
@@ -1540,20 +1540,49 @@ def test_no_hidden_sessions_checkbox_list_in_settings() -> None:
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_add_sessions_panel_exists() -> None:
|
||||
"""index.html must contain an add-sessions-panel element."""
|
||||
def test_manage_view_panel_exists() -> None:
|
||||
"""index.html must contain a manage-view-panel element (renamed from add-sessions-panel)."""
|
||||
soup = _SOUP
|
||||
assert soup.find(id="add-sessions-panel"), (
|
||||
"index.html must contain an element with id='add-sessions-panel'"
|
||||
assert soup.find(id="manage-view-panel"), (
|
||||
"index.html must contain an element with id='manage-view-panel'"
|
||||
)
|
||||
|
||||
|
||||
def test_add_sessions_panel_has_role_dialog() -> None:
|
||||
"""add-sessions-panel must have role='dialog' and aria-modal='true'."""
|
||||
def test_manage_view_panel_has_role_dialog() -> None:
|
||||
"""manage-view-panel must have role='dialog' and aria-modal='true'."""
|
||||
soup = _SOUP
|
||||
panel = soup.find(id="add-sessions-panel")
|
||||
assert panel, "Missing #add-sessions-panel"
|
||||
assert panel.get("role") == "dialog", "add-sessions-panel must have role='dialog'"
|
||||
panel = soup.find(id="manage-view-panel")
|
||||
assert panel, "Missing #manage-view-panel"
|
||||
assert panel.get("role") == "dialog", "manage-view-panel must have role='dialog'"
|
||||
assert panel.get("aria-modal") == "true", (
|
||||
"add-sessions-panel must have aria-modal='true'"
|
||||
"manage-view-panel must have aria-modal='true'"
|
||||
)
|
||||
|
||||
|
||||
def test_add_sessions_btn_removed() -> None:
|
||||
"""index.html must NOT contain #add-sessions-btn (removed in Issue 5).
|
||||
|
||||
The '+ Add' header button was removed — Manage View is now accessed via
|
||||
the dropdown's 'Manage [ViewName]...' item.
|
||||
"""
|
||||
soup = _SOUP
|
||||
assert not soup.find(id="add-sessions-btn"), (
|
||||
"index.html must NOT contain id='add-sessions-btn' — "
|
||||
"the button was removed; use the dropdown 'Manage [ViewName]...' item instead"
|
||||
)
|
||||
|
||||
|
||||
def test_kill_session_command_label() -> None:
|
||||
"""Settings Commands tab must use 'Kill session command' not 'Delete session command'.
|
||||
|
||||
Terminology must match the flyout menu which uses 'Kill Session'.
|
||||
"""
|
||||
html = HTML_PATH.read_text()
|
||||
assert "Kill session command" in html or "Kill Session Command" in html, (
|
||||
"Settings Commands tab must use 'Kill session command' — "
|
||||
"terminology must match the flyout's 'Kill Session' action"
|
||||
)
|
||||
assert "Delete session command" not in html and "Delete Session Command" not in html, (
|
||||
"Settings Commands tab must not use 'Delete session command' — "
|
||||
"rename to 'Kill session command' to match the flyout"
|
||||
)
|
||||
|
||||
@@ -3502,8 +3502,11 @@ def test_delete_active_view_persists_active_view_to_server() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_rename_active_view_persists_active_view_to_server() -> None:
|
||||
"""When renaming the active view in renderViewsSettingsTab, PATCH /api/state with active_view."""
|
||||
def test_views_settings_tab_no_inline_rename_commit() -> None:
|
||||
"""renderViewsSettingsTab must NOT have inline rename (commitRename removed in Issue 6).
|
||||
|
||||
Rename now lives in the Manage View panel, not the settings tab.
|
||||
"""
|
||||
match = re.search(
|
||||
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
@@ -3511,15 +3514,10 @@ def test_rename_active_view_persists_active_view_to_server() -> None:
|
||||
)
|
||||
assert match, "renderViewsSettingsTab function not found"
|
||||
body = match.group(1)
|
||||
# Find the commitRename block or rename path
|
||||
rename_idx = body.find("commitRename")
|
||||
assert rename_idx >= 0, "commitRename not found in renderViewsSettingsTab"
|
||||
rename_block = body[rename_idx:]
|
||||
assert "api(" in rename_block, (
|
||||
"renderViewsSettingsTab rename path must call api() to persist active_view"
|
||||
)
|
||||
assert "active_view" in rename_block, (
|
||||
"renderViewsSettingsTab rename path must PATCH /api/state with active_view"
|
||||
# Inline rename (commitRename) must be gone — it moved to the Manage View panel
|
||||
assert "commitRename" not in body, (
|
||||
"renderViewsSettingsTab must not have inline commitRename — "
|
||||
"rename moved to the Manage View panel (Issue 6)"
|
||||
)
|
||||
|
||||
|
||||
@@ -3567,8 +3565,8 @@ def test_show_new_view_input_reserved_name_check_is_case_insensitive() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_rename_reserved_name_check_is_case_insensitive() -> None:
|
||||
"""The rename commitRename reserved name check must use toLowerCase()."""
|
||||
def test_rename_reserved_name_check_moved_to_manage_view_panel() -> None:
|
||||
"""renderViewsSettingsTab must NOT contain commitRename — it moved to Manage View panel (Issue 6)."""
|
||||
match = re.search(
|
||||
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
@@ -3576,19 +3574,17 @@ def test_rename_reserved_name_check_is_case_insensitive() -> None:
|
||||
)
|
||||
assert match, "renderViewsSettingsTab function not found"
|
||||
body = match.group(1)
|
||||
rename_idx = body.find("commitRename")
|
||||
assert rename_idx >= 0, "commitRename not found in renderViewsSettingsTab"
|
||||
rename_block = body[rename_idx:]
|
||||
assert "toLowerCase" in rename_block, (
|
||||
"commitRename reserved name check must use toLowerCase() — spec requires case-insensitive"
|
||||
# commitRename was removed from settings tab — it now lives in openManageViewPanel
|
||||
assert "commitRename" not in body, (
|
||||
"renderViewsSettingsTab must not have commitRename — rename moved to Manage View panel (Issue 6)"
|
||||
)
|
||||
|
||||
|
||||
# ─── Fix 8: rename input maxLength ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_rename_input_has_max_length() -> None:
|
||||
"""renderViewsSettingsTab rename input must have maxLength = 30."""
|
||||
def test_rename_input_not_in_settings_tab() -> None:
|
||||
"""renderViewsSettingsTab must NOT have views-settings-rename-input (Issue 6: inline rename removed)."""
|
||||
match = re.search(
|
||||
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
@@ -3596,12 +3592,9 @@ def test_rename_input_has_max_length() -> None:
|
||||
)
|
||||
assert match, "renderViewsSettingsTab function not found"
|
||||
body = match.group(1)
|
||||
rename_idx = body.find("views-settings-rename-input")
|
||||
assert rename_idx >= 0, "rename input not found in renderViewsSettingsTab"
|
||||
rename_block = body[rename_idx : rename_idx + 400]
|
||||
assert "maxLength" in rename_block, (
|
||||
"renderViewsSettingsTab rename input must set maxLength = 30 "
|
||||
"(matches creation input constraint)"
|
||||
assert "views-settings-rename-input" not in body, (
|
||||
"renderViewsSettingsTab must NOT have views-settings-rename-input — "
|
||||
"inline rename was moved to the Manage View panel (Issue 6)"
|
||||
)
|
||||
|
||||
|
||||
@@ -3889,40 +3882,40 @@ def test_do_kill_inline_shows_confirmation_buttons() -> None:
|
||||
# ============================================================
|
||||
|
||||
|
||||
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_open_manage_view_panel_function_exists() -> None:
|
||||
"""app.js must define an openManageViewPanel function (renamed from openAddSessionsPanel)."""
|
||||
assert "function openManageViewPanel" in _JS, (
|
||||
"app.js must contain an openManageViewPanel 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_close_manage_view_panel_function_exists() -> None:
|
||||
"""app.js must define a closeManageViewPanel function (renamed from closeAddSessionsPanel)."""
|
||||
assert "function closeManageViewPanel" in _JS, (
|
||||
"app.js must contain a closeManageViewPanel 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_render_manage_view_list_function_exists() -> None:
|
||||
"""app.js must define a renderManageViewList function (renamed from renderAddSessionsList)."""
|
||||
assert "function renderManageViewList" in _JS, (
|
||||
"app.js must contain a renderManageViewList 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]
|
||||
def test_manage_view_uses_immediate_commit() -> None:
|
||||
"""renderManageViewList must PATCH immediately on checkbox change (no batch Done)."""
|
||||
fn_body = _JS.split("function renderManageViewList")[1].split("\nfunction ")[0]
|
||||
assert "PATCH" in fn_body or "api(" in fn_body, (
|
||||
"renderAddSessionsList must fire PATCH on each checkbox change (immediate commit)"
|
||||
"renderManageViewList 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]
|
||||
def test_manage_view_shows_device_name() -> None:
|
||||
"""renderManageViewList must show device name next to each session."""
|
||||
fn_body = _JS.split("function renderManageViewList")[1].split("\nfunction ")[0]
|
||||
assert "deviceName" in fn_body or "device" in fn_body, (
|
||||
"renderAddSessionsList must show device name for disambiguation"
|
||||
"renderManageViewList must show device name for disambiguation"
|
||||
)
|
||||
|
||||
|
||||
@@ -3947,23 +3940,22 @@ def test_open_flyout_menu_checks_mobile() -> None:
|
||||
# ─── Task 11: Add Sessions entry point in grid ───────────────────────────────
|
||||
|
||||
|
||||
def test_render_grid_has_add_sessions_affordance() -> None:
|
||||
"""app.js must have an 'Add Sessions' affordance for user views.
|
||||
def test_render_view_dropdown_has_manage_view_affordance() -> None:
|
||||
"""renderViewDropdown must include a 'Manage [ViewName]...' action for user views.
|
||||
|
||||
The affordance moved from a tile in renderGrid to a header button (#add-sessions-btn)
|
||||
managed by updateAddSessionsButton(). Either the old tile approach or the new header
|
||||
button approach satisfies this requirement.
|
||||
The affordance moved from a header button to the dropdown's 'Manage [ViewName]...' item.
|
||||
This item should open the Manage View panel for the current user view.
|
||||
"""
|
||||
fn_body = _JS.split("function renderGrid")[1].split("\nfunction ")[0]
|
||||
# Old approach: tile in the grid | New approach: header button via updateAddSessionsButton
|
||||
has_affordance = (
|
||||
"add-sessions" in fn_body.lower()
|
||||
or "openAddSessionsPanel" in fn_body
|
||||
or "updateAddSessionsButton" in _JS
|
||||
match = re.search(
|
||||
r"function renderViewDropdown\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert has_affordance, (
|
||||
"app.js must have an 'Add Sessions' affordance for user views — "
|
||||
"either as a tile in renderGrid or as a header button via updateAddSessionsButton"
|
||||
assert match, "renderViewDropdown function not found"
|
||||
body = match.group(1)
|
||||
assert "manage-view" in body, (
|
||||
"renderViewDropdown must include a 'Manage [ViewName]...' action with data-action='manage-view' "
|
||||
"for user views — this replaced the old #add-sessions-btn header button"
|
||||
)
|
||||
|
||||
|
||||
@@ -4028,21 +4020,21 @@ def test_flyout_delegation_handler_no_stop_propagation() -> None:
|
||||
# ── BUG 2: disclosure has no hover-based show/hide in renderAddSessionsList ──
|
||||
|
||||
|
||||
def test_render_add_sessions_list_no_hover_disclosure() -> None:
|
||||
"""renderAddSessionsList must NOT have mouseover/mouseout handlers for disclosure.
|
||||
def test_render_manage_view_list_no_hover_disclosure() -> None:
|
||||
"""renderManageViewList must NOT have mouseover/mouseout handlers for disclosure.
|
||||
|
||||
BUG: The disclosure was shown only on hover via JS mouseover/mouseout. This broke
|
||||
on mobile (no hover) and was inconsistent. Fix: disclose statically — remove CSS
|
||||
display:none and remove the hover handlers. The disclosure appears whenever the
|
||||
hidden item is in the HTML (it's already conditionally rendered for hidden items).
|
||||
"""
|
||||
fn_body = _JS.split("function renderAddSessionsList")[1].split("\nfunction ")[0]
|
||||
fn_body = _JS.split("function renderManageViewList")[1].split("\nfunction ")[0]
|
||||
assert "onmouseover" not in fn_body, (
|
||||
"renderAddSessionsList must not use onmouseover to show disclosure — "
|
||||
"renderManageViewList must not use onmouseover to show disclosure — "
|
||||
"the disclosure must be statically visible (BUG 2 fix)"
|
||||
)
|
||||
assert "onmouseout" not in fn_body, (
|
||||
"renderAddSessionsList must not use onmouseout to hide disclosure — "
|
||||
"renderManageViewList must not use onmouseout to hide disclosure — "
|
||||
"the disclosure must be statically visible (BUG 2 fix)"
|
||||
)
|
||||
|
||||
@@ -4349,8 +4341,8 @@ def test_render_view_dropdown_shows_user_view_session_count() -> None:
|
||||
|
||||
# — Issue 3: Empty new view opens Add Sessions panel ———————————————————
|
||||
|
||||
def test_show_new_view_input_calls_open_add_sessions_panel() -> None:
|
||||
"""showNewViewInput must call openAddSessionsPanel after creating a new view."""
|
||||
def test_show_new_view_input_calls_open_manage_view_panel() -> None:
|
||||
"""showNewViewInput must call openManageViewPanel after creating a new view."""
|
||||
match = re.search(
|
||||
r"function showNewViewInput\s*\(\s*\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
@@ -4358,14 +4350,14 @@ def test_show_new_view_input_calls_open_add_sessions_panel() -> None:
|
||||
)
|
||||
assert match, "showNewViewInput function not found"
|
||||
body = match.group(1)
|
||||
assert "openAddSessionsPanel" in body, (
|
||||
"showNewViewInput must call openAddSessionsPanel() after creating a new view — "
|
||||
"so the user immediately sees the Add Sessions panel for their empty view"
|
||||
assert "openManageViewPanel" in body, (
|
||||
"showNewViewInput must call openManageViewPanel() after creating a new view — "
|
||||
"so the user immediately sees the Manage View panel for their empty view"
|
||||
)
|
||||
|
||||
|
||||
def test_show_sidebar_new_view_input_calls_open_add_sessions_panel() -> None:
|
||||
"""showSidebarNewViewInput must call openAddSessionsPanel after creating a new view."""
|
||||
def test_show_sidebar_new_view_input_calls_open_manage_view_panel() -> None:
|
||||
"""showSidebarNewViewInput must call openManageViewPanel after creating a new view."""
|
||||
match = re.search(
|
||||
r"function showSidebarNewViewInput\s*\(\s*\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
@@ -4373,8 +4365,8 @@ def test_show_sidebar_new_view_input_calls_open_add_sessions_panel() -> None:
|
||||
)
|
||||
assert match, "showSidebarNewViewInput function not found"
|
||||
body = match.group(1)
|
||||
assert "openAddSessionsPanel" in body, (
|
||||
"showSidebarNewViewInput must call openAddSessionsPanel() after creating a new view"
|
||||
assert "openManageViewPanel" in body, (
|
||||
"showSidebarNewViewInput must call openManageViewPanel() after creating a new view"
|
||||
)
|
||||
|
||||
|
||||
@@ -4417,16 +4409,16 @@ def test_open_flyout_submenu_new_view_creates_and_switches() -> None:
|
||||
|
||||
# — Issue 5: Add Sessions header button ————————————————————————————————
|
||||
|
||||
def test_update_add_sessions_button_function_exists() -> None:
|
||||
"""updateAddSessionsButton function must exist in app.js."""
|
||||
assert "function updateAddSessionsButton" in _JS, (
|
||||
"updateAddSessionsButton must be defined in app.js — "
|
||||
"shows/hides the '+ Add' header button based on active view"
|
||||
def test_update_add_sessions_button_removed() -> None:
|
||||
"""updateAddSessionsButton must be REMOVED — the + Add button is gone (Issue 5)."""
|
||||
assert "function updateAddSessionsButton" not in _JS, (
|
||||
"updateAddSessionsButton must be removed — the #add-sessions-btn header button "
|
||||
"was removed and replaced by the 'Manage [ViewName]...' dropdown item"
|
||||
)
|
||||
|
||||
|
||||
def test_switch_view_calls_update_add_sessions_button() -> None:
|
||||
"""switchView must call updateAddSessionsButton to update header button visibility."""
|
||||
def test_switch_view_no_update_add_sessions_button() -> None:
|
||||
"""switchView must NOT call updateAddSessionsButton (it was removed in Issue 5)."""
|
||||
match = re.search(
|
||||
r"function switchView\s*\(\w+\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
|
||||
_JS,
|
||||
@@ -4434,22 +4426,22 @@ def test_switch_view_calls_update_add_sessions_button() -> None:
|
||||
)
|
||||
assert match, "switchView function not found"
|
||||
body = match.group(1)
|
||||
assert "updateAddSessionsButton" in body, (
|
||||
"switchView must call updateAddSessionsButton() to show/hide the header Add button"
|
||||
assert "updateAddSessionsButton" not in body, (
|
||||
"switchView must not call updateAddSessionsButton() — it was removed in Issue 5"
|
||||
)
|
||||
|
||||
|
||||
def test_render_grid_no_muxplex_app_onclick() -> None:
|
||||
"""renderGrid must not use the broken window.MuxplexApp.openAddSessionsPanel onclick."""
|
||||
"""renderGrid must not use the broken window.MuxplexApp.openManageViewPanel onclick."""
|
||||
fn_body = _JS.split("function renderGrid")[1].split("\nfunction ")[0]
|
||||
assert "MuxplexApp.openAddSessionsPanel" not in fn_body, (
|
||||
"renderGrid must not use onclick='window.MuxplexApp.openAddSessionsPanel()' — "
|
||||
"this was broken; the Add Sessions entry point moved to the header button"
|
||||
"this was broken; the Manage View entry point moved to the dropdown"
|
||||
)
|
||||
|
||||
|
||||
def test_bind_static_event_listeners_binds_add_sessions_btn() -> None:
|
||||
"""bindStaticEventListeners must bind #add-sessions-btn click to openAddSessionsPanel."""
|
||||
def test_bind_static_event_listeners_no_add_sessions_btn() -> None:
|
||||
"""bindStaticEventListeners must NOT bind #add-sessions-btn (button was removed in Issue 5)."""
|
||||
match = re.search(
|
||||
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
|
||||
_JS,
|
||||
@@ -4457,8 +4449,9 @@ def test_bind_static_event_listeners_binds_add_sessions_btn() -> None:
|
||||
)
|
||||
assert match, "bindStaticEventListeners function not found"
|
||||
body = match.group(1)
|
||||
assert "add-sessions-btn" in body, (
|
||||
"bindStaticEventListeners must bind #add-sessions-btn click to openAddSessionsPanel"
|
||||
assert "add-sessions-btn" not in body, (
|
||||
"bindStaticEventListeners must not bind #add-sessions-btn — "
|
||||
"the button was removed; Manage View is reached via the dropdown"
|
||||
)
|
||||
|
||||
|
||||
@@ -4488,3 +4481,151 @@ def test_tile_options_btn_css_not_absolute() -> None:
|
||||
".tile-options-btn must not use position:absolute — "
|
||||
"it should be an inline flex item inside tile-header to prevent device badge overlap"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# UX Overhaul — 9 refinements from live testing
|
||||
# ============================================================
|
||||
|
||||
|
||||
# — Issue 1: Sidebar dropdown "New View" click-dismiss race ——————————————
|
||||
|
||||
|
||||
def test_sidebar_click_outside_has_new_view_input_guard() -> None:
|
||||
"""Sidebar click-outside handler must guard against new-view input dismiss race.
|
||||
|
||||
Race condition: clicking '+ New View' in the sidebar triggers the click-outside
|
||||
handler before the input appears. Guard: check for .view-dropdown__new-input
|
||||
presence and return early if found.
|
||||
"""
|
||||
match = re.search(
|
||||
r"// Click-outside closes the sidebar view dropdown\s*\n\s*"
|
||||
r"document\.addEventListener\('click',\s*function\(e\)\s*\{(.*?)\}\s*\);",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "Click-outside handler for sidebar view dropdown not found"
|
||||
body = match.group(1)
|
||||
assert ".view-dropdown__new-input" in body, (
|
||||
"Sidebar click-outside handler must guard: "
|
||||
"if (sidebarDropdown.querySelector('.view-dropdown__new-input')) return; "
|
||||
"— prevents race where the new-view input is dismissed immediately"
|
||||
)
|
||||
|
||||
|
||||
# — Issue 4: Dropdown structure — "Manage [ViewName]…" ————————————————
|
||||
|
||||
|
||||
def test_render_view_dropdown_has_manage_view_item_for_user_view() -> None:
|
||||
"""renderViewDropdown must include 'Manage [ViewName]...' action for user views."""
|
||||
match = re.search(
|
||||
r"function renderViewDropdown\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "renderViewDropdown function not found"
|
||||
body = match.group(1)
|
||||
assert "manage-view" in body, (
|
||||
"renderViewDropdown must include a 'Manage [ViewName]...' action "
|
||||
"with data-action='manage-view' for user views"
|
||||
)
|
||||
|
||||
|
||||
def test_render_sidebar_dropdown_has_manage_view_item_for_user_view() -> None:
|
||||
"""renderSidebarViewDropdown must include 'Manage [ViewName]...' action for user views."""
|
||||
match = re.search(
|
||||
r"function renderSidebarViewDropdown\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "renderSidebarViewDropdown function not found"
|
||||
body = match.group(1)
|
||||
assert "manage-view" in body, (
|
||||
"renderSidebarViewDropdown must include a 'Manage [ViewName]...' action "
|
||||
"with data-action='manage-view' for user views"
|
||||
)
|
||||
|
||||
|
||||
# — Issue 5: No #add-sessions-btn + updateAddSessionsButton removed ————————
|
||||
|
||||
|
||||
def test_domcontentloaded_no_update_add_sessions_button_call() -> None:
|
||||
"""DOMContentLoaded handler must NOT call updateAddSessionsButton (it was removed)."""
|
||||
assert "updateAddSessionsButton" not in _JS, (
|
||||
"updateAddSessionsButton must be completely removed from app.js — "
|
||||
"the #add-sessions-btn header button was replaced by the dropdown manage-view item"
|
||||
)
|
||||
|
||||
|
||||
# — Issue 6: Manage View settings tab ——————————————————————————————————
|
||||
|
||||
|
||||
def test_views_settings_tab_has_manage_button_per_row() -> None:
|
||||
"""renderViewsSettingsTab must include a 'Manage' button per view row."""
|
||||
match = re.search(
|
||||
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "renderViewsSettingsTab function not found"
|
||||
body = match.group(1)
|
||||
assert "manage-view" in body.lower() or "Manage" in body, (
|
||||
"renderViewsSettingsTab must include a 'Manage' button per view row "
|
||||
"that opens the Manage View panel for that view"
|
||||
)
|
||||
|
||||
|
||||
def test_views_settings_tab_has_new_view_button_at_bottom() -> None:
|
||||
"""renderViewsSettingsTab must include a '+ New View' button at the bottom."""
|
||||
match = re.search(
|
||||
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "renderViewsSettingsTab function not found"
|
||||
body = match.group(1)
|
||||
assert "New View" in body or "new-view" in body, (
|
||||
"renderViewsSettingsTab must include a '+ New View' button at the bottom"
|
||||
)
|
||||
|
||||
|
||||
def test_views_settings_tab_no_inline_rename_on_name_click() -> None:
|
||||
"""renderViewsSettingsTab must NOT have click-to-rename on the name span.
|
||||
|
||||
Rename now lives in the Manage View panel, not in the settings tab.
|
||||
"""
|
||||
match = re.search(
|
||||
r"function renderViewsSettingsTab\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "renderViewsSettingsTab function not found"
|
||||
body = match.group(1)
|
||||
# The old inline rename code clicked on views-settings-name span to show an input
|
||||
assert "views-settings-rename-input" not in body, (
|
||||
"renderViewsSettingsTab must not have inline rename via views-settings-rename-input — "
|
||||
"rename now lives in the Manage View panel"
|
||||
)
|
||||
|
||||
|
||||
# — Issue 7: Dropdown label fix on page reload —————————————————————————
|
||||
|
||||
|
||||
def test_domcontentloaded_calls_render_view_dropdown_after_restore() -> None:
|
||||
"""DOMContentLoaded must call renderViewDropdown() after restoreState() completes.
|
||||
|
||||
Bug: restoreState() sets _activeView but the dropdown label stays as 'All Sessions'.
|
||||
Fix: call renderViewDropdown() after restoreState() so the label reflects _activeView.
|
||||
"""
|
||||
# Find the DOMContentLoaded handler section (from DOMContentLoaded to end of file)
|
||||
idx = _JS.find("document.addEventListener('DOMContentLoaded'")
|
||||
assert idx >= 0, "DOMContentLoaded handler not found"
|
||||
handler_section = _JS[idx:]
|
||||
# restoreState must appear before renderViewDropdown in the handler
|
||||
restore_idx = handler_section.find("restoreState()")
|
||||
assert restore_idx >= 0, "restoreState call not found in DOMContentLoaded section"
|
||||
after_restore = handler_section[restore_idx:]
|
||||
assert "renderViewDropdown" in after_restore, (
|
||||
"DOMContentLoaded must call renderViewDropdown() after restoreState() so the "
|
||||
"dropdown label correctly reflects _activeView on page reload"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user