feat: add sidebar view switcher dropdown in fullscreen mode (task-10)
- HTML: Replace .sidebar-title in sidebar-header with a view dropdown containing #sidebar-view-dropdown-trigger and #sidebar-view-dropdown-menu - JS: Add renderSidebarViewDropdown() for populating sidebar menu (no action buttons) - JS: Add toggleSidebarViewDropdown() for open/close with aria-expanded - JS: Update switchView() to also update #sidebar-view-label in sidebar - JS: Bind sidebar trigger click and delegated menu click in bindStaticEventListeners() - CSS: Add .sidebar-view-dropdown, .sidebar-view-trigger, .sidebar-view-trigger:hover, and #sidebar-view-dropdown-menu position overrides - Tests: Add test_sidebar_view_dropdown_exists and test_sidebar_view_dropdown_menu_exists - Tests: Update test_html_session_sidebar_structure to reflect new dropdown structure
This commit is contained in:
@@ -898,6 +898,62 @@ function closeViewDropdown() {
|
|||||||
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the sidebar view dropdown menu (same data as the header dropdown,
|
||||||
|
* but no action buttons — navigation only).
|
||||||
|
*/
|
||||||
|
function renderSidebarViewDropdown() {
|
||||||
|
var menu = $('sidebar-view-dropdown-menu');
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
var views = (_serverSettings && _serverSettings.views) || [];
|
||||||
|
var hidden = (_serverSettings && _serverSettings.hidden_sessions) || [];
|
||||||
|
var hiddenCount = hidden.length;
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
// — All Sessions (always first)
|
||||||
|
var allActive = _activeView === 'all' ? ' view-dropdown-item--active' : '';
|
||||||
|
html += '<button class="view-dropdown-item' + allActive + '" data-view="all"><span class="view-dropdown-shortcut">1</span> All Sessions</button>';
|
||||||
|
|
||||||
|
// — User views
|
||||||
|
if (views.length > 0) {
|
||||||
|
html += '<div class="view-dropdown-separator"></div>';
|
||||||
|
for (var i = 0; i < views.length && i < 7; i++) {
|
||||||
|
var v = views[i];
|
||||||
|
var vActive = _activeView === v.name ? ' view-dropdown-item--active' : '';
|
||||||
|
html += '<button class="view-dropdown-item' + vActive + '" data-view="' + escapeHtml(v.name) + '"><span class="view-dropdown-shortcut">' + (i + 2) + '</span> ' + escapeHtml(v.name) + '</button>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// — Hidden (N) (always last system view)
|
||||||
|
html += '<div class="view-dropdown-separator"></div>';
|
||||||
|
var hiddenActive = _activeView === 'hidden' ? ' view-dropdown-item--active' : '';
|
||||||
|
html += '<button class="view-dropdown-item' + hiddenActive + '" data-view="hidden"><span class="view-dropdown-shortcut">9</span> Hidden <span class="view-dropdown-badge">' + hiddenCount + '</span></button>';
|
||||||
|
|
||||||
|
menu.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the sidebar view dropdown open/closed.
|
||||||
|
* Calls renderSidebarViewDropdown() when opening to ensure fresh content.
|
||||||
|
*/
|
||||||
|
function toggleSidebarViewDropdown() {
|
||||||
|
var menu = $('sidebar-view-dropdown-menu');
|
||||||
|
var trigger = $('sidebar-view-dropdown-trigger');
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
var isOpen = !menu.classList.contains('hidden');
|
||||||
|
if (isOpen) {
|
||||||
|
menu.classList.add('hidden');
|
||||||
|
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
} else {
|
||||||
|
menu.classList.remove('hidden');
|
||||||
|
if (trigger) trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
renderSidebarViewDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show an inline text input inside the view dropdown for creating a new view.
|
* Show an inline text input inside the view dropdown for creating a new view.
|
||||||
* Replaces the '+ New View' button with a text input inside the dropdown menu.
|
* Replaces the '+ New View' button with a text input inside the dropdown menu.
|
||||||
@@ -1235,6 +1291,17 @@ function switchView(viewName) {
|
|||||||
renderGrid(_currentSessions || []);
|
renderGrid(_currentSessions || []);
|
||||||
renderSidebar(_currentSessions || [], _viewingSession);
|
renderSidebar(_currentSessions || [], _viewingSession);
|
||||||
renderViewDropdown();
|
renderViewDropdown();
|
||||||
|
// Update sidebar view label to match the active view
|
||||||
|
var sidebarLabel = $('sidebar-view-label');
|
||||||
|
if (sidebarLabel) {
|
||||||
|
if (viewName === 'all') {
|
||||||
|
sidebarLabel.textContent = 'All Sessions';
|
||||||
|
} else if (viewName === 'hidden') {
|
||||||
|
sidebarLabel.textContent = 'Hidden';
|
||||||
|
} else {
|
||||||
|
sidebarLabel.textContent = viewName;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Persist active view — fire and forget
|
// Persist active view — fire and forget
|
||||||
api('PATCH', '/api/state', { active_view: viewName }).catch(function() {});
|
api('PATCH', '/api/state', { active_view: viewName }).catch(function() {});
|
||||||
}
|
}
|
||||||
@@ -2777,6 +2844,23 @@ function bindStaticEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sidebar view dropdown — trigger opens/closes, delegated item clicks switch view
|
||||||
|
var sidebarViewTrigger = $('sidebar-view-dropdown-trigger');
|
||||||
|
if (sidebarViewTrigger) on(sidebarViewTrigger, 'click', toggleSidebarViewDropdown);
|
||||||
|
|
||||||
|
var sidebarViewMenu = $('sidebar-view-dropdown-menu');
|
||||||
|
if (sidebarViewMenu) {
|
||||||
|
sidebarViewMenu.addEventListener('click', function(e) {
|
||||||
|
var item = e.target.closest('[data-view]');
|
||||||
|
if (item) {
|
||||||
|
switchView(item.dataset.view);
|
||||||
|
// Close sidebar dropdown after selection
|
||||||
|
sidebarViewMenu.classList.add('hidden');
|
||||||
|
if (sidebarViewTrigger) sidebarViewTrigger.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Click-outside closes the dropdown
|
// Click-outside closes the dropdown
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
var dropdown = $('view-dropdown-menu');
|
var dropdown = $('view-dropdown-menu');
|
||||||
@@ -3204,6 +3288,9 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
closeViewDropdown,
|
closeViewDropdown,
|
||||||
showNewViewInput,
|
showNewViewInput,
|
||||||
switchView,
|
switchView,
|
||||||
|
// Sidebar view dropdown
|
||||||
|
renderSidebarViewDropdown,
|
||||||
|
toggleSidebarViewDropdown,
|
||||||
// Manage Views settings tab
|
// Manage Views settings tab
|
||||||
renderViewsSettingsTab,
|
renderViewsSettingsTab,
|
||||||
_saveViewsAndRerender,
|
_saveViewsAndRerender,
|
||||||
|
|||||||
@@ -49,7 +49,13 @@
|
|||||||
<div class="view-body">
|
<div class="view-body">
|
||||||
<div id="session-sidebar" class="session-sidebar">
|
<div id="session-sidebar" class="session-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<span class="sidebar-title">Sessions</span>
|
<div class="sidebar-view-dropdown" id="sidebar-view-dropdown">
|
||||||
|
<button id="sidebar-view-dropdown-trigger" class="sidebar-view-trigger" aria-haspopup="true" aria-expanded="false" aria-controls="sidebar-view-dropdown-menu">
|
||||||
|
<span id="sidebar-view-label">All Sessions</span>
|
||||||
|
<span class="view-dropdown__caret" aria-hidden="true">▾</span>
|
||||||
|
</button>
|
||||||
|
<div id="sidebar-view-dropdown-menu" class="view-dropdown__menu hidden" role="menu" aria-label="Switch view"></div>
|
||||||
|
</div>
|
||||||
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" aria-label="Collapse session list">‹</button>
|
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" aria-label="Collapse session list">‹</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sidebar-list" class="sidebar-list"></div>
|
<div id="sidebar-list" class="sidebar-list"></div>
|
||||||
|
|||||||
@@ -487,6 +487,41 @@ body {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar view switcher dropdown */
|
||||||
|
.sidebar-view-dropdown {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-view-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-view-trigger:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-view-dropdown-menu {
|
||||||
|
left: 0;
|
||||||
|
top: calc(100% + 2px);
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-list {
|
.sidebar-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -195,19 +195,16 @@ def test_html_reconnect_overlay_outside_view_body() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_html_session_sidebar_structure() -> None:
|
def test_html_session_sidebar_structure() -> None:
|
||||||
"""#session-sidebar must contain .sidebar-header (with .sidebar-title and #sidebar-collapse-btn) and #sidebar-list."""
|
"""#session-sidebar must contain .sidebar-header (with #sidebar-view-dropdown and #sidebar-collapse-btn) and #sidebar-list."""
|
||||||
soup = _SOUP
|
soup = _SOUP
|
||||||
sidebar = soup.find(id="session-sidebar")
|
sidebar = soup.find(id="session-sidebar")
|
||||||
assert sidebar is not None, "Missing #session-sidebar"
|
assert sidebar is not None, "Missing #session-sidebar"
|
||||||
# .sidebar-header
|
# .sidebar-header
|
||||||
sidebar_header = sidebar.find(class_="sidebar-header")
|
sidebar_header = sidebar.find(class_="sidebar-header")
|
||||||
assert sidebar_header is not None, "Missing .sidebar-header inside #session-sidebar"
|
assert sidebar_header is not None, "Missing .sidebar-header inside #session-sidebar"
|
||||||
# .sidebar-title with text 'Sessions'
|
# #sidebar-view-dropdown (replaces old .sidebar-title)
|
||||||
sidebar_title = sidebar_header.find(class_="sidebar-title")
|
sidebar_dropdown = sidebar_header.find(id="sidebar-view-dropdown")
|
||||||
assert sidebar_title is not None, "Missing .sidebar-title inside .sidebar-header"
|
assert sidebar_dropdown is not None, "Missing #sidebar-view-dropdown inside .sidebar-header"
|
||||||
assert "Sessions" in sidebar_title.get_text(), (
|
|
||||||
f".sidebar-title text must contain 'Sessions', got: {sidebar_title.get_text()!r}"
|
|
||||||
)
|
|
||||||
# #sidebar-collapse-btn
|
# #sidebar-collapse-btn
|
||||||
collapse_btn = sidebar_header.find(id="sidebar-collapse-btn")
|
collapse_btn = sidebar_header.find(id="sidebar-collapse-btn")
|
||||||
assert collapse_btn is not None, (
|
assert collapse_btn is not None, (
|
||||||
@@ -1501,3 +1498,25 @@ def test_settings_has_views_panel() -> None:
|
|||||||
# Must contain the empty message element
|
# Must contain the empty message element
|
||||||
empty_msg = panel.find(id="views-settings-empty")
|
empty_msg = panel.find(id="views-settings-empty")
|
||||||
assert empty_msg is not None, "Missing #views-settings-empty inside views panel"
|
assert empty_msg is not None, "Missing #views-settings-empty inside views panel"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Sidebar View Switcher (task-10)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_sidebar_view_dropdown_exists() -> None:
|
||||||
|
"""#sidebar-view-dropdown-trigger must exist in the sidebar header."""
|
||||||
|
soup = _SOUP
|
||||||
|
trigger = soup.find(id="sidebar-view-dropdown-trigger")
|
||||||
|
assert trigger is not None, "Missing #sidebar-view-dropdown-trigger"
|
||||||
|
assert trigger.name == "button", (
|
||||||
|
f"#sidebar-view-dropdown-trigger must be a <button>, got: {trigger.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sidebar_view_dropdown_menu_exists() -> None:
|
||||||
|
"""#sidebar-view-dropdown-menu must exist in the sidebar header."""
|
||||||
|
soup = _SOUP
|
||||||
|
menu = soup.find(id="sidebar-view-dropdown-menu")
|
||||||
|
assert menu is not None, "Missing #sidebar-view-dropdown-menu"
|
||||||
|
|||||||
Reference in New Issue
Block a user