From e0700ef04da32e62352e1a29c4797dc2a763f0ee Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 15 Apr 2026 21:02:05 -0700 Subject: [PATCH] feat: add FLYOUT_MENU_MAP data map and _buildFlyoutMenuItems builder --- muxplex/frontend/app.js | 76 +++++++++++++++++++++++++++++++ muxplex/tests/test_frontend_js.py | 40 ++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 3da069d..508c172 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -139,6 +139,82 @@ let _flyoutSessionKey = null; let _flyoutSessionName = null; let _flyoutRemoteId = null; +/** + * Data map of menu item definitions keyed by view type. + * Each entry is an array of item config objects with: + * { label, action, className?, separator? } + * The 'user' view type gets the active view name injected at render time. + */ +var FLYOUT_MENU_MAP = { + 'all': [ + { label: 'Add to View\u2026', action: 'add-to-view', className: 'flyout-menu__item--has-submenu' }, + { label: 'Hide', action: 'hide' }, + { separator: true }, + { label: 'Kill Session', action: 'kill', className: 'flyout-menu__item--danger' }, + ], + 'user': [ + { label: 'Add to View\u2026', action: 'add-to-view', className: 'flyout-menu__item--has-submenu' }, + { label: 'Remove from {viewName}', action: 'remove-from-view' }, + { label: 'Hide', action: 'hide' }, + { separator: true }, + { label: 'Kill Session', action: 'kill', className: 'flyout-menu__item--danger' }, + ], + 'hidden': [ + { label: 'Unhide', action: 'unhide' }, + { label: 'Unhide & Add to View\u2026', action: 'unhide-add-to-view', className: 'flyout-menu__item--has-submenu' }, + { separator: true }, + { label: 'Kill Session', action: 'kill', className: 'flyout-menu__item--danger' }, + ], +}; + +/** + * Build the flyout menu HTML string based on the active view type. + * Uses FLYOUT_MENU_MAP to generate items — no if/else chains. + * @returns {string} HTML for the menu items + */ +function _buildFlyoutMenuItems() { + // Determine view type: 'all', 'hidden', or 'user' + var viewType = _activeView; + if (viewType !== 'all' && viewType !== 'hidden') { + viewType = 'user'; + } + + var items = FLYOUT_MENU_MAP[viewType] || FLYOUT_MENU_MAP['all']; + var html = ''; + + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.separator) { + html += ''; + continue; + } + + var label = item.label; + // Inject view name for "Remove from {viewName}" + if (label.indexOf('{viewName}') !== -1) { + var displayName = _activeView; + if (displayName.length > 20) { + displayName = displayName.substring(0, 20) + '\u2026'; + } + label = label.replace('{viewName}', escapeHtml(displayName)); + } + + var cls = 'flyout-menu__item'; + if (item.className) cls += ' ' + item.className; + + var titleAttr = ''; + if (item.action === 'remove-from-view' && _activeView && _activeView.length > 20) { + titleAttr = ' title="Remove from ' + escapeHtml(_activeView) + '"'; + } + + html += ''; + } + + return html; +} + // ─── Settings state ─────────────────────────────────────────────────────────── let _settingsOpen = false; let _serverSettings = null; diff --git a/muxplex/tests/test_frontend_js.py b/muxplex/tests/test_frontend_js.py index e11a46d..fcaad56 100644 --- a/muxplex/tests/test_frontend_js.py +++ b/muxplex/tests/test_frontend_js.py @@ -3748,3 +3748,43 @@ def test_old_tile_delete_handler_removed() -> None: assert "tile-delete" not in bind_body, ( "The old .tile-delete delegated handler must be removed from bindStaticEventListeners" ) + + +# ─── Task 4: Context-dependent menu items — FLYOUT_MENU_MAP ────────────────── + + +def test_flyout_menu_map_exists() -> None: + """app.js must define a FLYOUT_MENU_MAP data structure.""" + assert "FLYOUT_MENU_MAP" in _JS, ( + "app.js must contain a FLYOUT_MENU_MAP data structure" + ) + + +def test_flyout_menu_map_has_three_view_types() -> None: + """FLYOUT_MENU_MAP must have keys for 'all', 'user', and 'hidden'.""" + # The map should reference all three view types + map_section = _JS.split("FLYOUT_MENU_MAP")[1].split("};")[0] + assert "'all'" in map_section or '"all"' in map_section, ( + "FLYOUT_MENU_MAP must include an 'all' key" + ) + assert "'user'" in map_section or '"user"' in map_section, ( + "FLYOUT_MENU_MAP must include a 'user' key" + ) + assert "'hidden'" in map_section or '"hidden"' in map_section, ( + "FLYOUT_MENU_MAP must include a 'hidden' key" + ) + + +def test_build_flyout_menu_items_function_exists() -> None: + """app.js must define a _buildFlyoutMenuItems function.""" + assert "function _buildFlyoutMenuItems" in _JS, ( + "app.js must contain a _buildFlyoutMenuItems function" + ) + + +def test_build_flyout_uses_menu_map() -> None: + """_buildFlyoutMenuItems must reference FLYOUT_MENU_MAP.""" + fn_body = _JS.split("function _buildFlyoutMenuItems")[1].split("\nfunction ")[0] + assert "FLYOUT_MENU_MAP" in fn_body, ( + "_buildFlyoutMenuItems must reference FLYOUT_MENU_MAP (data-driven, not if/else)" + )