1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00

Use entity naming in more cards and badges (#27541)

* Add support for button card, glance card and entities card

* Add tests

* Add support for attribute and button row

* Add support to heading badge

* Undo changes from rows

* Add comment
This commit is contained in:
Paul Bottein
2025-10-19 14:08:28 +02:00
committed by GitHub
parent ef0da0a7ee
commit 8a42d15bde
10 changed files with 141 additions and 26 deletions

View File

@@ -20,6 +20,7 @@ import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-input-helper-text";
import "../ha-sortable";
interface EntityNameOption {
@@ -239,7 +240,6 @@ export class HaEntityNamePicker extends LitElement {
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.helper=${this.helper}
.items=${options}
allow-custom-value
item-id-path="value"
@@ -253,9 +253,20 @@ export class HaEntityNamePicker extends LitElement {
</ha-combo-box>
</mwc-menu-surface>
</div>
${this._renderHelper()}
`;
}
private _renderHelper() {
return this.helper
? html`
<ha-input-helper-text .disabled=${this.disabled}>
${this.helper}
</ha-input-helper-text>
`
: nothing;
}
private _onClosed(ev) {
ev.stopPropagation();
this._opened = false;
@@ -510,6 +521,11 @@ export class HaEntityNamePicker extends LitElement {
.sortable-drag {
cursor: grabbing;
}
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
}
`;
}

View File

@@ -179,7 +179,7 @@ export class HaGenericPicker extends LitElement {
}
ha-input-helper-text {
display: block;
margin: 8px 0 0;
margin: var(--ha-space-2) 0 0;
}
`,
];

View File

@@ -9,13 +9,14 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { transform } from "../../../common/decorators/transform";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import {
stateColorBrightness,
stateColorCss,
@@ -40,6 +41,7 @@ import type { FrontendLocaleData } from "../../../data/translation";
import type { Themes } from "../../../data/ws-themes";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { hasAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -49,8 +51,6 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { ButtonCardConfig } from "./types";
import { computeCssColor } from "../../../common/color/compute-color";
import { stateActive } from "../../../common/entity/state_active";
export const getEntityDefaultButtonAction = (entityId?: string) =>
entityId && DOMAINS_TOGGLE.has(computeDomain(entityId))
@@ -183,9 +183,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
`;
}
const name = this._config.show_name
? this._config.name || (stateObj ? computeStateName(stateObj) : "")
: "";
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
return html`
<ha-card
@@ -195,8 +197,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
role="button"
aria-label=${this._config.name ||
(stateObj ? computeStateName(stateObj) : "")}
aria-label=${name}
tabindex=${ifDefined(
hasAction(this._config.tap_action) ? "0" : undefined
)}

View File

@@ -272,7 +272,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${name}
>
</state-display>
`;

View File

@@ -4,6 +4,7 @@ import {
type EntityNameItem,
} from "../../../../common/entity/compute_entity_name_display";
import type { HomeAssistant } from "../../../../types";
import { ensureArray } from "../../../../common/array/ensure-array";
/**
* Computes the display name for an entity in Lovelace (cards and badges).
@@ -15,9 +16,24 @@ import type { HomeAssistant } from "../../../../types";
*/
export const computeLovelaceEntityName = (
hass: HomeAssistant,
stateObj: HassEntity,
stateObj: HassEntity | undefined,
nameConfig: string | EntityNameItem | EntityNameItem[] | undefined
): string =>
typeof nameConfig === "string"
? nameConfig
: hass.formatEntityName(stateObj, nameConfig || DEFAULT_ENTITY_NAME);
): string => {
if (typeof nameConfig === "string") {
return nameConfig;
}
const config = nameConfig || DEFAULT_ENTITY_NAME;
if (stateObj) {
return hass.formatEntityName(stateObj, config);
}
// If entity is not found, fall back to text parts in config
// This allows for static names even when the entity is missing
// e.g. for a card that doesn't require an entity
const textParts = ensureArray(config)
.filter((item) => item.type === "text")
.map((item) => ("text" in item ? item.text : ""));
if (textParts.length) {
return textParts.join(" ");
}
return "";
};

View File

@@ -5,6 +5,7 @@ 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 { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -16,13 +17,14 @@ import type { ButtonCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
show_name: optional(boolean()),
icon: optional(string()),
show_icon: optional(boolean()),
@@ -68,7 +70,13 @@ export class HuiButtonCardEditor
(entityId: string | undefined) =>
[
{ name: "entity", selector: { entity: {} } },
{ name: "name", selector: { text: {} } },
{
name: "name",
selector: {
entity_name: { default_name: DEFAULT_ENTITY_NAME },
},
context: { entity: "entity" },
},
{
name: "",
type: "grid",

View File

@@ -13,6 +13,7 @@ import {
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
@@ -27,6 +28,7 @@ import type { LovelaceGenericElementEditor } from "../../types";
import "../conditions/ha-card-conditions-editor";
import { configElementStyle } from "../config-elements/config-elements-style";
import { actionConfigStruct } from "../structs/action-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
type: "entity",
@@ -37,7 +39,7 @@ export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
const entityConfigStruct = object({
type: optional(string()),
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
icon: optional(string()),
state_content: optional(union([string(), array(string())])),
show_state: optional(boolean()),
@@ -92,8 +94,11 @@ export class HuiHeadingEntityEditor
{
name: "name",
selector: {
text: {},
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
name: "icon",

View File

@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-heading-badge";
@@ -16,6 +15,7 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
@@ -137,7 +137,11 @@ export class HuiEntityHeadingBadge
"--icon-color": color,
};
const name = config.name || computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
return html`
<ha-heading-badge
@@ -166,7 +170,7 @@ export class HuiEntityHeadingBadge
.hass=${this.hass}
.stateObj=${stateObj}
.content=${config.state_content}
.name=${config.name}
.name=${name}
dash-unavailable
></state-display>
`

View File

@@ -5,7 +5,6 @@ import { customElement, property } from "lit/decorators";
import { join } from "lit/directives/join";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import "../components/ha-relative-time";
import { isUnavailableState } from "../data/entity";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor";
@@ -100,8 +99,8 @@ class StateDisplay extends LitElement {
return this.hass!.formatEntityState(stateObj);
}
if (content === "name") {
return html`${this.name || computeStateName(stateObj)}`;
if (content === "name" && this.name) {
return html`${this.name}`;
}
let relativeDateTime: string | Date | undefined;

View File

@@ -77,4 +77,71 @@ describe("computeLovelaceEntityName", () => {
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig);
});
describe("when stateObj is undefined", () => {
it("returns empty string when nameConfig is undefined", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const result = computeLovelaceEntityName(hass, undefined, undefined);
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns text from single text EntityNameItem", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = { type: "text" as const, text: "Custom Text" };
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("Custom Text");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns joined text from multiple text EntityNameItems", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "text" as const, text: "First" },
{ type: "text" as const, text: "Second" },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("First Second");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns only text items when mixed with non-text items", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "text" as const, text: "Prefix" },
{ type: "device" as const },
{ type: "text" as const, text: "Suffix" },
{ type: "entity" as const },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("Prefix Suffix");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns empty string when no text items in config", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "device" as const },
{ type: "entity" as const },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
});
});