diff --git a/src/panels/lovelace/cards/heading/hui-heading-entity.ts b/src/panels/lovelace/cards/heading/hui-heading-entity.ts
new file mode 100644
index 000000000000..f18e0c348163
--- /dev/null
+++ b/src/panels/lovelace/cards/heading/hui-heading-entity.ts
@@ -0,0 +1,113 @@
+import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
+import { customElement, property } from "lit/decorators";
+import { ifDefined } from "lit/directives/if-defined";
+import memoizeOne from "memoize-one";
+import "../../../../components/ha-card";
+import "../../../../components/ha-icon";
+import "../../../../components/ha-icon-next";
+import "../../../../components/ha-state-icon";
+import { ActionHandlerEvent } from "../../../../data/lovelace/action_handler";
+import "../../../../state-display/state-display";
+import { HomeAssistant } from "../../../../types";
+import { actionHandler } from "../../common/directives/action-handler-directive";
+import { handleAction } from "../../common/handle-action";
+import { hasAction } from "../../common/has-action";
+import type { HeadingEntityConfig } from "../types";
+
+@customElement("hui-heading-entity")
+export class HuiHeadingEntity extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public config!: HeadingEntityConfig | string;
+
+ private _handleAction(ev: ActionHandlerEvent) {
+ const config: HeadingEntityConfig = {
+ tap_action: {
+ action: "none",
+ },
+ ...this._config(this.config),
+ };
+ handleAction(this, this.hass!, config, ev.detail.action!);
+ }
+
+ private _config = memoizeOne(
+ (configOrString: HeadingEntityConfig | string): HeadingEntityConfig => {
+ const config =
+ typeof configOrString === "string"
+ ? { entity: configOrString }
+ : configOrString;
+
+ return {
+ tap_action: {
+ action: "none",
+ },
+ ...config,
+ };
+ }
+ );
+
+ protected render() {
+ const config = this._config(this.config);
+
+ const stateObj = this.hass!.states[config.entity];
+
+ if (!stateObj) {
+ return nothing;
+ }
+
+ const actionable = hasAction(config.tap_action);
+
+ return html`
+
+
+
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ [role="button"] {
+ cursor: pointer;
+ }
+ .entity {
+ display: flex;
+ flex-direction: row;
+ white-space: nowrap;
+ align-items: center;
+ gap: 3px;
+ color: var(--secondary-text-color);
+ font-family: Roboto;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: 0.1px;
+ --mdc-icon-size: 14px;
+ }
+ .entity ha-state-icon {
+ --ha-icon-display: block;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-heading-entity": HuiHeadingEntity;
+ }
+}
diff --git a/src/panels/lovelace/cards/hui-heading-card.ts b/src/panels/lovelace/cards/hui-heading-card.ts
index af97daf3905d..4e3b569d98ec 100644
--- a/src/panels/lovelace/cards/hui-heading-card.ts
+++ b/src/panels/lovelace/cards/hui-heading-card.ts
@@ -16,7 +16,8 @@ import type {
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
-import type { HeadingCardConfig, HeadingCardEntityConfig } from "./types";
+import "./heading/hui-heading-entity";
+import type { HeadingCardConfig } from "./types";
@customElement("hui-heading-card")
export class HuiHeadingCard extends LitElement implements LovelaceCard {
@@ -91,8 +92,11 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
${this._config.entities?.length
? html`
- ${this._config.entities.map((config) =>
- this._renderEntity(config)
+ ${this._config.entities.map(
+ (config) => html`
+
+
+ `
)}
`
@@ -102,54 +106,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
`;
}
- private _handleEntityAction(ev: ActionHandlerEvent) {
- const config = {
- tap_action: {
- action: "none",
- },
- ...(ev.currentTarget as any).config,
- };
-
- handleAction(this, this.hass!, config, ev.detail.action!);
- }
-
- _renderEntity(entityConfig: string | HeadingCardEntityConfig) {
- const config =
- typeof entityConfig === "string"
- ? { entity: entityConfig }
- : entityConfig;
-
- const stateObj = this.hass!.states[config.entity];
-
- if (!stateObj) {
- return nothing;
- }
-
- const actionable = hasAction(config.tap_action || { action: "none" });
-
- return html`
-
-
-
-
- `;
- }
-
static get styles(): CSSResultGroup {
return css`
ha-card {
@@ -231,24 +187,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
justify-content: flex-end;
gap: 4px 10px;
}
- .entities .entity {
- display: flex;
- flex-direction: row;
- white-space: nowrap;
- align-items: center;
- gap: 3px;
- color: var(--secondary-text-color);
- font-family: Roboto;
- font-size: 14px;
- font-style: normal;
- font-weight: 500;
- line-height: 20px; /* 142.857% */
- letter-spacing: 0.1px;
- --mdc-icon-size: 14px;
- }
- .entities .entity ha-state-icon {
- --ha-icon-display: block;
- }
`;
}
}
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index a957bef817ed..a6e41ffd9844 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -503,7 +503,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
features?: LovelaceCardFeatureConfig[];
}
-export interface HeadingCardEntityConfig {
+export interface HeadingEntityConfig {
entity: string;
content?: string | string[];
icon?: string;
@@ -515,5 +515,5 @@ export interface HeadingCardConfig extends LovelaceCardConfig {
heading?: string;
icon?: string;
tap_action?: ActionConfig;
- entities?: (string | HeadingCardEntityConfig)[];
+ entities?: (string | HeadingEntityConfig)[];
}
diff --git a/src/panels/lovelace/editor/badge-editor/hui-badge-element-editor.ts b/src/panels/lovelace/editor/badge-editor/hui-badge-element-editor.ts
index 1fdeddb564d9..0fbe0d0e1450 100644
--- a/src/panels/lovelace/editor/badge-editor/hui-badge-element-editor.ts
+++ b/src/panels/lovelace/editor/badge-editor/hui-badge-element-editor.ts
@@ -5,13 +5,13 @@ import { customElement, state } from "lit/decorators";
import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import { getBadgeElementClass } from "../../create-element/create-badge-element";
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
-import { HuiElementEditor } from "../hui-element-editor";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
import "./hui-badge-visibility-editor";
const tabs = ["config", "visibility"] as const;
@customElement("hui-badge-element-editor")
-export class HuiBadgeElementEditor extends HuiElementEditor {
+export class HuiBadgeElementEditor extends HuiTypedElementEditor {
@state() private _currTab: (typeof tabs)[number] = tabs[0];
protected async getConfigElement(): Promise {
@@ -88,7 +88,7 @@ export class HuiBadgeElementEditor extends HuiElementEditor
static get styles(): CSSResultGroup {
return [
- HuiElementEditor.styles,
+ HuiTypedElementEditor.styles,
css`
mwc-tab-bar {
text-transform: uppercase;
diff --git a/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts
index 8e605b12d400..51804864afb0 100644
--- a/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts
@@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { getCardElementClass } from "../../create-element/create-card-element";
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
-import { HuiElementEditor } from "../hui-element-editor";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
import "./hui-card-layout-editor";
import "./hui-card-visibility-editor";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
@@ -13,7 +13,7 @@ import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"
const tabs = ["config", "visibility", "layout"] as const;
@customElement("hui-card-element-editor")
-export class HuiCardElementEditor extends HuiElementEditor {
+export class HuiCardElementEditor extends HuiTypedElementEditor {
@property({ type: Boolean, attribute: "show-visibility-tab" })
public showVisibilityTab = false;
@@ -119,7 +119,7 @@ export class HuiCardElementEditor extends HuiElementEditor {
static get styles(): CSSResultGroup {
return [
- HuiElementEditor.styles,
+ HuiTypedElementEditor.styles,
css`
mwc-tab-bar {
text-transform: uppercase;
diff --git a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
index 5c32d1ddb413..fbc664dd5735 100644
--- a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts
@@ -8,11 +8,10 @@ import {
array,
assert,
assign,
- literal,
+ enums,
object,
optional,
string,
- union,
} from "superstruct";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../common/translations/localize";
@@ -24,17 +23,14 @@ import type {
} from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
-import type {
- HeadingCardConfig,
- HeadingCardEntityConfig,
-} from "../../cards/types";
+import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
import { UiAction } from "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types";
-import "../hui-sub-form-editor";
+import "../hui-sub-element-editor";
import { processEditorEntities } from "../process-editor-entities";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
-import { SubFormEditorData } from "../types";
+import { SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-entities-editor";
@@ -43,7 +39,7 @@ const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
- heading_style: optional(union([literal("title"), literal("subtitle")])),
+ heading_style: optional(enums(["title", "subtitle"])),
heading: optional(string()),
icon: optional(string()),
tap_action: optional(actionConfigStruct),
@@ -51,13 +47,6 @@ const cardConfigStruct = assign(
})
);
-const entityConfigStruct = object({
- entity: string(),
- content: optional(union([string(), array(string())])),
- icon: optional(string()),
- tap_action: optional(actionConfigStruct),
-});
-
@customElement("hui-heading-card-editor")
export class HuiHeadingCardEditor
extends LitElement
@@ -68,17 +57,13 @@ export class HuiHeadingCardEditor
@state() private _config?: HeadingCardConfig;
@state()
- private _entityFormEditorData?: SubFormEditorData;
+ private _subElementEditorConfig?: SubElementEditorConfig;
public setConfig(config: HeadingCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
- public _assertEntityConfig(config: HeadingCardEntityConfig): void {
- assert(config, entityConfigStruct);
- }
-
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
@@ -123,68 +108,27 @@ export class HuiHeadingCardEditor
] as const satisfies readonly HaFormSchema[]
);
- private _entitySchema = memoizeOne(
- () =>
- [
- {
- name: "entity",
- selector: { entity: {} },
- },
- {
- name: "icon",
- selector: { icon: {} },
- context: { icon_entity: "entity" },
- },
- {
- name: "content",
- selector: { ui_state_content: {} },
- context: { filter_entity: "entity" },
- },
- {
- name: "interactions",
- type: "expandable",
- flatten: true,
- iconPath: mdiGestureTap,
- schema: [
- {
- name: "tap_action",
- selector: {
- ui_action: {
- default_action: "none",
- },
- },
- },
- ],
- },
- ] as const satisfies readonly HaFormSchema[]
- );
-
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return cache(
- this._entityFormEditorData ? this._renderEntityForm() : this._renderForm()
+ this._subElementEditorConfig
+ ? this._renderEntityForm()
+ : this._renderForm()
);
}
private _renderEntityForm() {
- const schema = this._entitySchema();
return html`
-
-
+
`;
}
@@ -239,7 +183,7 @@ export class HuiHeadingCardEditor
const config = {
...this._config,
- entities: ev.detail.entities as HeadingCardEntityConfig[],
+ entities: ev.detail.entities as HeadingEntityConfig[],
};
fireEvent(this, "config-changed", { config });
@@ -256,30 +200,30 @@ export class HuiHeadingCardEditor
fireEvent(this, "config-changed", { config });
}
- private _subFormChanged(ev: CustomEvent): void {
+ private _subElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
- const value = ev.detail.value;
+ const value = ev.detail.config;
- const newEntities = this._config!.entities
+ const newConfigEntities = this._config!.entities
? [...this._config!.entities]
: [];
if (!value) {
- newEntities.splice(this._entityFormEditorData!.index!, 1);
+ newConfigEntities.splice(this._subElementEditorConfig!.index!, 1);
this._goBack();
} else {
- newEntities[this._entityFormEditorData!.index!] = value;
+ newConfigEntities[this._subElementEditorConfig!.index!] = value;
}
- this._config = { ...this._config!, entities: newEntities };
+ this._config = { ...this._config!, entities: newConfigEntities };
- this._entityFormEditorData = {
- ...this._entityFormEditorData!,
- data: value,
+ this._subElementEditorConfig = {
+ ...this._subElementEditorConfig!,
+ elementConfig: value,
};
fireEvent(this, "config-changed", { config: this._config });
@@ -287,31 +231,17 @@ export class HuiHeadingCardEditor
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
const entities = this._entities(this._config!.entities);
- this._entityFormEditorData = {
- data: entities[ev.detail.index],
+ this._subElementEditorConfig = {
+ elementConfig: entities[ev.detail.index],
index: ev.detail.index,
+ type: "heading-entity",
};
}
private _goBack(): void {
- this._entityFormEditorData = undefined;
+ this._subElementEditorConfig = undefined;
}
- private _computeEntityLabelCallback = (
- schema: SchemaUnion>
- ) => {
- switch (schema.name) {
- case "content":
- return this.hass!.localize(
- `ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}`
- );
- default:
- return this.hass!.localize(
- `ui.panel.lovelace.editor.card.generic.${schema.name}`
- );
- }
- };
-
private _computeLabelCallback = (
schema: SchemaUnion>
) => {
diff --git a/src/panels/lovelace/editor/dashboard-strategy-editor/hui-dashboard-strategy-element-editor.ts b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-dashboard-strategy-element-editor.ts
index 69984ec1096d..b90eae0ade77 100644
--- a/src/panels/lovelace/editor/dashboard-strategy-editor/hui-dashboard-strategy-element-editor.ts
+++ b/src/panels/lovelace/editor/dashboard-strategy-editor/hui-dashboard-strategy-element-editor.ts
@@ -2,10 +2,10 @@ import { customElement } from "lit/decorators";
import { LovelaceDashboardStrategyConfig } from "../../../../data/lovelace/config/types";
import { getLovelaceStrategy } from "../../strategies/get-strategy";
import { LovelaceStrategyEditor } from "../../strategies/types";
-import { HuiElementEditor } from "../hui-element-editor";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
@customElement("hui-dashboard-strategy-element-editor")
-export class HuiDashboardStrategyElementEditor extends HuiElementEditor {
+export class HuiDashboardStrategyElementEditor extends HuiTypedElementEditor {
protected async getConfigElement(): Promise<
LovelaceStrategyEditor | undefined
> {
diff --git a/src/panels/lovelace/editor/entity-row-editor/hui-row-element-editor.ts b/src/panels/lovelace/editor/entity-row-editor/hui-row-element-editor.ts
index 2471121ccdfc..262f1a444951 100644
--- a/src/panels/lovelace/editor/entity-row-editor/hui-row-element-editor.ts
+++ b/src/panels/lovelace/editor/entity-row-editor/hui-row-element-editor.ts
@@ -2,12 +2,12 @@ import { customElement } from "lit/decorators";
import { getRowElementClass } from "../../create-element/create-row-element";
import { LovelaceRowConfig } from "../../entity-rows/types";
import type { LovelaceRowEditor } from "../../types";
-import { HuiElementEditor } from "../hui-element-editor";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
const GENERIC_ROW_TYPE = "generic-row";
@customElement("hui-row-element-editor")
-export class HuiRowElementEditor extends HuiElementEditor {
+export class HuiRowElementEditor extends HuiTypedElementEditor {
protected get configElementType(): string | undefined {
if (!this.value?.type && "entity" in this.value!) {
return GENERIC_ROW_TYPE;
diff --git a/src/panels/lovelace/editor/feature-editor/hui-card-feature-element-editor.ts b/src/panels/lovelace/editor/feature-editor/hui-card-feature-element-editor.ts
index 019a72a87f0f..4602740ee304 100644
--- a/src/panels/lovelace/editor/feature-editor/hui-card-feature-element-editor.ts
+++ b/src/panels/lovelace/editor/feature-editor/hui-card-feature-element-editor.ts
@@ -8,10 +8,10 @@ import type {
LovelaceConfigForm,
LovelaceCardFeatureEditor,
} from "../../types";
-import { HuiElementEditor } from "../hui-element-editor";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
@customElement("hui-card-feature-element-editor")
-export class HuiCardFeatureElementEditor extends HuiElementEditor<
+export class HuiCardFeatureElementEditor extends HuiTypedElementEditor<
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext
> {
diff --git a/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-element-editor.ts b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-element-editor.ts
index 07cd2ae0d897..6e1ee68eb600 100644
--- a/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-element-editor.ts
+++ b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-element-editor.ts
@@ -2,10 +2,10 @@ import { customElement } from "lit/decorators";
import { getHeaderFooterElementClass } from "../../create-element/create-header-footer-element";
import type { LovelaceHeaderFooterConfig } from "../../header-footer/types";
import type { LovelaceHeaderFooterEditor } from "../../types";
-import { HuiElementEditor } from "../hui-element-editor";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
@customElement("hui-headerfooter-element-editor")
-export class HuiHeaderFooterElementEditor extends HuiElementEditor {
+export class HuiHeaderFooterElementEditor extends HuiTypedElementEditor {
protected async getConfigElement(): Promise<
LovelaceHeaderFooterEditor | undefined
> {
diff --git a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts
new file mode 100644
index 000000000000..7d6f63a3c22a
--- /dev/null
+++ b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts
@@ -0,0 +1,140 @@
+import { mdiGestureTap } from "@mdi/js";
+import { css, html, LitElement, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import memoizeOne from "memoize-one";
+import { array, assert, object, optional, string, union } from "superstruct";
+import { fireEvent } from "../../../../common/dom/fire_event";
+import "../../../../components/ha-form/ha-form";
+import type {
+ HaFormSchema,
+ SchemaUnion,
+} from "../../../../components/ha-form/types";
+import type { HomeAssistant } from "../../../../types";
+import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
+import type { LovelaceGenericElementEditor } from "../../types";
+import { configElementStyle } from "../config-elements/config-elements-style";
+import { actionConfigStruct } from "../structs/action-struct";
+
+const entityConfigStruct = object({
+ entity: string(),
+ content: optional(union([string(), array(string())])),
+ icon: optional(string()),
+ tap_action: optional(actionConfigStruct),
+});
+
+@customElement("hui-heading-entity-editor")
+export class HuiHeadingEntityEditor
+ extends LitElement
+ implements LovelaceGenericElementEditor
+{
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @state() private _config?: HeadingEntityConfig;
+
+ public setConfig(config: HeadingEntityConfig): void {
+ assert(config, entityConfigStruct);
+ this._config = config;
+ }
+
+ private _schema = memoizeOne(
+ () =>
+ [
+ {
+ name: "entity",
+ selector: { entity: {} },
+ },
+ {
+ name: "icon",
+ selector: { icon: {} },
+ context: { icon_entity: "entity" },
+ },
+ {
+ name: "content",
+ selector: { ui_state_content: {} },
+ context: { filter_entity: "entity" },
+ },
+ {
+ name: "interactions",
+ type: "expandable",
+ flatten: true,
+ iconPath: mdiGestureTap,
+ schema: [
+ {
+ name: "tap_action",
+ selector: {
+ ui_action: {
+ default_action: "none",
+ },
+ },
+ },
+ ],
+ },
+ ] as const satisfies readonly HaFormSchema[]
+ );
+
+ protected render() {
+ if (!this.hass || !this._config) {
+ return nothing;
+ }
+
+ const schema = this._schema();
+
+ return html`
+
+ `;
+ }
+
+ private _valueChanged(ev: CustomEvent): void {
+ ev.stopPropagation();
+ if (!this._config || !this.hass) {
+ return;
+ }
+
+ const config = ev.detail.value as HeadingCardConfig;
+
+ fireEvent(this, "config-changed", { config });
+ }
+
+ private _computeLabelCallback = (
+ schema: SchemaUnion>
+ ) => {
+ switch (schema.name) {
+ case "content":
+ return this.hass!.localize(
+ `ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}`
+ );
+ default:
+ return this.hass!.localize(
+ `ui.panel.lovelace.editor.card.generic.${schema.name}`
+ );
+ }
+ };
+
+ static get styles() {
+ return [
+ configElementStyle,
+ css`
+ .container {
+ display: flex;
+ flex-direction: column;
+ }
+ ha-form {
+ display: block;
+ margin-bottom: 24px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-heading-entity-editor": HuiHeadingEntityEditor;
+ }
+}
diff --git a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts
new file mode 100644
index 000000000000..ada27a39d28b
--- /dev/null
+++ b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-element-editor.ts
@@ -0,0 +1,20 @@
+import { customElement } from "lit/decorators";
+import { HeadingEntityConfig } from "../../cards/types";
+import { HuiElementEditor } from "../hui-element-editor";
+import type { HuiHeadingEntityEditor } from "./hui-heading-entity-editor";
+
+@customElement("hui-heading-entity-element-editor")
+export class HuiHeadingEntityElementEditor extends HuiElementEditor {
+ protected async getConfigElement(): Promise<
+ HuiHeadingEntityEditor | undefined
+ > {
+ await import("./hui-heading-entity-editor");
+ return document.createElement("hui-heading-entity-editor");
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-heading-entity-element-editor": HuiHeadingEntityElementEditor;
+ }
+}
diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts
index b3be3fb3bd8d..af75a557b9e8 100644
--- a/src/panels/lovelace/editor/hui-element-editor.ts
+++ b/src/panels/lovelace/editor/hui-element-editor.ts
@@ -1,4 +1,3 @@
-import "@material/mwc-button";
import { dump, load } from "js-yaml";
import {
CSSResultGroup,
@@ -7,6 +6,7 @@ import {
TemplateResult,
css,
html,
+ nothing,
} from "lit";
import { property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -16,34 +16,18 @@ import "../../../components/ha-alert";
import "../../../components/ha-circular-progress";
import "../../../components/ha-code-editor";
import type { HaCodeEditor } from "../../../components/ha-code-editor";
-import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
-import { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { LovelaceConfig } from "../../../data/lovelace/config/types";
import type { HomeAssistant } from "../../../types";
-import { LovelaceCardFeatureConfig } from "../card-features/types";
-import type { LovelaceRowConfig } from "../entity-rows/types";
-import { LovelaceHeaderFooterConfig } from "../header-footer/types";
-import { LovelaceElementConfig } from "../elements/types";
import type {
LovelaceConfigForm,
LovelaceGenericElementEditor,
} from "../types";
-import "./card-editor/hui-card-visibility-editor";
import type { HuiFormEditor } from "./config-elements/hui-form-editor";
-import "./config-elements/hui-generic-entity-row-editor";
import { GUISupportError } from "./gui-support-error";
import { EditSubElementEvent, GUIModeChangedEvent } from "./types";
-import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
-
-export interface ConfigChangedEvent {
- config:
- | LovelaceCardConfig
- | LovelaceRowConfig
- | LovelaceHeaderFooterConfig
- | LovelaceCardFeatureConfig
- | LovelaceStrategyConfig
- | LovelaceElementConfig
- | LovelaceBadgeConfig;
+
+export interface ConfigChangedEvent {
+ config: T;
error?: string;
guiModeAvailable?: boolean;
}
@@ -56,17 +40,16 @@ declare global {
}
}
-export interface UIConfigChangedEvent extends Event {
+export interface UIConfigChangedEvent extends Event {
detail: {
- config:
- | LovelaceCardConfig
- | LovelaceRowConfig
- | LovelaceHeaderFooterConfig
- | LovelaceCardFeatureConfig;
+ config: T;
};
}
-export abstract class HuiElementEditor extends LitElement {
+export abstract class HuiElementEditor<
+ T extends object = object,
+ C = any,
+> extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace?: LovelaceConfig;
@@ -79,8 +62,6 @@ export abstract class HuiElementEditor extends LitElement {
@state() private _configElement?: LovelaceGenericElementEditor;
- @state() private _configElementType?: string;
-
@state() private _guiMode = true;
// Error: Configuration broken - do not save
@@ -199,10 +180,6 @@ export abstract class HuiElementEditor extends LitElement {
return undefined;
}
- protected get configElementType(): string | undefined {
- return this.value ? (this.value as any).type : undefined;
- }
-
protected renderConfigElement(): TemplateResult {
return html`${this._configElement}`;
}
@@ -239,45 +216,51 @@ export abstract class HuiElementEditor extends LitElement {
>
`}
- ${this._guiSupported === false && this.configElementType
+ ${this._guiSupported === false && this._loading === false
? html`
-
- ${this.hass.localize("ui.errors.config.editor_not_available", {
- type: this.configElementType,
- })}
-
+
+ ${this.hass.localize(
+ "ui.errors.config.visual_editor_not_supported_reason_type"
+ )}
+
+ ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
+
`
- : ""}
+ : nothing}
${this.hasError
? html`
-
- ${this.hass.localize("ui.errors.config.error_detected")}:
-
+
${this._errors!.map((error) => html`- ${error}
`)}
-
+
`
- : ""}
+ : nothing}
${this.hasWarning
? html`
- ${this._warnings!.length > 0 && this._warnings![0] !== undefined
- ? html`
- ${this._warnings!.map(
- (warning) => html`- ${warning}
`
- )}
-
`
- : ""}
+
+ ${this._warnings!.map((warning) => html`- ${warning}
`)}
+
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
`
- : ""}
+ : nothing}
`;
}
@@ -300,7 +283,7 @@ export abstract class HuiElementEditor extends LitElement {
}
}
- private _handleUIConfigChanged(ev: UIConfigChangedEvent) {
+ private _handleUIConfigChanged(ev: UIConfigChangedEvent) {
ev.stopPropagation();
const config = ev.detail.config;
Object.keys(config).forEach((key) => {
@@ -319,66 +302,62 @@ export abstract class HuiElementEditor extends LitElement {
}
}
+ protected async unloadConfigElement(): Promise {
+ this._configElement = undefined;
+ this._guiSupported = undefined;
+ }
+
+ protected async loadConfigElement(): Promise {
+ if (this._configElement) return;
+
+ let configElement = await this.getConfigElement();
+
+ if (!configElement) {
+ const form = await this.getConfigForm();
+ if (form) {
+ await import("./config-elements/hui-form-editor");
+ configElement = document.createElement("hui-form-editor");
+ const { schema, assertConfig, computeLabel, computeHelper } = form;
+ (configElement as HuiFormEditor).schema = schema;
+ if (computeLabel) {
+ (configElement as HuiFormEditor).computeLabel = computeLabel;
+ }
+ if (computeHelper) {
+ (configElement as HuiFormEditor).computeHelper = computeHelper;
+ }
+ if (assertConfig) {
+ (configElement as HuiFormEditor).assertConfig = assertConfig;
+ }
+ }
+ }
+
+ if (configElement) {
+ configElement.hass = this.hass;
+ if ("lovelace" in configElement) {
+ configElement.lovelace = this.lovelace;
+ }
+ configElement.context = this.context;
+ configElement.addEventListener("config-changed", (ev) =>
+ this._handleUIConfigChanged(ev as UIConfigChangedEvent)
+ );
+ this._guiSupported = true;
+ } else {
+ this._guiSupported = false;
+ }
+
+ this._configElement = configElement;
+ }
+
private async _updateConfigElement(): Promise {
if (!this.value) {
return;
}
- let configElement: LovelaceGenericElementEditor | undefined;
-
try {
this._errors = undefined;
this._warnings = undefined;
- if (this._configElementType !== this.configElementType) {
- // If the type has changed, we need to load a new GUI editor
- this._guiSupported = undefined;
- this._configElement = undefined;
-
- if (!this.configElementType) {
- throw new Error(
- this.hass.localize("ui.errors.config.no_type_provided")
- );
- }
-
- this._configElementType = this.configElementType;
-
- this._loading = true;
- configElement = await this.getConfigElement();
-
- if (!configElement) {
- const form = await this.getConfigForm();
- if (form) {
- await import("./config-elements/hui-form-editor");
- configElement = document.createElement("hui-form-editor");
- const { schema, assertConfig, computeLabel, computeHelper } = form;
- (configElement as HuiFormEditor).schema = schema;
- if (computeLabel) {
- (configElement as HuiFormEditor).computeLabel = computeLabel;
- }
- if (computeHelper) {
- (configElement as HuiFormEditor).computeHelper = computeHelper;
- }
- if (assertConfig) {
- (configElement as HuiFormEditor).assertConfig = assertConfig;
- }
- }
- }
-
- if (configElement) {
- configElement.hass = this.hass;
- if ("lovelace" in configElement) {
- configElement.lovelace = this.lovelace;
- }
- configElement.context = this.context;
- configElement.addEventListener("config-changed", (ev) =>
- this._handleUIConfigChanged(ev as UIConfigChangedEvent)
- );
-
- this._configElement = configElement;
- this._guiSupported = true;
- }
- }
+ await this.loadConfigElement();
if (this._configElement) {
// Setup GUI editor and check that it can handle the current config
@@ -428,26 +407,6 @@ export abstract class HuiElementEditor extends LitElement {
ha-code-editor {
--code-mirror-max-height: calc(100vh - 245px);
}
- .error,
- .warning,
- .info {
- word-break: break-word;
- margin-top: 8px;
- }
- .error {
- color: var(--error-color);
- }
- .warning {
- color: var(--warning-color);
- }
- .warning ul,
- .error ul {
- margin: 4px 0;
- }
- .warning li,
- .error li {
- white-space: pre-wrap;
- }
ha-circular-progress {
display: block;
margin: auto;
diff --git a/src/panels/lovelace/editor/hui-sub-element-editor.ts b/src/panels/lovelace/editor/hui-sub-element-editor.ts
index 1755144d88ab..6ad6b5ef815b 100644
--- a/src/panels/lovelace/editor/hui-sub-element-editor.ts
+++ b/src/panels/lovelace/editor/hui-sub-element-editor.ts
@@ -1,4 +1,3 @@
-import "@material/mwc-button";
import { mdiCodeBraces, mdiListBoxOutline } from "@mdi/js";
import {
css,
@@ -13,14 +12,13 @@ import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import type { HomeAssistant } from "../../../types";
-import type { LovelaceRowConfig } from "../entity-rows/types";
-import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
import "./entity-row-editor/hui-row-element-editor";
+import "./feature-editor/hui-card-feature-element-editor";
import "./header-footer-editor/hui-header-footer-element-editor";
+import "./heading-entity/hui-heading-entity-element-editor";
import type { HuiElementEditor } from "./hui-element-editor";
-import "./feature-editor/hui-card-feature-element-editor";
-import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
import "./picture-element-editor/hui-picture-element-element-editor";
+import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
declare global {
interface HASSDomEvents {
@@ -40,11 +38,14 @@ export class HuiSubElementEditor extends LitElement {
@state() private _guiMode = true;
- @query(".editor") private _editorElement?: HuiElementEditor<
- LovelaceRowConfig | LovelaceHeaderFooterConfig
- >;
+ @query(".editor") private _editorElement?: HuiElementEditor;
protected render(): TemplateResult {
+ const elementType =
+ this.config.elementConfig && "type" in this.config.elementConfig
+ ? this.config.elementConfig.type
+ : "";
+
return html`
- ${this.config.type === "row"
- ? html`
-
- `
- : this.config.type === "header" || this.config.type === "footer"
- ? html`
-
- `
- : this.config.type === "feature"
- ? html`
-
- `
- : this.config.type === "element"
- ? html`
-
- `
- : nothing}
+ ${this._renderEditor()}
`;
}
+ private _renderEditor() {
+ const type = this.config.type;
+ switch (type) {
+ case "row":
+ return html`
+
+ `;
+ case "header":
+ case "footer":
+ return html`
+
+ `;
+ case "element":
+ return html`
+
+ `;
+ case "feature":
+ return html`
+
+ `;
+ case "heading-entity":
+ return html`
+
+ `;
+ default:
+ return nothing;
+ }
+ }
+
private _goBack(): void {
fireEvent(this, "go-back");
}
diff --git a/src/panels/lovelace/editor/hui-sub-form-editor.ts b/src/panels/lovelace/editor/hui-sub-form-editor.ts
deleted file mode 100644
index 014ff865f3c9..000000000000
--- a/src/panels/lovelace/editor/hui-sub-form-editor.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import "@material/mwc-button";
-import { mdiCodeBraces, mdiListBoxOutline } from "@mdi/js";
-import {
- css,
- CSSResultGroup,
- html,
- LitElement,
- nothing,
- PropertyValues,
- TemplateResult,
-} from "lit";
-import { customElement, property, state } from "lit/decorators";
-import { fireEvent } from "../../../common/dom/fire_event";
-import "../../../components/ha-form/ha-form";
-import "../../../components/ha-icon-button";
-import "../../../components/ha-icon-button-prev";
-import "../../../components/ha-yaml-editor";
-import "../../../components/ha-alert";
-import type { HomeAssistant } from "../../../types";
-import type { LovelaceConfigForm } from "../types";
-import type { EditSubFormEvent } from "./types";
-import { handleStructError } from "../../../common/structs/handle-errors";
-
-declare global {
- interface HASSDomEvents {
- "go-back": undefined;
- "edit-sub-form": EditSubFormEvent;
- }
-}
-
-@customElement("hui-sub-form-editor")
-export class HuiSubFormEditor extends LitElement {
- public hass!: HomeAssistant;
-
- @property() public label?: string;
-
- @property({ attribute: false }) public data!: T;
-
- public schema!: LovelaceConfigForm["schema"];
-
- public assertConfig?: (config: T) => void;
-
- public computeLabel?: LovelaceConfigForm["computeLabel"];
-
- public computeHelper?: LovelaceConfigForm["computeHelper"];
-
- @state() public _yamlMode = false;
-
- @state() private _errors?: string[];
-
- @state() private _warnings?: string[];
-
- protected render(): TemplateResult {
- const uiAvailable = !this.hasWarning && !this.hasError;
-
- return html`
-
- ${this._yamlMode
- ? html`
-
- `
- : html`
-
-
- `}
- ${this.hasError
- ? html`
-
- ${this.hass.localize("ui.errors.config.error_detected")}:
-
-
- ${this._errors!.map((error) => html`- ${error}
`)}
-
-
- `
- : nothing}
- ${this.hasWarning
- ? html`
-
- ${this._warnings!.length > 0 && this._warnings![0] !== undefined
- ? html`
-
- ${this._warnings!.map(
- (warning) => html`- ${warning}
`
- )}
-
- `
- : nothing}
- ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
-
- `
- : nothing}
- `;
- }
-
- protected willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has("data")) {
- if (this.assertConfig) {
- try {
- this.assertConfig(this.data);
- this._warnings = undefined;
- this._errors = undefined;
- } catch (err: any) {
- const msgs = handleStructError(this.hass, err);
- this._warnings = msgs.warnings ?? [err.message];
- this._errors = msgs.errors || undefined;
- this._yamlMode = true;
- }
- }
- }
- }
-
- public get hasWarning(): boolean {
- return this._warnings !== undefined && this._warnings.length > 0;
- }
-
- public get hasError(): boolean {
- return this._errors !== undefined && this._errors.length > 0;
- }
-
- private _goBack(): void {
- fireEvent(this, "go-back");
- }
-
- private _toggleMode(): void {
- this._yamlMode = !this._yamlMode;
- }
-
- private _valueChanged(ev: CustomEvent): void {
- ev.stopPropagation();
- const value = (ev.detail.value ?? (ev.target as any).value ?? {}) as T;
- fireEvent(this, "value-changed", { value });
- }
-
- static get styles(): CSSResultGroup {
- return css`
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .back-title {
- display: flex;
- align-items: center;
- font-size: 18px;
- }
- `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "hui-sub-form-editor": HuiSubFormEditor;
- }
-}
diff --git a/src/panels/lovelace/editor/hui-typed-element-editor.ts b/src/panels/lovelace/editor/hui-typed-element-editor.ts
new file mode 100644
index 000000000000..72189151f886
--- /dev/null
+++ b/src/panels/lovelace/editor/hui-typed-element-editor.ts
@@ -0,0 +1,30 @@
+import { state } from "lit/decorators";
+import { HuiElementEditor } from "./hui-element-editor";
+
+export abstract class HuiTypedElementEditor<
+ T extends object,
+ C = any,
+> extends HuiElementEditor {
+ @state() private _configElementType?: string;
+
+ protected get configElementType(): string | undefined {
+ return this.value ? (this.value as any).type : undefined;
+ }
+
+ protected async loadConfigElement(): Promise {
+ // If the type has changed, we need to unload the current editor and load the new one
+ if (this._configElementType !== this.configElementType) {
+ this.unloadConfigElement();
+
+ if (!this.configElementType) {
+ throw new Error(
+ this.hass.localize("ui.errors.config.no_type_provided")
+ );
+ }
+
+ this._configElementType = this.configElementType;
+ }
+
+ return super.loadConfigElement();
+ }
+}
diff --git a/src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts b/src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts
index 946061d3660a..638fc023af24 100644
--- a/src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts
+++ b/src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts
@@ -1,11 +1,11 @@
import { customElement } from "lit/decorators";
import { LovelaceElementConfig } from "../../elements/types";
import type { LovelacePictureElementEditor } from "../../types";
-import { HuiElementEditor } from "../hui-element-editor";
+import { HuiTypedElementEditor } from "../hui-typed-element-editor";
import { getPictureElementClass } from "../../create-element/create-picture-element";
@customElement("hui-picture-element-element-editor")
-export class HuiPictureElementElementEditor extends HuiElementEditor {
+export class HuiPictureElementElementEditor extends HuiTypedElementEditor {
protected get configElementType(): string | undefined {
return this.value?.type === "action-button"
? "service-button"
diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts
index c8de2a15b049..771f982a8d7d 100644
--- a/src/panels/lovelace/editor/types.ts
+++ b/src/panels/lovelace/editor/types.ts
@@ -9,6 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LovelaceElementConfig } from "../elements/types";
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
+import { HeadingEntityConfig } from "../cards/types";
export interface YamlChangedEvent extends Event {
detail: {
@@ -95,19 +96,11 @@ export interface SubElementEditorConfig {
| LovelaceRowConfig
| LovelaceHeaderFooterConfig
| LovelaceCardFeatureConfig
- | LovelaceElementConfig;
- type: "header" | "footer" | "row" | "feature" | "element";
+ | LovelaceElementConfig
+ | HeadingEntityConfig;
+ type: "header" | "footer" | "row" | "feature" | "element" | "heading-entity";
}
export interface EditSubElementEvent {
subElementConfig: SubElementEditorConfig;
}
-
-export interface SubFormEditorData {
- index?: number;
- data?: T;
-}
-
-export interface EditSubFormEvent {
- subFormData: SubFormEditorData;
-}
diff --git a/src/translations/en.json b/src/translations/en.json
index 5e2976e1b41b..31b311ba27b3 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1796,11 +1796,13 @@
},
"errors": {
"config": {
+ "visual_editor_not_supported": "Visual editor not supported",
+ "visual_editor_not_supported_reason_type": "The visual editor is not available for this type of element.",
+ "edit_in_yaml_supported": "You can still edit your config using YAML.",
+ "configuration_error": "Configuration error",
+ "configuration_error_reason": "The configuration is not valid. Check the logs for more information.",
"no_type_provided": "No type provided.",
- "error_detected": "Configuration errors detected",
- "editor_not_available": "No visual editor available for type ''{type}''.",
"editor_not_supported": "Visual editor is not supported for this configuration",
- "edit_in_yaml_supported": "You can still edit your config in YAML.",
"key_missing": "Required key ''{key}'' is missing.",
"key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.",
"key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).",
@@ -6379,6 +6381,7 @@
"row": "Entity row editor",
"feature": "Feature editor",
"element": "Element editor",
+ "heading-entity": "Entity editor",
"element_type": "{type} element editor"
}
}