From c1bfca2a8c652b686af9a4523141cf9b499bd797 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 19 Aug 2026 14:53:51 -0400 Subject: [PATCH 1/2] Group the Admin Settings navigation and render it from one definition Admin Settings presented 18 tabs in a single flat list, and that list was written out twice: once as the top tab strip and once as the sidebar. The two copies had already drifted. Tab order differed between them, and Agents, Custom Pages and Search and Extract each carried a different label depending on which navigation you were looking at. Both now render from admin_settings_nav.ADMIN_NAV, which describes the navigation as groups containing tabs containing sections. A change is made once and appears in both. The map was generated from the existing sidebar markup rather than retyped, so it is faithful by construction, and the rendered destinations were compared before and after: the same 18 tabs and the same 86 sections, with per-tab section order preserved. That comparison also surfaced a gap. The Shared Conversation File Approvals card, added upstream while this work was in progress, had no navigation entry at all and could only be found by scrolling. It is now listed like every other setting, which fixes a pre-existing parity failure. The group level is added on top: collapsible headers with persisted state in the sidebar, and a row of group pills that filters the tab strip in the tab layout. Opening a tab reveals its group first in either layout, so a deep link or a cross-reference can never activate a pane whose tab is out of view. Sidebar search now spans groups as well as tabs and sections. No cards move in this change. Field names and card ids are unchanged at 462 and 110, so saving is untouched, and the 75 functional test files covering admin_settings.html go from 33 pre-existing failures to 32. Tests that asserted against hand-written markup now assert against the map, which is the contract both renderings depend on, and a new test rejects a malformed map, a destination with nothing behind it, or a tab hardcoded back into the strip. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/admin_settings_nav.py | 344 ++++++++++ application/single_app/app.py | 2 + application/single_app/config.py | 2 +- .../static/js/admin/admin_sidebar_nav.js | 277 ++++++-- .../single_app/templates/_sidebar_nav.html | 599 ++---------------- .../single_app/templates/admin_settings.html | 148 +---- docs/explanation/release_notes.md | 23 + .../test_admin_latest_features_tab.py | 63 +- .../test_admin_send_feedback_tab.py | 2 - .../test_admin_settings_nav_map.py | 176 +++++ ...test_admin_settings_sidebar_card_parity.py | 53 +- .../test_data_management_security_patterns.py | 44 +- functional_tests/test_inbound_mcp_admin_ui.py | 25 +- functional_tests/test_support/nav.py | 34 + .../test_support_menu_user_feature.py | 9 +- .../test_workflow_access_controls.py | 10 +- 16 files changed, 1042 insertions(+), 769 deletions(-) create mode 100644 application/single_app/admin_settings_nav.py create mode 100644 functional_tests/test_admin_settings_nav_map.py create mode 100644 functional_tests/test_support/nav.py diff --git a/application/single_app/admin_settings_nav.py b/application/single_app/admin_settings_nav.py new file mode 100644 index 000000000..75cc98bab --- /dev/null +++ b/application/single_app/admin_settings_nav.py @@ -0,0 +1,344 @@ +# admin_settings_nav.py +"""Single source of truth for the Admin Settings navigation. + +The top tab strip and the sidebar previously maintained the same +structure twice, by hand, and had already drifted: tab order differed +between them and three tabs carried different labels in each. Both now +render from this one definition, so they cannot disagree. + +Structure is GROUP -> TAB -> SECTION. A section points at a card id in +the rendered page; card ids are stable and are what cross-references +resolve against, so cards can move between tabs without breaking links. +""" + + +ADMIN_NAV = [ + { + "id": "appearance", + "label": "Appearance", + "icon": "bi-palette", + "tabs": [ + { + "id": "general", + "label": "General", + "icon": "bi-gear", + "sections": [ + {"id": "branding-section", "label": "Branding", "icon": "bi-palette"}, + {"id": "home-page-text-section", "label": "Home Page Text", "icon": "bi-house"}, + {"id": "appearance-section", "label": "Appearance", "icon": "bi-brush"}, + {"id": "health-check-section", "label": "Health Check", "icon": "bi-heart-pulse"}, + {"id": "swagger-section", "label": "API Documentation", "icon": "bi-file-earmark-code"}, + {"id": "classification-banner-section", "label": "Classification Banner", "icon": "bi-shield-exclamation"}, + {"id": "ai-notice-section", "label": "Chat AI Notice", "icon": "bi-robot"}, + {"id": "terms-of-use-section", "label": "Terms of Use", "icon": "bi-door-open"}, + {"id": "support-menu-section", "label": "Support", "icon": "bi-life-preserver"}, + {"id": "external-links-section", "label": "External Links", "icon": "bi-box-arrow-up-right"}, + {"id": "system-settings-section", "label": "System Settings", "icon": "bi-gear-fill"}, + ], + }, + { + "id": "custom-pages", + "label": "Custom Pages", + "icon": "bi-window-plus", + "sections": [ + {"id": "custom-pages-section", "label": "Static Pages", "icon": "bi-file-earmark-richtext"}, + ], + }, + ], + }, + { + "id": "chat", + "label": "Chat", + "icon": "bi-chat-square-text", + "tabs": [ + { + "id": "citation", + "label": "Citations", + "icon": "bi-quote", + "sections": [ + {"id": "standard-citations-section", "label": "Standard", "icon": "bi-quote"}, + {"id": "enhanced-citations-section", "label": "Enhanced", "icon": "bi-star"}, + ], + }, + ], + }, + { + "id": "ai-models", + "label": "AI Models", + "icon": "bi-cpu", + "tabs": [ + { + "id": "ai-models", + "label": "AI Models", + "icon": "bi-cpu", + "sections": [ + {"id": "multi-endpoint-configuration", "label": "Model Endpoints", "icon": "bi-hdd-network"}, + {"id": "processing-thoughts-section", "label": "Processing Thoughts", "icon": "bi-stars"}, + {"id": "shared-conversation-file-approvals-section", "label": "Shared Conversation File Approvals", "icon": "bi-file-earmark-lock"}, + {"id": "embeddings-config", "label": "Embeddings", "icon": "bi-vector-pen"}, + {"id": "image-config", "label": "Image Generation", "icon": "bi-image"}, + {"id": "gpt-config", "label": "Chat Model", "icon": "bi-chat-square-text"}, + ], + }, + ], + }, + { + "id": "agents-actions", + "label": "Agents & Actions", + "icon": "bi-robot", + "tabs": [ + { + "id": "agents", + "label": "Agents and Actions", + "icon": "bi-robot", + "sections": [ + {"id": "document-action-capabilities-card", "label": "Document Action Capabilities", "icon": "bi-files"}, + {"id": "agents-config", "label": "Agents Configuration", "icon": "bi-robot"}, + {"id": "agent-template-approvals-section", "label": "Agent Template Approvals", "icon": "bi-layers", "condition": "enable_agent_template_gallery"}, + {"id": "actions-config", "label": "Actions Configuration", "icon": "bi-plugin"}, + {"id": "inbound-mcp-configuration", "label": "Inbound MCP", "icon": "bi-diagram-3", "condition": "mcp_ui_enabled"}, + ], + }, + ], + }, + { + "id": "workspaces", + "label": "Workspaces", + "icon": "bi-folder", + "tabs": [ + { + "id": "workspaces", + "label": "Workspaces", + "icon": "bi-folder", + "sections": [ + {"id": "personal-workspaces-section", "label": "Personal Workspaces", "icon": "bi-person"}, + {"id": "workflow-settings-section", "label": "Workflow", "icon": "bi-diagram-3"}, + {"id": "file-download-settings-section", "label": "File Downloads", "icon": "bi-download"}, + {"id": "group-workspaces-section", "label": "Group Workspaces", "icon": "bi-people"}, + {"id": "public-workspaces-section", "label": "Public Workspaces", "icon": "bi-globe"}, + {"id": "file-sharing-section", "label": "File Sharing", "icon": "bi-share"}, + {"id": "chat-file-uploads-section", "label": "Chat File Uploads", "icon": "bi-paperclip"}, + {"id": "metadata-extraction-section", "label": "Metadata Extraction", "icon": "bi-file-earmark-code"}, + {"id": "multimodal-vision-section", "label": "Multi-Modal Vision Analysis", "icon": "bi-eye"}, + {"id": "document-classification-section", "label": "Document Classification", "icon": "bi-tags"}, + {"id": "retention-policy-section", "label": "Retention Policy", "icon": "bi-hourglass-split"}, + {"id": "workspace-scope-lock-section", "label": "Workspace Scope Lock", "icon": "bi-lock"}, + {"id": "user-agreement-section", "label": "User Agreement", "icon": "bi-file-earmark-check"}, + ], + }, + { + "id": "workspace-identities", + "label": "Global Identities", + "icon": "bi-person-badge", + "sections": [], + }, + ], + }, + { + "id": "knowledge", + "label": "Knowledge", + "icon": "bi-search", + "tabs": [ + { + "id": "search-extract", + "label": "Search & Extract", + "icon": "bi-search", + "sections": [ + {"id": "web-search-section", "label": "Web Search", "icon": "bi-globe"}, + {"id": "url-access-section", "label": "URL Access", "icon": "bi-link-45deg"}, + {"id": "source-review-section", "label": "Deep Research", "icon": "bi-binoculars"}, + {"id": "azure-ai-search-section", "label": "Azure AI Search", "icon": "bi-search"}, + {"id": "document-intelligence-section", "label": "Document Intelligence", "icon": "bi-file-earmark-text"}, + {"id": "chunk-size-section", "label": "Chunk Sizes", "icon": "bi-collection"}, + {"id": "video-intelligence-section", "label": "AI Video Intelligence", "icon": "bi-play-circle"}, + {"id": "ai-voice-chat-section", "label": "AI Voice Conversations", "icon": "bi-mic"}, + ], + }, + { + "id": "file-sync", + "label": "File Sync", + "icon": "bi-arrow-repeat", + "sections": [ + {"id": "file-sync-section", "label": "File Sync", "icon": "bi-arrow-repeat"}, + {"id": "file-sync-source-types-section", "label": "Visible Source Types", "icon": "bi-sliders"}, + {"id": "file-sync-personal-section", "label": "Personal Workspace Sync", "icon": "bi-person"}, + {"id": "file-sync-group-section", "label": "Group Workspace Sync", "icon": "bi-people"}, + {"id": "file-sync-public-section", "label": "Public Workspace Sync", "icon": "bi-globe"}, + ], + }, + ], + }, + { + "id": "security", + "label": "Security", + "icon": "bi-shield-lock", + "tabs": [ + { + "id": "security", + "label": "Security", + "icon": "bi-shield-lock", + "sections": [ + {"id": "keyvault-section", "label": "Key Vault", "icon": "bi-safe"}, + ], + }, + { + "id": "safety", + "label": "Safety", + "icon": "bi-shield-check", + "sections": [ + {"id": "content-safety-section", "label": "Content Safety", "icon": "bi-shield-exclamation"}, + {"id": "user-feedback-section", "label": "User Feedback", "icon": "bi-chat-square-heart"}, + {"id": "desktop-notifications-section", "label": "Desktop Conversation Notifications", "icon": "bi-bell"}, + {"id": "permissions-section", "label": "Permissions", "icon": "bi-person-check"}, + {"id": "conversation-archiving-section", "label": "Conversation Archiving", "icon": "bi-archive"}, + ], + }, + ], + }, + { + "id": "governance", + "label": "Governance", + "icon": "bi-clipboard-check", + "tabs": [ + { + "id": "governance", + "label": "Governance", + "icon": "bi-braces-asterisk", + "sections": [ + {"id": "governance-feature-toggles-section", "label": "Governance Feature Toggles", "icon": "bi-braces-asterisk"}, + {"id": "governance-mcp-destination-section", "label": "MCP Action Destination Governance", "icon": "bi-diagram-3"}, + {"id": "governance-inbound-mcp-section", "label": "Inbound MCP Source Governance", "icon": "bi-box-arrow-in-down-right"}, + {"id": "governance-feature-policies-section", "label": "Feature Policies", "icon": "bi-sliders2"}, + {"id": "governance-item-policies-section", "label": "Delegated Item Policies", "icon": "bi-list-check"}, + ], + }, + ], + }, + { + "id": "backup-recovery", + "label": "Backup & Recovery", + "icon": "bi-database", + "tabs": [ + { + "id": "data-management", + "label": "Backup, Migrate & Restore", + "icon": "bi-database-check", + "sections": [ + {"id": "data-management-readiness-section", "label": "Start Here", "icon": "bi-compass"}, + {"id": "data-management-backup-section", "label": "Backup", "icon": "bi-archive"}, + {"id": "data-management-schedule-section", "label": "Schedule", "icon": "bi-calendar-event"}, + {"id": "data-management-storage-section", "label": "Storage", "icon": "bi-hdd"}, + {"id": "data-management-encryption-section", "label": "Encryption", "icon": "bi-key"}, + {"id": "data-management-migration-section", "label": "Migration", "icon": "bi-arrow-left-right"}, + {"id": "data-management-cosmos-editor-section", "label": "Cosmos Editor", "icon": "bi-database-exclamation"}, + {"id": "data-management-backup-inventory-section", "label": "Backup Inventory & Restore", "icon": "bi-box-seam"}, + {"id": "data-management-jobs-section", "label": "Jobs", "icon": "bi-clock-history"}, + ], + }, + ], + }, + { + "id": "scale", + "label": "Scale", + "icon": "bi-speedometer2", + "tabs": [ + { + "id": "scale", + "label": "Scale", + "icon": "bi-diagram-3", + "sections": [ + {"id": "redis-cache-section", "label": "Redis Cache", "icon": "bi-database"}, + {"id": "redis-monitoring-section", "label": "Redis Metrics", "icon": "bi-activity"}, + {"id": "conversation-cache-section", "label": "Conversation Cache", "icon": "bi-chat-square-text"}, + {"id": "document-access-index-section", "label": "DAI Metrics", "icon": "bi-diagram-3"}, + {"id": "cosmos-maintenance-section", "label": "Cosmos Maintenance", "icon": "bi-tools"}, + {"id": "cosmos-throughput-section", "label": "Cosmos DB Throughput", "icon": "bi-speedometer2"}, + {"id": "cosmos-throughput-metrics-table-section", "label": "Cosmos Metrics", "icon": "bi-table"}, + {"id": "front-door-section", "label": "Azure Front Door", "icon": "bi-door-open"}, + ], + }, + ], + }, + { + "id": "operations", + "label": "Operations", + "icon": "bi-activity", + "tabs": [ + { + "id": "control-center-config", + "label": "Control Center", + "icon": "bi-speedometer2", + "sections": [ + {"id": "control-center-auto-refresh-section", "label": "Automatic Data Refresh", "icon": "bi-calendar-check"}, + {"id": "control-center-overview-section", "label": "Control Center Access", "icon": "bi-gear-wide-connected"}, + ], + }, + { + "id": "logging", + "label": "Logging", + "icon": "bi-journal-text", + "sections": [ + {"id": "application-insights-section", "label": "Application Insights", "icon": "bi-graph-up"}, + {"id": "debug-logging-section", "label": "Debug Logging", "icon": "bi-bug"}, + {"id": "file-processing-logs-section", "label": "File Process Logging", "icon": "bi-file-earmark-text"}, + ], + }, + ], + }, + { + "id": "help", + "label": "Help", + "icon": "bi-life-preserver", + "tabs": [ + { + "id": "send-feedback", + "label": "Send Feedback", + "icon": "bi-envelope-paper", + "sections": [ + {"id": "send-feedback-overview-card", "label": "Overview", "icon": "bi-info-circle"}, + {"id": "send-feedback-bug-card", "label": "Report a Bug", "icon": "bi-bug"}, + {"id": "send-feedback-feature-card", "label": "Request a Feature", "icon": "bi-lightbulb"}, + ], + }, + { + "id": "latest-features", + "label": "Latest Features", + "icon": "bi-lightning-charge", + "sections": [], + # Rendered specially: it can be hidden per user, carries a New + # badge and a hide/unhide menu, and its sections are generated + # from the release catalogue rather than declared here. + "render": "latest_features", + }, + ], + }, +] + + +def iter_tabs(): + """Yield (group, tab) pairs in navigation order.""" + for group in ADMIN_NAV: + for tab in group["tabs"]: + yield group, tab + + +def get_tab_ids(): + """Return every tab id in navigation order.""" + return [tab["id"] for _, tab in iter_tabs()] + + +def get_group_for_tab(tab_id): + """Return the group owning a tab, or None when the tab is unknown.""" + for group, tab in iter_tabs(): + if tab["id"] == tab_id: + return group + return None + + +def get_section_ids(): + """Return every section target declared across the navigation.""" + return [ + section["id"] + for _, tab in iter_tabs() + for section in tab["sections"] + ] \ No newline at end of file diff --git a/application/single_app/app.py b/application/single_app/app.py index fb6eade46..35cc2d336 100644 --- a/application/single_app/app.py +++ b/application/single_app/app.py @@ -31,6 +31,7 @@ from functions_content import * from functions_documents import * from functions_latest_features_nav import should_hide_latest_features_nav +from admin_settings_nav import ADMIN_NAV from functions_search import * from functions_settings import * from functions_mcp_server_config import is_mcp_ui_enabled @@ -598,6 +599,7 @@ def inject_settings(): app_settings=public_settings, user_settings=user_settings, custom_pages_nav=custom_pages_nav, + admin_nav=ADMIN_NAV, latest_features_current_version=VERSION, latest_features_nav_hidden=latest_features_nav_hidden, latest_features_nav_hidden_by_development=IS_DEVELOPMENT, diff --git a/application/single_app/config.py b/application/single_app/config.py index e342eef4a..60b856bf6 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -96,7 +96,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.260.009" +VERSION = "0.260.010" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/static/js/admin/admin_sidebar_nav.js b/application/single_app/static/js/admin/admin_sidebar_nav.js index a5d85ad8d..0f9b7dcaf 100644 --- a/application/single_app/static/js/admin/admin_sidebar_nav.js +++ b/application/single_app/static/js/admin/admin_sidebar_nav.js @@ -1,6 +1,10 @@ // admin_sidebar_nav.js // Admin Sidebar Navigation document.addEventListener('DOMContentLoaded', function() { + // The top-nav group pills exist only in the tab layout, so they are wired + // independently of the sidebar. + setupAdminGroupPills(); + // Only initialize if we're on admin settings page with sidebar nav if (!document.getElementById('admin-settings-toggle')) return; @@ -77,6 +81,10 @@ function initAdminSidebarNav() { }); } + // Set up group expand/collapse. Groups are the level above tabs, so a + // collapsed group hides its tabs without affecting which tab is active. + setupAdminGroupToggles(); + // Set up tab navigation document.querySelectorAll('.admin-nav-tab').forEach(tabLink => { tabLink.addEventListener('click', function(e) { @@ -149,7 +157,139 @@ function initAdminSidebarNav() { } } +function setupAdminGroupToggles() { + document.querySelectorAll('[data-admin-group-toggle]').forEach(toggle => { + toggle.addEventListener('click', function (e) { + e.preventDefault(); + const groupId = this.getAttribute('data-admin-group-toggle'); + setAdminGroupExpanded(groupId, !isAdminGroupExpanded(groupId), true); + }); + }); +} + +/** + * Wire the top-nav group pills, which filter the tab strip to one group. + * Only relevant in the tab layout; the sidebar layout uses group headers. + */ +function setupAdminGroupPills() { + const pills = document.querySelectorAll('[data-admin-group-pill]'); + if (!pills.length) { + return; + } + + pills.forEach(pill => { + pill.addEventListener('click', function () { + showAdminGroupTabs(this.getAttribute('data-admin-group-pill'), true); + }); + }); +} + +/** + * Show one group's tabs in the top strip. + * @param {string} groupId Group to reveal. + * @param {boolean} activateFirstTab Whether to open that group's first tab. + */ +function showAdminGroupTabs(groupId, activateFirstTab) { + document.querySelectorAll('[data-admin-group-pill]').forEach(pill => { + const selected = pill.getAttribute('data-admin-group-pill') === groupId; + pill.classList.toggle('active', selected); + pill.setAttribute('aria-selected', selected ? 'true' : 'false'); + }); + + let firstTabButton = null; + document.querySelectorAll('.admin-tab-item').forEach(item => { + const inGroup = item.getAttribute('data-admin-group') === groupId; + item.hidden = !inGroup; + if (inGroup && !firstTabButton) { + firstTabButton = item.querySelector('button[data-bs-target]'); + } + }); + + if (activateFirstTab && firstTabButton) { + const target = firstTabButton.getAttribute('data-bs-target') || ''; + showAdminTab(target.replace('#', '')); + } +} + +/** + * Reveal the group owning a tab in the top strip, so a deep link or a + * cross-reference never activates a pane whose tab is filtered out of view. + * @param {string} tabId Tab pane id. + */ +function revealAdminGroupPillForTab(tabId) { + const item = document.querySelector(`.admin-tab-item [data-bs-target="#${tabId}"]`); + const groupItem = item && item.closest('.admin-tab-item'); + if (!groupItem) { + return; + } + + const groupId = groupItem.getAttribute('data-admin-group'); + if (groupId && groupItem.hidden) { + showAdminGroupTabs(groupId, false); + } +} + +function getAdminGroupElements(groupId) { + return { + toggle: document.querySelector(`[data-admin-group-toggle="${groupId}"]`), + list: document.getElementById(`admin-group-${groupId}`), + }; +} + +function isAdminGroupExpanded(groupId) { + const { list } = getAdminGroupElements(groupId); + return Boolean(list) && !list.classList.contains('d-none'); +} + +/** + * Expand or collapse a nav group. + * @param {string} groupId Group identifier. + * @param {boolean} expanded Desired state. + * @param {boolean} persist Whether to remember the state for this user. + */ +function setAdminGroupExpanded(groupId, expanded, persist) { + const { toggle, list } = getAdminGroupElements(groupId); + if (!toggle || !list) { + return; + } + + list.classList.toggle('d-none', !expanded); + toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false'); + + const caret = toggle.querySelector('.admin-nav-group-caret'); + if (caret) { + caret.classList.toggle('collapsed', !expanded); + } + + if (persist && typeof window.setPersistentSidebarMenuExpanded === 'function') { + window.setPersistentSidebarMenuExpanded(`adminGroup:${groupId}`, expanded); + } +} + +/** + * Open the group that owns a tab, so activating a tab never leaves it hidden. + * @param {string} tabId Tab pane id. + */ +function revealAdminGroupForTab(tabId) { + const tabLink = document.querySelector(`.admin-nav-tab[data-tab="${tabId}"]`); + const groupItem = tabLink && tabLink.closest('[data-admin-group]'); + if (!groupItem) { + return; + } + + const groupId = groupItem.getAttribute('data-admin-group'); + if (!isAdminGroupExpanded(groupId)) { + setAdminGroupExpanded(groupId, true, true); + } +} + function showAdminTab(tabId) { + // Open the owning group first, in whichever layout is active, so + // activating a tab never leaves it hidden behind a collapsed group header + // or filtered out of the top strip. + revealAdminGroupForTab(tabId); + revealAdminGroupPillForTab(tabId); + const bootstrapTabButton = document.querySelector(`button.nav-link[data-bs-target="#${tabId}"]`); if (bootstrapTabButton && typeof bootstrap !== 'undefined' && typeof bootstrap.Tab === 'function') { const tab = bootstrap.Tab.getOrCreateInstance(bootstrapTabButton); @@ -247,112 +387,171 @@ style.textContent = ` .admin-search-hidden { display: none !important; } + .admin-nav-group { + font-weight: 500; + letter-spacing: 0.01em; + } + .admin-nav-group:hover { + background-color: rgba(0, 0, 0, 0.05); + } + .admin-nav-group-caret { + font-size: 0.75em; + transition: transform 0.2s ease; + } + .admin-nav-group-caret.collapsed { + transform: rotate(-90deg); + } `; document.head.appendChild(style); // Admin search functionality function filterAdminSections(searchTerm) { const normalizedSearch = searchTerm.toLowerCase().trim(); - + if (!normalizedSearch) { - // Show all sections if search is empty showAllAdminSections(); return; } - + let hasVisibleSections = false; - - // Get all admin nav items + + const groupItems = document.querySelectorAll('.admin-nav-group-item'); const allTabs = document.querySelectorAll('.admin-nav-tab'); const allSections = document.querySelectorAll('.admin-nav-section'); - - // Hide all sections and tabs initially + + // Start from everything hidden, then reveal what matches. + groupItems.forEach(group => group.classList.add('admin-search-hidden')); + document.querySelectorAll('.admin-nav-group').forEach(group => { + group.classList.remove('admin-search-highlight'); + }); + allTabs.forEach(tab => { tab.closest('li').classList.add('admin-search-hidden'); - // Hide submenu + tab.classList.remove('admin-search-highlight'); const submenu = document.getElementById(tab.getAttribute('data-tab') + '-submenu'); if (submenu) { submenu.style.display = 'none'; } }); - + allSections.forEach(section => { section.closest('li').classList.add('admin-search-hidden'); section.classList.remove('admin-search-highlight'); }); - - // Search through tabs and sections + + /** + * Reveal the group containing an element and expand its tab list, so a + * match is never left hidden behind a collapsed group. + * @param {HTMLElement} element Element inside a group. + */ + const revealGroupOf = (element) => { + const groupItem = element.closest('.admin-nav-group-item'); + if (!groupItem) { + return; + } + groupItem.classList.remove('admin-search-hidden'); + const list = groupItem.querySelector('.admin-nav-group-tabs'); + if (list) { + list.classList.remove('d-none'); + } + }; + + // A group matching by name reveals everything it holds. + document.querySelectorAll('.admin-nav-group').forEach(group => { + const label = group.querySelector('.nav-text'); + if (!label || !label.textContent.toLowerCase().includes(normalizedSearch)) { + return; + } + + group.classList.add('admin-search-highlight'); + revealGroupOf(group); + hasVisibleSections = true; + + const groupItem = group.closest('.admin-nav-group-item'); + groupItem.querySelectorAll('.admin-nav-tab').forEach(tab => { + tab.closest('li').classList.remove('admin-search-hidden'); + }); + }); + allTabs.forEach(tab => { - const tabText = tab.querySelector('.nav-text').textContent.toLowerCase(); + const label = tab.querySelector('.nav-text'); + const tabText = label ? label.textContent.toLowerCase() : ''; const tabId = tab.getAttribute('data-tab'); let tabHasMatch = false; - - // Check if tab name matches + if (tabText.includes(normalizedSearch)) { tab.closest('li').classList.remove('admin-search-hidden'); tab.classList.add('admin-search-highlight'); + revealGroupOf(tab); tabHasMatch = true; hasVisibleSections = true; - - // Show submenu for matched tab + const submenu = document.getElementById(tabId + '-submenu'); if (submenu) { submenu.style.display = 'block'; - // Show all sections under this tab submenu.querySelectorAll('.admin-nav-section').forEach(section => { section.closest('li').classList.remove('admin-search-hidden'); }); } } - - // Check sections under this tab + const sections = document.querySelectorAll(`.admin-nav-section[data-tab="${tabId}"]`); let sectionHasMatch = false; - + sections.forEach(section => { - const sectionText = section.querySelector('.nav-text').textContent.toLowerCase(); - + const sectionLabel = section.querySelector('.nav-text'); + const sectionText = sectionLabel ? sectionLabel.textContent.toLowerCase() : ''; + if (sectionText.includes(normalizedSearch)) { - // Show the section section.closest('li').classList.remove('admin-search-hidden'); section.classList.add('admin-search-highlight'); sectionHasMatch = true; hasVisibleSections = true; - - // Show the parent tab + tab.closest('li').classList.remove('admin-search-hidden'); - - // Show the submenu + revealGroupOf(tab); + const submenu = document.getElementById(tabId + '-submenu'); if (submenu) { submenu.style.display = 'block'; } } }); - - // If tab has matching sections but doesn't match itself, remove tab highlight + + // A tab surfaced only because a section matched is not itself a hit. if (sectionHasMatch && !tabHasMatch) { tab.classList.remove('admin-search-highlight'); } }); - - // Show "No results" message if nothing found + showSearchResults(hasVisibleSections, normalizedSearch); } function showAllAdminSections() { - // Remove all search-related classes and show all items - document.querySelectorAll('.admin-nav-tab, .admin-nav-section').forEach(item => { - item.closest('li').classList.remove('admin-search-hidden'); + // Restore the normal browsing state: nothing hidden, nothing highlighted, + // submenus closed, and groups back to their persisted expansion. + document.querySelectorAll('.admin-nav-group-item').forEach(group => { + group.classList.remove('admin-search-hidden'); + }); + + document.querySelectorAll('.admin-nav-tab, .admin-nav-section, .admin-nav-group').forEach(item => { + const listItem = item.closest('li'); + if (listItem) { + listItem.classList.remove('admin-search-hidden'); + } item.classList.remove('admin-search-highlight'); }); - - // Hide all submenus (normal collapsed state) + document.querySelectorAll('[id$="-submenu"]').forEach(submenu => { submenu.style.display = 'none'; }); - - // Remove search results message + + document.querySelectorAll('[data-admin-group-toggle]').forEach(toggle => { + const groupId = toggle.getAttribute('data-admin-group-toggle'); + const expanded = toggle.getAttribute('aria-expanded') === 'true'; + setAdminGroupExpanded(groupId, expanded, false); + }); + hideSearchResults(); } @@ -389,4 +588,4 @@ function hideSearchResults() { if (noResults) { noResults.remove(); } -} \ No newline at end of file +} diff --git a/application/single_app/templates/_sidebar_nav.html b/application/single_app/templates/_sidebar_nav.html index d3419b8d0..9c1cf3b79 100644 --- a/application/single_app/templates/_sidebar_nav.html +++ b/application/single_app/templates/_sidebar_nav.html @@ -441,559 +441,28 @@
-
diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index 4071a778a..5c887efee 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -936,127 +936,43 @@

12. Enhanced Citations and Image Generation

{% set nav_layout = user_settings.get('settings', {}).get('navLayout') %} {% if not (nav_layout == 'sidebar' or (not nav_layout and app_settings.enable_left_nav_default)) %} -