mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-19 12:48:56 +01:00
Add shortcut card (#51562)
* Add shortcut card * Improve action support
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { mdiDevices, mdiTextureBox } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import { getPanelIcon, getPanelTitle, SYSTEM_PANELS } from "../data/panel";
|
||||
import { SYSTEM_PANELS } from "../data/panel";
|
||||
import { computeNavigationPathInfo } from "../data/compute-navigation-path-info";
|
||||
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";
|
||||
@@ -31,6 +30,12 @@ const RELATED_SORT_PREFIX = {
|
||||
device: "2_device",
|
||||
} as const;
|
||||
|
||||
const createSortingLabel = (...parts: (string | undefined)[]) =>
|
||||
parts
|
||||
.filter(Boolean)
|
||||
.map((part) => (part!.startsWith("/") ? `zzz${part}` : part))
|
||||
.join("_");
|
||||
|
||||
interface NavigationItem extends PickerComboBoxItem {
|
||||
group: NavigationGroup;
|
||||
domain?: string;
|
||||
@@ -276,22 +281,16 @@ export class HaNavigationPicker extends LitElement {
|
||||
for (const panel of panels) {
|
||||
if (SYSTEM_PANELS.includes(panel.id)) continue;
|
||||
const path = `/${panel.url_path}`;
|
||||
const panelTitle = getPanelTitle(this.hass, panel);
|
||||
const primary = panelTitle || path;
|
||||
const resolved = computeNavigationPathInfo(this.hass!, path);
|
||||
const isDashboardPanel =
|
||||
panel.component_name === "lovelace" ||
|
||||
PANEL_DASHBOARDS.includes(panel.id);
|
||||
const panelItem: NavigationItem = {
|
||||
id: path,
|
||||
primary,
|
||||
secondary: panelTitle ? path : undefined,
|
||||
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
||||
sorting_label: [
|
||||
primary.startsWith("/") ? `zzz${primary}` : primary,
|
||||
path,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("_"),
|
||||
primary: resolved.label,
|
||||
secondary: resolved.label !== path ? path : undefined,
|
||||
icon: resolved.icon || "mdi:view-dashboard",
|
||||
sorting_label: createSortingLabel(resolved.label, path),
|
||||
group: isDashboardPanel ? "dashboards" : "other_routes",
|
||||
};
|
||||
|
||||
@@ -307,17 +306,17 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
config.views.forEach((view, index) => {
|
||||
const viewPath = `/${panel.url_path}/${view.path ?? index}`;
|
||||
const viewPrimary =
|
||||
view.title ?? (view.path ? titleCase(view.path) : `${index}`);
|
||||
const viewInfo = computeNavigationPathInfo(
|
||||
this.hass!,
|
||||
viewPath,
|
||||
config
|
||||
);
|
||||
views.push({
|
||||
id: viewPath,
|
||||
secondary: viewPath,
|
||||
icon: view.icon ?? "mdi:view-compact",
|
||||
primary: viewPrimary,
|
||||
sorting_label: [
|
||||
viewPrimary.startsWith("/") ? `zzz${viewPrimary}` : viewPrimary,
|
||||
viewPath,
|
||||
].join("_"),
|
||||
icon: viewInfo.icon || "mdi:view-compact",
|
||||
primary: viewInfo.label,
|
||||
sorting_label: createSortingLabel(viewInfo.label, viewPath),
|
||||
group: "views",
|
||||
});
|
||||
});
|
||||
@@ -399,28 +398,20 @@ export class HaNavigationPicker extends LitElement {
|
||||
relatedAreaIds.add(context.area_id);
|
||||
}
|
||||
|
||||
const createSortingLabel = (
|
||||
prefix: string,
|
||||
primary: string,
|
||||
path: string
|
||||
) =>
|
||||
[prefix, primary.startsWith("/") ? `zzz${primary}` : primary, path]
|
||||
.filter(Boolean)
|
||||
.join("_");
|
||||
|
||||
const relatedItems: NavigationItem[] = [];
|
||||
for (const deviceId of relatedDeviceIds) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
const primary = device?.name_by_user ?? device?.name ?? deviceId;
|
||||
const path = `/config/devices/device/${deviceId}`;
|
||||
const resolved = computeNavigationPathInfo(this.hass, path);
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary,
|
||||
primary: resolved.label,
|
||||
secondary: path,
|
||||
icon_path: mdiDevices,
|
||||
icon: resolved.icon,
|
||||
icon_path: resolved.iconPath,
|
||||
sorting_label: createSortingLabel(
|
||||
RELATED_SORT_PREFIX.device,
|
||||
primary,
|
||||
resolved.label,
|
||||
path
|
||||
),
|
||||
group: "related",
|
||||
@@ -431,20 +422,18 @@ export class HaNavigationPicker extends LitElement {
|
||||
}
|
||||
|
||||
for (const areaId of relatedAreaIds) {
|
||||
const area = this.hass.areas[areaId];
|
||||
const primary = area?.name ?? areaId;
|
||||
|
||||
// Area dashboard view
|
||||
const viewPath = `/home/${computeAreaPath(areaId)}`;
|
||||
const resolvedArea = computeNavigationPathInfo(this.hass, viewPath);
|
||||
relatedItems.push({
|
||||
id: viewPath,
|
||||
primary,
|
||||
primary: resolvedArea.label,
|
||||
secondary: viewPath,
|
||||
icon: area?.icon ?? undefined,
|
||||
icon_path: area?.icon ? undefined : mdiTextureBox,
|
||||
icon: resolvedArea.icon,
|
||||
icon_path: resolvedArea.icon ? undefined : resolvedArea.iconPath,
|
||||
sorting_label: createSortingLabel(
|
||||
RELATED_SORT_PREFIX.area_view,
|
||||
primary,
|
||||
resolvedArea.label,
|
||||
viewPath
|
||||
),
|
||||
group: "related",
|
||||
@@ -456,14 +445,14 @@ export class HaNavigationPicker extends LitElement {
|
||||
id: path,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.navigation-picker.area_settings",
|
||||
{ area: primary }
|
||||
{ area: resolvedArea.label }
|
||||
),
|
||||
secondary: path,
|
||||
icon: area?.icon ?? undefined,
|
||||
icon_path: area?.icon ? undefined : mdiTextureBox,
|
||||
icon: resolvedArea.icon,
|
||||
icon_path: resolvedArea.icon ? undefined : resolvedArea.iconPath,
|
||||
sorting_label: createSortingLabel(
|
||||
RELATED_SORT_PREFIX.area,
|
||||
primary,
|
||||
resolvedArea.label,
|
||||
path
|
||||
),
|
||||
group: "related",
|
||||
|
||||
@@ -78,7 +78,7 @@ export class HaTextSelector extends LitElement {
|
||||
return html`<ha-input
|
||||
.name=${this.name}
|
||||
.value=${this.value || ""}
|
||||
.placeholder=${this.placeholder || ""}
|
||||
.placeholder=${this.placeholder || this.selector.text?.placeholder || ""}
|
||||
.hint=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.type=${this.selector.text?.type}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ActionConfig } from "../../data/lovelace/config/action";
|
||||
import type { UiActionSelector } from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-action-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ActionRelatedContext } from "../../panels/lovelace/components/hui-action-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-selector-ui_action")
|
||||
export class HaSelectorUiAction extends LitElement {
|
||||
@@ -21,10 +21,13 @@ export class HaSelectorUiAction extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hui-action-editor
|
||||
.label=${this.label}
|
||||
.required=${this.required}
|
||||
.hass=${this.hass}
|
||||
.config=${this.value}
|
||||
.context=${this.context}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { mdiDevices, mdiLink, mdiTextureBox } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { computeDeviceName } from "../common/entity/compute_device_name";
|
||||
import { getLovelaceCollection } from "./lovelace";
|
||||
import type { LovelaceRawConfig } from "./lovelace/config/types";
|
||||
import { computeViewIcon, computeViewTitle } from "./lovelace/config/view";
|
||||
import {
|
||||
getPanelIcon,
|
||||
getPanelIconPath,
|
||||
getPanelTitleFromUrlPath,
|
||||
} from "./panel";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface NavigationPathInfo {
|
||||
label: string;
|
||||
icon?: string;
|
||||
iconPath: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_NAVIGATION_PATH_INFO: NavigationPathInfo = {
|
||||
label: "",
|
||||
iconPath: mdiLink,
|
||||
};
|
||||
|
||||
const AREA_VIEW_PREFIX = "areas-";
|
||||
|
||||
/**
|
||||
* Resolve a navigation path to a display label and icon.
|
||||
* Works synchronously for panels, areas, and devices.
|
||||
* For lovelace views, pass the dashboard config to resolve view title/icon.
|
||||
*/
|
||||
export const computeNavigationPathInfo = (
|
||||
hass: HomeAssistant,
|
||||
path: string,
|
||||
lovelaceConfig?: LovelaceRawConfig
|
||||
): NavigationPathInfo => {
|
||||
const segments = path.replace(/^\//, "").split(/[/?]/);
|
||||
const panelUrlPath = segments[0];
|
||||
const subPath = segments[1];
|
||||
|
||||
// /config/areas/area/{areaId}
|
||||
if (
|
||||
panelUrlPath === "config" &&
|
||||
segments[1] === "areas" &&
|
||||
segments[2] === "area" &&
|
||||
segments[3]
|
||||
) {
|
||||
return computeAreaNavigationPathInfo(hass, segments[3]);
|
||||
}
|
||||
|
||||
// /config/devices/device/{deviceId}
|
||||
if (
|
||||
panelUrlPath === "config" &&
|
||||
segments[1] === "devices" &&
|
||||
segments[2] === "device" &&
|
||||
segments[3]
|
||||
) {
|
||||
return computeDeviceNavigationPathInfo(hass, segments[3]);
|
||||
}
|
||||
|
||||
const panel = panelUrlPath ? hass.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);
|
||||
}
|
||||
|
||||
const isDashboard = panel?.component_name === "lovelace";
|
||||
|
||||
const panelInfo: NavigationPathInfo = {
|
||||
label: getPanelTitleFromUrlPath(hass, panelUrlPath) || panelUrlPath,
|
||||
icon: panelIcon || (isDashboard ? "mdi:view-dashboard" : undefined),
|
||||
iconPath: panelIconPath || mdiLink,
|
||||
};
|
||||
|
||||
// Lovelace view path
|
||||
if (subPath && lovelaceConfig && "views" in lovelaceConfig) {
|
||||
const viewIndex = lovelaceConfig.views.findIndex(
|
||||
(v, index) => (v.path ?? String(index)) === subPath
|
||||
);
|
||||
if (viewIndex !== -1) {
|
||||
const view = lovelaceConfig.views[viewIndex];
|
||||
return {
|
||||
...panelInfo,
|
||||
label: computeViewTitle(view, viewIndex),
|
||||
icon: computeViewIcon(view),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return panelInfo;
|
||||
};
|
||||
|
||||
const computeAreaNavigationPathInfo = (
|
||||
hass: HomeAssistant,
|
||||
areaId: string
|
||||
): NavigationPathInfo => {
|
||||
const area = hass.areas[areaId];
|
||||
return {
|
||||
label: area?.name || areaId,
|
||||
icon: area?.icon || undefined,
|
||||
iconPath: mdiTextureBox,
|
||||
};
|
||||
};
|
||||
|
||||
const computeDeviceNavigationPathInfo = (
|
||||
hass: HomeAssistant,
|
||||
deviceId: string
|
||||
): NavigationPathInfo => {
|
||||
const device = hass.devices[deviceId];
|
||||
return {
|
||||
label: (device ? computeDeviceName(device) : undefined) || deviceId,
|
||||
iconPath: mdiDevices,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to navigation path info updates.
|
||||
* Resolves synchronously first, then subscribes to lovelace config
|
||||
* updates for view paths.
|
||||
*/
|
||||
export const subscribeNavigationPathInfo = (
|
||||
hass: HomeAssistant,
|
||||
path: string,
|
||||
onChange: (info: NavigationPathInfo) => void
|
||||
): UnsubscribeFunc | undefined => {
|
||||
const info = computeNavigationPathInfo(hass, path);
|
||||
onChange(info);
|
||||
|
||||
const segments = path.replace(/^\//, "").split(/[/?]/);
|
||||
const panelUrlPath = segments[0];
|
||||
const panel = panelUrlPath ? hass.panels[panelUrlPath] : undefined;
|
||||
|
||||
if (segments[1] && panel?.component_name === "lovelace") {
|
||||
let current = info;
|
||||
const collection = getLovelaceCollection(hass.connection, panelUrlPath);
|
||||
return collection.subscribe((config) => {
|
||||
const newInfo = computeNavigationPathInfo(hass, path, config);
|
||||
if (newInfo.label !== current.label || newInfo.icon !== current.icon) {
|
||||
current = newInfo;
|
||||
onChange(newInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { titleCase } from "../../../common/string/title-case";
|
||||
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
|
||||
import type { MediaSelectorValue } from "../../selector";
|
||||
import type { LovelaceBadgeConfig } from "./badge";
|
||||
@@ -93,3 +94,11 @@ export function isStrategyView(
|
||||
): view is LovelaceStrategyViewConfig {
|
||||
return "strategy" in view;
|
||||
}
|
||||
|
||||
export const computeViewTitle = (
|
||||
view: LovelaceBaseViewConfig,
|
||||
index: number
|
||||
): string => view.title ?? (view.path ? titleCase(view.path) : String(index));
|
||||
|
||||
export const computeViewIcon = (view: LovelaceBaseViewConfig): string =>
|
||||
view.icon ?? "mdi:view-compact";
|
||||
|
||||
@@ -494,6 +494,7 @@ export interface StringSelector {
|
||||
| "color";
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
placeholder?: string;
|
||||
autocomplete?: string;
|
||||
multiple?: true;
|
||||
} | null;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { mdiLink, mdiMicrophone, mdiOpenInNew, mdiRoomService } from "@mdi/js";
|
||||
import type { NavigationPathInfo } from "../../../data/compute-navigation-path-info";
|
||||
import type { ActionConfig } from "../../../data/lovelace/config/action";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
const DEFAULT: NavigationPathInfo = { label: "", iconPath: mdiLink };
|
||||
|
||||
export const getShortcutCardDefaults = (
|
||||
hass: HomeAssistant,
|
||||
action: ActionConfig | undefined,
|
||||
navInfo: NavigationPathInfo
|
||||
): NavigationPathInfo => {
|
||||
switch (action?.action) {
|
||||
case "navigate":
|
||||
return navInfo;
|
||||
case "assist":
|
||||
return {
|
||||
label: hass.localize(
|
||||
"ui.panel.lovelace.editor.action-editor.actions.assist"
|
||||
),
|
||||
iconPath: mdiMicrophone,
|
||||
};
|
||||
case "call-service":
|
||||
case "perform-action":
|
||||
return {
|
||||
label: action.perform_action || "",
|
||||
iconPath: mdiRoomService,
|
||||
};
|
||||
case "url":
|
||||
return {
|
||||
label: action.url_path || "",
|
||||
iconPath: mdiOpenInNew,
|
||||
};
|
||||
default:
|
||||
return DEFAULT;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,207 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/tile/ha-tile-container";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import {
|
||||
DEFAULT_NAVIGATION_PATH_INFO,
|
||||
subscribeNavigationPathInfo,
|
||||
type NavigationPathInfo,
|
||||
} from "../../../data/compute-navigation-path-info";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import { getShortcutCardDefaults } from "./hui-shortcut-card-defaults";
|
||||
import { tileCardStyle } from "./tile/tile-card-style";
|
||||
import type { ShortcutCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-shortcut-card")
|
||||
export class HuiShortcutCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-shortcut-card-editor");
|
||||
return document.createElement("hui-shortcut-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): ShortcutCardConfig {
|
||||
return {
|
||||
type: "shortcut",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/home",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: ShortcutCardConfig;
|
||||
|
||||
private _navInfo: NavigationPathInfo = DEFAULT_NAVIGATION_PATH_INFO;
|
||||
|
||||
private _unsubNavInfo?: UnsubscribeFunc;
|
||||
|
||||
private _subscribedPath?: string;
|
||||
|
||||
public setConfig(config: ShortcutCardConfig): void {
|
||||
this._config = {
|
||||
tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._updateNavInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubNavInfo?.();
|
||||
this._unsubNavInfo = undefined;
|
||||
this._subscribedPath = undefined;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return this._config?.vertical ? 2 : 1;
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
if (this._config?.vertical) {
|
||||
return {
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
min_columns: 3,
|
||||
min_rows: 2,
|
||||
};
|
||||
}
|
||||
return {
|
||||
columns: 6,
|
||||
rows: 1,
|
||||
min_columns: 6,
|
||||
min_rows: 1,
|
||||
};
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("hass") || changedProps.has("_config")) {
|
||||
this._updateNavInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateNavInfo(): void {
|
||||
if (!this.hass) return;
|
||||
|
||||
const action = this._config?.tap_action;
|
||||
const navPath =
|
||||
action?.action === "navigate" ? action.navigation_path : undefined;
|
||||
|
||||
if (navPath === this._subscribedPath) return;
|
||||
|
||||
this._unsubNavInfo?.();
|
||||
this._unsubNavInfo = undefined;
|
||||
this._subscribedPath = navPath;
|
||||
|
||||
if (!navPath) {
|
||||
this._navInfo = DEFAULT_NAVIGATION_PATH_INFO;
|
||||
return;
|
||||
}
|
||||
|
||||
this._unsubNavInfo = subscribeNavigationPathInfo(
|
||||
this.hass,
|
||||
navPath,
|
||||
(info) => {
|
||||
this._navInfo = info;
|
||||
this.requestUpdate();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private get _hasCardAction() {
|
||||
return (
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const defaults = getShortcutCardDefaults(
|
||||
this.hass,
|
||||
this._config.tap_action,
|
||||
this._navInfo
|
||||
);
|
||||
const label = this._config.label || defaults.label;
|
||||
const description = this._config.description;
|
||||
const icon = this._config.icon || defaults.icon;
|
||||
const iconPath = icon ? undefined : defaults.iconPath;
|
||||
|
||||
const color = this._config.color
|
||||
? computeCssColor(this._config.color)
|
||||
: undefined;
|
||||
|
||||
const style = color ? { "--tile-color": color } : {};
|
||||
|
||||
return html`
|
||||
<ha-card style=${styleMap(style)}>
|
||||
<ha-tile-container
|
||||
.vertical=${Boolean(this._config.vertical)}
|
||||
.interactive=${this._hasCardAction}
|
||||
.actionHandlerOptions=${{
|
||||
hasHold: hasAction(this._config.hold_action),
|
||||
hasDoubleClick: hasAction(this._config.double_tap_action),
|
||||
}}
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-tile-icon
|
||||
slot="icon"
|
||||
.icon=${icon || undefined}
|
||||
.iconPath=${iconPath}
|
||||
></ha-tile-icon>
|
||||
<ha-tile-info slot="info">
|
||||
<span slot="primary">${label}</span>
|
||||
${description
|
||||
? html`<span slot="secondary">${description}</span>`
|
||||
: nothing}
|
||||
</ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
tileCardStyle,
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-shortcut-card": HuiShortcutCard;
|
||||
}
|
||||
}
|
||||
@@ -674,6 +674,17 @@ export interface HomeSummaryCard extends LovelaceCardConfig {
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface ShortcutCardConfig extends LovelaceCardConfig {
|
||||
label?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
vertical?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface ToggleGroupCardConfig extends LovelaceCardConfig {
|
||||
title: string;
|
||||
entities: string[];
|
||||
|
||||
@@ -87,6 +87,8 @@ export class HuiActionEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public context?: ActionRelatedContext;
|
||||
|
||||
@@ -139,12 +141,25 @@ export class HuiActionEditor extends LitElement {
|
||||
|
||||
const actions = this.actions ?? DEFAULT_ACTIONS;
|
||||
|
||||
let action = this.config?.action || "default";
|
||||
let action = this.config?.action || (this.required ? "" : "default");
|
||||
|
||||
if (action === "call-service") {
|
||||
action = "perform-action";
|
||||
}
|
||||
|
||||
const defaultOption = {
|
||||
value: "default",
|
||||
label: `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.action-editor.actions.default_action"
|
||||
)} ${
|
||||
this.defaultAction
|
||||
? ` (${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.action-editor.actions.${this.defaultAction}`
|
||||
).toLowerCase()})`
|
||||
: ""
|
||||
}`,
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="dropdown">
|
||||
<ha-select
|
||||
@@ -153,19 +168,7 @@ export class HuiActionEditor extends LitElement {
|
||||
@selected=${this._actionPicked}
|
||||
.value=${action}
|
||||
.options=${[
|
||||
{
|
||||
value: "default",
|
||||
label: `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.action-editor.actions.default_action"
|
||||
)}
|
||||
${
|
||||
this.defaultAction
|
||||
? ` (${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.action-editor.actions.${this.defaultAction}`
|
||||
).toLowerCase()})`
|
||||
: ""
|
||||
}`,
|
||||
},
|
||||
...(this.required ? [] : [defaultOption]),
|
||||
...actions.map((actn) => ({
|
||||
value: actn,
|
||||
label: this.hass!.localize(
|
||||
@@ -251,7 +254,7 @@ export class HuiActionEditor extends LitElement {
|
||||
if (action === value) {
|
||||
return;
|
||||
}
|
||||
if (value === "default") {
|
||||
if (value === "default" || !value) {
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ const LAZY_LOAD_TYPES = {
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||
shortcut: () => import("../cards/hui-shortcut-card"),
|
||||
"discovered-devices": () => import("../cards/hui-discovered-devices-card"),
|
||||
repairs: () => import("../cards/hui-repairs-card"),
|
||||
updates: () => import("../cards/hui-updates-card"),
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import { mdiGestureTap, mdiTextShort } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import {
|
||||
DEFAULT_NAVIGATION_PATH_INFO,
|
||||
subscribeNavigationPathInfo,
|
||||
type NavigationPathInfo,
|
||||
} from "../../../../data/compute-navigation-path-info";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getShortcutCardDefaults } from "../../cards/hui-shortcut-card-defaults";
|
||||
import type { ShortcutCardConfig } from "../../cards/types";
|
||||
import type { UiAction } from "../../components/hui-action-editor";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
const actions: UiAction[] = [
|
||||
"navigate",
|
||||
"url",
|
||||
"assist",
|
||||
"perform-action",
|
||||
"none",
|
||||
];
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
label: optional(string()),
|
||||
description: optional(string()),
|
||||
icon: optional(string()),
|
||||
color: optional(string()),
|
||||
vertical: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
})
|
||||
);
|
||||
|
||||
@customElement("hui-shortcut-card-editor")
|
||||
export class HuiShortcutCardEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: ShortcutCardConfig;
|
||||
|
||||
private _navInfo: NavigationPathInfo = DEFAULT_NAVIGATION_PATH_INFO;
|
||||
|
||||
private _unsubNavInfo?: UnsubscribeFunc;
|
||||
|
||||
private _subscribedPath?: string;
|
||||
|
||||
public setConfig(config: ShortcutCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._updateNavInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubNavInfo?.();
|
||||
this._unsubNavInfo = undefined;
|
||||
this._subscribedPath = undefined;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("hass") || changedProps.has("_config")) {
|
||||
this._updateNavInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateNavInfo(): void {
|
||||
if (!this.hass) return;
|
||||
|
||||
const action = this._config?.tap_action;
|
||||
const navPath =
|
||||
action?.action === "navigate" ? action.navigation_path : undefined;
|
||||
|
||||
if (navPath === this._subscribedPath) return;
|
||||
|
||||
this._unsubNavInfo?.();
|
||||
this._unsubNavInfo = undefined;
|
||||
this._subscribedPath = navPath;
|
||||
|
||||
if (!navPath) {
|
||||
this._navInfo = DEFAULT_NAVIGATION_PATH_INFO;
|
||||
return;
|
||||
}
|
||||
|
||||
this._unsubNavInfo = subscribeNavigationPathInfo(
|
||||
this.hass,
|
||||
navPath,
|
||||
(info) => {
|
||||
this._navInfo = info;
|
||||
this.requestUpdate();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, label: string, icon?: string) =>
|
||||
[
|
||||
{
|
||||
name: "tap_action",
|
||||
required: true,
|
||||
selector: {
|
||||
ui_action: {
|
||||
actions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
flatten: true,
|
||||
type: "expandable",
|
||||
expanded: true,
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "label",
|
||||
selector: {
|
||||
text: { placeholder: label },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
selector: { text: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {
|
||||
placeholder: icon || "mdi:link",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: { default_color: "primary" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: ["horizontal", "vertical"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
|
||||
),
|
||||
value,
|
||||
image: {
|
||||
src: `/static/images/form/tile_content_layout_${value}.svg`,
|
||||
src_dark: `/static/images/form/tile_content_layout_${value}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "additional_interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "hold_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
actions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "double_tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
actions,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const defaults = getShortcutCardDefaults(
|
||||
this.hass,
|
||||
this._config.tap_action,
|
||||
this._navInfo
|
||||
);
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
content_layout: this._config.vertical ? "vertical" : "horizontal",
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
defaults.label,
|
||||
defaults.icon
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { ...ev.detail.value } as ShortcutCardConfig & {
|
||||
content_layout?: string;
|
||||
};
|
||||
|
||||
if (config.content_layout) {
|
||||
config.vertical = config.content_layout === "vertical";
|
||||
delete config.content_layout;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) => {
|
||||
switch (schema.name) {
|
||||
case "label":
|
||||
case "content_layout":
|
||||
case "additional_interactions":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.shortcut.${schema.name}`
|
||||
);
|
||||
case "description":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.shortcut.card_description"
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static styles = configElementStyle;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-shortcut-card-editor": HuiShortcutCardEditor;
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,10 @@ export const coreCards: Card[] = [
|
||||
type: "heading",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "shortcut",
|
||||
showElement: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const energyCards: Card[] = [
|
||||
|
||||
@@ -9415,6 +9415,14 @@
|
||||
},
|
||||
"default_heading": "Kitchen"
|
||||
},
|
||||
"shortcut": {
|
||||
"name": "Shortcut",
|
||||
"description": "A tile that triggers an action like navigating to a page, opening a URL, or calling a service.",
|
||||
"label": "Label",
|
||||
"card_description": "Description",
|
||||
"content_layout": "[%key:ui::panel::lovelace::editor::card::tile::content_layout%]",
|
||||
"additional_interactions": "Additional interactions"
|
||||
},
|
||||
"map": {
|
||||
"name": "Map",
|
||||
"geo_location_sources": "Geolocation sources",
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { mdiDevices, mdiLink, mdiTextureBox } from "@mdi/js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeNavigationPathInfo } from "../../src/data/compute-navigation-path-info";
|
||||
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;
|
||||
|
||||
describe("computeNavigationPathInfo", () => {
|
||||
describe("panel paths", () => {
|
||||
it("resolves a panel with icon", () => {
|
||||
const hass = createHass({
|
||||
panels: {
|
||||
"my-panel": {
|
||||
url_path: "my-panel",
|
||||
title: "My Panel",
|
||||
icon: "mdi:star",
|
||||
component_name: "custom",
|
||||
},
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(hass, "/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");
|
||||
expect(result.label).toBe("unknown");
|
||||
expect(result.icon).toBeUndefined();
|
||||
expect(result.iconPath).toBe(mdiLink);
|
||||
});
|
||||
});
|
||||
|
||||
describe("area paths", () => {
|
||||
it("resolves /config/areas/area/{areaId}", () => {
|
||||
const hass = createHass({
|
||||
areas: {
|
||||
living_room: {
|
||||
area_id: "living_room",
|
||||
name: "Living Room",
|
||||
icon: "mdi:sofa",
|
||||
},
|
||||
} as unknown as HomeAssistant["areas"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/config/areas/area/living_room"
|
||||
);
|
||||
expect(result.label).toBe("Living Room");
|
||||
expect(result.icon).toBe("mdi:sofa");
|
||||
expect(result.iconPath).toBe(mdiTextureBox);
|
||||
});
|
||||
|
||||
it("resolves /home/areas-{areaId}", () => {
|
||||
const hass = createHass({
|
||||
areas: {
|
||||
kitchen: {
|
||||
area_id: "kitchen",
|
||||
name: "Kitchen",
|
||||
icon: null,
|
||||
},
|
||||
} as unknown as HomeAssistant["areas"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(hass, "/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 result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/config/areas/area/unknown_area"
|
||||
);
|
||||
expect(result.label).toBe("unknown_area");
|
||||
expect(result.iconPath).toBe(mdiTextureBox);
|
||||
});
|
||||
});
|
||||
|
||||
describe("device paths", () => {
|
||||
it("resolves /config/devices/device/{deviceId}", () => {
|
||||
const hass = createHass({
|
||||
devices: {
|
||||
abc123: {
|
||||
id: "abc123",
|
||||
name: "Smart Light",
|
||||
name_by_user: null,
|
||||
},
|
||||
} as unknown as HomeAssistant["devices"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/config/devices/device/abc123"
|
||||
);
|
||||
expect(result.label).toBe("Smart Light");
|
||||
expect(result.iconPath).toBe(mdiDevices);
|
||||
});
|
||||
|
||||
it("prefers user-defined device name", () => {
|
||||
const hass = createHass({
|
||||
devices: {
|
||||
abc123: {
|
||||
id: "abc123",
|
||||
name: "Smart Light",
|
||||
name_by_user: "My Light",
|
||||
},
|
||||
} as unknown as HomeAssistant["devices"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/config/devices/device/abc123"
|
||||
);
|
||||
expect(result.label).toBe("My Light");
|
||||
});
|
||||
|
||||
it("falls back to device id for unknown device", () => {
|
||||
const hass = createHass();
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/config/devices/device/unknown_device"
|
||||
);
|
||||
expect(result.label).toBe("unknown_device");
|
||||
expect(result.iconPath).toBe(mdiDevices);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lovelace view paths", () => {
|
||||
const lovelaceConfig: LovelaceConfig = {
|
||||
views: [
|
||||
{ title: "Overview", path: "overview", icon: "mdi:home" },
|
||||
{ title: "Lights", path: "lights" },
|
||||
{ path: "my-view" },
|
||||
{},
|
||||
],
|
||||
};
|
||||
|
||||
it("resolves view with title and icon", () => {
|
||||
const hass = createHass({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
title: "Dashboard",
|
||||
component_name: "lovelace",
|
||||
},
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/lovelace/overview",
|
||||
lovelaceConfig
|
||||
);
|
||||
expect(result.label).toBe("Overview");
|
||||
expect(result.icon).toBe("mdi:home");
|
||||
});
|
||||
|
||||
it("resolves view without icon using default", () => {
|
||||
const hass = createHass({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
title: "Dashboard",
|
||||
component_name: "lovelace",
|
||||
},
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/lovelace/lights",
|
||||
lovelaceConfig
|
||||
);
|
||||
expect(result.label).toBe("Lights");
|
||||
expect(result.icon).toBe("mdi:view-compact");
|
||||
});
|
||||
|
||||
it("uses titleCase of path when view has no title", () => {
|
||||
const hass = createHass({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
title: "Dashboard",
|
||||
component_name: "lovelace",
|
||||
},
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/lovelace/my-view",
|
||||
lovelaceConfig
|
||||
);
|
||||
expect(result.label).toBe("My-view");
|
||||
});
|
||||
|
||||
it("uses index as name when view has no title or path", () => {
|
||||
const hass = createHass({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
title: "Dashboard",
|
||||
component_name: "lovelace",
|
||||
},
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/lovelace/3",
|
||||
lovelaceConfig
|
||||
);
|
||||
expect(result.label).toBe("3");
|
||||
});
|
||||
|
||||
it("falls back to panel info when view not found", () => {
|
||||
const hass = createHass({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
title: "Dashboard",
|
||||
component_name: "lovelace",
|
||||
},
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(
|
||||
hass,
|
||||
"/lovelace/nonexistent",
|
||||
lovelaceConfig
|
||||
);
|
||||
expect(result.label).toBe("Dashboard");
|
||||
});
|
||||
|
||||
it("falls back to panel info when no lovelace config provided", () => {
|
||||
const hass = createHass({
|
||||
panels: {
|
||||
lovelace: {
|
||||
url_path: "lovelace",
|
||||
title: "Dashboard",
|
||||
component_name: "lovelace",
|
||||
},
|
||||
} as unknown as HomeAssistant["panels"],
|
||||
});
|
||||
|
||||
const result = computeNavigationPathInfo(hass, "/lovelace/overview");
|
||||
expect(result.label).toBe("Dashboard");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user