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:
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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 "";
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user