feat: task-6 header dropdown view switching (renderViewDropdown, toggleViewDropdown, closeViewDropdown, switchView)
- Add renderViewDropdown() populating #view-dropdown-menu with All Sessions, user views, Hidden (count), + New View, Manage Views; updates label - Add toggleViewDropdown() toggling hidden class and aria-expanded - Add closeViewDropdown() hiding menu and cleaning up inline inputs - Add switchView(viewName) setting _activeView, re-rendering, persisting active_view via PATCH /api/state (fire-and-forget) - Restore _activeView from server state in restoreState() - Bind view dropdown event listeners in bindStaticEventListeners(): trigger click, delegated menu item clicks, click-outside dismiss - Export all 4 new functions in module.exports - Add 5 new tests: render_view_dropdown_function_exists, render_view_dropdown_exported, toggle_view_dropdown_function_exists, switch_view_function_exists, switch_view_patches_state
This commit is contained in:
@@ -218,6 +218,9 @@ async function restoreState() {
|
|||||||
try {
|
try {
|
||||||
const res = await api('GET', '/api/state');
|
const res = await api('GET', '/api/state');
|
||||||
const state = await res.json();
|
const state = await res.json();
|
||||||
|
if (state.active_view) {
|
||||||
|
_activeView = state.active_view;
|
||||||
|
}
|
||||||
if (state.active_session) {
|
if (state.active_session) {
|
||||||
await openSession(state.active_session, {
|
await openSession(state.active_session, {
|
||||||
skipAnimation: true,
|
skipAnimation: true,
|
||||||
@@ -804,6 +807,112 @@ function renderFilterBar(container, allSessions) {
|
|||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// View dropdown — render, open/close, view switching
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate #view-dropdown-menu with the full view list and update the label.
|
||||||
|
* Called on open and after a view switch.
|
||||||
|
*/
|
||||||
|
function renderViewDropdown() {
|
||||||
|
var menu = $('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, shortcut 1)
|
||||||
|
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 (shortcuts 2–8)
|
||||||
|
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, shortcut 9)
|
||||||
|
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>';
|
||||||
|
|
||||||
|
// — Actions
|
||||||
|
html += '<div class="view-dropdown-separator"></div>';
|
||||||
|
html += '<button class="view-dropdown-item view-dropdown-action" data-action="new-view">+ New View</button>';
|
||||||
|
html += '<button class="view-dropdown-item view-dropdown-action" data-action="manage-views">Manage Views\u2026</button>';
|
||||||
|
|
||||||
|
menu.innerHTML = html;
|
||||||
|
|
||||||
|
// Update the label
|
||||||
|
var label = $('view-dropdown-label');
|
||||||
|
if (label) {
|
||||||
|
if (_activeView === 'all') {
|
||||||
|
label.textContent = 'All Sessions';
|
||||||
|
} else if (_activeView === 'hidden') {
|
||||||
|
label.textContent = 'Hidden';
|
||||||
|
} else {
|
||||||
|
label.textContent = escapeHtml(_activeView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the view dropdown open/closed.
|
||||||
|
* Calls renderViewDropdown() when opening to ensure fresh content.
|
||||||
|
*/
|
||||||
|
function toggleViewDropdown() {
|
||||||
|
var menu = $('view-dropdown-menu');
|
||||||
|
var trigger = $('view-dropdown-trigger');
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
var isOpen = !menu.classList.contains('hidden');
|
||||||
|
if (isOpen) {
|
||||||
|
closeViewDropdown();
|
||||||
|
} else {
|
||||||
|
menu.classList.remove('hidden');
|
||||||
|
if (trigger) trigger.setAttribute('aria-expanded', 'true');
|
||||||
|
renderViewDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the view dropdown. Removes inline new-view input if present.
|
||||||
|
*/
|
||||||
|
function closeViewDropdown() {
|
||||||
|
var menu = $('view-dropdown-menu');
|
||||||
|
var trigger = $('view-dropdown-trigger');
|
||||||
|
if (menu) {
|
||||||
|
menu.classList.add('hidden');
|
||||||
|
// Remove any inline new-view input
|
||||||
|
var newViewInput = menu.querySelector('.view-dropdown-new-input');
|
||||||
|
if (newViewInput) newViewInput.remove();
|
||||||
|
}
|
||||||
|
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a named view. Updates _activeView, re-renders the grid and sidebar,
|
||||||
|
* updates the dropdown label, and persists the change via PATCH /api/state.
|
||||||
|
* @param {string} viewName - 'all', 'hidden', or a user view name.
|
||||||
|
*/
|
||||||
|
function switchView(viewName) {
|
||||||
|
_activeView = viewName;
|
||||||
|
closeViewDropdown();
|
||||||
|
renderGrid(_currentSessions || []);
|
||||||
|
renderSidebar(_currentSessions || [], _viewingSession);
|
||||||
|
renderViewDropdown();
|
||||||
|
// Persist active view — fire and forget
|
||||||
|
api('PATCH', '/api/state', { active_view: viewName }).catch(function() {});
|
||||||
|
}
|
||||||
|
|
||||||
function renderGrid(sessions) {
|
function renderGrid(sessions) {
|
||||||
var grid = $('session-grid');
|
var grid = $('session-grid');
|
||||||
var emptyState = $('empty-state');
|
var emptyState = $('empty-state');
|
||||||
@@ -2258,6 +2367,36 @@ function bindStaticEventListeners() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
on($('back-btn'), 'click', closeSession);
|
on($('back-btn'), 'click', closeSession);
|
||||||
|
|
||||||
|
// View dropdown — trigger opens/closes, delegated item clicks switch view
|
||||||
|
var viewDropdownTrigger = $('view-dropdown-trigger');
|
||||||
|
if (viewDropdownTrigger) on(viewDropdownTrigger, 'click', toggleViewDropdown);
|
||||||
|
|
||||||
|
var viewDropdownMenu = $('view-dropdown-menu');
|
||||||
|
if (viewDropdownMenu) {
|
||||||
|
viewDropdownMenu.addEventListener('click', function(e) {
|
||||||
|
var item = e.target.closest('[data-view]');
|
||||||
|
if (item) {
|
||||||
|
switchView(item.dataset.view);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var action = e.target.closest('[data-action]');
|
||||||
|
if (action) {
|
||||||
|
// action handlers (new-view, manage-views) — placeholder for future tasks
|
||||||
|
closeViewDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click-outside closes the dropdown
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var dropdown = $('view-dropdown-menu');
|
||||||
|
if (!dropdown || dropdown.classList.contains('hidden')) return;
|
||||||
|
var trigger = $('view-dropdown-trigger');
|
||||||
|
if (trigger && trigger.contains(e.target)) return;
|
||||||
|
if (!dropdown.contains(e.target)) closeViewDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
var newSessionBtn = $('new-session-btn');
|
var newSessionBtn = $('new-session-btn');
|
||||||
if (newSessionBtn) on(newSessionBtn, 'click', function() { showNewSessionInput(newSessionBtn); });
|
if (newSessionBtn) on(newSessionBtn, 'click', function() { showNewSessionInput(newSessionBtn); });
|
||||||
var sidebarNewSessionBtn = $('sidebar-new-session-btn');
|
var sidebarNewSessionBtn = $('sidebar-new-session-btn');
|
||||||
@@ -2670,6 +2809,11 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
killSession,
|
killSession,
|
||||||
// Filter bar
|
// Filter bar
|
||||||
renderFilterBar,
|
renderFilterBar,
|
||||||
|
// View dropdown
|
||||||
|
renderViewDropdown,
|
||||||
|
toggleViewDropdown,
|
||||||
|
closeViewDropdown,
|
||||||
|
switchView,
|
||||||
// Federation tiles
|
// Federation tiles
|
||||||
buildStatusTileHTML,
|
buildStatusTileHTML,
|
||||||
// Constants
|
// Constants
|
||||||
|
|||||||
@@ -3112,3 +3112,56 @@ def test_create_new_session_references_active_view() -> None:
|
|||||||
assert "_activeView" in body, (
|
assert "_activeView" in body, (
|
||||||
"createNewSession must reference _activeView to auto-add new session to the active user view"
|
"createNewSession must reference _activeView to auto-add new session to the active user view"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Header Dropdown — JS Render + Open/Close + View Switching (task-6)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_view_dropdown_function_exists() -> None:
|
||||||
|
"""renderViewDropdown function must exist in app.js."""
|
||||||
|
assert "function renderViewDropdown" in _JS, (
|
||||||
|
"renderViewDropdown must be defined in app.js"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_view_dropdown_exported() -> None:
|
||||||
|
"""module.exports must export renderViewDropdown."""
|
||||||
|
match = re.search(
|
||||||
|
r"module\.exports\s*=\s*\{(.*?)\};",
|
||||||
|
_JS,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert match, "module.exports block not found"
|
||||||
|
exports = match.group(1)
|
||||||
|
assert "renderViewDropdown" in exports, (
|
||||||
|
"module.exports must export renderViewDropdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_view_dropdown_function_exists() -> None:
|
||||||
|
"""toggleViewDropdown function must exist in app.js."""
|
||||||
|
assert "function toggleViewDropdown" in _JS, (
|
||||||
|
"toggleViewDropdown must be defined in app.js"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_switch_view_function_exists() -> None:
|
||||||
|
"""switchView function must exist in app.js."""
|
||||||
|
assert "function switchView" in _JS, (
|
||||||
|
"switchView must be defined in app.js"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_switch_view_patches_state() -> None:
|
||||||
|
"""switchView must PATCH /api/state to persist active_view."""
|
||||||
|
match = re.search(
|
||||||
|
r"function switchView\s*\(\w+\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
|
||||||
|
_JS,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert match, "switchView function not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "PATCH" in body, "switchView must use PATCH method"
|
||||||
|
assert "/api/state" in body, "switchView must PATCH /api/state"
|
||||||
|
|||||||
Reference in New Issue
Block a user