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`
${this.hass.localize("panel.config")} diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 00bc203163..65b54f54f2 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -116,7 +116,6 @@ export const configSections: Record = { dashboard_2: [ { path: "/config/matter", - name: "Matter", iconPath: "M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z", iconColor: "#2458B3", @@ -125,7 +124,6 @@ export const configSections: Record = { }, { path: "/config/zha", - name: "Zigbee", iconPath: mdiZigbee, iconColor: "#E74011", component: "zha", @@ -133,7 +131,6 @@ export const configSections: Record = { }, { path: "/config/zwave_js", - name: "Z-Wave", iconPath: mdiZWave, iconColor: "#153163", component: "zwave_js", @@ -141,7 +138,6 @@ export const configSections: Record = { }, { path: "/knx", - name: "KNX", iconPath: "M 3.9861338,14.261456 3.7267552,13.934877 6.3179131,11.306266 H 4.466374 l -2.6385205,2.68258 V 11.312882 H 0.00440574 L 0,17.679803 l 1.8278535,5.43e-4 v -1.818482 l 0.7225444,-0.732459 2.1373588,2.543782 2.1869324,-5.44e-4 M 24,17.680369 21.809238,17.669359 19.885559,15.375598 17.640262,17.68037 h -1.828407 l 3.236048,-3.302138 -2.574075,-3.067547 2.135161,0.0016 1.610309,1.87687 1.866403,-1.87687 h 1.828429 l -2.857742,2.87478 m -10.589867,-2.924898 2.829625,3.990552 -0.01489,-3.977887 1.811889,-0.0044 0.0011,6.357564 -2.093314,-5.44e-4 -2.922133,-3.947594 -0.0314,3.947594 H 8.2581097 V 11.261677 M 11.971203,6.3517488 c 0,0 2.800714,-0.093203 6.172001,1.0812045 3.462393,1.0898845 5.770926,3.4695627 5.770926,3.4695627 l -1.823898,-5.43e-4 C 22.088532,10.900273 20.577938,9.4271528 17.660223,8.5024618 15.139256,7.703366 12.723057,7.645835 12.111178,7.6449876 l -9.71e-4,0.0011 c 0,0 -0.0259,-6.4e-4 -0.07527,-9.714e-4 -0.04726,3.33e-4 -0.07201,9.714e-4 -0.07201,9.714e-4 v -0.00113 C 11.337007,7.6453728 8.8132091,7.7001736 6.2821829,8.5024618 3.3627914,9.4276738 1.8521646,10.901973 1.8521646,10.901973 l -1.82398708,5.43e-4 C 0.03128403,10.899322 2.339143,8.5221038 5.799224,7.4329533 9.170444,6.2585642 11.971203,6.3517488 11.971203,6.3517488 Z", iconColor: "#4EAA66", @@ -150,7 +146,6 @@ export const configSections: Record = { }, { path: "/config/thread", - name: "Thread", iconPath: "m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z", iconColor: "#ED7744", @@ -159,7 +154,6 @@ export const configSections: Record = { }, { path: "/config/bluetooth", - name: "Bluetooth", iconPath: mdiBluetooth, iconColor: "#0082FC", component: "bluetooth", @@ -167,7 +161,6 @@ export const configSections: Record = { }, { path: "/insteon", - name: "Insteon", iconPath: "m 12.001571,6.3842473 h 0.02973 c 3.652189,0 6.767389,-2.29456 7.987462,-5.5177193 L 15.389382,0 Z m 0,0 h -0.02972 c -3.6522186,0 -6.7673314,-2.2918546 -7.9874477,-5.5177193 h -0.00271 L 8.6111273,0 Z M 6.3840436,11.999287 v -0.02972 c 0,-3.6524074 -2.2944727,-6.7675928 -5.51754469,-7.9877383 L 0,8.6114473 Z m 0,0 v 0.02964 c 0,3.652378 -2.2917818,6.767578 -5.51754469,7.987796 v 0.0026 L 0,15.389818 Z M 24,8.6114473 23.133527,3.9818327 v 0.00269 C 19.907636,5.2046836 17.616,8.3198691 17.616,11.972276 v 0.02966 0.02972 0.0027 c 0,3.65232 2.2944,6.76752 5.517527,7.987738 L 24,15.392436 17.616,12.001935 Z M 20.018618,23.133527 15.389091,24 11.99872,17.615709 h 0.02964 c 3.652218,0 6.767418,2.291927 7.987491,5.517818 z M 11.99872,17.615709 8.6082618,24 3.9788364,23.133527 C 5.1989527,19.9104 8.3140655,17.615709 11.966284,17.615709 h 0.0027 z", iconColor: "#E4002C", diff --git a/src/translations/en.json b/src/translations/en.json index 482fef31c4..9120c04c1d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1447,7 +1447,14 @@ "app_info": "{app} info", "shortcuts": "[%key:ui::panel::config::info::shortcuts%]", "labs": "[%key:ui::panel::config::labs::caption%]", - "developer-tools": "[%key:ui::panel::config::dashboard::developer_tools::main%]" + "developer-tools": "[%key:ui::panel::config::dashboard::developer_tools::main%]", + "matter": "[%key:ui::panel::config::dashboard::matter::main%]", + "zha": "[%key:ui::panel::config::dashboard::zha::main%]", + "zwave_js": "[%key:ui::panel::config::dashboard::zwave_js::main%]", + "thread": "[%key:ui::panel::config::dashboard::thread::main%]", + "bluetooth": "[%key:ui::panel::config::dashboard::bluetooth::main%]", + "knx": "[%key:ui::panel::config::dashboard::knx::main%]", + "insteon": "[%key:ui::panel::config::dashboard::insteon::main%]" } }, "filter_placeholder": "Search entities", @@ -2386,24 +2393,31 @@ "secondary": "Loading..." }, "zwave_js": { + "main": "Z-Wave", "secondary": "Sub-GHz mesh protocol" }, "zha": { + "main": "Zigbee", "secondary": "Low-power mesh network" }, "matter": { + "main": "Matter", "secondary": "Cross-vendor smart home standard" }, "thread": { + "main": "Thread", "secondary": "Mesh network often used for Matter devices" }, "bluetooth": { + "main": "Bluetooth", "secondary": "Local device connectivity" }, "knx": { + "main": "KNX", "secondary": "Building automation standard" }, "insteon": { + "main": "Insteon", "secondary": "Dual-mesh home automation" } },