diff --git a/src/common/config/filter_navigation_pages.ts b/src/common/config/filter_navigation_pages.ts new file mode 100644 index 0000000000..3a8a28f229 --- /dev/null +++ b/src/common/config/filter_navigation_pages.ts @@ -0,0 +1,29 @@ +import type { PageNavigation } from "../../layouts/hass-tabs-subpage"; +import type { HomeAssistant } from "../../types"; +import { canShowPage } from "./can_show_page"; + +export interface NavigationFilterOptions { + /** Whether there are Bluetooth config entries (pre-fetched by caller) */ + hasBluetoothConfigEntries?: boolean; +} + +/** + * Filters navigation pages based on visibility rules. + * Handles special cases like Bluetooth (requires config entries) + * and external app configuration. + */ +export const filterNavigationPages = ( + hass: HomeAssistant, + pages: PageNavigation[], + options: NavigationFilterOptions = {} +): PageNavigation[] => + pages.filter((page) => { + if (page.path === "#external-app-configuration") { + return hass.auth.external?.config.hasSettingsScreen; + } + // Only show Bluetooth page if there are Bluetooth config entries + if (page.component === "bluetooth") { + return options.hasBluetoothConfigEntries ?? false; + } + return canShowPage(hass, page); + }); diff --git a/src/data/quick_bar.ts b/src/data/quick_bar.ts index ba9d5168bd..2536f312a2 100644 --- a/src/data/quick_bar.ts +++ b/src/data/quick_bar.ts @@ -5,7 +5,10 @@ import { mdiServerNetwork, mdiStorePlus, } from "@mdi/js"; -import { canShowPage } from "../common/config/can_show_page"; +import { + filterNavigationPages, + type NavigationFilterOptions, +} from "../common/config/filter_navigation_pages"; import { componentsWithService } from "../common/config/components_with_service"; import { isComponentLoaded } from "../common/config/is_component_loaded"; import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; @@ -99,33 +102,30 @@ const getNavigationInfoFromConfig = ( }; const generateNavigationConfigSectionCommands = ( - hass: HomeAssistant + hass: HomeAssistant, + filterOptions: NavigationFilterOptions = {} ): BaseNavigationCommand[] => { if (!hass.user?.is_admin) { return []; } const items: NavigationInfo[] = []; + const allPages = Object.values(configSections).flat(); + const visiblePages = filterNavigationPages(hass, allPages, filterOptions); - Object.values(configSections).forEach((sectionPages) => { - sectionPages.forEach((page) => { - if (!canShowPage(hass, page)) { - return; - } + for (const page of visiblePages) { + const info = getNavigationInfoFromConfig(hass.localize, page); - const info = getNavigationInfoFromConfig(hass.localize, page); + if (!info) { + continue; + } + // Add to list, but only if we do not already have an entry for the same path and component + if (items.some((e) => e.path === info.path)) { + continue; + } - if (!info) { - return; - } - // Add to list, but only if we do not already have an entry for the same path and component - if (items.some((e) => e.path === info.path)) { - return; - } - - items.push(info); - }); - }); + items.push(info); + } return items; }; @@ -149,7 +149,8 @@ const finalizeNavigationCommands = ( export const generateNavigationCommands = ( hass: HomeAssistant, - apps?: HassioAddonInfo[] + apps?: HassioAddonInfo[], + filterOptions: NavigationFilterOptions = {} ): NavigationComboBoxItem[] => { const panelItems = generateNavigationPanelCommands( hass.localize, @@ -157,7 +158,10 @@ export const generateNavigationCommands = ( apps ); - const sectionItems = generateNavigationConfigSectionCommands(hass); + const sectionItems = generateNavigationConfigSectionCommands( + hass, + filterOptions + ); const appItems: BaseNavigationCommand[] = []; if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) { appItems.push({ diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index b53298a831..0d527247e7 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -45,6 +45,7 @@ import { type ActionCommandComboBoxItem, type NavigationComboBoxItem, } from "../../data/quick_bar"; +import type { NavigationFilterOptions } from "../../common/config/filter_navigation_pages"; import { multiTermSortedSearch, type FuseWeightedKey, @@ -81,6 +82,8 @@ export class QuickBar extends LitElement { private _addons?: HassioAddonInfo[]; + private _navigationFilterOptions: NavigationFilterOptions = {}; + private _translationsLoaded = false; // #region lifecycle @@ -105,6 +108,12 @@ export class QuickBar extends LitElement { this._configEntryLookup = Object.fromEntries( configEntries.map((entry) => [entry.entry_id, entry]) ); + // Derive Bluetooth config entries status for navigation filtering + this._navigationFilterOptions = { + hasBluetoothConfigEntries: configEntries.some( + (entry) => entry.domain === "bluetooth" + ), + }; } catch (err) { // eslint-disable-next-line no-console console.error("Error fetching config entries for quick bar", err); @@ -397,7 +406,8 @@ export class QuickBar extends LitElement { if (!section || section === "navigate") { let navigateItems = this._generateNavigationCommandsMemoized( this.hass, - this._addons + this._addons, + this._navigationFilterOptions ).sort(this._sortBySortingLabel); if (filter) { @@ -563,7 +573,11 @@ export class QuickBar extends LitElement { ); private _generateNavigationCommandsMemoized = memoizeOne( - generateNavigationCommands + ( + hass: HomeAssistant, + apps: HassioAddonInfo[] | undefined, + filterOptions: NavigationFilterOptions + ) => generateNavigationCommands(hass, apps, filterOptions) ); private _generateActionCommandsMemoized = memoizeOne(generateActionCommands); diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index be67f75325..89b33c526a 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -1,7 +1,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { canShowPage } from "../../../common/config/can_show_page"; +import { filterNavigationPages } from "../../../common/config/filter_navigation_pages"; import "../../../components/ha-card"; import "../../../components/ha-icon-next"; import "../../../components/ha-navigation-list"; @@ -30,38 +30,29 @@ class HaConfigNavigation extends LitElement { } protected render(): TemplateResult { - const pages = this.pages - .filter((page) => { - if (page.path === "#external-app-configuration") { - return this.hass.auth.external?.config.hasSettingsScreen; - } - // Only show Bluetooth page if there are Bluetooth config entries - if (page.component === "bluetooth") { - return this._hasBluetoothConfigEntries; - } - return canShowPage(this.hass, page); - }) - .map((page) => ({ - ...page, - name: - page.name || - this.hass.localize( - `ui.panel.config.dashboard.${page.translationKey}.main` - ), - description: - page.component === "cloud" && (page.info as CloudStatus) - ? page.info.logged_in - ? ` + const pages = filterNavigationPages(this.hass, this.pages, { + hasBluetoothConfigEntries: this._hasBluetoothConfigEntries, + }).map((page) => ({ + ...page, + name: + page.name || + this.hass.localize( + `ui.panel.config.dashboard.${page.translationKey}.main` + ), + description: + page.component === "cloud" && (page.info as CloudStatus) + ? page.info.logged_in + ? ` ${this.hass.localize( "ui.panel.config.cloud.description_login" )} ` - : ` + : ` ${this.hass.localize( "ui.panel.config.cloud.description_features" )} ` - : ` + : ` ${ page.description || this.hass.localize( @@ -69,7 +60,7 @@ class HaConfigNavigation extends LitElement { ) } `, - })); + })); return html`