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`
@@ -52,21 +53,21 @@ export class HuiSubElementEditor extends LitElement { .label=${this.hass!.localize("ui.common.back")} @click=${this._goBack} > - ${this.config?.type === "element" + + ${this.config?.type === "element" ? this.hass.localize( `ui.panel.lovelace.editor.sub-element-editor.types.element_type`, { type: this.hass.localize( - `ui.panel.lovelace.editor.card.picture-elements.element_types.${this.config?.elementConfig?.type}` - ) || this.config?.elementConfig?.type, + `ui.panel.lovelace.editor.card.picture-elements.element_types.${elementType}` + ) || elementType, } ) : this.hass.localize( `ui.panel.lovelace.editor.sub-element-editor.types.${this.config?.type}` - )} + )} +
- ${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.label} -
- -
- ${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" } }