mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-28 08:33:47 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22a10917f2 | ||
|
|
c139d4b802 |
No files matched your search
@@ -3,6 +3,8 @@ import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { subscribeOneCollection } from "../common/util/subscribe-one";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
@@ -13,12 +15,30 @@ import { SYSTEM_PANELS } from "../data/panel";
|
||||
import {
|
||||
CONFIG_SUB_ROUTES,
|
||||
computeNavigationPathInfo,
|
||||
type NavigationPathInfoOptions,
|
||||
} from "../data/compute-navigation-path-info";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
uiContext,
|
||||
} from "../data/context";
|
||||
import { findRelated, type RelatedResult } from "../data/search";
|
||||
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantRegistries,
|
||||
HomeAssistantUI,
|
||||
Panels,
|
||||
ValueChangedEvent,
|
||||
} from "../types";
|
||||
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-domain-icon";
|
||||
@@ -50,8 +70,6 @@ interface NavigationItem extends PickerComboBoxItem {
|
||||
|
||||
@customElement("ha-navigation-picker")
|
||||
export class HaNavigationPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -70,6 +88,28 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public context?: ActionRelatedContext;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
@transform<HomeAssistantUI, Panels | undefined>({
|
||||
transformer: (ui) => ui?.panels ?? undefined,
|
||||
})
|
||||
private _panels?: Panels;
|
||||
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries?: HomeAssistantRegistries;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: HomeAssistantConfig;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: HomeAssistantConnection;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api?: HomeAssistantApi;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._loadNavigationItems();
|
||||
}
|
||||
@@ -98,7 +138,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
? [
|
||||
{
|
||||
id: "related",
|
||||
label: this.hass.localize(
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.related"
|
||||
),
|
||||
},
|
||||
@@ -106,23 +146,27 @@ export class HaNavigationPicker extends LitElement {
|
||||
: []),
|
||||
{
|
||||
id: "dashboards",
|
||||
label: this.hass.localize("ui.components.navigation-picker.dashboards"),
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.dashboards"
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "views",
|
||||
label: this.hass.localize("ui.components.navigation-picker.views"),
|
||||
label: this._i18n.localize("ui.components.navigation-picker.views"),
|
||||
},
|
||||
...(this._navigationGroups.apps.length
|
||||
? [
|
||||
{
|
||||
id: "apps",
|
||||
label: this.hass.localize("ui.components.navigation-picker.apps"),
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.apps"
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "other_routes",
|
||||
label: this.hass.localize(
|
||||
label: this._i18n.localize(
|
||||
"ui.components.navigation-picker.other_routes"
|
||||
),
|
||||
},
|
||||
@@ -140,7 +184,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.sections=${sections}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
.customValueLabel=${this._i18n.localize(
|
||||
"ui.components.navigation-picker.add_custom_path"
|
||||
)}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
@@ -268,7 +312,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
}
|
||||
if (!section && groupItems.length) {
|
||||
items.push(
|
||||
this.hass.localize(`ui.components.navigation-picker.${group}`)
|
||||
this._i18n.localize(`ui.components.navigation-picker.${group}`)
|
||||
);
|
||||
}
|
||||
items.push(...groupItems);
|
||||
@@ -290,22 +334,25 @@ export class HaNavigationPicker extends LitElement {
|
||||
caseInsensitiveStringCompare(
|
||||
itemA.sorting_label!,
|
||||
itemB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
this._i18n.locale.language
|
||||
);
|
||||
|
||||
private async _loadNavigationItems() {
|
||||
await this._loadConfigEntries();
|
||||
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
|
||||
id,
|
||||
...panel,
|
||||
}));
|
||||
const pathInfoOptions = this._pathInfoOptions();
|
||||
const panels = Object.entries(pathInfoOptions.panels).map(
|
||||
([id, panel]) => ({
|
||||
id,
|
||||
...panel,
|
||||
})
|
||||
);
|
||||
const lovelacePanels = panels.filter(
|
||||
(panel) => panel.component_name === "lovelace"
|
||||
);
|
||||
|
||||
const viewConfigs = await Promise.all(
|
||||
lovelacePanels.map((panel) =>
|
||||
fetchConfig(this.hass!.connection, panel.url_path, true)
|
||||
fetchConfig(this._connection!.connection, panel.url_path, true)
|
||||
.then((config) => [panel.id, config] as [string, typeof config])
|
||||
.catch((_) => [panel.id, undefined] as [string, undefined])
|
||||
)
|
||||
@@ -324,7 +371,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
// Skip app panels — they are handled by the ingress panels fetch below
|
||||
if (panel.component_name === "app") continue;
|
||||
const path = `/${panel.url_path}`;
|
||||
const resolved = computeNavigationPathInfo(this.hass!, path);
|
||||
const resolved = computeNavigationPathInfo(pathInfoOptions, path);
|
||||
const isDashboardPanel =
|
||||
panel.component_name === "lovelace" ||
|
||||
PANEL_DASHBOARDS.includes(panel.id);
|
||||
@@ -350,7 +397,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
config.views.forEach((view, index) => {
|
||||
const viewPath = `/${panel.url_path}/${view.path ?? index}`;
|
||||
const viewInfo = computeNavigationPathInfo(
|
||||
this.hass!,
|
||||
pathInfoOptions,
|
||||
viewPath,
|
||||
config
|
||||
);
|
||||
@@ -366,15 +413,18 @@ export class HaNavigationPicker extends LitElement {
|
||||
}
|
||||
|
||||
// Fetch all ingress add-on panels
|
||||
if (isComponentLoaded(this.hass!.config, "hassio")) {
|
||||
if (
|
||||
this._hassConfig?.config &&
|
||||
isComponentLoaded(this._hassConfig.config, "hassio")
|
||||
) {
|
||||
try {
|
||||
const ingressPanels = await subscribeOneCollection(
|
||||
getIngressPanelInfoCollection(this.hass!.connection)
|
||||
getIngressPanelInfoCollection(this._connection!.connection)
|
||||
);
|
||||
for (const slug of Object.keys(ingressPanels)) {
|
||||
const path = `/app/${slug}`;
|
||||
const resolved = computeNavigationPathInfo(
|
||||
this.hass!,
|
||||
pathInfoOptions,
|
||||
path,
|
||||
undefined,
|
||||
ingressPanels
|
||||
@@ -396,7 +446,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
for (const [subPath, route] of Object.entries(CONFIG_SUB_ROUTES)) {
|
||||
const path = `/config/${subPath}`;
|
||||
const label = this.hass!.localize(route.translationKey) || subPath;
|
||||
const label = this._i18n.localize(route.translationKey) || subPath;
|
||||
otherRoutes.push({
|
||||
id: path,
|
||||
primary: label,
|
||||
@@ -445,9 +495,10 @@ export class HaNavigationPicker extends LitElement {
|
||||
...this._navigationGroups.apps,
|
||||
...this._navigationGroups.other_routes,
|
||||
];
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
if (!this.hass || (!this.context?.entity_id && !this.context?.area_id)) {
|
||||
if (!this.context?.entity_id && !this.context?.area_id) {
|
||||
updateRelatedItems([]);
|
||||
return;
|
||||
}
|
||||
@@ -472,8 +523,8 @@ export class HaNavigationPicker extends LitElement {
|
||||
let relatedResult: RelatedResult | undefined;
|
||||
try {
|
||||
relatedResult = context.entity_id
|
||||
? await findRelated(this.hass, "entity", context.entity_id)
|
||||
: await findRelated(this.hass, "area", context.area_id!);
|
||||
? await findRelated(this._api!, "entity", context.entity_id)
|
||||
: await findRelated(this._api!, "area", context.area_id!);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching related items for navigation picker", err);
|
||||
@@ -486,11 +537,12 @@ export class HaNavigationPicker extends LitElement {
|
||||
relatedAreaIds.add(context.area_id);
|
||||
}
|
||||
|
||||
const pathInfoOptions = this._pathInfoOptions();
|
||||
const relatedItems: NavigationItem[] = [];
|
||||
for (const deviceId of relatedDeviceIds) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
const device = pathInfoOptions.devices[deviceId];
|
||||
const path = `/config/devices/device/${deviceId}`;
|
||||
const resolved = computeNavigationPathInfo(this.hass, path);
|
||||
const resolved = computeNavigationPathInfo(pathInfoOptions, path);
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary: resolved.label,
|
||||
@@ -512,7 +564,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
for (const areaId of relatedAreaIds) {
|
||||
// Area dashboard view
|
||||
const viewPath = `/home/${computeAreaPath(areaId)}`;
|
||||
const resolvedArea = computeNavigationPathInfo(this.hass, viewPath);
|
||||
const resolvedArea = computeNavigationPathInfo(pathInfoOptions, viewPath);
|
||||
relatedItems.push({
|
||||
id: viewPath,
|
||||
primary: resolvedArea.label,
|
||||
@@ -531,7 +583,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
const path = `/config/areas/area/${areaId}`;
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary: this.hass.localize(
|
||||
primary: this._i18n.localize(
|
||||
"ui.components.navigation-picker.area_settings",
|
||||
{ area: resolvedArea.label }
|
||||
),
|
||||
@@ -550,13 +602,22 @@ export class HaNavigationPicker extends LitElement {
|
||||
return relatedItems;
|
||||
}
|
||||
|
||||
private _pathInfoOptions(): NavigationPathInfoOptions {
|
||||
return {
|
||||
localize: this._i18n.localize,
|
||||
panels: this._panels ?? {},
|
||||
areas: this._registries?.areas ?? {},
|
||||
devices: this._registries?.devices ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
if (Object.keys(this._configEntryLookup).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
const configEntries = await getConfigEntries(this._api!);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
configEntries.map((entry) => [entry.entry_id, entry])
|
||||
);
|
||||
|
||||
@@ -2,14 +2,11 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NavigationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ActionRelatedContext } from "../../panels/lovelace/components/hui-action-editor";
|
||||
import "../ha-navigation-picker";
|
||||
|
||||
@customElement("ha-selector-navigation")
|
||||
export class HaNavigationSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: NavigationSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -27,7 +24,6 @@ export class HaNavigationSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-navigation-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
mdiShape,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
Connection,
|
||||
HassConfig,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { computeDeviceName } from "../common/entity/compute_device_name";
|
||||
import {
|
||||
@@ -25,13 +29,25 @@ import {
|
||||
getPanelIconPath,
|
||||
getPanelTitleFromUrlPath,
|
||||
} from "./panel";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../common/translations/localize";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "./device/device_registry";
|
||||
import type { Panels } from "../types";
|
||||
|
||||
export type NavigationPathInfoSources = Pick<
|
||||
HomeAssistant,
|
||||
"localize" | "panels" | "areas" | "devices" | "config" | "connection"
|
||||
>;
|
||||
export interface NavigationPathInfoOptions {
|
||||
localize: LocalizeFunc;
|
||||
panels: Panels;
|
||||
areas: Record<string, AreaRegistryEntry>;
|
||||
devices: Record<string, DeviceRegistryEntry>;
|
||||
}
|
||||
|
||||
export interface SubscribeNavigationPathInfoOptions extends NavigationPathInfoOptions {
|
||||
config: HassConfig;
|
||||
connection: Connection;
|
||||
}
|
||||
|
||||
export interface NavigationPathInfo {
|
||||
label: string;
|
||||
@@ -86,7 +102,7 @@ export const CONFIG_SUB_ROUTES: Record<
|
||||
* For lovelace views, pass the dashboard config to resolve view title/icon.
|
||||
*/
|
||||
export const computeNavigationPathInfo = (
|
||||
hass: NavigationPathInfoSources,
|
||||
options: NavigationPathInfoOptions,
|
||||
path: string,
|
||||
lovelaceConfig?: LovelaceRawConfig,
|
||||
ingressPanels?: IngressPanelInfoMap
|
||||
@@ -102,7 +118,7 @@ export const computeNavigationPathInfo = (
|
||||
segments[2] === "area" &&
|
||||
segments[3]
|
||||
) {
|
||||
return computeAreaNavigationPathInfo(hass, segments[3]);
|
||||
return computeAreaNavigationPathInfo(options.areas, segments[3]);
|
||||
}
|
||||
|
||||
// /config/devices/device/{deviceId}
|
||||
@@ -112,7 +128,7 @@ export const computeNavigationPathInfo = (
|
||||
segments[2] === "device" &&
|
||||
segments[3]
|
||||
) {
|
||||
return computeDeviceNavigationPathInfo(hass, segments[3]);
|
||||
return computeDeviceNavigationPathInfo(options.devices, segments[3]);
|
||||
}
|
||||
|
||||
// /app/<slug> (ingress addon panel)
|
||||
@@ -124,25 +140,25 @@ export const computeNavigationPathInfo = (
|
||||
if (panelUrlPath === "config" && subPath && subPath in CONFIG_SUB_ROUTES) {
|
||||
const route = CONFIG_SUB_ROUTES[subPath];
|
||||
return {
|
||||
label: hass.localize(route.translationKey) || subPath,
|
||||
label: options.localize(route.translationKey) || subPath,
|
||||
iconPath: route.iconPath,
|
||||
};
|
||||
}
|
||||
|
||||
const panel = panelUrlPath ? hass.panels[panelUrlPath] : undefined;
|
||||
const panel = panelUrlPath ? options.panels[panelUrlPath] : undefined;
|
||||
const panelIcon = panel ? getPanelIcon(panel) : undefined;
|
||||
const panelIconPath = panel ? getPanelIconPath(panel) : undefined;
|
||||
|
||||
// /home/areas-{areaId} (area dashboard view)
|
||||
if (subPath?.startsWith(AREA_VIEW_PREFIX)) {
|
||||
const areaId = subPath.slice(AREA_VIEW_PREFIX.length);
|
||||
return computeAreaNavigationPathInfo(hass, areaId);
|
||||
return computeAreaNavigationPathInfo(options.areas, areaId);
|
||||
}
|
||||
|
||||
const isDashboard = panel?.component_name === "lovelace";
|
||||
|
||||
const panelInfo: NavigationPathInfo = {
|
||||
label: getPanelTitleFromUrlPath(hass, panelUrlPath) || panelUrlPath,
|
||||
label: getPanelTitleFromUrlPath(options, panelUrlPath) || panelUrlPath,
|
||||
icon: panelIcon || (isDashboard ? "mdi:view-dashboard" : undefined),
|
||||
iconPath: panelIconPath || mdiLink,
|
||||
};
|
||||
@@ -166,10 +182,10 @@ export const computeNavigationPathInfo = (
|
||||
};
|
||||
|
||||
const computeAreaNavigationPathInfo = (
|
||||
hass: Pick<HomeAssistant, "areas">,
|
||||
areas: Record<string, AreaRegistryEntry>,
|
||||
areaId: string
|
||||
): NavigationPathInfo => {
|
||||
const area = hass.areas[areaId];
|
||||
const area = areas[areaId];
|
||||
return {
|
||||
label: area?.name || areaId,
|
||||
icon: area?.icon || undefined,
|
||||
@@ -178,10 +194,10 @@ const computeAreaNavigationPathInfo = (
|
||||
};
|
||||
|
||||
const computeDeviceNavigationPathInfo = (
|
||||
hass: Pick<HomeAssistant, "devices">,
|
||||
devices: Record<string, DeviceRegistryEntry>,
|
||||
deviceId: string
|
||||
): NavigationPathInfo => {
|
||||
const device = hass.devices[deviceId];
|
||||
const device = devices[deviceId];
|
||||
return {
|
||||
label: (device ? computeDeviceName(device) : undefined) || deviceId,
|
||||
iconPath: mdiDevices,
|
||||
@@ -206,7 +222,7 @@ const computeIngressNavigationPathInfo = (
|
||||
* updates for view paths and ingress panel info for app paths.
|
||||
*/
|
||||
export const subscribeNavigationPathInfo = (
|
||||
hass: NavigationPathInfoSources,
|
||||
options: SubscribeNavigationPathInfoOptions,
|
||||
path: string,
|
||||
onChange: (info: NavigationPathInfo) => void
|
||||
): UnsubscribeFunc | undefined => {
|
||||
@@ -217,13 +233,13 @@ export const subscribeNavigationPathInfo = (
|
||||
if (
|
||||
panelUrlPath === APP_PANEL &&
|
||||
segments[1] &&
|
||||
isComponentLoaded(hass.config, "hassio")
|
||||
isComponentLoaded(options.config, "hassio")
|
||||
) {
|
||||
try {
|
||||
const collection = getIngressPanelInfoCollection(hass.connection);
|
||||
const collection = getIngressPanelInfoCollection(options.connection);
|
||||
// Use cached state for immediate resolution if available
|
||||
const info = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
path,
|
||||
undefined,
|
||||
collection.state
|
||||
@@ -232,7 +248,7 @@ export const subscribeNavigationPathInfo = (
|
||||
let current = info;
|
||||
return collection.subscribe((panels) => {
|
||||
const newInfo = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
path,
|
||||
undefined,
|
||||
panels
|
||||
@@ -247,15 +263,15 @@ export const subscribeNavigationPathInfo = (
|
||||
}
|
||||
}
|
||||
|
||||
const info = computeNavigationPathInfo(hass, path);
|
||||
const info = computeNavigationPathInfo(options, path);
|
||||
onChange(info);
|
||||
|
||||
const panel = panelUrlPath ? hass.panels[panelUrlPath] : undefined;
|
||||
const panel = panelUrlPath ? options.panels[panelUrlPath] : undefined;
|
||||
if (segments[1] && panel?.component_name === "lovelace") {
|
||||
let current = info;
|
||||
const collection = getLovelaceCollection(hass.connection, panelUrlPath);
|
||||
const collection = getLovelaceCollection(options.connection, panelUrlPath);
|
||||
return collection.subscribe((config) => {
|
||||
const newInfo = computeNavigationPathInfo(hass, path, config);
|
||||
const newInfo = computeNavigationPathInfo(options, path, config);
|
||||
if (newInfo.label !== current.label || newInfo.icon !== current.icon) {
|
||||
current = newInfo;
|
||||
onChange(newInfo);
|
||||
|
||||
@@ -122,7 +122,6 @@ export class HomeShortcutsEditor extends LitElement {
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<ha-navigation-picker
|
||||
.hass=${this.hass}
|
||||
.addButtonLabel=${this.hass.localize(
|
||||
"ui.panel.home.editor.add_custom_shortcut"
|
||||
)}
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { mdiDevices, mdiLink, mdiPuzzle, mdiTextureBox } from "@mdi/js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeNavigationPathInfo } from "../../src/data/compute-navigation-path-info";
|
||||
import {
|
||||
computeNavigationPathInfo,
|
||||
type NavigationPathInfoOptions,
|
||||
} from "../../src/data/compute-navigation-path-info";
|
||||
import type { IngressPanelInfoMap } from "../../src/data/hassio/ingress";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import type { LovelaceConfig } from "../../src/data/lovelace/config/types";
|
||||
|
||||
const createHass = (overrides: Partial<HomeAssistant> = {}): HomeAssistant =>
|
||||
({
|
||||
panels: {},
|
||||
areas: {},
|
||||
devices: {},
|
||||
localize: () => "",
|
||||
...overrides,
|
||||
}) as unknown as HomeAssistant;
|
||||
const createOptions = (
|
||||
overrides: Partial<NavigationPathInfoOptions> = {}
|
||||
): NavigationPathInfoOptions => ({
|
||||
panels: {},
|
||||
areas: {},
|
||||
devices: {},
|
||||
localize: () => "",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("computeNavigationPathInfo", () => {
|
||||
describe("panel paths", () => {
|
||||
it("resolves a panel with icon", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
"my-panel": {
|
||||
url_path: "my-panel",
|
||||
@@ -28,15 +32,15 @@ describe("computeNavigationPathInfo", () => {
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(hass, "/my-panel");
|
||||
const result = computeNavigationPathInfo(options, "/my-panel");
|
||||
expect(result.label).toBe("My Panel");
|
||||
expect(result.icon).toBe("mdi:star");
|
||||
expect(result.iconPath).toBe(mdiLink);
|
||||
});
|
||||
|
||||
it("falls back to url path for unknown panel", () => {
|
||||
const hass = createHass();
|
||||
const result = computeNavigationPathInfo(hass, "/unknown");
|
||||
const options = createOptions();
|
||||
const result = computeNavigationPathInfo(options, "/unknown");
|
||||
expect(result.label).toBe("unknown");
|
||||
expect(result.icon).toBeUndefined();
|
||||
expect(result.iconPath).toBe(mdiLink);
|
||||
@@ -45,7 +49,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
|
||||
describe("area paths", () => {
|
||||
it("resolves /config/areas/area/{areaId}", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
areas: {
|
||||
living_room: {
|
||||
area_id: "living_room",
|
||||
@@ -56,7 +60,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/config/areas/area/living_room"
|
||||
);
|
||||
expect(result.label).toBe("Living Room");
|
||||
@@ -65,7 +69,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("resolves /home/areas-{areaId}", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
areas: {
|
||||
kitchen: {
|
||||
area_id: "kitchen",
|
||||
@@ -75,16 +79,16 @@ describe("computeNavigationPathInfo", () => {
|
||||
} as unknown as HomeAssistant["areas"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(hass, "/home/areas-kitchen");
|
||||
const result = computeNavigationPathInfo(options, "/home/areas-kitchen");
|
||||
expect(result.label).toBe("Kitchen");
|
||||
expect(result.icon).toBeUndefined();
|
||||
expect(result.iconPath).toBe(mdiTextureBox);
|
||||
});
|
||||
|
||||
it("falls back to area id for unknown area", () => {
|
||||
const hass = createHass();
|
||||
const options = createOptions();
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/config/areas/area/unknown_area"
|
||||
);
|
||||
expect(result.label).toBe("unknown_area");
|
||||
@@ -94,7 +98,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
|
||||
describe("device paths", () => {
|
||||
it("resolves /config/devices/device/{deviceId}", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
devices: {
|
||||
abc123: {
|
||||
id: "abc123",
|
||||
@@ -105,7 +109,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/config/devices/device/abc123"
|
||||
);
|
||||
expect(result.label).toBe("Smart Light");
|
||||
@@ -113,7 +117,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("prefers user-defined device name", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
devices: {
|
||||
abc123: {
|
||||
id: "abc123",
|
||||
@@ -124,16 +128,16 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/config/devices/device/abc123"
|
||||
);
|
||||
expect(result.label).toBe("My Light");
|
||||
});
|
||||
|
||||
it("falls back to device id for unknown device", () => {
|
||||
const hass = createHass();
|
||||
const options = createOptions();
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/config/devices/device/unknown_device"
|
||||
);
|
||||
expect(result.label).toBe("unknown_device");
|
||||
@@ -152,7 +156,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
};
|
||||
|
||||
it("resolves view with title and icon", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
@@ -163,7 +167,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/lovelace/overview",
|
||||
lovelaceConfig
|
||||
);
|
||||
@@ -172,7 +176,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("resolves view without icon using default", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
@@ -183,7 +187,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/lovelace/lights",
|
||||
lovelaceConfig
|
||||
);
|
||||
@@ -192,7 +196,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("uses titleCase of path when view has no title", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
@@ -203,7 +207,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/lovelace/my-view",
|
||||
lovelaceConfig
|
||||
);
|
||||
@@ -211,7 +215,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("uses index as name when view has no title or path", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
@@ -222,7 +226,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/lovelace/3",
|
||||
lovelaceConfig
|
||||
);
|
||||
@@ -230,7 +234,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("falls back to panel info when view not found", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
@@ -241,7 +245,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/lovelace/nonexistent",
|
||||
lovelaceConfig
|
||||
);
|
||||
@@ -249,7 +253,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("falls back to panel info when no lovelace config provided", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
@@ -259,7 +263,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(hass, "/lovelace/overview");
|
||||
const result = computeNavigationPathInfo(options, "/lovelace/overview");
|
||||
expect(result.label).toBe("Dashboard");
|
||||
});
|
||||
});
|
||||
@@ -277,9 +281,9 @@ describe("computeNavigationPathInfo", () => {
|
||||
};
|
||||
|
||||
it("resolves /app/<slug> with ingress panels data", () => {
|
||||
const hass = createHass();
|
||||
const options = createOptions();
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/app/my_addon",
|
||||
undefined,
|
||||
ingressPanels
|
||||
@@ -290,17 +294,17 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("falls back to slug when ingress panels not provided", () => {
|
||||
const hass = createHass();
|
||||
const result = computeNavigationPathInfo(hass, "/app/my_addon");
|
||||
const options = createOptions();
|
||||
const result = computeNavigationPathInfo(options, "/app/my_addon");
|
||||
expect(result.label).toBe("my_addon");
|
||||
expect(result.icon).toBeUndefined();
|
||||
expect(result.iconPath).toBe(mdiPuzzle);
|
||||
});
|
||||
|
||||
it("falls back to slug when addon not found in ingress panels", () => {
|
||||
const hass = createHass();
|
||||
const options = createOptions();
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/app/unknown_addon",
|
||||
undefined,
|
||||
ingressPanels
|
||||
@@ -311,9 +315,9 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("resolves addon with empty icon as undefined", () => {
|
||||
const hass = createHass();
|
||||
const options = createOptions();
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/app/no_icon_addon",
|
||||
undefined,
|
||||
ingressPanels
|
||||
@@ -324,7 +328,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
});
|
||||
|
||||
it("does not resolve /app without a slug", () => {
|
||||
const hass = createHass({
|
||||
const options = createOptions({
|
||||
panels: {
|
||||
app: {
|
||||
url_path: "app",
|
||||
@@ -334,7 +338,7 @@ describe("computeNavigationPathInfo", () => {
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
options,
|
||||
"/app",
|
||||
undefined,
|
||||
ingressPanels
|
||||
|
||||
Reference in new issue
Block a user