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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Replaces the '+ New View' button with a text input inside the dropdown menu.
|
||||
@@ -1235,6 +1291,17 @@ function switchView(viewName) {
|
||||
renderGrid(_currentSessions || []);
|
||||
renderSidebar(_currentSessions || [], _viewingSession);
|
||||
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
|
||||
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
|
||||
document.addEventListener('click', function(e) {
|
||||
var dropdown = $('view-dropdown-menu');
|
||||
@@ -3204,6 +3288,9 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
closeViewDropdown,
|
||||
showNewViewInput,
|
||||
switchView,
|
||||
// Sidebar view dropdown
|
||||
renderSidebarViewDropdown,
|
||||
toggleSidebarViewDropdown,
|
||||
// Manage Views settings tab
|
||||
renderViewsSettingsTab,
|
||||
_saveViewsAndRerender,
|
||||
|
||||
@@ -49,7 +49,13 @@
|
||||
<div class="view-body">
|
||||
<div id="session-sidebar" class="session-sidebar">
|
||||
<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>
|
||||
</div>
|
||||
<div id="sidebar-list" class="sidebar-list"></div>
|
||||
|
||||
@@ -487,6 +487,41 @@ body {
|
||||
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 {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -195,19 +195,16 @@ def test_html_reconnect_overlay_outside_view_body() -> 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
|
||||
sidebar = soup.find(id="session-sidebar")
|
||||
assert sidebar is not None, "Missing #session-sidebar"
|
||||
# .sidebar-header
|
||||
sidebar_header = sidebar.find(class_="sidebar-header")
|
||||
assert sidebar_header is not None, "Missing .sidebar-header inside #session-sidebar"
|
||||
# .sidebar-title with text 'Sessions'
|
||||
sidebar_title = sidebar_header.find(class_="sidebar-title")
|
||||
assert sidebar_title is not None, "Missing .sidebar-title inside .sidebar-header"
|
||||
assert "Sessions" in sidebar_title.get_text(), (
|
||||
f".sidebar-title text must contain 'Sessions', got: {sidebar_title.get_text()!r}"
|
||||
)
|
||||
# #sidebar-view-dropdown (replaces old .sidebar-title)
|
||||
sidebar_dropdown = sidebar_header.find(id="sidebar-view-dropdown")
|
||||
assert sidebar_dropdown is not None, "Missing #sidebar-view-dropdown inside .sidebar-header"
|
||||
# #sidebar-collapse-btn
|
||||
collapse_btn = sidebar_header.find(id="sidebar-collapse-btn")
|
||||
assert collapse_btn is not None, (
|
||||
@@ -1501,3 +1498,25 @@ def test_settings_has_views_panel() -> None:
|
||||
# Must contain the empty message element
|
||||
empty_msg = panel.find(id="views-settings-empty")
|
||||
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