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)) %} -