From f234171832fbe8c4ad27909a6de42f609cf06d89 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 4 Jul 2024 15:17:48 +0200 Subject: [PATCH 01/32] Add new entity badge --- src/data/lovelace/config/badge.ts | 3 + .../lovelace/badges/hui-entity-badge.ts | 154 +++++++++++++ src/panels/lovelace/badges/types.ts | 7 + src/panels/lovelace/cards/hui-badge.ts | 209 ++++++++++++++++++ .../create-element/create-badge-element.ts | 5 +- .../create-element/create-element-base.ts | 6 + 6 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 src/panels/lovelace/badges/hui-entity-badge.ts create mode 100644 src/panels/lovelace/cards/hui-badge.ts diff --git a/src/data/lovelace/config/badge.ts b/src/data/lovelace/config/badge.ts index 661464a9352d..1f8b447075ec 100644 --- a/src/data/lovelace/config/badge.ts +++ b/src/data/lovelace/config/badge.ts @@ -1,4 +1,7 @@ +import { Condition } from "../../../panels/lovelace/common/validate-condition"; + export interface LovelaceBadgeConfig { type?: string; [key: string]: any; + visibility?: Condition[]; } diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts new file mode 100644 index 000000000000..5944a410afd8 --- /dev/null +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -0,0 +1,154 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { ifDefined } from "lit/directives/if-defined"; +import { styleMap } from "lit/directives/style-map"; +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 { stateActive } from "../../../common/entity/state_active"; +import { stateColorCss } from "../../../common/entity/state_color"; +import "../../../components/ha-ripple"; +import "../../../components/ha-state-icon"; +import { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; +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 { LovelaceBadge } from "../types"; +import { EntityBadgeConfig } from "./types"; + +@customElement("hui-entity-badge") +export class HuiEntityBadge extends LitElement implements LovelaceBadge { + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() protected _config?: EntityBadgeConfig; + + public setConfig(config: EntityBadgeConfig): void { + this._config = config; + } + + get hasAction() { + return ( + !this._config?.tap_action || + hasAction(this._config?.tap_action) || + hasAction(this._config?.hold_action) || + hasAction(this._config?.double_tap_action) + ); + } + + private _computeStateColor = memoizeOne( + (stateObj: HassEntity, color?: string) => { + // Use custom color if active + if (color) { + return stateActive(stateObj) ? computeCssColor(color) : undefined; + } + + // Use light color if the light support rgb + if ( + computeDomain(stateObj.entity_id) === "light" && + stateObj.attributes.rgb_color + ) { + const hsvColor = rgb2hsv(stateObj.attributes.rgb_color); + + // Modify the real rgb color for better contrast + if (hsvColor[1] < 0.4) { + // Special case for very light color (e.g: white) + if (hsvColor[1] < 0.1) { + hsvColor[2] = 225; + } else { + hsvColor[1] = 0.4; + } + } + return rgb2hex(hsv2rgb(hsvColor)); + } + + // Fallback to state color + return stateColorCss(stateObj); + } + ); + + protected render() { + if (!this._config || !this.hass) { + return nothing; + } + + const stateObj = this.hass.states[this._config.entity!]; + + const color = this._computeStateColor(stateObj, this._config.color); + + const style = { + "--badge-color": color, + }; + + return html` +
+ + + ${this.hass.formatEntityState(stateObj)} +
+ `; + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + static get styles(): CSSResultGroup { + return css` + .badge { + position: relative; + --ha-ripple-color: var(--badge-color); + --ha-ripple-hover-opacity: 0.04; + --ha-ripple-pressed-opacity: 0.12; + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 8px; + height: 32px; + padding: 6px 16px 6px 12px; + margin: calc(-1 * var(--ha-card-border-width, 1px)); + box-sizing: border-box; + width: auto; + border-radius: 16px; + background-color: var(--card-background-color, white); + border-width: var(--ha-card-border-width, 1px); + border-style: solid; + border-color: var( + --ha-card-border-color, + var(--divider-color, #e0e0e0) + ); + --mdc-icon-size: 18px; + cursor: pointer; + color: var(--primary-text-color); + text-align: center; + font-family: Roboto; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.1px; + } + ha-state-icon { + color: var(--badge-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-entity-badge": HuiEntityBadge; + } +} diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index 345c1c3d8e41..434c0f7c6069 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -25,3 +25,10 @@ export interface StateLabelBadgeConfig extends LovelaceBadgeConfig { hold_action?: ActionConfig; double_tap_action?: ActionConfig; } + +export interface EntityBadgeConfig extends LovelaceBadgeConfig { + type: "entity"; + entity: string; + icon?: string; + tap_action?: ActionConfig; +} diff --git a/src/panels/lovelace/cards/hui-badge.ts b/src/panels/lovelace/cards/hui-badge.ts new file mode 100644 index 000000000000..625c791763c6 --- /dev/null +++ b/src/panels/lovelace/cards/hui-badge.ts @@ -0,0 +1,209 @@ +import { PropertyValues, ReactiveElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { MediaQueriesListener } from "../../../common/dom/media_query"; +import "../../../components/ha-svg-icon"; +import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; +import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; +import type { HomeAssistant } from "../../../types"; +import { + attachConditionMediaQueriesListeners, + checkConditionsMet, +} from "../common/validate-condition"; +import { createBadgeElement } from "../create-element/create-badge-element"; +import { createErrorBadgeConfig } from "../create-element/create-element-base"; +import type { LovelaceCard } from "../types"; + +declare global { + interface HASSDomEvents { + "badge-updated": undefined; + } +} + +@customElement("hui-badge") +export class HuiBadge extends ReactiveElement { + @property({ attribute: false }) public preview = false; + + @property({ attribute: false }) public config?: LovelaceCardConfig; + + @property({ attribute: false }) public hass?: HomeAssistant; + + private _elementConfig?: LovelaceBadgeConfig; + + public load() { + if (!this.config) { + throw new Error("Cannot build badge without config"); + } + this._loadElement(this.config); + } + + private _element?: LovelaceCard; + + private _listeners: MediaQueriesListener[] = []; + + protected createRenderRoot() { + return this; + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._clearMediaQueries(); + } + + public connectedCallback() { + super.connectedCallback(); + this._listenMediaQueries(); + this._updateVisibility(); + } + + private _updateElement(config: LovelaceCardConfig) { + if (!this._element) { + return; + } + this._element.setConfig(config); + this._elementConfig = config; + fireEvent(this, "badge-updated"); + } + + private _loadElement(config: LovelaceCardConfig) { + this._element = createBadgeElement(config); + this._elementConfig = config; + if (this.hass) { + this._element.hass = this.hass; + } + this._element.preview = this.preview; + this._element.addEventListener( + "ll-upgrade", + (ev: Event) => { + ev.stopPropagation(); + if (this.hass) { + this._element!.hass = this.hass; + } + fireEvent(this, "badge-updated"); + }, + { once: true } + ); + this._element.addEventListener( + "ll-rebuild", + (ev: Event) => { + ev.stopPropagation(); + this._loadElement(config); + fireEvent(this, "badge-updated"); + }, + { once: true } + ); + while (this.lastChild) { + this.removeChild(this.lastChild); + } + this._updateVisibility(); + } + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if (!this._element) { + this.load(); + } + } + + protected update(changedProps: PropertyValues) { + super.update(changedProps); + + if (this._element) { + if (changedProps.has("config")) { + const elementConfig = this._elementConfig; + if (this.config !== elementConfig && this.config) { + const typeChanged = this.config?.type !== elementConfig?.type; + if (typeChanged) { + this._loadElement(this.config); + } else { + this._updateElement(this.config); + } + } + } + if (changedProps.has("hass")) { + try { + if (this.hass) { + this._element.hass = this.hass; + } + } catch (e: any) { + this._loadElement(createErrorBadgeConfig(e.message, null)); + } + } + if (changedProps.has("preview")) { + try { + this._element.preview = this.preview; + } catch (e: any) { + this._loadElement(createErrorCardConfig(e.message, null)); + } + } + } + + if (changedProps.has("hass") || changedProps.has("preview")) { + this._updateVisibility(); + } + } + + private _clearMediaQueries() { + this._listeners.forEach((unsub) => unsub()); + this._listeners = []; + } + + private _listenMediaQueries() { + this._clearMediaQueries(); + if (!this.config?.visibility) { + return; + } + const conditions = this.config.visibility; + const hasOnlyMediaQuery = + conditions.length === 1 && + conditions[0].condition === "screen" && + !!conditions[0].media_query; + + this._listeners = attachConditionMediaQueriesListeners( + this.config.visibility, + (matches) => { + this._updateVisibility(hasOnlyMediaQuery && matches); + } + ); + } + + private _updateVisibility(forceVisible?: boolean) { + if (!this._element || !this.hass) { + return; + } + + if (this._element.hidden) { + this._setElementVisibility(false); + return; + } + + const visible = + forceVisible || + this.preview || + !this.config?.visibility || + checkConditionsMet(this.config.visibility, this.hass); + this._setElementVisibility(visible); + } + + private _setElementVisibility(visible: boolean) { + if (!this._element) return; + + if (this.hidden !== !visible) { + this.style.setProperty("display", visible ? "" : "none"); + this.toggleAttribute("hidden", !visible); + } + + if (!visible && this._element.parentElement) { + this.removeChild(this._element); + } else if (visible && !this._element.parentElement) { + this.appendChild(this._element); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-badge": HuiBadge; + } +} diff --git a/src/panels/lovelace/create-element/create-badge-element.ts b/src/panels/lovelace/create-element/create-badge-element.ts index 2335a48c9988..51c4d622f19a 100644 --- a/src/panels/lovelace/create-element/create-badge-element.ts +++ b/src/panels/lovelace/create-element/create-badge-element.ts @@ -1,8 +1,9 @@ import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; import "../badges/hui-state-label-badge"; +import "../badges/hui-entity-badge"; import { createLovelaceElement } from "./create-element-base"; -const ALWAYS_LOADED_TYPES = new Set(["error", "state-label"]); +const ALWAYS_LOADED_TYPES = new Set(["error", "state-label", "entity"]); const LAZY_LOAD_TYPES = { "entity-filter": () => import("../badges/hui-entity-filter-badge"), }; @@ -14,5 +15,5 @@ export const createBadgeElement = (config: LovelaceBadgeConfig) => ALWAYS_LOADED_TYPES, LAZY_LOAD_TYPES, undefined, - "state-label" + "entity" ); diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index 5ba763733cf2..4c2a6490ea11 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -93,6 +93,12 @@ export const createErrorCardConfig = (error, origConfig) => ({ origConfig, }); +export const createErrorBadgeConfig = (error, origConfig) => ({ + type: "error", + error, + origConfig, +}); + const _createElement = ( tag: string, config: CreateElementConfigTypes[T]["config"] From 43307a025b0b71f6b4376495c2ed7e4925dab890 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 8 Jul 2024 15:29:05 +0200 Subject: [PATCH 02/32] Improve badge render --- src/data/lovelace/config/view.ts | 2 +- .../lovelace/views/hui-sections-view.ts | 43 +++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/data/lovelace/config/view.ts b/src/data/lovelace/config/view.ts index db0385173da2..0366e7b816a3 100644 --- a/src/data/lovelace/config/view.ts +++ b/src/data/lovelace/config/view.ts @@ -27,7 +27,7 @@ export interface LovelaceBaseViewConfig { export interface LovelaceViewConfig extends LovelaceBaseViewConfig { type?: string; - badges?: Array; + badges?: LovelaceBadgeConfig[]; cards?: LovelaceCardConfig[]; sections?: LovelaceSectionRawConfig[]; } diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index d50e1af18f9c..a09a85149d2a 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -47,11 +47,20 @@ export class SectionsView extends LitElement implements LovelaceViewElement { private _sectionConfigKeys = new WeakMap(); - private _getKey(sectionConfig: HuiSection) { - if (!this._sectionConfigKeys.has(sectionConfig)) { - this._sectionConfigKeys.set(sectionConfig, Math.random().toString()); + private _badgeConfigKeys = new WeakMap(); + + private _getBadgeKey(badge: LovelaceBadge) { + if (!this._badgeConfigKeys.has(badge)) { + this._badgeConfigKeys.set(badge, Math.random().toString()); } - return this._sectionConfigKeys.get(sectionConfig)!; + return this._badgeConfigKeys.get(badge)!; + } + + private _getSectionKey(section: HuiSection) { + if (!this._sectionConfigKeys.has(section)) { + this._sectionConfigKeys.set(section, Math.random().toString()); + } + return this._sectionConfigKeys.get(section)!; } private _computeSectionsCount() { @@ -82,10 +91,20 @@ export class SectionsView extends LitElement implements LovelaceViewElement { const maxColumnsCount = this._config?.max_columns; + const badges = this.badges; + return html` - ${this.badges.length > 0 - ? html`
${this.badges}
` - : ""} + ${badges?.length > 0 + ? html` +
+ ${repeat( + badges, + (badge) => this._getBadgeKey(badge), + (badge) => html`
${badge}
` + )} +
+ ` + : nothing} ${repeat( sections, - (section) => this._getKey(section), + (section) => this._getSectionKey(section), (section, idx) => { (section as any).itemPath = [idx]; return html` @@ -236,6 +255,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement { } .badges { + display: flex; + align-items: flex-start; + justify-content: center; + gap: 8px; margin: 4px 0; padding: var(--row-gap) var(--column-gap); padding-bottom: 0; @@ -243,6 +266,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement { text-align: center; } + .badge { + display: block; + } + .container > * { position: relative; max-width: var(--column-max-width); From 024fc95a1c9e8d72cb284d102fbc2386ba69de49 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 8 Jul 2024 15:46:58 +0200 Subject: [PATCH 03/32] Add edit mode --- .../components/hui-badge-edit-mode.ts | 304 ++++++++++++++++++ .../lovelace/views/hui-sections-view.ts | 18 +- 2 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 src/panels/lovelace/components/hui-badge-edit-mode.ts diff --git a/src/panels/lovelace/components/hui-badge-edit-mode.ts b/src/panels/lovelace/components/hui-badge-edit-mode.ts new file mode 100644 index 000000000000..b3dd24452b7f --- /dev/null +++ b/src/panels/lovelace/components/hui-badge-edit-mode.ts @@ -0,0 +1,304 @@ +import "@material/mwc-button"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import { + mdiContentCopy, + mdiContentCut, + mdiContentDuplicate, + mdiDelete, + mdiDotsVertical, + mdiPencil, +} from "@mdi/js"; +import deepClone from "deep-clone-simple"; +import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { storage } from "../../../common/decorators/storage"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-button-menu"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-list-item"; +import "../../../components/ha-svg-icon"; +import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; +import { + LovelaceCardPath, + findLovelaceCards, + getLovelaceContainerPath, + parseLovelaceCardPath, +} from "../editor/lovelace-path"; +import { Lovelace } from "../types"; + +@customElement("hui-badge-edit-mode") +export class HuiCardEditMode extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public lovelace!: Lovelace; + + @property({ type: Array }) public path!: LovelaceCardPath; + + @property({ type: Boolean }) public hiddenOverlay = false; + + @state() + public _menuOpened: boolean = false; + + @state() + public _hover: boolean = false; + + @state() + public _focused: boolean = false; + + @storage({ + key: "lovelaceClipboard", + state: false, + subscribe: false, + storage: "sessionStorage", + }) + protected _clipboard?: LovelaceCardConfig; + + private get _cards() { + const containerPath = getLovelaceContainerPath(this.path!); + return findLovelaceCards(this.lovelace!.config, containerPath)!; + } + + private _touchStarted = false; + + protected firstUpdated(): void { + this.addEventListener("focus", () => { + this._focused = true; + }); + this.addEventListener("blur", () => { + this._focused = false; + }); + this.addEventListener("touchstart", () => { + this._touchStarted = true; + }); + this.addEventListener("touchend", () => { + setTimeout(() => { + this._touchStarted = false; + }, 10); + }); + this.addEventListener("mouseenter", () => { + if (this._touchStarted) return; + this._hover = true; + }); + this.addEventListener("mouseout", () => { + this._hover = false; + }); + this.addEventListener("click", () => { + this._hover = true; + document.addEventListener("click", this._documentClicked); + }); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + document.removeEventListener("click", this._documentClicked); + } + + _documentClicked = (ev) => { + this._hover = ev.composedPath().includes(this); + document.removeEventListener("click", this._documentClicked); + }; + + protected render(): TemplateResult { + const showOverlay = + (this._hover || this._menuOpened || this._focused) && !this.hiddenOverlay; + + return html` +
+
+
+
+ +
+ + + + + + ${this.hass.localize( + "ui.panel.lovelace.editor.edit_card.duplicate" + )} + + + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")} + + + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")} + +
  • + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")} + + +
    +
    + `; + } + + private _handleOpened() { + this._menuOpened = true; + } + + private _handleClosed() { + this._menuOpened = false; + } + + private _handleAction(ev: CustomEvent) { + switch (ev.detail.index) { + case 0: + this._duplicateCard(); + break; + case 1: + this._copyCard(); + break; + case 2: + this._cutCard(); + break; + case 3: + this._deleteCard(true); + break; + } + } + + private _duplicateCard(): void { + const { cardIndex } = parseLovelaceCardPath(this.path!); + const containerPath = getLovelaceContainerPath(this.path!); + const cardConfig = this._cards![cardIndex]; + showEditCardDialog(this, { + lovelaceConfig: this.lovelace!.config, + saveConfig: this.lovelace!.saveConfig, + path: containerPath, + cardConfig, + }); + } + + private _editCard(ev): void { + if (ev.defaultPrevented) { + return; + } + if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") { + return; + } + ev.preventDefault(); + ev.stopPropagation(); + fireEvent(this, "ll-edit-card", { path: this.path! }); + } + + private _cutCard(): void { + this._copyCard(); + this._deleteCard(false); + } + + private _copyCard(): void { + const { cardIndex } = parseLovelaceCardPath(this.path!); + const cardConfig = this._cards[cardIndex]; + this._clipboard = deepClone(cardConfig); + } + + private _deleteCard(confirm: boolean): void { + fireEvent(this, "ll-delete-card", { path: this.path!, confirm }); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .badge-overlay { + position: absolute; + opacity: 0; + pointer-events: none; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 180ms ease-in-out; + } + + .badge-overlay.visible { + opacity: 1; + pointer-events: auto; + } + + .badge-wrapper { + position: relative; + height: 100%; + z-index: 0; + } + + .edit { + outline: none !important; + cursor: pointer; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--ha-card-border-radius, 12px); + z-index: 0; + } + .edit-overlay { + position: absolute; + inset: 0; + opacity: 0.8; + background-color: var(--primary-background-color); + border-radius: var(--ha-card-border-radius, 12px); + z-index: 0; + } + .edit ha-svg-icon { + display: flex; + position: relative; + color: var(--primary-text-color); + border-radius: 50%; + padding: 4px; + background: var(--secondary-background-color); + --mdc-icon-size: 16px; + } + .more { + position: absolute; + right: -8px; + top: -8px; + inset-inline-end: -10px; + inset-inline-start: initial; + } + .more ha-icon-button { + cursor: pointer; + border-radius: 50%; + background: var(--secondary-background-color); + --mdc-icon-button-size: 24px; + --mdc-icon-size: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-badge-edit-mode": HuiCardEditMode; + } +} diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index a09a85149d2a..5efd6b8711bb 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -17,6 +17,7 @@ import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; +import "../components/hui-badge-edit-mode"; import { addSection, deleteSection, moveSection } from "../editor/config-util"; import { findLovelaceContainer } from "../editor/lovelace-path"; import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog"; @@ -100,7 +101,21 @@ export class SectionsView extends LitElement implements LovelaceViewElement { ${repeat( badges, (badge) => this._getBadgeKey(badge), - (badge) => html`
    ${badge}
    ` + (badge, idx) => html` +
    + ${editMode + ? html` + + ${badge} + + ` + : badge} +
    + ` )} ` @@ -268,6 +283,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { .badge { display: block; + position: relative; } .container > * { From 990852695143d41fe865ca892e53afc155a5a8e8 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 9 Jul 2024 14:21:02 +0200 Subject: [PATCH 04/32] Add editor --- src/data/lovelace/config/section.ts | 2 + src/panels/lovelace/cards/hui-badge.ts | 19 +- .../components/hui-badge-edit-mode.ts | 24 +- .../lovelace/components/hui-card-edit-mode.ts | 4 +- .../lovelace/components/hui-card-options.ts | 4 +- .../badge-editor/hui-badge-element-editor.ts | 36 ++ .../badge-editor/hui-dialog-edit-badge.ts | 546 ++++++++++++++++++ .../badge-editor/show-edit-badge-dialog.ts | 30 + .../card-editor/hui-dialog-edit-card.ts | 4 +- src/panels/lovelace/editor/config-util.ts | 148 ++++- src/panels/lovelace/editor/lovelace-path.ts | 80 +-- src/panels/lovelace/types.ts | 4 + src/panels/lovelace/views/hui-view.ts | 12 + 13 files changed, 829 insertions(+), 84 deletions(-) create mode 100644 src/panels/lovelace/editor/badge-editor/hui-badge-element-editor.ts create mode 100644 src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts create mode 100644 src/panels/lovelace/editor/badge-editor/show-edit-badge-dialog.ts diff --git a/src/data/lovelace/config/section.ts b/src/data/lovelace/config/section.ts index bfd54042a590..0f32ed054770 100644 --- a/src/data/lovelace/config/section.ts +++ b/src/data/lovelace/config/section.ts @@ -1,4 +1,5 @@ import type { Condition } from "../../../panels/lovelace/common/validate-condition"; +import { LovelaceBadgeConfig } from "./badge"; import type { LovelaceCardConfig } from "./card"; import type { LovelaceStrategyConfig } from "./strategy"; @@ -10,6 +11,7 @@ export interface LovelaceBaseSectionConfig { export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig { type?: string; cards?: LovelaceCardConfig[]; + badges?: LovelaceBadgeConfig[]; // Not supported yet } export interface LovelaceStrategySectionConfig diff --git a/src/panels/lovelace/cards/hui-badge.ts b/src/panels/lovelace/cards/hui-badge.ts index 625c791763c6..4ba4e2cd6d8c 100644 --- a/src/panels/lovelace/cards/hui-badge.ts +++ b/src/panels/lovelace/cards/hui-badge.ts @@ -4,7 +4,6 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { MediaQueriesListener } from "../../../common/dom/media_query"; import "../../../components/ha-svg-icon"; import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; -import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { HomeAssistant } from "../../../types"; import { attachConditionMediaQueriesListeners, @@ -12,7 +11,7 @@ import { } from "../common/validate-condition"; import { createBadgeElement } from "../create-element/create-badge-element"; import { createErrorBadgeConfig } from "../create-element/create-element-base"; -import type { LovelaceCard } from "../types"; +import type { LovelaceBadge } from "../types"; declare global { interface HASSDomEvents { @@ -24,7 +23,7 @@ declare global { export class HuiBadge extends ReactiveElement { @property({ attribute: false }) public preview = false; - @property({ attribute: false }) public config?: LovelaceCardConfig; + @property({ attribute: false }) public config?: LovelaceBadgeConfig; @property({ attribute: false }) public hass?: HomeAssistant; @@ -37,7 +36,7 @@ export class HuiBadge extends ReactiveElement { this._loadElement(this.config); } - private _element?: LovelaceCard; + private _element?: LovelaceBadge; private _listeners: MediaQueriesListener[] = []; @@ -56,7 +55,7 @@ export class HuiBadge extends ReactiveElement { this._updateVisibility(); } - private _updateElement(config: LovelaceCardConfig) { + private _updateElement(config: LovelaceBadgeConfig) { if (!this._element) { return; } @@ -65,13 +64,12 @@ export class HuiBadge extends ReactiveElement { fireEvent(this, "badge-updated"); } - private _loadElement(config: LovelaceCardConfig) { + private _loadElement(config: LovelaceBadgeConfig) { this._element = createBadgeElement(config); this._elementConfig = config; if (this.hass) { this._element.hass = this.hass; } - this._element.preview = this.preview; this._element.addEventListener( "ll-upgrade", (ev: Event) => { @@ -130,13 +128,6 @@ export class HuiBadge extends ReactiveElement { this._loadElement(createErrorBadgeConfig(e.message, null)); } } - if (changedProps.has("preview")) { - try { - this._element.preview = this.preview; - } catch (e: any) { - this._loadElement(createErrorCardConfig(e.message, null)); - } - } } if (changedProps.has("hass") || changedProps.has("preview")) { diff --git a/src/panels/lovelace/components/hui-badge-edit-mode.ts b/src/panels/lovelace/components/hui-badge-edit-mode.ts index b3dd24452b7f..dbc8e16d731b 100644 --- a/src/panels/lovelace/components/hui-badge-edit-mode.ts +++ b/src/panels/lovelace/components/hui-badge-edit-mode.ts @@ -21,10 +21,10 @@ import "../../../components/ha-svg-icon"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; +import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog"; import { LovelaceCardPath, - findLovelaceCards, + findLovelaceItems, getLovelaceContainerPath, parseLovelaceCardPath, } from "../editor/lovelace-path"; @@ -57,9 +57,9 @@ export class HuiCardEditMode extends LitElement { }) protected _clipboard?: LovelaceCardConfig; - private get _cards() { + private get _badges() { const containerPath = getLovelaceContainerPath(this.path!); - return findLovelaceCards(this.lovelace!.config, containerPath)!; + return findLovelaceItems("badges", this.lovelace!.config, containerPath)!; } private _touchStarted = false; @@ -111,8 +111,8 @@ export class HuiCardEditMode extends LitElement {
    @@ -188,16 +188,16 @@ export class HuiCardEditMode extends LitElement { private _duplicateCard(): void { const { cardIndex } = parseLovelaceCardPath(this.path!); const containerPath = getLovelaceContainerPath(this.path!); - const cardConfig = this._cards![cardIndex]; - showEditCardDialog(this, { + const badgeConfig = this._badges![cardIndex]; + showEditBadgeDialog(this, { lovelaceConfig: this.lovelace!.config, saveConfig: this.lovelace!.saveConfig, path: containerPath, - cardConfig, + badgeConfig, }); } - private _editCard(ev): void { + private _editBadge(ev): void { if (ev.defaultPrevented) { return; } @@ -206,7 +206,7 @@ export class HuiCardEditMode extends LitElement { } ev.preventDefault(); ev.stopPropagation(); - fireEvent(this, "ll-edit-card", { path: this.path! }); + fireEvent(this, "ll-edit-badge", { path: this.path! }); } private _cutCard(): void { @@ -216,7 +216,7 @@ export class HuiCardEditMode extends LitElement { private _copyCard(): void { const { cardIndex } = parseLovelaceCardPath(this.path!); - const cardConfig = this._cards[cardIndex]; + const cardConfig = this._badges[cardIndex]; this._clipboard = deepClone(cardConfig); } diff --git a/src/panels/lovelace/components/hui-card-edit-mode.ts b/src/panels/lovelace/components/hui-card-edit-mode.ts index a3f611cefe0d..f66e6c5a5f1b 100644 --- a/src/panels/lovelace/components/hui-card-edit-mode.ts +++ b/src/panels/lovelace/components/hui-card-edit-mode.ts @@ -24,7 +24,7 @@ import { HomeAssistant } from "../../../types"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { LovelaceCardPath, - findLovelaceCards, + findLovelaceItems, getLovelaceContainerPath, parseLovelaceCardPath, } from "../editor/lovelace-path"; @@ -59,7 +59,7 @@ export class HuiCardEditMode extends LitElement { private get _cards() { const containerPath = getLovelaceContainerPath(this.path!); - return findLovelaceCards(this.lovelace!.config, containerPath)!; + return findLovelaceItems("cards", this.lovelace!.config, containerPath)!; } private _touchStarted = false; diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index db87e8d291ce..163f32589571 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -46,7 +46,7 @@ import { } from "../editor/config-util"; import { LovelaceCardPath, - findLovelaceCards, + findLovelaceItems, getLovelaceContainerPath, parseLovelaceCardPath, } from "../editor/lovelace-path"; @@ -91,7 +91,7 @@ export class HuiCardOptions extends LitElement { private get _cards() { const containerPath = getLovelaceContainerPath(this.path!); - return findLovelaceCards(this.lovelace!.config, containerPath)!; + return findLovelaceItems("cards", this.lovelace!.config, containerPath)!; } protected render(): TemplateResult { 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 new file mode 100644 index 000000000000..5746ee81e9cf --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/hui-badge-element-editor.ts @@ -0,0 +1,36 @@ +import { customElement } from "lit/decorators"; +import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; +import { getCardElementClass } from "../../create-element/create-card-element"; +import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types"; +import { HuiElementEditor } from "../hui-element-editor"; + +@customElement("hui-badge-element-editor") +export class HuiBadgeElementEditor extends HuiElementEditor { + protected async getConfigElement(): Promise { + const elClass = await getCardElementClass(this.configElementType!); + + // Check if a GUI editor exists + if (elClass && elClass.getConfigElement) { + return elClass.getConfigElement(); + } + + return undefined; + } + + protected async getConfigForm(): Promise { + const elClass = await getCardElementClass(this.configElementType!); + + // Check if a schema exists + if (elClass && elClass.getConfigForm) { + return elClass.getConfigForm(); + } + + return undefined; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-badge-element-editor": HuiBadgeElementEditor; + } +} diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts new file mode 100644 index 000000000000..f326063eb651 --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -0,0 +1,546 @@ +import { mdiClose, mdiHelpCircle } from "@mdi/js"; +import deepFreeze from "deep-freeze"; +import { + CSSResultGroup, + LitElement, + PropertyValues, + css, + html, + nothing, +} from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import type { HASSDomEvent } from "../../../../common/dom/fire_event"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import "../../../../components/ha-circular-progress"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-icon-button"; +import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; +import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; +import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; +import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; +import { + getCustomCardEntry, + isCustomType, + stripCustomPrefix, +} from "../../../../data/lovelace_custom_cards"; +import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; +import "../../cards/hui-badge"; +import "../../sections/hui-section"; +import { addBadge, replaceBadge } from "../config-util"; +import { getCardDocumentationURL } from "../get-card-documentation-url"; +import type { ConfigChangedEvent } from "../hui-element-editor"; +import { findLovelaceContainer } from "../lovelace-path"; +import type { GUIModeChangedEvent } from "../types"; +import "./hui-badge-element-editor"; +import type { HuiBadgeElementEditor } from "./hui-badge-element-editor"; +import type { EditBadgeDialogParams } from "./show-edit-badge-dialog"; + +declare global { + // for fire event + interface HASSDomEvents { + "reload-lovelace": undefined; + } + // for add event listener + interface HTMLElementEventMap { + "reload-lovelace": HASSDomEvent; + } +} + +@customElement("hui-dialog-edit-badge") +export class HuiDialogEditBadge + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) public large = false; + + @state() private _params?: EditBadgeDialogParams; + + @state() private _badgeConfig?: LovelaceBadgeConfig; + + @state() private _containerConfig!: + | LovelaceViewConfig + | LovelaceSectionConfig; + + @state() private _saving = false; + + @state() private _error?: string; + + @state() private _guiModeAvailable? = true; + + @query("hui-badge-element-editor") + private _badgeEditorEl?: HuiBadgeElementEditor; + + @state() private _GUImode = true; + + @state() private _documentationURL?: string; + + @state() private _dirty = false; + + @state() private _isEscapeEnabled = true; + + public async showDialog(params: EditBadgeDialogParams): Promise { + this._params = params; + this._GUImode = true; + this._guiModeAvailable = true; + + const containerConfig = findLovelaceContainer( + params.lovelaceConfig, + params.path + ); + + if ("strategy" in containerConfig) { + throw new Error("Can't edit strategy"); + } + + this._containerConfig = containerConfig; + + if ("badgeConfig" in params) { + this._badgeConfig = params.badgeConfig; + this._dirty = true; + } else { + this._badgeConfig = this._containerConfig.badges?.[params.badgeIndex]; + } + + this.large = false; + if (this._badgeConfig && !Object.isFrozen(this._badgeConfig)) { + this._badgeConfig = deepFreeze(this._badgeConfig); + } + } + + public closeDialog(): boolean { + this._isEscapeEnabled = true; + window.removeEventListener("dialog-closed", this._enableEscapeKeyClose); + window.removeEventListener("hass-more-info", this._disableEscapeKeyClose); + if (this._dirty) { + this._confirmCancel(); + return false; + } + this._params = undefined; + this._badgeConfig = undefined; + this._error = undefined; + this._documentationURL = undefined; + this._dirty = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + return true; + } + + protected updated(changedProps: PropertyValues): void { + if ( + !this._badgeConfig || + this._documentationURL !== undefined || + !changedProps.has("_badgeConfig") + ) { + return; + } + + const oldConfig = changedProps.get("_badgeConfig") as LovelaceCardConfig; + + if (oldConfig?.type !== this._badgeConfig!.type) { + this._documentationURL = this._badgeConfig!.type + ? getCardDocumentationURL(this.hass, this._badgeConfig!.type) + : undefined; + } + } + + private _enableEscapeKeyClose = (ev: any) => { + if (ev.detail.dialog === "ha-more-info-dialog") { + this._isEscapeEnabled = true; + } + }; + + private _disableEscapeKeyClose = () => { + this._isEscapeEnabled = false; + }; + + protected render() { + if (!this._params) { + return nothing; + } + + let heading: string; + if (this._badgeConfig && this._badgeConfig.type) { + let badgeName: string | undefined; + if (isCustomType(this._badgeConfig.type)) { + // prettier-ignore + badgeName = getCustomCardEntry( + stripCustomPrefix(this._badgeConfig.type) + )?.name; + // Trim names that end in " Card" so as not to redundantly duplicate it + if (badgeName?.toLowerCase().endsWith(" card")) { + badgeName = badgeName.substring(0, badgeName.length - 5); + } + } else { + badgeName = this.hass!.localize( + `ui.panel.lovelace.editor.card.${this._badgeConfig.type}.name` + ); + } + heading = this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.typed_header", + { type: badgeName } + ); + } else if (!this._badgeConfig) { + heading = this._containerConfig.title + ? this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.pick_card_view_title", + { name: this._containerConfig.title } + ) + : this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card"); + } else { + heading = this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.header" + ); + } + + return html` + + + + ${heading} + ${this._documentationURL !== undefined + ? html` + + + + ` + : nothing} + +
    +
    + +
    +
    + + ${this._error + ? html` + + ` + : ``} +
    +
    + ${this._badgeConfig !== undefined + ? html` + + ${this.hass!.localize( + !this._badgeEditorEl || this._GUImode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + + ` + : ""} +
    + + ${this.hass!.localize("ui.common.cancel")} + + ${this._badgeConfig !== undefined && this._dirty + ? html` + + ${this._saving + ? html` + + ` + : this.hass!.localize("ui.common.save")} + + ` + : ``} +
    +
    + `; + } + + private _enlarge() { + this.large = !this.large; + } + + private _ignoreKeydown(ev: KeyboardEvent) { + ev.stopPropagation(); + } + + private _handleConfigChanged(ev: HASSDomEvent) { + this._badgeConfig = deepFreeze(ev.detail.config); + this._error = ev.detail.error; + this._guiModeAvailable = ev.detail.guiModeAvailable; + this._dirty = true; + } + + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._GUImode = ev.detail.guiMode; + this._guiModeAvailable = ev.detail.guiModeAvailable; + } + + private _toggleMode(): void { + this._badgeEditorEl?.toggleMode(); + } + + private _opened() { + window.addEventListener("dialog-closed", this._enableEscapeKeyClose); + window.addEventListener("hass-more-info", this._disableEscapeKeyClose); + this._badgeEditorEl?.focusYamlEditor(); + } + + private get _canSave(): boolean { + if (this._saving) { + return false; + } + if (this._badgeConfig === undefined) { + return false; + } + if (this._badgeEditorEl && this._badgeEditorEl.hasError) { + return false; + } + return true; + } + + private async _confirmCancel() { + // Make sure the open state of this dialog is handled before the open state of confirm dialog + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + const confirm = await showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.unsaved_changes" + ), + text: this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.confirm_cancel" + ), + dismissText: this.hass!.localize("ui.common.stay"), + confirmText: this.hass!.localize("ui.common.leave"), + }); + if (confirm) { + this._cancel(); + } + } + + private _cancel(ev?: Event) { + if (ev) { + ev.stopPropagation(); + } + this._dirty = false; + this.closeDialog(); + } + + private async _save(): Promise { + if (!this._canSave) { + return; + } + if (!this._dirty) { + this.closeDialog(); + return; + } + this._saving = true; + const path = this._params!.path; + await this._params!.saveConfig( + "badgeConfig" in this._params! + ? addBadge(this._params!.lovelaceConfig, path, this._badgeConfig!) + : replaceBadge( + this._params!.lovelaceConfig, + [...path, this._params!.badgeIndex], + this._badgeConfig! + ) + ); + this._saving = false; + this._dirty = false; + showSaveSuccessToast(this, this.hass); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + :host { + --code-mirror-max-height: calc(100vh - 176px); + } + + ha-dialog { + --mdc-dialog-max-width: 100px; + --dialog-z-index: 6; + --dialog-surface-position: fixed; + --dialog-surface-top: 40px; + --mdc-dialog-max-width: 90vw; + --dialog-content-padding: 24px 12px; + } + + .content { + width: calc(90vw - 48px); + max-width: 1000px; + } + + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + height: 100%; + --mdc-dialog-max-height: 100%; + --dialog-surface-top: 0px; + --mdc-dialog-max-width: 100vw; + } + .content { + width: 100%; + max-width: 100%; + } + } + + @media all and (min-width: 451px) and (min-height: 501px) { + :host([large]) .content { + max-width: none; + } + } + + .center { + margin-left: auto; + margin-right: auto; + } + + .content { + display: flex; + flex-direction: column; + } + + .content hui-badge { + display: block; + padding: 4px; + margin: 0 auto; + max-width: 390px; + } + .content hui-section { + display: block; + padding: 4px; + margin: 0 auto; + max-width: var(--ha-view-sections-column-max-width, 500px); + } + .content .element-editor { + margin: 0 10px; + } + + @media (min-width: 1000px) { + .content { + flex-direction: row; + } + .content > * { + flex-basis: 0; + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + } + .content hui-badge { + padding: 8px 10px; + margin: auto 0px; + max-width: 500px; + } + .content hui-section { + padding: 8px 10px; + margin: auto 0px; + max-width: var(--ha-view-sections-column-max-width, 500px); + } + } + .hidden { + display: none; + } + .element-editor { + margin-bottom: 8px; + } + .blur { + filter: blur(2px) grayscale(100%); + } + .element-preview { + position: relative; + height: max-content; + background: var(--primary-background-color); + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + } + .element-preview ha-circular-progress { + top: 50%; + left: 50%; + position: absolute; + z-index: 10; + } + hui-badge { + padding-top: 8px; + margin-bottom: 4px; + display: block; + box-sizing: border-box; + } + .gui-mode-button { + margin-right: auto; + margin-inline-end: auto; + margin-inline-start: initial; + } + .header { + display: flex; + align-items: center; + justify-content: space-between; + } + ha-dialog-header a { + color: inherit; + text-decoration: none; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-edit-badge": HuiDialogEditBadge; + } +} diff --git a/src/panels/lovelace/editor/badge-editor/show-edit-badge-dialog.ts b/src/panels/lovelace/editor/badge-editor/show-edit-badge-dialog.ts new file mode 100644 index 000000000000..e7640e988780 --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/show-edit-badge-dialog.ts @@ -0,0 +1,30 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; +import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; +import { LovelaceContainerPath } from "../lovelace-path"; + +export type EditBadgeDialogParams = { + lovelaceConfig: LovelaceConfig; + saveConfig: (config: LovelaceConfig) => void; + path: LovelaceContainerPath; +} & ( + | { + badgeIndex: number; + } + | { + badgeConfig: LovelaceBadgeConfig; + } +); + +export const importEditBadgeDialog = () => import("./hui-dialog-edit-badge"); + +export const showEditBadgeDialog = ( + element: HTMLElement, + editBadgeDialogParams: EditBadgeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-edit-badge", + dialogImport: importEditBadgeDialog, + dialogParams: editBadgeDialogParams, + }); +}; diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index eb9294f9e555..f4a551b45eed 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -31,7 +31,7 @@ import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; import "../../sections/hui-section"; -import { addCard, replaceCard } from "../config-util"; +import { addCard, replaceBadge } from "../config-util"; import { getCardDocumentationURL } from "../get-card-documentation-url"; import type { ConfigChangedEvent } from "../hui-element-editor"; import { findLovelaceContainer } from "../lovelace-path"; @@ -431,7 +431,7 @@ export class HuiDialogEditCard await this._params!.saveConfig( "cardConfig" in this._params! ? addCard(this._params!.lovelaceConfig, path, this._cardConfig!) - : replaceCard( + : replaceBadge( this._params!.lovelaceConfig, [...path, this._params!.cardIndex], this._cardConfig! diff --git a/src/panels/lovelace/editor/config-util.ts b/src/panels/lovelace/editor/config-util.ts index 0207a90e974d..7eb5d5f105e2 100644 --- a/src/panels/lovelace/editor/config-util.ts +++ b/src/panels/lovelace/editor/config-util.ts @@ -1,3 +1,4 @@ +import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section"; import { LovelaceConfig } from "../../../data/lovelace/config/types"; @@ -9,13 +10,13 @@ import type { HomeAssistant } from "../../../types"; import { LovelaceCardPath, LovelaceContainerPath, - findLovelaceCards, findLovelaceContainer, + findLovelaceItems, getLovelaceContainerPath, parseLovelaceCardPath, parseLovelaceContainerPath, - updateLovelaceCards, updateLovelaceContainer, + updateLovelaceItems, } from "./lovelace-path"; export const addCard = ( @@ -23,9 +24,9 @@ export const addCard = ( path: LovelaceContainerPath, cardConfig: LovelaceCardConfig ): LovelaceConfig => { - const cards = findLovelaceCards(config, path); + const cards = findLovelaceItems("cards", config, path); const newCards = cards ? [...cards, cardConfig] : [cardConfig]; - const newConfig = updateLovelaceCards(config, path, newCards); + const newConfig = updateLovelaceItems("cards", config, path, newCards); return newConfig; }; @@ -34,9 +35,9 @@ export const addCards = ( path: LovelaceContainerPath, cardConfigs: LovelaceCardConfig[] ): LovelaceConfig => { - const cards = findLovelaceCards(config, path); + const cards = findLovelaceItems("cards", config, path); const newCards = cards ? [...cards, ...cardConfigs] : [...cardConfigs]; - const newConfig = updateLovelaceCards(config, path, newCards); + const newConfig = updateLovelaceItems("cards", config, path, newCards); return newConfig; }; @@ -48,13 +49,18 @@ export const replaceCard = ( const { cardIndex } = parseLovelaceCardPath(path); const containerPath = getLovelaceContainerPath(path); - const cards = findLovelaceCards(config, containerPath); + const cards = findLovelaceItems("cards", config, containerPath); const newCards = (cards ?? []).map((origConf, ind) => ind === cardIndex ? cardConfig : origConf ); - const newConfig = updateLovelaceCards(config, containerPath, newCards); + const newConfig = updateLovelaceItems( + "cards", + config, + containerPath, + newCards + ); return newConfig; }; @@ -65,11 +71,16 @@ export const deleteCard = ( const { cardIndex } = parseLovelaceCardPath(path); const containerPath = getLovelaceContainerPath(path); - const cards = findLovelaceCards(config, containerPath); + const cards = findLovelaceItems("cards", config, containerPath); const newCards = (cards ?? []).filter((_origConf, ind) => ind !== cardIndex); - const newConfig = updateLovelaceCards(config, containerPath, newCards); + const newConfig = updateLovelaceItems( + "cards", + config, + containerPath, + newCards + ); return newConfig; }; @@ -81,13 +92,18 @@ export const insertCard = ( const { cardIndex } = parseLovelaceCardPath(path); const containerPath = getLovelaceContainerPath(path); - const cards = findLovelaceCards(config, containerPath); + const cards = findLovelaceItems("cards", config, containerPath); const newCards = cards ? [...cards.slice(0, cardIndex), cardConfig, ...cards.slice(cardIndex)] : [cardConfig]; - const newConfig = updateLovelaceCards(config, containerPath, newCards); + const newConfig = updateLovelaceItems( + "cards", + config, + containerPath, + newCards + ); return newConfig; }; @@ -99,7 +115,7 @@ export const moveCardToIndex = ( const { cardIndex } = parseLovelaceCardPath(path); const containerPath = getLovelaceContainerPath(path); - const cards = findLovelaceCards(config, containerPath); + const cards = findLovelaceItems("cards", config, containerPath); const newCards = cards ? [...cards] : []; @@ -110,7 +126,12 @@ export const moveCardToIndex = ( newCards.splice(oldIndex, 1); newCards.splice(newIndex, 0, card); - const newConfig = updateLovelaceCards(config, containerPath, newCards); + const newConfig = updateLovelaceItems( + "cards", + config, + containerPath, + newCards + ); return newConfig; }; @@ -132,7 +153,7 @@ export const moveCardToContainer = ( } const fromContainerPath = getLovelaceContainerPath(fromPath); - const cards = findLovelaceCards(config, fromContainerPath); + const cards = findLovelaceItems("cards", config, fromContainerPath); const card = cards![fromCardIndex]; let newConfig = addCard(config, toPath, card); @@ -148,7 +169,7 @@ export const moveCard = ( ): LovelaceConfig => { const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath); const fromContainerPath = getLovelaceContainerPath(fromPath); - const cards = findLovelaceCards(config, fromContainerPath); + const cards = findLovelaceItems("cards", config, fromContainerPath); const card = cards![fromCardIndex]; let newConfig = deleteCard(config, fromPath); @@ -298,3 +319,98 @@ export const moveSection = ( return newConfig; }; + +export const addBadge = ( + config: LovelaceConfig, + path: LovelaceContainerPath, + badgeConfig: LovelaceBadgeConfig +): LovelaceConfig => { + const badges = findLovelaceItems("badges", config, path); + const newBadges = badges ? [...badges, badgeConfig] : [badgeConfig]; + const newConfig = updateLovelaceItems("badges", config, path, newBadges); + return newConfig; +}; + +export const replaceBadge = ( + config: LovelaceConfig, + path: LovelaceCardPath, + cardConfig: LovelaceBadgeConfig +): LovelaceConfig => { + const { cardIndex } = parseLovelaceCardPath(path); + const containerPath = getLovelaceContainerPath(path); + + const badges = findLovelaceItems("badges", config, containerPath); + + const newBadges = (badges ?? []).map((origConf, ind) => + ind === cardIndex ? cardConfig : origConf + ); + + const newConfig = updateLovelaceItems( + "badges", + config, + containerPath, + newBadges + ); + return newConfig; +}; + +export const deleteBadge = ( + config: LovelaceConfig, + path: LovelaceCardPath +): LovelaceConfig => { + const { cardIndex } = parseLovelaceCardPath(path); + const containerPath = getLovelaceContainerPath(path); + + const badges = findLovelaceItems("badges", config, containerPath); + + const newBadges = (badges ?? []).filter( + (_origConf, ind) => ind !== cardIndex + ); + + const newConfig = updateLovelaceItems( + "badges", + config, + containerPath, + newBadges + ); + return newConfig; +}; + +export const insertBadge = ( + config: LovelaceConfig, + path: LovelaceCardPath, + badgeConfig: LovelaceBadgeConfig +) => { + const { cardIndex } = parseLovelaceCardPath(path); + const containerPath = getLovelaceContainerPath(path); + + const badges = findLovelaceItems("badges", config, containerPath); + + const newBadges = badges + ? [...badges.slice(0, cardIndex), badgeConfig, ...badges.slice(cardIndex)] + : [badgeConfig]; + + const newConfig = updateLovelaceItems( + "badges", + config, + containerPath, + newBadges + ); + return newConfig; +}; + +export const moveBadge = ( + config: LovelaceConfig, + fromPath: LovelaceCardPath, + toPath: LovelaceCardPath +): LovelaceConfig => { + const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath); + const fromContainerPath = getLovelaceContainerPath(fromPath); + const badges = findLovelaceItems("badges", config, fromContainerPath); + const badge = badges![fromCardIndex]; + + let newConfig = deleteBadge(config, fromPath); + newConfig = insertBadge(newConfig, toPath, badge); + + return newConfig; +}; diff --git a/src/panels/lovelace/editor/lovelace-path.ts b/src/panels/lovelace/editor/lovelace-path.ts index d4527126ba23..9a105bea2441 100644 --- a/src/panels/lovelace/editor/lovelace-path.ts +++ b/src/panels/lovelace/editor/lovelace-path.ts @@ -1,3 +1,4 @@ +import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceSectionRawConfig, @@ -80,35 +81,6 @@ export const findLovelaceContainer: FindLovelaceContainer = ( return section; }; -export const findLovelaceCards = ( - config: LovelaceConfig, - path: LovelaceContainerPath -): LovelaceCardConfig[] | undefined => { - const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path); - - const view = config.views[viewIndex]; - - if (!view) { - throw new Error("View does not exist"); - } - if (isStrategyView(view)) { - throw new Error("Can not find cards in a strategy view"); - } - if (sectionIndex === undefined) { - return view.cards; - } - - const section = view.sections?.[sectionIndex]; - - if (!section) { - throw new Error("Section does not exist"); - } - if (isStrategySection(section)) { - throw new Error("Can not find cards in a strategy section"); - } - return section.cards; -}; - export const updateLovelaceContainer = ( config: LovelaceConfig, path: LovelaceContainerPath, @@ -153,10 +125,16 @@ export const updateLovelaceContainer = ( }; }; -export const updateLovelaceCards = ( +type LovelaceItemKeys = { + cards: LovelaceCardConfig[]; + badges: LovelaceBadgeConfig[]; +}; + +export const updateLovelaceItems = ( + key: T, config: LovelaceConfig, path: LovelaceContainerPath, - cards: LovelaceCardConfig[] + items: LovelaceItemKeys[T] ): LovelaceConfig => { const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path); @@ -164,13 +142,13 @@ export const updateLovelaceCards = ( const newViews = config.views.map((view, vIndex) => { if (vIndex !== viewIndex) return view; if (isStrategyView(view)) { - throw new Error("Can not update cards in a strategy view"); + throw new Error(`Can not update ${key} in a strategy view`); } if (sectionIndex === undefined) { updated = true; return { ...view, - cards, + [key]: items, }; } @@ -181,12 +159,12 @@ export const updateLovelaceCards = ( const newSections = view.sections.map((section, sIndex) => { if (sIndex !== sectionIndex) return section; if (isStrategySection(section)) { - throw new Error("Can not update cards in a strategy section"); + throw new Error(`Can not update ${key} in a strategy section`); } updated = true; return { ...section, - cards, + [key]: items, }; }); return { @@ -196,10 +174,40 @@ export const updateLovelaceCards = ( }); if (!updated) { - throw new Error("Can not update cards in a non-existing view/section"); + throw new Error(`Can not update ${key} in a non-existing view/section`); } return { ...config, views: newViews, }; }; + +export const findLovelaceItems = ( + key: T, + config: LovelaceConfig, + path: LovelaceContainerPath +): LovelaceItemKeys[T] | undefined => { + const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path); + + const view = config.views[viewIndex]; + + if (!view) { + throw new Error("View does not exist"); + } + if (isStrategyView(view)) { + throw new Error("Can not find cards in a strategy view"); + } + if (sectionIndex === undefined) { + return view[key] as LovelaceItemKeys[T] | undefined; + } + + const section = view.sections?.[sectionIndex]; + + if (!section) { + throw new Error("Section does not exist"); + } + if (isStrategySection(section)) { + throw new Error("Can not find cards in a strategy section"); + } + return view[key] as LovelaceItemKeys[T] | undefined; +}; diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 4c7ae4d164bc..a90bde32a6ac 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -107,6 +107,10 @@ export interface LovelaceCardEditor extends LovelaceGenericElementEditor { setConfig(config: LovelaceCardConfig): void; } +export interface LovelaceBadgeEditor extends LovelaceGenericElementEditor { + setConfig(config: LovelaceBadgeConfig): void; +} + export interface LovelaceHeaderFooterEditor extends LovelaceGenericElementEditor { setConfig(config: LovelaceHeaderFooterConfig): void; diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index ea0bb5a89512..069ce5b3f6b7 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -22,6 +22,7 @@ import type { HuiCard } from "../cards/hui-card"; import { processConfigEntities } from "../common/process-config-entities"; import { createBadgeElement } from "../create-element/create-badge-element"; import { createViewElement } from "../create-element/create-view-element"; +import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog"; import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { deleteCard } from "../editor/config-util"; @@ -43,11 +44,13 @@ declare global { "ll-create-card": { suggested?: string[] } | undefined; "ll-edit-card": { path: LovelaceCardPath }; "ll-delete-card": { path: LovelaceCardPath; confirm: boolean }; + "ll-edit-badge": { path: LovelaceCardPath }; } interface HTMLElementEventMap { "ll-create-card": HASSDomEvent; "ll-edit-card": HASSDomEvent; "ll-delete-card": HASSDomEvent; + "ll-edit-badge": HASSDomEvent; } } @@ -329,6 +332,15 @@ export class HUIView extends ReactiveElement { this.lovelace.saveConfig(newLovelace); } }); + this._layoutElement.addEventListener("ll-edit-badge", (ev) => { + const { cardIndex } = parseLovelaceCardPath(ev.detail.path); + showEditBadgeDialog(this, { + lovelaceConfig: this.lovelace.config, + saveConfig: this.lovelace.saveConfig, + path: [this.index], + badgeIndex: cardIndex, + }); + }); } private _createBadges(config: LovelaceViewConfig): void { From d761dc38c91cbefe5b2d6c2ae49f413b0b27a367 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 9 Jul 2024 14:34:16 +0200 Subject: [PATCH 05/32] Increase height --- src/panels/lovelace/badges/hui-entity-badge.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 5944a410afd8..574d9b960d15 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -116,12 +116,12 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { flex-direction: row; align-items: center; gap: 8px; - height: 32px; + height: 36px; padding: 6px 16px 6px 12px; margin: calc(-1 * var(--ha-card-border-width, 1px)); box-sizing: border-box; width: auto; - border-radius: 16px; + border-radius: 18px; background-color: var(--card-background-color, white); border-width: var(--ha-card-border-width, 1px); border-style: solid; From 60e02b93af29544e62c62010a4f9afab686ae002 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 9 Jul 2024 14:42:09 +0200 Subject: [PATCH 06/32] Use hui-badge --- src/data/lovelace.ts | 5 +- src/panels/lovelace/views/hui-view.ts | 75 +++++++-------------------- 2 files changed, 23 insertions(+), 57 deletions(-) diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index bfd794ef8de0..81c80faaee4d 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -3,9 +3,10 @@ import { getCollection, HassEventBase, } from "home-assistant-js-websocket"; +import { HuiBadge } from "../panels/lovelace/cards/hui-badge"; import type { HuiCard } from "../panels/lovelace/cards/hui-card"; import type { HuiSection } from "../panels/lovelace/sections/hui-section"; -import { Lovelace, LovelaceBadge } from "../panels/lovelace/types"; +import { Lovelace } from "../panels/lovelace/types"; import { HomeAssistant } from "../types"; import { LovelaceSectionConfig } from "./lovelace/config/section"; import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types"; @@ -21,7 +22,7 @@ export interface LovelaceViewElement extends HTMLElement { narrow?: boolean; index?: number; cards?: HuiCard[]; - badges?: LovelaceBadge[]; + badges?: HuiBadge[]; sections?: HuiSection[]; isStrategy: boolean; setConfig(config: LovelaceViewConfig): void; diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 069ce5b3f6b7..08176dee94d8 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -13,14 +13,11 @@ import { isStrategyView, } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; -import { - createErrorBadgeConfig, - createErrorBadgeElement, -} from "../badges/hui-error-badge"; +import "../cards/hui-badge"; +import type { HuiBadge } from "../cards/hui-badge"; import "../cards/hui-card"; import type { HuiCard } from "../cards/hui-card"; import { processConfigEntities } from "../common/process-config-entities"; -import { createBadgeElement } from "../create-element/create-badge-element"; import { createViewElement } from "../create-element/create-view-element"; import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog"; import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; @@ -35,7 +32,7 @@ import { createErrorSectionConfig } from "../sections/hui-error-section"; import "../sections/hui-section"; import type { HuiSection } from "../sections/hui-section"; import { generateLovelaceViewStrategy } from "../strategies/get-strategy"; -import type { Lovelace, LovelaceBadge } from "../types"; +import type { Lovelace } from "../types"; import { DEFAULT_VIEW_LAYOUT, PANEL_VIEW_LAYOUT } from "./const"; declare global { @@ -66,7 +63,7 @@ export class HUIView extends ReactiveElement { @state() private _cards: HuiCard[] = []; - @state() private _badges: LovelaceBadge[] = []; + @state() private _badges: HuiBadge[] = []; @state() private _sections: HuiSection[] = []; @@ -89,20 +86,16 @@ export class HUIView extends ReactiveElement { return element; } - public createBadgeElement(badgeConfig: LovelaceBadgeConfig) { - const element = createBadgeElement(badgeConfig) as LovelaceBadge; - try { - element.hass = this.hass; - } catch (e: any) { - return createErrorBadgeElement(createErrorBadgeConfig(e.message)); - } - element.addEventListener( - "ll-badge-rebuild", - () => { - this._rebuildBadge(element, badgeConfig); - }, - { once: true } - ); + public _createBadgeElement(badgeConfig: LovelaceBadgeConfig) { + const element = document.createElement("hui-badge"); + element.hass = this.hass; + element.preview = this.lovelace.editMode; + element.config = badgeConfig; + element.addEventListener("badge-updated", (ev: Event) => { + ev.stopPropagation(); + this._badges = [...this._badges]; + }); + element.load(); return element; } @@ -172,11 +165,7 @@ export class HUIView extends ReactiveElement { // Config has not changed. Just props if (changedProperties.has("hass")) { this._badges.forEach((badge) => { - try { - badge.hass = this.hass; - } catch (e: any) { - this._rebuildBadge(badge, createErrorBadgeConfig(e.message)); - } + badge.hass = this.hass; }); this._cards.forEach((element) => { @@ -349,14 +338,11 @@ export class HUIView extends ReactiveElement { return; } - const badges = processConfigEntities(config.badges as any); - this._badges = badges.map((badge) => { - const element = createBadgeElement(badge); - try { - element.hass = this.hass; - } catch (e: any) { - return createErrorBadgeElement(createErrorBadgeConfig(e.message)); - } + const badges = processConfigEntities( + config.badges as any + ) as LovelaceBadgeConfig[]; + this._badges = badges.map((badgeConfig) => { + const element = this._createBadgeElement(badgeConfig); return element; }); } @@ -386,27 +372,6 @@ export class HUIView extends ReactiveElement { }); } - private _rebuildBadge( - badgeElToReplace: LovelaceBadge, - config: LovelaceBadgeConfig - ): void { - let newBadgeEl = this.createBadgeElement(config); - try { - newBadgeEl.hass = this.hass; - } catch (e: any) { - newBadgeEl = createErrorBadgeElement(createErrorBadgeConfig(e.message)); - } - if (badgeElToReplace.parentElement) { - badgeElToReplace.parentElement!.replaceChild( - newBadgeEl, - badgeElToReplace - ); - } - this._badges = this._badges!.map((curBadgeEl) => - curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl - ); - } - private _rebuildSection( sectionElToReplace: HuiSection, config: LovelaceSectionConfig From 2484a8dea64b5aa4e42c42531f53982843877f9c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 9 Jul 2024 15:17:32 +0200 Subject: [PATCH 07/32] Add editor --- .../lovelace/badges/hui-entity-badge.ts | 15 ++++- src/panels/lovelace/badges/types.ts | 5 +- .../components/hui-badge-edit-mode.ts | 35 +--------- .../create-element/create-badge-element.ts | 10 ++- .../create-element/create-element-base.ts | 3 +- .../badge-editor/hui-badge-element-editor.ts | 6 +- .../hui-entity-badge-editor.ts | 60 +++++++++++++++++ src/panels/lovelace/types.ts | 10 +++ .../lovelace/views/hui-sections-view.ts | 64 ++++++++++++++++--- src/panels/lovelace/views/hui-view.ts | 20 +++++- 10 files changed, 177 insertions(+), 51 deletions(-) create mode 100644 src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 574d9b960d15..4246ad356027 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -21,6 +21,11 @@ import { EntityBadgeConfig } from "./types"; @customElement("hui-entity-badge") export class HuiEntityBadge extends LitElement implements LovelaceBadge { + public static async getConfigForm() { + return (await import("../editor/config-elements/hui-entity-badge-editor")) + .default; + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() protected _config?: EntityBadgeConfig; @@ -74,7 +79,12 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { return nothing; } - const stateObj = this.hass.states[this._config.entity!]; + const entityId = this._config.entity; + const stateObj = entityId ? this.hass.states[entityId] : undefined; + + if (!stateObj) { + return nothing; + } const color = this._computeStateColor(stateObj, this._config.color); @@ -112,13 +122,12 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { --ha-ripple-color: var(--badge-color); --ha-ripple-hover-opacity: 0.04; --ha-ripple-pressed-opacity: 0.12; - display: inline-flex; + display: flex; flex-direction: row; align-items: center; gap: 8px; height: 36px; padding: 6px 16px 6px 12px; - margin: calc(-1 * var(--ha-card-border-width, 1px)); box-sizing: border-box; width: auto; border-radius: 18px; diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index 434c0f7c6069..9011c3e7e640 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -28,7 +28,10 @@ export interface StateLabelBadgeConfig extends LovelaceBadgeConfig { export interface EntityBadgeConfig extends LovelaceBadgeConfig { type: "entity"; - entity: string; + entity?: string; icon?: string; + color?: string; tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; } diff --git a/src/panels/lovelace/components/hui-badge-edit-mode.ts b/src/panels/lovelace/components/hui-badge-edit-mode.ts index dbc8e16d731b..a2355346f44b 100644 --- a/src/panels/lovelace/components/hui-badge-edit-mode.ts +++ b/src/panels/lovelace/components/hui-badge-edit-mode.ts @@ -1,14 +1,10 @@ -import "@material/mwc-button"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { - mdiContentCopy, - mdiContentCut, mdiContentDuplicate, mdiDelete, mdiDotsVertical, mdiPencil, } from "@mdi/js"; -import deepClone from "deep-clone-simple"; import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -138,14 +134,6 @@ export class HuiCardEditMode extends LitElement { "ui.panel.lovelace.editor.edit_card.duplicate" )} - - - ${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")} - - - - ${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")} -
  • ${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")} @@ -174,13 +162,7 @@ export class HuiCardEditMode extends LitElement { this._duplicateCard(); break; case 1: - this._copyCard(); - break; - case 2: - this._cutCard(); - break; - case 3: - this._deleteCard(true); + this._deleteCard(); break; } } @@ -209,19 +191,8 @@ export class HuiCardEditMode extends LitElement { fireEvent(this, "ll-edit-badge", { path: this.path! }); } - private _cutCard(): void { - this._copyCard(); - this._deleteCard(false); - } - - private _copyCard(): void { - const { cardIndex } = parseLovelaceCardPath(this.path!); - const cardConfig = this._badges[cardIndex]; - this._clipboard = deepClone(cardConfig); - } - - private _deleteCard(confirm: boolean): void { - fireEvent(this, "ll-delete-card", { path: this.path!, confirm }); + private _deleteCard(): void { + fireEvent(this, "ll-delete-badge", { path: this.path! }); } static get styles(): CSSResultGroup { diff --git a/src/panels/lovelace/create-element/create-badge-element.ts b/src/panels/lovelace/create-element/create-badge-element.ts index 51c4d622f19a..a6f8efeb6383 100644 --- a/src/panels/lovelace/create-element/create-badge-element.ts +++ b/src/panels/lovelace/create-element/create-badge-element.ts @@ -1,7 +1,10 @@ import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; -import "../badges/hui-state-label-badge"; import "../badges/hui-entity-badge"; -import { createLovelaceElement } from "./create-element-base"; +import "../badges/hui-state-label-badge"; +import { + createLovelaceElement, + getLovelaceElementClass, +} from "./create-element-base"; const ALWAYS_LOADED_TYPES = new Set(["error", "state-label", "entity"]); const LAZY_LOAD_TYPES = { @@ -17,3 +20,6 @@ export const createBadgeElement = (config: LovelaceBadgeConfig) => undefined, "entity" ); + +export const getBadgeElementClass = (type: string) => + getLovelaceElementClass(type, "badge", ALWAYS_LOADED_TYPES, LAZY_LOAD_TYPES); diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index 4c2a6490ea11..a70d30b49775 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -19,6 +19,7 @@ import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types"; import { LovelaceBadge, + LovelaceBadgeConstructor, LovelaceCard, LovelaceCardConstructor, LovelaceCardFeature, @@ -39,7 +40,7 @@ interface CreateElementConfigTypes { badge: { config: LovelaceBadgeConfig; element: LovelaceBadge; - constructor: unknown; + constructor: LovelaceBadgeConstructor; }; element: { config: LovelaceElementConfig; 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 5746ee81e9cf..d00a7c58d043 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 @@ -1,13 +1,13 @@ import { customElement } from "lit/decorators"; import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; -import { getCardElementClass } from "../../create-element/create-card-element"; +import { getBadgeElementClass } from "../../create-element/create-badge-element"; import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types"; import { HuiElementEditor } from "../hui-element-editor"; @customElement("hui-badge-element-editor") export class HuiBadgeElementEditor extends HuiElementEditor { protected async getConfigElement(): Promise { - const elClass = await getCardElementClass(this.configElementType!); + const elClass = await getBadgeElementClass(this.configElementType!); // Check if a GUI editor exists if (elClass && elClass.getConfigElement) { @@ -18,7 +18,7 @@ export class HuiBadgeElementEditor extends HuiElementEditor } protected async getConfigForm(): Promise { - const elClass = await getCardElementClass(this.configElementType!); + const elClass = await getBadgeElementClass(this.configElementType!); // Check if a schema exists if (elClass && elClass.getConfigForm) { diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts new file mode 100644 index 000000000000..b1c94502b1be --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -0,0 +1,60 @@ +import { assert, assign, object, optional, string } from "superstruct"; +import { LocalizeFunc } from "../../../../common/translations/localize"; +import { HaFormSchema } from "../../../../components/ha-form/types"; +import { EntityCardConfig } from "../../cards/types"; +import { LovelaceConfigForm } from "../../types"; +import { actionConfigStruct } from "../structs/action-struct"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; + +const struct = assign( + baseLovelaceCardConfig, + object({ + entity: optional(string()), + icon: optional(string()), + color: optional(string()), + tap_action: optional(actionConfigStruct), + }) +); + +const SCHEMA = [ + { name: "entity", required: true, selector: { entity: {} } }, + { + type: "grid", + name: "", + schema: [ + { + name: "icon", + selector: { + icon: {}, + }, + context: { + icon_entity: "entity", + }, + }, + { + name: "color", + selector: { + ui_color: { + default_color: true, + }, + }, + }, + ], + }, + { name: "tap_action", selector: { ui_action: {} } }, +] as HaFormSchema[]; + +const entityBadgeConfigForm: LovelaceConfigForm = { + schema: SCHEMA, + assertConfig: (config: EntityCardConfig) => assert(config, struct), + computeLabel: (schema: HaFormSchema, localize: LocalizeFunc) => { + if (schema.name === "theme") { + return `${localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${localize("ui.panel.lovelace.editor.card.config.optional")})`; + } + return localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + }, +}; + +export default entityBadgeConfigForm; diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index a90bde32a6ac..5a522ea156e3 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -82,6 +82,16 @@ export interface LovelaceCardConstructor extends Constructor { getConfigForm?: () => LovelaceConfigForm; } +export interface LovelaceBadgeConstructor extends Constructor { + getStubConfig?: ( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ) => LovelaceBadgeConfig; + getConfigElement?: () => LovelaceBadgeEditor; + getConfigForm?: () => LovelaceConfigForm; +} + export interface LovelaceHeaderFooterConstructor extends Constructor { getStubConfig?: ( diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index 5efd6b8711bb..728b1d842953 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -1,4 +1,10 @@ -import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js"; +import { + mdiArrowAll, + mdiDelete, + mdiPencil, + mdiPlus, + mdiViewGridPlus, +} from "@mdi/js"; import { CSSResultGroup, LitElement, @@ -10,6 +16,7 @@ import { import { customElement, property, state } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; import { styleMap } from "lit/directives/style-map"; +import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-icon-button"; import "../../../components/ha-sortable"; import "../../../components/ha-svg-icon"; @@ -17,12 +24,13 @@ import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; +import { HuiBadge } from "../cards/hui-badge"; import "../components/hui-badge-edit-mode"; import { addSection, deleteSection, moveSection } from "../editor/config-util"; import { findLovelaceContainer } from "../editor/lovelace-path"; import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog"; import { HuiSection } from "../sections/hui-section"; -import type { Lovelace, LovelaceBadge } from "../types"; +import type { Lovelace } from "../types"; @customElement("hui-sections-view") export class SectionsView extends LitElement implements LovelaceViewElement { @@ -36,7 +44,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { @property({ attribute: false }) public sections: HuiSection[] = []; - @property({ attribute: false }) public badges: LovelaceBadge[] = []; + @property({ attribute: false }) public badges: HuiBadge[] = []; @state() private _config?: LovelaceViewConfig; @@ -48,9 +56,9 @@ export class SectionsView extends LitElement implements LovelaceViewElement { private _sectionConfigKeys = new WeakMap(); - private _badgeConfigKeys = new WeakMap(); + private _badgeConfigKeys = new WeakMap(); - private _getBadgeKey(badge: LovelaceBadge) { + private _getBadgeKey(badge: HuiBadge) { if (!this._badgeConfigKeys.has(badge)) { this._badgeConfigKeys.set(badge, Math.random().toString()); } @@ -117,6 +125,22 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
    ` )} + ${editMode + ? html` + + ` + : nothing}
    ` : nothing} @@ -175,7 +199,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { ${editMode ? html` + +
    + ${repeat( + badges, + (badge) => this._getBadgeKey(badge), + (badge, idx) => html` +
    + ${editMode + ? html` + + ${badge} + + ` + : badge} +
    ` - : nothing} -
    + )} + ${editMode + ? html` + + ` + : nothing} + +
    ` : nothing} Date: Mon, 15 Jul 2024 14:00:54 +0200 Subject: [PATCH 09/32] Fix editor translations --- src/data/lovelace.ts | 2 +- .../lovelace/{cards => badges}/hui-badge.ts | 0 .../badge-editor/hui-dialog-edit-badge.ts | 33 +++++++++---------- .../card-editor/hui-dialog-edit-card.ts | 2 +- ....ts => get-dashboard-documentation-url.ts} | 13 +++++++- .../lovelace/views/hui-sections-view.ts | 2 +- src/panels/lovelace/views/hui-view.ts | 4 +-- src/translations/en.json | 29 ++++++++++++++-- 8 files changed, 59 insertions(+), 26 deletions(-) rename src/panels/lovelace/{cards => badges}/hui-badge.ts (100%) rename src/panels/lovelace/editor/{get-card-documentation-url.ts => get-dashboard-documentation-url.ts} (56%) diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 81c80faaee4d..1b4f47aa3803 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -3,7 +3,7 @@ import { getCollection, HassEventBase, } from "home-assistant-js-websocket"; -import { HuiBadge } from "../panels/lovelace/cards/hui-badge"; +import { HuiBadge } from "../panels/lovelace/badges/hui-badge"; import type { HuiCard } from "../panels/lovelace/cards/hui-card"; import type { HuiSection } from "../panels/lovelace/sections/hui-section"; import { Lovelace } from "../panels/lovelace/types"; diff --git a/src/panels/lovelace/cards/hui-badge.ts b/src/panels/lovelace/badges/hui-badge.ts similarity index 100% rename from src/panels/lovelace/cards/hui-badge.ts rename to src/panels/lovelace/badges/hui-badge.ts diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts index f326063eb651..608df6905864 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -17,7 +17,6 @@ import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; import "../../../../components/ha-icon-button"; import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; -import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import { @@ -30,10 +29,10 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; -import "../../cards/hui-badge"; +import "../../badges/hui-badge"; import "../../sections/hui-section"; import { addBadge, replaceBadge } from "../config-util"; -import { getCardDocumentationURL } from "../get-card-documentation-url"; +import { getBadgeDocumentationURL } from "../get-dashboard-documentation-url"; import type { ConfigChangedEvent } from "../hui-element-editor"; import { findLovelaceContainer } from "../lovelace-path"; import type { GUIModeChangedEvent } from "../types"; @@ -141,11 +140,11 @@ export class HuiDialogEditBadge return; } - const oldConfig = changedProps.get("_badgeConfig") as LovelaceCardConfig; + const oldConfig = changedProps.get("_badgeConfig") as LovelaceBadgeConfig; if (oldConfig?.type !== this._badgeConfig!.type) { this._documentationURL = this._badgeConfig!.type - ? getCardDocumentationURL(this.hass, this._badgeConfig!.type) + ? getBadgeDocumentationURL(this.hass, this._badgeConfig!.type) : undefined; } } @@ -174,28 +173,28 @@ export class HuiDialogEditBadge stripCustomPrefix(this._badgeConfig.type) )?.name; // Trim names that end in " Card" so as not to redundantly duplicate it - if (badgeName?.toLowerCase().endsWith(" card")) { - badgeName = badgeName.substring(0, badgeName.length - 5); + if (badgeName?.toLowerCase().endsWith(" badge")) { + badgeName = badgeName.substring(0, badgeName.length - 6); } } else { badgeName = this.hass!.localize( - `ui.panel.lovelace.editor.card.${this._badgeConfig.type}.name` + `ui.panel.lovelace.editor.badge.${this._badgeConfig.type}.name` ); } heading = this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.typed_header", + "ui.panel.lovelace.editor.edit_badge.typed_header", { type: badgeName } ); } else if (!this._badgeConfig) { heading = this._containerConfig.title ? this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.pick_card_view_title", + "ui.panel.lovelace.editor.edit_badge.pick_badge_view_title", { name: this._containerConfig.title } ) - : this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card"); + : this.hass!.localize("ui.panel.lovelace.editor.edit_badge.pick_badge"); } else { heading = this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.header" + "ui.panel.lovelace.editor.edit_badge.header" ); } @@ -255,7 +254,7 @@ export class HuiDialogEditBadge ? html` ` : ``} @@ -271,8 +270,8 @@ export class HuiDialogEditBadge > ${this.hass!.localize( !this._badgeEditorEl || this._GUImode - ? "ui.panel.lovelace.editor.edit_card.show_code_editor" - : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + ? "ui.panel.lovelace.editor.edit_badge.show_code_editor" + : "ui.panel.lovelace.editor.edit_badge.show_visual_editor" )} ` @@ -355,10 +354,10 @@ export class HuiDialogEditBadge }); const confirm = await showConfirmationDialog(this, { title: this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.unsaved_changes" + "ui.panel.lovelace.editor.edit_badge.unsaved_changes" ), text: this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.confirm_cancel" + "ui.panel.lovelace.editor.edit_badge.confirm_cancel" ), dismissText: this.hass!.localize("ui.common.stay"), confirmText: this.hass!.localize("ui.common.leave"), diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index f4a551b45eed..d753befa3998 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -32,7 +32,7 @@ import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; import "../../sections/hui-section"; import { addCard, replaceBadge } from "../config-util"; -import { getCardDocumentationURL } from "../get-card-documentation-url"; +import { getCardDocumentationURL } from "../get-dashboard-documentation-url"; import type { ConfigChangedEvent } from "../hui-element-editor"; import { findLovelaceContainer } from "../lovelace-path"; import type { GUIModeChangedEvent } from "../types"; diff --git a/src/panels/lovelace/editor/get-card-documentation-url.ts b/src/panels/lovelace/editor/get-dashboard-documentation-url.ts similarity index 56% rename from src/panels/lovelace/editor/get-card-documentation-url.ts rename to src/panels/lovelace/editor/get-dashboard-documentation-url.ts index e312463d4b76..76ea49131592 100644 --- a/src/panels/lovelace/editor/get-card-documentation-url.ts +++ b/src/panels/lovelace/editor/get-dashboard-documentation-url.ts @@ -14,5 +14,16 @@ export const getCardDocumentationURL = ( return getCustomCardEntry(stripCustomPrefix(type))?.documentationURL; } - return `${documentationUrl(hass, "/lovelace/")}${type}`; + return `${documentationUrl(hass, "/dashboards/")}${type}`; +}; + +export const getBadgeDocumentationURL = ( + hass: HomeAssistant, + type: string +): string | undefined => { + if (isCustomType(type)) { + return getCustomCardEntry(stripCustomPrefix(type))?.documentationURL; + } + + return `${documentationUrl(hass, "/dashboards/badges")}`; }; diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index b62cdbc1fef7..d85415180e4f 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -25,7 +25,7 @@ import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; -import { HuiBadge } from "../cards/hui-badge"; +import { HuiBadge } from "../badges/hui-badge"; import "../components/hui-badge-edit-mode"; import { addSection, diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index eeaa61aef827..08619d292028 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -13,8 +13,8 @@ import { isStrategyView, } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; -import "../cards/hui-badge"; -import type { HuiBadge } from "../cards/hui-badge"; +import "../badges/hui-badge"; +import type { HuiBadge } from "../badges/hui-badge"; import "../cards/hui-card"; import type { HuiCard } from "../cards/hui-card"; import { processConfigEntities } from "../common/process-config-entities"; diff --git a/src/translations/en.json b/src/translations/en.json index 4302e553afac..a0cbc4d4a5ab 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5559,11 +5559,28 @@ "tab_layout": "Layout", "visibility": { "explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown." - }, - "layout": { - "explanation": "Configure how the card will appear on the dashboard. This settings will override the default size and position of the card." } }, + "edit_badge": { + "header": "Badge configuration", + "typed_header": "{type} Badge configuration", + "pick_badge": "Which badge would you like to add?", + "pick_badge_title": "Which badge would you like to add to {name}", + "toggle_editor": "[%key:ui::panel::lovelace::editor::edit_card::toggle_editor%]", + "unsaved_changes": "[%key:ui::panel::lovelace::editor::edit_card::unsaved_changes%]", + "confirm_cancel": "[%key:ui::panel::lovelace::editor::edit_card::confirm_cancel%]", + "show_visual_editor": "[%key:ui::panel::lovelace::editor::edit_card::show_visual_editor%]", + "show_code_editor": "[%key:ui::panel::lovelace::editor::edit_card::show_code_editor%]", + "edit_ui": "[%key:ui::panel::config::automation::editor::edit_ui%]", + "edit_yaml": "[%key:ui::panel::config::automation::editor::edit_yaml%]", + "add": "Add badge", + "edit": "[%key:ui::panel::lovelace::editor::edit_card::edit%]", + "clear": "[%key:ui::panel::lovelace::editor::edit_card::clear%]", + "delete": "[%key:ui::panel::lovelace::editor::edit_card::delete%]", + "copy": "[%key:ui::panel::lovelace::editor::edit_card::copy%]", + "cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]", + "duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]" + }, "move_card": { "header": "Choose a view to move the card to", "error_title": "Impossible to move the card", @@ -6011,6 +6028,12 @@ "twice_daily": "Twice daily" } }, + "badge": { + "entity": { + "name": "Entity", + "description": "The Entity badge gives you a quick overview of your entity's state." + } + }, "features": { "name": "Features", "not_compatible": "Not compatible", From 609f102ea11fe3aac99b3121cf5ef77f43149cc4 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 14:03:16 +0200 Subject: [PATCH 10/32] Fix icon --- src/panels/lovelace/badges/hui-entity-badge.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 4246ad356027..47b2a5553d85 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -105,7 +105,11 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { tabindex=${ifDefined(this.hasAction ? "0" : undefined)} > - + ${this.hass.formatEntityState(stateObj)} `; From b1e6eb485eb14b02ecba1ca31960c52a0b2126d2 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 14:05:31 +0200 Subject: [PATCH 11/32] Fix inactive color --- src/panels/lovelace/badges/hui-entity-badge.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 47b2a5553d85..2594c7787b86 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -122,6 +122,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { static get styles(): CSSResultGroup { return css` .badge { + --badge-color: var(--state-inactive-color); position: relative; --ha-ripple-color: var(--badge-color); --ha-ripple-hover-opacity: 0.04; From 72c12ec4768e9c2e99c0a6ba43be650d254d3bba Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 14:28:09 +0200 Subject: [PATCH 12/32] Add state content --- .../lovelace/badges/hui-entity-badge.ts | 19 +- src/panels/lovelace/cards/hui-tile-card.ts | 4 +- .../hui-entity-badge-editor.ts | 217 ++++++++++++++---- src/translations/en.json | 7 +- 4 files changed, 196 insertions(+), 51 deletions(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 2594c7787b86..f783175b78d9 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -16,14 +16,14 @@ 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 { LovelaceBadge } from "../types"; +import { LovelaceBadge, LovelaceBadgeEditor } from "../types"; import { EntityBadgeConfig } from "./types"; @customElement("hui-entity-badge") export class HuiEntityBadge extends LitElement implements LovelaceBadge { - public static async getConfigForm() { - return (await import("../editor/config-elements/hui-entity-badge-editor")) - .default; + public static async getConfigElement(): Promise { + await import("../editor/config-elements/hui-entity-badge-editor"); + return document.createElement("hui-entity-badge-editor"); } @property({ attribute: false }) public hass?: HomeAssistant; @@ -92,6 +92,15 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { "--badge-color": color, }; + const stateDisplay = html` + + + `; + return html`
    - ${this.hass.formatEntityState(stateObj)} + ${stateDisplay}
    `; } diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index 50d283814339..d35f44aa7aac 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -244,7 +244,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { const color = this._computeStateColor(stateObj, this._config.color); const domain = computeDomain(stateObj.entity_id); - const localizedState = this._config.hide_state + const stateDisplay = this._config.hide_state ? nothing : html` ${this._config.features diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts index b1c94502b1be..c7cf90b907fa 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -1,60 +1,191 @@ -import { assert, assign, object, optional, string } from "superstruct"; +import { mdiGestureTap, mdiPalette } from "@mdi/js"; +import { LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { + array, + assert, + assign, + object, + optional, + string, + union, +} from "superstruct"; +import { fireEvent } from "../../../../common/dom/fire_event"; import { LocalizeFunc } from "../../../../common/translations/localize"; -import { HaFormSchema } from "../../../../components/ha-form/types"; -import { EntityCardConfig } from "../../cards/types"; -import { LovelaceConfigForm } from "../../types"; +import "../../../../components/ha-form/ha-form"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../types"; +import { EntityBadgeConfig } from "../../badges/types"; +import type { LovelaceBadgeEditor } from "../../types"; +import "../hui-sub-element-editor"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { configElementStyle } from "./config-elements-style"; +import "./hui-card-features-editor"; -const struct = assign( +const badgeConfigStruct = assign( baseLovelaceCardConfig, object({ entity: optional(string()), icon: optional(string()), + state_content: optional(union([string(), array(string())])), color: optional(string()), tap_action: optional(actionConfigStruct), }) ); -const SCHEMA = [ - { name: "entity", required: true, selector: { entity: {} } }, - { - type: "grid", - name: "", - schema: [ - { - name: "icon", - selector: { - icon: {}, - }, - context: { - icon_entity: "entity", +@customElement("hui-entity-badge-editor") +export class HuiEntityBadgeEditor + extends LitElement + implements LovelaceBadgeEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EntityBadgeConfig; + + public setConfig(config: EntityBadgeConfig): void { + assert(config, badgeConfigStruct); + this._config = config; + } + + private _schema = memoizeOne( + (localize: LocalizeFunc) => + [ + { name: "entity", selector: { entity: {} } }, + { + name: "", + type: "expandable", + iconPath: mdiPalette, + title: localize(`ui.panel.lovelace.editor.badge.entity.appearance`), + schema: [ + { + name: "", + type: "grid", + schema: [ + { + name: "icon", + selector: { + icon: {}, + }, + context: { icon_entity: "entity" }, + }, + { + name: "color", + selector: { + ui_color: { default_color: true }, + }, + }, + ], + }, + + { + name: "state_content", + selector: { + ui_state_content: {}, + }, + context: { + filter_entity: "entity", + }, + }, + ], }, - }, - { - name: "color", - selector: { - ui_color: { - default_color: true, - }, + { + name: "", + type: "expandable", + title: localize(`ui.panel.lovelace.editor.badge.entity.actions`), + iconPath: mdiGestureTap, + schema: [ + { + name: "tap_action", + selector: { + ui_action: { + default_action: "more-info", + }, + }, + }, + ], }, - }, - ], - }, - { name: "tap_action", selector: { ui_action: {} } }, -] as HaFormSchema[]; - -const entityBadgeConfigForm: LovelaceConfigForm = { - schema: SCHEMA, - assertConfig: (config: EntityCardConfig) => assert(config, struct), - computeLabel: (schema: HaFormSchema, localize: LocalizeFunc) => { - if (schema.name === "theme") { - return `${localize( - "ui.panel.lovelace.editor.card.generic.theme" - )} (${localize("ui.panel.lovelace.editor.card.config.optional")})`; + ] as const satisfies readonly HaFormSchema[] + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; } - return localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); - }, -}; -export default entityBadgeConfigForm; + const schema = this._schema(this.hass!.localize); + + const data = this._config; + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + + const newConfig = ev.detail.value as EntityBadgeConfig; + + const config: EntityBadgeConfig = { + ...newConfig, + }; + + if (!config.state_content) { + delete config.state_content; + } + + fireEvent(this, "config-changed", { config }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "color": + case "state_content": + return this.hass!.localize( + `ui.panel.lovelace.editor.badge.entity.${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-entity-badge-editor": HuiEntityBadgeEditor; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index a0cbc4d4a5ab..423ccccc48f4 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6031,7 +6031,12 @@ "badge": { "entity": { "name": "Entity", - "description": "The Entity badge gives you a quick overview of your entity's state." + "description": "The Entity badge gives you a quick overview of your entity.", + "color": "Color", + "actions": "Actions", + "appearance": "Appearance", + "default_color": "Default color (state)", + "state_content": "State content" } }, "features": { From 638632ead2d7a42bcc314bb7c93f0f55b6db25e3 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 14:43:23 +0200 Subject: [PATCH 13/32] Add default config --- .../lovelace/badges/hui-entity-badge.ts | 22 ++++++++++++++++ .../lovelace/editor/get-badge-stub-config.ts | 26 +++++++++++++++++++ src/panels/lovelace/views/hui-view.ts | 14 +++++++--- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 src/panels/lovelace/editor/get-badge-stub-config.ts diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index f783175b78d9..01e182cfad3e 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -18,6 +18,7 @@ import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { LovelaceBadge, LovelaceBadgeEditor } from "../types"; import { EntityBadgeConfig } from "./types"; +import { findEntities } from "../common/find-entities"; @customElement("hui-entity-badge") export class HuiEntityBadge extends LitElement implements LovelaceBadge { @@ -26,6 +27,27 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { return document.createElement("hui-entity-badge-editor"); } + public static getStubConfig( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ): EntityBadgeConfig { + const includeDomains = ["sensor", "light", "switch"]; + const maxEntities = 1; + const foundEntities = findEntities( + hass, + maxEntities, + entities, + entitiesFallback, + includeDomains + ); + + return { + type: "entity", + entity: foundEntities[0] || "", + }; + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() protected _config?: EntityBadgeConfig; diff --git a/src/panels/lovelace/editor/get-badge-stub-config.ts b/src/panels/lovelace/editor/get-badge-stub-config.ts new file mode 100644 index 000000000000..63c64e57a82a --- /dev/null +++ b/src/panels/lovelace/editor/get-badge-stub-config.ts @@ -0,0 +1,26 @@ +import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; +import { HomeAssistant } from "../../../types"; +import { getBadgeElementClass } from "../create-element/create-badge-element"; + +export const getBadgeStubConfig = async ( + hass: HomeAssistant, + type: string, + entities: string[], + entitiesFallback: string[] +): Promise => { + let badgeConfig: LovelaceCardConfig = { type }; + + const elClass = await getBadgeElementClass(type); + + if (elClass && elClass.getStubConfig) { + const classStubConfig = await elClass.getStubConfig( + hass, + entities, + entitiesFallback + ); + + badgeConfig = { ...badgeConfig, ...classStubConfig }; + } + + return badgeConfig; +}; diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 08619d292028..a4391a753b0d 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -24,6 +24,7 @@ import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dia import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { deleteBadge, deleteCard } from "../editor/config-util"; import { confDeleteCard } from "../editor/delete-card"; +import { getBadgeStubConfig } from "../editor/get-badge-stub-config"; import { LovelaceCardPath, parseLovelaceCardPath, @@ -325,14 +326,19 @@ export class HUIView extends ReactiveElement { this.lovelace.saveConfig(newLovelace); } }); - this._layoutElement.addEventListener("ll-create-badge", () => { + this._layoutElement.addEventListener("ll-create-badge", async () => { + const defaultConfig = await getBadgeStubConfig( + this.hass, + "entity", + Object.keys(this.hass.entities), + [] + ); + showEditBadgeDialog(this, { lovelaceConfig: this.lovelace.config, saveConfig: this.lovelace.saveConfig, path: [this.index], - badgeConfig: { - type: "entity", - }, + badgeConfig: defaultConfig, }); }); this._layoutElement.addEventListener("ll-edit-badge", (ev) => { From b2c7d0bf4ff1c4f8f3cc8fb11d5b0065c2ab28cf Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 14:53:12 +0200 Subject: [PATCH 14/32] Fix types --- src/panels/lovelace/views/hui-masonry-view.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts index 520856ad7ba5..3edf99193f8e 100644 --- a/src/panels/lovelace/views/hui-masonry-view.ts +++ b/src/panels/lovelace/views/hui-masonry-view.ts @@ -15,9 +15,10 @@ import "../../../components/ha-svg-icon"; import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; +import { HuiBadge } from "../badges/hui-badge"; import { HuiCard } from "../cards/hui-card"; import { computeCardSize } from "../common/compute-card-size"; -import type { Lovelace, LovelaceBadge } from "../types"; +import type { Lovelace } from "../types"; // Find column with < 5 size, else smallest column const getColumnIndex = (columnSizes: number[], size: number) => { @@ -50,7 +51,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement { @property({ attribute: false }) public cards: HuiCard[] = []; - @property({ attribute: false }) public badges: LovelaceBadge[] = []; + @property({ attribute: false }) public badges: HuiBadge[] = []; @state() private _columns?: number; From ded9346a61ec91ab39aef4614e91ced95cdc1e68 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 14:56:57 +0200 Subject: [PATCH 15/32] Add custom badge support to editor --- src/data/lovelace_custom_cards.ts | 18 +++++++++++++++++- .../badge-editor/hui-dialog-edit-badge.ts | 4 ++-- .../editor/get-dashboard-documentation-url.ts | 3 ++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/data/lovelace_custom_cards.ts b/src/data/lovelace_custom_cards.ts index 0e4a7d79ae8c..67a795045928 100644 --- a/src/data/lovelace_custom_cards.ts +++ b/src/data/lovelace_custom_cards.ts @@ -8,6 +8,14 @@ export interface CustomCardEntry { documentationURL?: string; } +export interface CustomBadgeEntry { + type: string; + name?: string; + description?: string; + preview?: boolean; + documentationURL?: string; +} + export interface CustomCardFeatureEntry { type: string; name?: string; @@ -18,6 +26,7 @@ export interface CustomCardFeatureEntry { export interface CustomCardsWindow { customCards?: CustomCardEntry[]; customCardFeatures?: CustomCardFeatureEntry[]; + customBadges?: CustomBadgeEntry[]; /** * @deprecated Use customCardFeatures */ @@ -34,8 +43,11 @@ if (!("customCards" in customCardsWindow)) { if (!("customCardFeatures" in customCardsWindow)) { customCardsWindow.customCardFeatures = []; } +if (!("customBadges" in customCardsWindow)) { + customCardsWindow.customCardFeatures = []; +} if (!("customTileFeatures" in customCardsWindow)) { - customCardsWindow.customTileFeatures = []; + customCardsWindow.customBadges = []; } export const customCards = customCardsWindow.customCards!; @@ -43,10 +55,14 @@ export const getCustomCardFeatures = () => [ ...customCardsWindow.customCardFeatures!, ...customCardsWindow.customTileFeatures!, ]; +export const customBadges = customCardsWindow.customBadges!; export const getCustomCardEntry = (type: string) => customCards.find((card) => card.type === type); +export const getCustomBadgeEntry = (type: string) => + customBadges.find((badge) => badge.type === type); + export const isCustomType = (type: string) => type.startsWith(CUSTOM_TYPE_PREFIX); diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts index 608df6905864..cb349d950640 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -20,7 +20,7 @@ import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import { - getCustomCardEntry, + getCustomBadgeEntry, isCustomType, stripCustomPrefix, } from "../../../../data/lovelace_custom_cards"; @@ -169,7 +169,7 @@ export class HuiDialogEditBadge let badgeName: string | undefined; if (isCustomType(this._badgeConfig.type)) { // prettier-ignore - badgeName = getCustomCardEntry( + badgeName = getCustomBadgeEntry( stripCustomPrefix(this._badgeConfig.type) )?.name; // Trim names that end in " Card" so as not to redundantly duplicate it diff --git a/src/panels/lovelace/editor/get-dashboard-documentation-url.ts b/src/panels/lovelace/editor/get-dashboard-documentation-url.ts index 76ea49131592..aada4e5978e3 100644 --- a/src/panels/lovelace/editor/get-dashboard-documentation-url.ts +++ b/src/panels/lovelace/editor/get-dashboard-documentation-url.ts @@ -1,4 +1,5 @@ import { + getCustomBadgeEntry, getCustomCardEntry, isCustomType, stripCustomPrefix, @@ -22,7 +23,7 @@ export const getBadgeDocumentationURL = ( type: string ): string | undefined => { if (isCustomType(type)) { - return getCustomCardEntry(stripCustomPrefix(type))?.documentationURL; + return getCustomBadgeEntry(stripCustomPrefix(type))?.documentationURL; } return `${documentationUrl(hass, "/dashboards/badges")}`; From d4c57050531754416c8a973e5aee2014e3af2d4c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 15:19:38 +0200 Subject: [PATCH 16/32] Fix custom badges --- src/data/lovelace_custom_cards.ts | 4 ++-- src/panels/lovelace/badges/hui-entity-badge.ts | 14 +++++++++++--- src/panels/lovelace/views/hui-sections-view.ts | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/data/lovelace_custom_cards.ts b/src/data/lovelace_custom_cards.ts index 67a795045928..c79cfb4584e8 100644 --- a/src/data/lovelace_custom_cards.ts +++ b/src/data/lovelace_custom_cards.ts @@ -44,10 +44,10 @@ if (!("customCardFeatures" in customCardsWindow)) { customCardsWindow.customCardFeatures = []; } if (!("customBadges" in customCardsWindow)) { - customCardsWindow.customCardFeatures = []; + customCardsWindow.customBadges = []; } if (!("customTileFeatures" in customCardsWindow)) { - customCardsWindow.customBadges = []; + customCardsWindow.customTileFeatures = []; } export const customCards = customCardsWindow.customCards!; diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 01e182cfad3e..9ee1e851af6e 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -1,6 +1,7 @@ import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; @@ -14,11 +15,11 @@ import "../../../components/ha-state-icon"; import { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; +import { findEntities } from "../common/find-entities"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { LovelaceBadge, LovelaceBadgeEditor } from "../types"; import { EntityBadgeConfig } from "./types"; -import { findEntities } from "../common/find-entities"; @customElement("hui-entity-badge") export class HuiEntityBadge extends LitElement implements LovelaceBadge { @@ -108,6 +109,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { return nothing; } + const active = stateActive(stateObj); const color = this._computeStateColor(stateObj, this._config.color); const style = { @@ -126,7 +128,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { return html`
    0 + ${badges?.length > 0 || editMode ? html` Date: Mon, 15 Jul 2024 15:47:51 +0200 Subject: [PATCH 17/32] Add new badges to masonry view --- src/panels/lovelace/badges/hui-view-badges.ts | 173 ++++++++++++++++++ src/panels/lovelace/views/hui-masonry-view.ts | 16 +- .../lovelace/views/hui-sections-view.ts | 164 ++--------------- 3 files changed, 196 insertions(+), 157 deletions(-) create mode 100644 src/panels/lovelace/badges/hui-view-badges.ts diff --git a/src/panels/lovelace/badges/hui-view-badges.ts b/src/panels/lovelace/badges/hui-view-badges.ts new file mode 100644 index 000000000000..2c0cee46fa08 --- /dev/null +++ b/src/panels/lovelace/badges/hui-view-badges.ts @@ -0,0 +1,173 @@ +import { mdiPlus } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-sortable"; +import type { HaSortableOptions } from "../../../components/ha-sortable"; +import "../../../components/ha-svg-icon"; +import { HomeAssistant } from "../../../types"; +import "../components/hui-badge-edit-mode"; +import { moveBadge } from "../editor/config-util"; +import { Lovelace } from "../types"; +import { HuiBadge } from "./hui-badge"; + +const BADGE_SORTABLE_OPTIONS: HaSortableOptions = { + delay: 100, + delayOnTouchOnly: true, + direction: "horizontal", + invertedSwapThreshold: 0.7, +} as HaSortableOptions; + +@customElement("hui-view-badges") +export class HuiStateLabelBadge extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public lovelace!: Lovelace; + + @property({ attribute: false }) public badges: HuiBadge[] = []; + + @property({ attribute: false }) public viewIndex!: number; + + @state() _dragging = false; + + private _badgeConfigKeys = new WeakMap(); + + private _getBadgeKey(badge: HuiBadge) { + if (!this._badgeConfigKeys.has(badge)) { + this._badgeConfigKeys.set(badge, Math.random().toString()); + } + return this._badgeConfigKeys.get(badge)!; + } + + private _badgeMoved(ev) { + ev.stopPropagation(); + const { oldIndex, newIndex, oldPath, newPath } = ev.detail; + const newConfig = moveBadge( + this.lovelace!.config, + [...oldPath, oldIndex] as [number, number, number], + [...newPath, newIndex] as [number, number, number] + ); + this.lovelace!.saveConfig(newConfig); + } + + private _dragStart() { + this._dragging = true; + } + + private _dragEnd() { + this._dragging = false; + } + + private _addBadge() { + fireEvent(this, "ll-create-badge"); + } + + render() { + if (!this.lovelace) return nothing; + + const editMode = this.lovelace.editMode; + + const badges = this.badges; + + return html` + ${badges?.length > 0 || editMode + ? html` + +
    + ${repeat( + badges, + (badge) => this._getBadgeKey(badge), + (badge, idx) => html` +
    + ${editMode + ? html` + + ${badge} + + ` + : badge} +
    + ` + )} + ${editMode + ? html` + + ` + : nothing} +
    +
    + ` + : nothing} + `; + } + + static get styles(): CSSResultGroup { + return css` + .badges { + display: flex; + align-items: flex-start; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + margin: 0; + } + + .badge { + display: block; + position: relative; + } + + .add { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + height: 36px; + padding: 6px 20px 6px 20px; + box-sizing: border-box; + width: auto; + border-radius: 18px; + background-color: transparent; + border-width: 2px; + border-style: dashed; + border-color: var(--primary-color); + --mdc-icon-size: 18px; + cursor: pointer; + color: var(--primary-text-color); + } + .add:focus { + border-style: solid; + } + `; + } +} diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts index 3edf99193f8e..500bc441cf56 100644 --- a/src/panels/lovelace/views/hui-masonry-view.ts +++ b/src/panels/lovelace/views/hui-masonry-view.ts @@ -16,6 +16,7 @@ import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; import { HuiBadge } from "../badges/hui-badge"; +import "../badges/hui-view-badges"; import { HuiCard } from "../cards/hui-card"; import { computeCardSize } from "../common/compute-card-size"; import type { Lovelace } from "../types"; @@ -79,9 +80,12 @@ export class MasonryView extends LitElement implements LovelaceViewElement { protected render(): TemplateResult { return html` - ${this.badges.length > 0 - ? html`
    ${this.badges}
    ` - : ""} +
    (); - private _badgeConfigKeys = new WeakMap(); - - private _getBadgeKey(badge: HuiBadge) { - if (!this._badgeConfigKeys.has(badge)) { - this._badgeConfigKeys.set(badge, Math.random().toString()); - } - return this._badgeConfigKeys.get(badge)!; - } - private _getSectionKey(section: HuiSection) { if (!this._sectionConfigKeys.has(section)) { this._sectionConfigKeys.set(section, Math.random().toString()); @@ -115,65 +87,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement { const maxColumnsCount = this._config?.max_columns; - const badges = this.badges; - return html` - ${badges?.length > 0 || editMode - ? html` - -
    - ${repeat( - badges, - (badge) => this._getBadgeKey(badge), - (badge, idx) => html` -
    - ${editMode - ? html` - - ${badge} - - ` - : badge} -
    - ` - )} - ${editMode - ? html` - - ` - : nothing} -
    -
    - ` - : nothing} + * { position: relative; max-width: var(--column-max-width); @@ -455,27 +334,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement { border-radius: var(--ha-card-border-radius, 12px); } - .add-badge { - position: relative; - display: flex; - flex-direction: row; - align-items: center; - gap: 8px; - height: 36px; - padding: 6px 20px 6px 20px; - box-sizing: border-box; - width: auto; - border-radius: 18px; - background-color: transparent; - border-width: 2px; - border-style: dashed; - border-color: var(--primary-color); - --mdc-icon-size: 18px; - cursor: pointer; - color: var(--primary-text-color); - } - .add-badge:focus { - border-style: solid; + hui-view-badges { + display: block; + margin: 16px 32px; + text-align: center; } `; } From e5b9701928cc6736832b578dc2a4da056d12b079 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 15 Jul 2024 16:04:02 +0200 Subject: [PATCH 18/32] fix lint --- src/panels/lovelace/badges/hui-view-badges.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/badges/hui-view-badges.ts b/src/panels/lovelace/badges/hui-view-badges.ts index 2c0cee46fa08..34071bbd365a 100644 --- a/src/panels/lovelace/badges/hui-view-badges.ts +++ b/src/panels/lovelace/badges/hui-view-badges.ts @@ -20,7 +20,7 @@ const BADGE_SORTABLE_OPTIONS: HaSortableOptions = { } as HaSortableOptions; @customElement("hui-view-badges") -export class HuiStateLabelBadge extends LitElement { +export class HuiViewBadges extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public lovelace!: Lovelace; @@ -171,3 +171,9 @@ export class HuiStateLabelBadge extends LitElement { `; } } + +declare global { + interface HTMLElementTagNameMap { + "hui-view-badges": HuiViewBadges; + } +} From 815ef6bc19ea81588ac388d210ac06eb2f2d7527 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 16 Jul 2024 10:32:08 +0200 Subject: [PATCH 19/32] Fix inactive color --- src/panels/lovelace/badges/hui-entity-badge.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 9ee1e851af6e..cdfb302162d9 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -154,7 +154,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { static get styles(): CSSResultGroup { return css` - :host() { + :host { --badge-color: var(--state-inactive-color); -webkit-tap-highlight-color: transparent; } @@ -195,6 +195,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { } ha-state-icon { color: var(--badge-color); + line-height: 0; } `; } From 69f30d8c64ffd941608ca3d94a1ad1d2ebeaef00 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 16 Jul 2024 18:54:13 +0200 Subject: [PATCH 20/32] Fix entity filter card --- .../badges/hui-entity-filter-badge.ts | 17 +++++++--- src/panels/lovelace/badges/hui-view-badges.ts | 28 ++++++++-------- .../badge-editor/hui-dialog-edit-badge.ts | 32 ++----------------- 3 files changed, 29 insertions(+), 48 deletions(-) diff --git a/src/panels/lovelace/badges/hui-entity-filter-badge.ts b/src/panels/lovelace/badges/hui-entity-filter-badge.ts index e6a889febe87..5cba77434c91 100644 --- a/src/panels/lovelace/badges/hui-entity-filter-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-filter-badge.ts @@ -8,9 +8,10 @@ import { checkConditionsMet, extractConditionEntityIds, } from "../common/validate-condition"; -import { createBadgeElement } from "../create-element/create-badge-element"; import { EntityFilterEntityConfig } from "../entity-rows/types"; import { LovelaceBadge } from "../types"; +import "./hui-badge"; +import type { HuiBadge } from "./hui-badge"; import { EntityFilterBadgeConfig } from "./types"; @customElement("hui-entity-filter-badge") @@ -18,11 +19,13 @@ export class HuiEntityFilterBadge extends ReactiveElement implements LovelaceBadge { + @property({ attribute: false }) public preview = false; + @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EntityFilterBadgeConfig; - private _elements?: LovelaceBadge[]; + private _elements?: HuiBadge[]; private _configEntities?: EntityFilterEntityConfig[]; @@ -121,8 +124,11 @@ export class HuiEntityFilterBadge if (!isSame) { this._elements = []; for (const badgeConfig of entitiesList) { - const element = createBadgeElement(badgeConfig); + const element = document.createElement("hui-badge"); element.hass = this.hass; + element.preview = this.preview; + element.config = badgeConfig; + element.load(); this._elements.push(element); } this._oldEntities = entitiesList; @@ -140,7 +146,10 @@ export class HuiEntityFilterBadge this.appendChild(element); } - this.style.display = "inline"; + this.style.display = "flex"; + this.style.flexWrap = "wrap"; + this.style.justifyContent = "center"; + this.style.gap = "8px"; } private haveEntitiesChanged(oldHass?: HomeAssistant): boolean { diff --git a/src/panels/lovelace/badges/hui-view-badges.ts b/src/panels/lovelace/badges/hui-view-badges.ts index 34071bbd365a..80aef231ea96 100644 --- a/src/panels/lovelace/badges/hui-view-badges.ts +++ b/src/panels/lovelace/badges/hui-view-badges.ts @@ -91,20 +91,18 @@ export class HuiViewBadges extends LitElement { badges, (badge) => this._getBadgeKey(badge), (badge, idx) => html` -
    - ${editMode - ? html` - - ${badge} - - ` - : badge} -
    + ${editMode + ? html` + + ${badge} + + ` + : badge} ` )} ${editMode @@ -141,7 +139,7 @@ export class HuiViewBadges extends LitElement { margin: 0; } - .badge { + hui-badge-edit-mode { display: block; position: relative; } diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts index cb349d950640..5d67dd630816 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -452,18 +452,6 @@ export class HuiDialogEditBadge flex-direction: column; } - .content hui-badge { - display: block; - padding: 4px; - margin: 0 auto; - max-width: 390px; - } - .content hui-section { - display: block; - padding: 4px; - margin: 0 auto; - max-width: var(--ha-view-sections-column-max-width, 500px); - } .content .element-editor { margin: 0 10px; } @@ -478,16 +466,6 @@ export class HuiDialogEditBadge flex-shrink: 1; min-width: 0; } - .content hui-badge { - padding: 8px 10px; - margin: auto 0px; - max-width: 500px; - } - .content hui-section { - padding: 8px 10px; - margin: auto 0px; - max-width: var(--ha-view-sections-column-max-width, 500px); - } } .hidden { display: none; @@ -502,9 +480,11 @@ export class HuiDialogEditBadge position: relative; height: max-content; background: var(--primary-background-color); - padding: 4px; + padding: 10px; border-radius: 4px; display: flex; + flex-direction: column; + justify-content: center; align-items: center; } .element-preview ha-circular-progress { @@ -513,12 +493,6 @@ export class HuiDialogEditBadge position: absolute; z-index: 10; } - hui-badge { - padding-top: 8px; - margin-bottom: 4px; - display: block; - box-sizing: border-box; - } .gui-mode-button { margin-right: auto; margin-inline-end: auto; From ad848d980273f9fba8f2cb30464ba86747f5d9b7 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 09:53:31 +0200 Subject: [PATCH 21/32] Add display type option --- .../lovelace/badges/hui-entity-badge.ts | 55 ++++++++++++++++--- src/panels/lovelace/badges/hui-view-badges.ts | 4 +- src/panels/lovelace/badges/types.ts | 2 + .../hui-entity-badge-editor.ts | 47 +++++++++++++++- .../lovelace/views/hui-sections-view.ts | 2 +- src/translations/en.json | 11 +++- 6 files changed, 106 insertions(+), 15 deletions(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index cdfb302162d9..dcbe78939282 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -21,6 +21,12 @@ import { hasAction } from "../common/has-action"; import { LovelaceBadge, LovelaceBadgeEditor } from "../types"; import { EntityBadgeConfig } from "./types"; +export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const; + +export type DisplayType = (typeof DISPLAY_TYPES)[number]; + +export const DEFAULT_DISPLAY_TYPE: DisplayType = "standard"; + @customElement("hui-entity-badge") export class HuiEntityBadge extends LitElement implements LovelaceBadge { public static async getConfigElement(): Promise { @@ -125,10 +131,16 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { `; + const name = this._config.name || stateObj.attributes.friendly_name; + + const displayType = this._config.display_type; + return html`
    - ${stateDisplay} + ${displayType !== "minimal" + ? html` + + ${displayType === "complete" + ? html`${name}` + : nothing} + ${stateDisplay} + + ` + : nothing}
    `; } @@ -168,7 +189,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { align-items: center; gap: 8px; height: 36px; - padding: 6px 16px 6px 12px; + padding: 6px 8px; box-sizing: border-box; width: auto; border-radius: 18px; @@ -181,17 +202,35 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { ); --mdc-icon-size: 18px; cursor: pointer; - color: var(--primary-text-color); text-align: center; font-family: Roboto; - font-size: 14px; + } + .badge.active { + --badge-color: var(--primary-color); + } + .content { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-right: 4px; + margin-inline-end: 4px; + margin-inline-start: initial; + } + .name { + font-size: 10px; font-style: normal; font-weight: 500; - line-height: 20px; + line-height: 10px; letter-spacing: 0.1px; + color: var(--secondary-text-color); } - .badge.active { - --badge-color: var(--primary-color); + .state { + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.1px; + color: var(--primary-text-color); } ha-state-icon { color: var(--badge-color); diff --git a/src/panels/lovelace/badges/hui-view-badges.ts b/src/panels/lovelace/badges/hui-view-badges.ts index 80aef231ea96..64bfa106b82e 100644 --- a/src/panels/lovelace/badges/hui-view-badges.ts +++ b/src/panels/lovelace/badges/hui-view-badges.ts @@ -79,12 +79,11 @@ export class HuiViewBadges extends LitElement { @drag-start=${this._dragStart} @drag-end=${this._dragEnd} group="badge" - draggable-selector=".badge" + draggable-selector="[data-sortable]" .path=${[this.viewIndex]} .rollback=${false} .options=${BADGE_SORTABLE_OPTIONS} invert-swap - no-style >
    ${repeat( @@ -94,6 +93,7 @@ export class HuiViewBadges extends LitElement { ${editMode ? html` ({ + value: type, + label: localize( + `ui.panel.lovelace.editor.badge.entity.display_type_options.${type}` + ), + })), + }, + }, + }, { name: "", type: "grid", schema: [ + { + name: "name", + selector: { + text: {}, + }, + }, { name: "icon", selector: { @@ -79,6 +108,12 @@ export class HuiEntityBadgeEditor ui_color: { default_color: true }, }, }, + { + name: "show_entity_picture", + selector: { + boolean: {}, + }, + }, ], }, @@ -119,7 +154,11 @@ export class HuiEntityBadgeEditor const schema = this._schema(this.hass!.localize); - const data = this._config; + const data = { ...this._config }; + + if (!data.display_type) { + data.display_type = DEFAULT_DISPLAY_TYPE; + } return html` Date: Wed, 17 Jul 2024 10:20:50 +0200 Subject: [PATCH 22/32] Add support for picture --- .../lovelace/badges/hui-entity-badge.ts | 63 ++++++++++++++++--- .../hui-entity-badge-editor.ts | 2 +- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index dcbe78939282..437e22e9bd49 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -20,6 +20,8 @@ import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { LovelaceBadge, LovelaceBadgeEditor } from "../types"; import { EntityBadgeConfig } from "./types"; +import { computeStateDomain } from "../../../common/entity/compute_state_domain"; +import { cameraUrlWithWidthHeight } from "../../../data/camera"; export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const; @@ -103,6 +105,21 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { } ); + private _getImageUrl(stateObj: HassEntity): string | undefined { + const entityPicture = + stateObj.attributes.entity_picture_local || + stateObj.attributes.entity_picture; + + if (!entityPicture) return undefined; + + let imageUrl = this.hass!.hassUrl(entityPicture); + if (computeStateDomain(stateObj) === "camera") { + imageUrl = cameraUrlWithWidthHeight(imageUrl, 32, 32); + } + + return imageUrl; + } + protected render() { if (!this._config || !this.hass) { return nothing; @@ -133,13 +150,18 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { const name = this._config.name || stateObj.attributes.friendly_name; - const displayType = this._config.display_type; + const displayType = this._config.display_type || DEFAULT_DISPLAY_TYPE; + + const imageUrl = this._config.show_entity_picture + ? this._getImageUrl(stateObj) + : undefined; return html`
    - + ${imageUrl + ? html`` + : html` + + `} ${displayType !== "minimal" ? html` @@ -187,9 +213,11 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { display: flex; flex-direction: row; align-items: center; + justify-content: center; gap: 8px; height: 36px; - padding: 6px 8px; + min-width: 36px; + padding: 0px 8px; box-sizing: border-box; width: auto; border-radius: 18px; @@ -212,9 +240,9 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { display: flex; flex-direction: column; align-items: flex-start; - margin-right: 4px; - margin-inline-end: 4px; - margin-inline-start: initial; + padding-right: 4px; + padding-inline-end: 4px; + padding-inline-start: initial; } .name { font-size: 10px; @@ -236,6 +264,21 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { color: var(--badge-color); line-height: 0; } + img { + width: 30px; + height: 30px; + border-radius: 50%; + object-fit: cover; + overflow: hidden; + } + .badge.minimal { + padding: 0; + } + .badge:not(.minimal) img { + margin-left: -6px; + margin-inline-start: -6px; + margin-inline-end: initial; + } `; } } diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts index 307643efc56d..e1df0ad90612 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -37,11 +37,11 @@ const badgeConfigStruct = assign( baseLovelaceCardConfig, object({ entity: optional(string()), + display_type: optional(enums(DISPLAY_TYPES)), name: optional(string()), icon: optional(string()), state_content: optional(union([string(), array(string())])), color: optional(string()), - display_type: optional(enums(DISPLAY_TYPES)), show_entity_picture: optional(boolean()), tap_action: optional(actionConfigStruct), }) From 2267c790f6ea390ab80da1ef82e337b32beaa55a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 10:51:38 +0200 Subject: [PATCH 23/32] Improve focus style --- src/panels/lovelace/badges/hui-entity-badge.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index 437e22e9bd49..a8733f7ffae2 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -210,6 +210,9 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { --ha-ripple-color: var(--badge-color); --ha-ripple-hover-opacity: 0.04; --ha-ripple-pressed-opacity: 0.12; + transition: + box-shadow 180ms ease-in-out, + border-color 180ms ease-in-out; display: flex; flex-direction: row; align-items: center; @@ -229,10 +232,21 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { var(--divider-color, #e0e0e0) ); --mdc-icon-size: 18px; - cursor: pointer; text-align: center; font-family: Roboto; } + .badge:focus-visible { + --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); + --shadow-focus: 0 0 0 1px var(--badge-color); + border-color: var(--badge-color); + box-shadow: var(--shadow-default), var(--shadow-focus); + } + [role="button"] { + cursor: pointer; + } + [role="button"]:focus { + outline: none; + } .badge.active { --badge-color: var(--primary-color); } From 19651a264c6116ad64f53dd971c5252486013916 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 11:05:47 +0200 Subject: [PATCH 24/32] Add visibility editor --- .../badge-editor/hui-badge-element-editor.ts | 75 ++++++++++++++++++- .../hui-badge-visibility-editor.ts | 59 +++++++++++++++ src/translations/en.json | 7 +- 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts 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 d00a7c58d043..dad6679e8c7b 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 @@ -1,11 +1,17 @@ -import { customElement } from "lit/decorators"; +import { css, CSSResultGroup, html, nothing, TemplateResult } from "lit"; +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 "./hui-badge-visibility-editor"; + +type Tab = "config" | "visibility"; @customElement("hui-badge-element-editor") export class HuiBadgeElementEditor extends HuiElementEditor { + @state() private _curTab: Tab = "config"; + protected async getConfigElement(): Promise { const elClass = await getBadgeElementClass(this.configElementType!); @@ -27,6 +33,73 @@ export class HuiBadgeElementEditor extends HuiElementEditor return undefined; } + + private _handleTabSelected(ev: CustomEvent): void { + if (!ev.detail.value) { + return; + } + this._curTab = ev.detail.value.id; + } + + private _configChanged(ev: CustomEvent): void { + ev.stopPropagation(); + this.value = ev.detail.value; + } + + protected renderConfigElement(): TemplateResult { + const displayedTabs: Tab[] = ["config", "visibility"]; + + let content: TemplateResult<1> | typeof nothing = nothing; + + switch (this._curTab) { + case "config": + content = html`${super.renderConfigElement()}`; + break; + case "visibility": + content = html` + + `; + break; + } + return html` + + ${displayedTabs.map( + (tab, index) => html` + + ${this.hass.localize( + `ui.panel.lovelace.editor.edit_badge.tab_${tab}` + )} + + ` + )} + + ${content} + `; + } + + static get styles(): CSSResultGroup { + return [ + HuiElementEditor.styles, + css` + paper-tabs { + --paper-tabs-selection-bar-color: var(--primary-color); + color: var(--primary-text-color); + text-transform: uppercase; + margin-bottom: 16px; + border-bottom: 1px solid var(--divider-color); + } + `, + ]; + } } declare global { diff --git a/src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts b/src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts new file mode 100644 index 000000000000..26a319ac36ee --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts @@ -0,0 +1,59 @@ +import { LitElement, html, css } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; +import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; +import { HomeAssistant } from "../../../../types"; +import { Condition } from "../../common/validate-condition"; +import "../conditions/ha-card-conditions-editor"; + +@customElement("hui-badge-visibility-editor") +export class HuiCardVisibilityEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public config!: LovelaceCardConfig; + + render() { + const conditions = this.config.visibility ?? []; + return html` +

    + ${this.hass.localize( + `ui.panel.lovelace.editor.edit_badge.visibility.explanation` + )} +

    + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const conditions = ev.detail.value as Condition[]; + const newConfig: LovelaceCardConfig = { + ...this.config, + visibility: conditions, + }; + if (newConfig.visibility?.length === 0) { + delete newConfig.visibility; + } + fireEvent(this, "value-changed", { value: newConfig }); + } + + static styles = css` + .intro { + margin: 0; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-badge-visibility-editor": HuiCardVisibilityEditor; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index d926becdd0e0..b7fd9391b97f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5579,7 +5579,12 @@ "delete": "[%key:ui::panel::lovelace::editor::edit_card::delete%]", "copy": "[%key:ui::panel::lovelace::editor::edit_card::copy%]", "cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]", - "duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]" + "duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]", + "tab_config": "Config", + "tab_visibility": "Visibility", + "visibility": { + "explanation": "The badge will be shown when ALL conditions below are fulfilled. If no conditions are set, the badge will always be shown." + } }, "move_card": { "header": "Choose a view to move the card to", From 19cd92a3d96b212cb4818e9251e83c468a44a44f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 11:11:40 +0200 Subject: [PATCH 25/32] Fix visibility --- src/panels/lovelace/badges/hui-badge.ts | 2 +- src/panels/lovelace/views/hui-view.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/panels/lovelace/badges/hui-badge.ts b/src/panels/lovelace/badges/hui-badge.ts index 4ba4e2cd6d8c..9b362fd173a5 100644 --- a/src/panels/lovelace/badges/hui-badge.ts +++ b/src/panels/lovelace/badges/hui-badge.ts @@ -21,7 +21,7 @@ declare global { @customElement("hui-badge") export class HuiBadge extends ReactiveElement { - @property({ attribute: false }) public preview = false; + @property({ type: Boolean }) public preview = false; @property({ attribute: false }) public config?: LovelaceBadgeConfig; diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index a4391a753b0d..f236a8ef832e 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -216,6 +216,9 @@ export class HUIView extends ReactiveElement { this._cards.forEach((element) => { element.preview = this.lovelace.editMode; }); + this._badges.forEach((element) => { + element.preview = this.lovelace.editMode; + }); } if (changedProperties.has("_cards")) { this._layoutElement.cards = this._cards; From 2402df0c08352c67c57fb49e167f854d7bf0b5bb Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 11:21:20 +0200 Subject: [PATCH 26/32] Fix add/delete card inside section --- src/panels/lovelace/editor/lovelace-path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/lovelace/editor/lovelace-path.ts b/src/panels/lovelace/editor/lovelace-path.ts index 9a105bea2441..69e52115904e 100644 --- a/src/panels/lovelace/editor/lovelace-path.ts +++ b/src/panels/lovelace/editor/lovelace-path.ts @@ -209,5 +209,5 @@ export const findLovelaceItems = ( if (isStrategySection(section)) { throw new Error("Can not find cards in a strategy section"); } - return view[key] as LovelaceItemKeys[T] | undefined; + return section[key] as LovelaceItemKeys[T] | undefined; }; From 0b4494be90345496de0b55eedb1840945f16babf Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 12:16:29 +0200 Subject: [PATCH 27/32] Fix translations --- src/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index b7fd9391b97f..83bbd40c71a5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5580,8 +5580,8 @@ "copy": "[%key:ui::panel::lovelace::editor::edit_card::copy%]", "cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]", "duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]", - "tab_config": "Config", - "tab_visibility": "Visibility", + "tab_config": "[%key:ui::panel::lovelace::editor::edit_card::tab_config%]", + "tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]", "visibility": { "explanation": "The badge will be shown when ALL conditions below are fulfilled. If no conditions are set, the badge will always be shown." } From 0e2e78ff692b1baa390d77bc39d62eadfd1370f0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 18:09:53 +0200 Subject: [PATCH 28/32] Add error badge --- .../lovelace/badges/hui-entity-badge.ts | 2 + src/panels/lovelace/badges/hui-error-badge.ts | 62 ++++++++++++++++--- src/panels/lovelace/badges/types.ts | 1 + .../create-element/create-element-base.ts | 35 ++++++++--- 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/panels/lovelace/badges/hui-entity-badge.ts b/src/panels/lovelace/badges/hui-entity-badge.ts index a8733f7ffae2..05a6839da0c1 100644 --- a/src/panels/lovelace/badges/hui-entity-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-badge.ts @@ -241,9 +241,11 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { border-color: var(--badge-color); box-shadow: var(--shadow-default), var(--shadow-focus); } + button, [role="button"] { cursor: pointer; } + button:focus, [role="button"]:focus { outline: none; } diff --git a/src/panels/lovelace/badges/hui-error-badge.ts b/src/panels/lovelace/badges/hui-error-badge.ts index aa02366f2632..19c6e9582623 100644 --- a/src/panels/lovelace/badges/hui-error-badge.ts +++ b/src/panels/lovelace/badges/hui-error-badge.ts @@ -1,10 +1,13 @@ -import { mdiAlert } from "@mdi/js"; +import { mdiAlertCircle } from "@mdi/js"; +import { dump } from "js-yaml"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, state } from "lit/decorators"; import "../../../components/ha-label-badge"; import "../../../components/ha-svg-icon"; import { HomeAssistant } from "../../../types"; +import { showAlertDialog } from "../custom-card-helpers"; import { LovelaceBadge } from "../types"; +import { HuiEntityBadge } from "./hui-entity-badge"; import { ErrorBadgeConfig } from "./types"; export const createErrorBadgeElement = (config) => { @@ -28,24 +31,65 @@ export class HuiErrorBadge extends LitElement implements LovelaceBadge { this._config = config; } + private _viewDetail() { + let dumped: string | undefined; + + if (this._config!.origConfig) { + try { + dumped = dump(this._config!.origConfig); + } catch (err: any) { + dumped = `[Error dumping ${this._config!.origConfig}]`; + } + } + + showAlertDialog(this, { + title: this._config?.error, + warning: true, + text: dumped ? html`
    ${dumped}
    ` : "", + }); + } + protected render() { if (!this._config) { return nothing; } return html` - - - + `; } static get styles(): CSSResultGroup { - return css` - :host { - --ha-label-badge-color: var(--label-badge-red, #fce588); - } - `; + return [ + HuiEntityBadge.styles, + css` + .badge.error { + --badge-color: var(--error-color); + border-color: var(--badge-color); + } + ha-svg-icon { + color: var(--badge-color); + } + .state { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + pre { + font-family: var(--code-font-family, monospace); + white-space: break-spaces; + user-select: text; + } + `, + ]; } } diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index 8fc4586439f9..37a35d47734d 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -13,6 +13,7 @@ export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig { export interface ErrorBadgeConfig extends LovelaceBadgeConfig { error: string; + origConfig: LovelaceBadgeConfig; } export interface StateLabelBadgeConfig extends LovelaceBadgeConfig { diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index a70d30b49775..65f3ecbcc5d8 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -12,7 +12,6 @@ import { stripCustomPrefix, } from "../../../data/lovelace_custom_cards"; import { LovelaceCardFeatureConfig } from "../card-features/types"; -import type { HuiErrorCard } from "../cards/hui-error-card"; import type { ErrorCardConfig } from "../cards/types"; import { LovelaceElement, LovelaceElementConfig } from "../elements/types"; import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types"; @@ -88,6 +87,20 @@ export const createErrorCardElement = (config: ErrorCardConfig) => { return el; }; +export const createErrorBadgeElement = (config: ErrorCardConfig) => { + const el = document.createElement("hui-error-badge"); + if (customElements.get("hui-error-badge")) { + el.setConfig(config); + } else { + import("../badges/hui-error-badge"); + customElements.whenDefined("hui-error-badge").then(() => { + customElements.upgrade(el); + el.setConfig(config); + }); + } + return el; +}; + export const createErrorCardConfig = (error, origConfig) => ({ type: "error", error, @@ -103,7 +116,7 @@ export const createErrorBadgeConfig = (error, origConfig) => ({ const _createElement = ( tag: string, config: CreateElementConfigTypes[T]["config"] -): CreateElementConfigTypes[T]["element"] | HuiErrorCard => { +): CreateElementConfigTypes[T]["element"] => { const element = document.createElement( tag ) as CreateElementConfigTypes[T]["element"]; @@ -113,11 +126,18 @@ const _createElement = ( }; const _createErrorElement = ( + tagSuffix: T, error: string, config: CreateElementConfigTypes[T]["config"] -): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config)); +): CreateElementConfigTypes[T]["element"] => { + if (tagSuffix === "badge") { + return createErrorBadgeElement(createErrorBadgeConfig(error, config)); + } + return createErrorCardElement(createErrorCardConfig(error, config)); +}; const _customCreate = ( + tagSuffix: T, tag: string, config: CreateElementConfigTypes[T]["config"] ) => { @@ -126,6 +146,7 @@ const _customCreate = ( } const element = _createErrorElement( + tagSuffix, `Custom element doesn't exist: ${tag}.`, config ); @@ -182,7 +203,7 @@ export const createLovelaceElement = ( domainTypes?: { _domain_not_found: string; [domain: string]: string }, // Default type if no type given. If given, entity types will not work. defaultType?: string -): CreateElementConfigTypes[T]["element"] | HuiErrorCard => { +): CreateElementConfigTypes[T]["element"] => { try { return tryCreateLovelaceElement( tagSuffix, @@ -195,7 +216,7 @@ export const createLovelaceElement = ( } catch (err: any) { // eslint-disable-next-line console.error(tagSuffix, config.type, err); - return _createErrorElement(err.message, config); + return _createErrorElement(tagSuffix, err.message, config); } }; @@ -210,7 +231,7 @@ export const tryCreateLovelaceElement = < domainTypes?: { _domain_not_found: string; [domain: string]: string }, // Default type if no type given. If given, entity types will not work. defaultType?: string -): CreateElementConfigTypes[T]["element"] | HuiErrorCard => { +): CreateElementConfigTypes[T]["element"] => { if (!config || typeof config !== "object") { throw new Error("Config is not an object"); } @@ -227,7 +248,7 @@ export const tryCreateLovelaceElement = < const customTag = config.type ? _getCustomTag(config.type) : undefined; if (customTag) { - return _customCreate(customTag, config); + return _customCreate(tagSuffix, customTag, config); } let type: string | undefined; From 8133b2fb67398838003eaa251286534bf04edcfc Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 17 Jul 2024 18:12:58 +0200 Subject: [PATCH 29/32] Rename classes --- src/panels/lovelace/components/hui-badge-edit-mode.ts | 4 ++-- .../editor/badge-editor/hui-badge-visibility-editor.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/panels/lovelace/components/hui-badge-edit-mode.ts b/src/panels/lovelace/components/hui-badge-edit-mode.ts index a2355346f44b..5ccee7a24a89 100644 --- a/src/panels/lovelace/components/hui-badge-edit-mode.ts +++ b/src/panels/lovelace/components/hui-badge-edit-mode.ts @@ -27,7 +27,7 @@ import { import { Lovelace } from "../types"; @customElement("hui-badge-edit-mode") -export class HuiCardEditMode extends LitElement { +export class HuiBadgeEditMode extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public lovelace!: Lovelace; @@ -270,6 +270,6 @@ export class HuiCardEditMode extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-badge-edit-mode": HuiCardEditMode; + "hui-badge-edit-mode": HuiBadgeEditMode; } } diff --git a/src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts b/src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts index 26a319ac36ee..f7f1612d9fe5 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-badge-visibility-editor.ts @@ -8,7 +8,7 @@ import { Condition } from "../../common/validate-condition"; import "../conditions/ha-card-conditions-editor"; @customElement("hui-badge-visibility-editor") -export class HuiCardVisibilityEditor extends LitElement { +export class HuiBadgeVisibilityEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public config!: LovelaceCardConfig; @@ -54,6 +54,6 @@ export class HuiCardVisibilityEditor extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-badge-visibility-editor": HuiCardVisibilityEditor; + "hui-badge-visibility-editor": HuiBadgeVisibilityEditor; } } From 1615af70e660d671b6a4ac7d6a7fc1f46ce9956b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 18 Jul 2024 14:29:38 +0200 Subject: [PATCH 30/32] Fix badge type --- src/data/lovelace/config/badge.ts | 5 +++++ src/data/lovelace/config/section.ts | 2 +- src/data/lovelace/config/view.ts | 2 +- .../editor/badge-editor/hui-dialog-edit-badge.ts | 9 +++++++-- .../editor/card-editor/hui-dialog-edit-card.ts | 6 +++--- .../config-elements/hui-entity-badge-editor.ts | 4 ++-- .../lovelace/editor/structs/base-badge-struct.ts | 6 ++++++ src/panels/lovelace/views/hui-view.ts | 15 ++++++++------- 8 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 src/panels/lovelace/editor/structs/base-badge-struct.ts diff --git a/src/data/lovelace/config/badge.ts b/src/data/lovelace/config/badge.ts index 1f8b447075ec..7226adc982d1 100644 --- a/src/data/lovelace/config/badge.ts +++ b/src/data/lovelace/config/badge.ts @@ -5,3 +5,8 @@ export interface LovelaceBadgeConfig { [key: string]: any; visibility?: Condition[]; } + +export const defaultBadgeConfig = (entity_id: string): LovelaceBadgeConfig => ({ + type: "entity", + entity: entity_id, +}); diff --git a/src/data/lovelace/config/section.ts b/src/data/lovelace/config/section.ts index 0f32ed054770..67dc1f57b9b6 100644 --- a/src/data/lovelace/config/section.ts +++ b/src/data/lovelace/config/section.ts @@ -11,7 +11,7 @@ export interface LovelaceBaseSectionConfig { export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig { type?: string; cards?: LovelaceCardConfig[]; - badges?: LovelaceBadgeConfig[]; // Not supported yet + badges?: (string | LovelaceBadgeConfig)[]; // Not supported yet } export interface LovelaceStrategySectionConfig diff --git a/src/data/lovelace/config/view.ts b/src/data/lovelace/config/view.ts index 0366e7b816a3..708d307ab581 100644 --- a/src/data/lovelace/config/view.ts +++ b/src/data/lovelace/config/view.ts @@ -27,7 +27,7 @@ export interface LovelaceBaseViewConfig { export interface LovelaceViewConfig extends LovelaceBaseViewConfig { type?: string; - badges?: LovelaceBadgeConfig[]; + badges?: (string | LovelaceBadgeConfig)[]; // Badge can be just an entity_id cards?: LovelaceCardConfig[]; sections?: LovelaceSectionRawConfig[]; } diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts index 5d67dd630816..0e03b226cacd 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -16,7 +16,10 @@ import "../../../../components/ha-circular-progress"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; import "../../../../components/ha-icon-button"; -import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; +import { + defaultBadgeConfig, + LovelaceBadgeConfig, +} from "../../../../data/lovelace/config/badge"; import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import { @@ -105,7 +108,9 @@ export class HuiDialogEditBadge this._badgeConfig = params.badgeConfig; this._dirty = true; } else { - this._badgeConfig = this._containerConfig.badges?.[params.badgeIndex]; + const badge = this._containerConfig.badges?.[params.badgeIndex]; + this._badgeConfig = + typeof badge === "string" ? defaultBadgeConfig(badge) : badge; } this.large = false; diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index d753befa3998..9b3c3aa9d151 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -30,15 +30,15 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; +import "../../cards/hui-card"; import "../../sections/hui-section"; -import { addCard, replaceBadge } from "../config-util"; +import { addCard, replaceCard } from "../config-util"; import { getCardDocumentationURL } from "../get-dashboard-documentation-url"; import type { ConfigChangedEvent } from "../hui-element-editor"; import { findLovelaceContainer } from "../lovelace-path"; import type { GUIModeChangedEvent } from "../types"; import "./hui-card-element-editor"; import type { HuiCardElementEditor } from "./hui-card-element-editor"; -import "../../cards/hui-card"; import type { EditCardDialogParams } from "./show-edit-card-dialog"; declare global { @@ -431,7 +431,7 @@ export class HuiDialogEditCard await this._params!.saveConfig( "cardConfig" in this._params! ? addCard(this._params!.lovelaceConfig, path, this._cardConfig!) - : replaceBadge( + : replaceCard( this._params!.lovelaceConfig, [...path, this._params!.cardIndex], this._cardConfig! diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts index e1df0ad90612..73f5b65c0708 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -29,12 +29,12 @@ import { EntityBadgeConfig } from "../../badges/types"; import type { LovelaceBadgeEditor } from "../../types"; import "../hui-sub-element-editor"; import { actionConfigStruct } from "../structs/action-struct"; -import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct"; import { configElementStyle } from "./config-elements-style"; import "./hui-card-features-editor"; const badgeConfigStruct = assign( - baseLovelaceCardConfig, + baseLovelaceBadgeConfig, object({ entity: optional(string()), display_type: optional(enums(DISPLAY_TYPES)), diff --git a/src/panels/lovelace/editor/structs/base-badge-struct.ts b/src/panels/lovelace/editor/structs/base-badge-struct.ts new file mode 100644 index 000000000000..b738119cef9a --- /dev/null +++ b/src/panels/lovelace/editor/structs/base-badge-struct.ts @@ -0,0 +1,6 @@ +import { object, string, any } from "superstruct"; + +export const baseLovelaceBadgeConfig = object({ + type: string(), + visibility: any(), +}); diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index f236a8ef832e..6c9cdf3381f2 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -5,19 +5,21 @@ import { HASSDomEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-state-label-badge"; import "../../../components/ha-svg-icon"; import type { LovelaceViewElement } from "../../../data/lovelace"; -import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; +import { + defaultBadgeConfig, + LovelaceBadgeConfig, +} from "../../../data/lovelace/config/badge"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; import { - LovelaceViewConfig, isStrategyView, + LovelaceViewConfig, } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; import "../badges/hui-badge"; import type { HuiBadge } from "../badges/hui-badge"; import "../cards/hui-card"; import type { HuiCard } from "../cards/hui-card"; -import { processConfigEntities } from "../common/process-config-entities"; import { createViewElement } from "../create-element/create-view-element"; import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog"; import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; @@ -365,10 +367,9 @@ export class HUIView extends ReactiveElement { return; } - const badges = processConfigEntities( - config.badges as any - ) as LovelaceBadgeConfig[]; - this._badges = badges.map((badgeConfig) => { + this._badges = config.badges.map((badge) => { + const badgeConfig = + typeof badge === "string" ? defaultBadgeConfig(badge) : badge; const element = this._createBadgeElement(badgeConfig); return element; }); From 6357bfa1e9cfce1451d9bd77ef9a0ee453deaa3b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 18 Jul 2024 14:43:15 +0200 Subject: [PATCH 31/32] Remove badges from section type --- src/data/lovelace/config/section.ts | 2 -- .../editor/badge-editor/hui-dialog-edit-badge.ts | 11 ++++------- src/panels/lovelace/editor/lovelace-path.ts | 5 ++++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/data/lovelace/config/section.ts b/src/data/lovelace/config/section.ts index 67dc1f57b9b6..bfd54042a590 100644 --- a/src/data/lovelace/config/section.ts +++ b/src/data/lovelace/config/section.ts @@ -1,5 +1,4 @@ import type { Condition } from "../../../panels/lovelace/common/validate-condition"; -import { LovelaceBadgeConfig } from "./badge"; import type { LovelaceCardConfig } from "./card"; import type { LovelaceStrategyConfig } from "./strategy"; @@ -11,7 +10,6 @@ export interface LovelaceBaseSectionConfig { export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig { type?: string; cards?: LovelaceCardConfig[]; - badges?: (string | LovelaceBadgeConfig)[]; // Not supported yet } export interface LovelaceStrategySectionConfig diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts index 0e03b226cacd..19ae477d5e23 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -1,12 +1,12 @@ import { mdiClose, mdiHelpCircle } from "@mdi/js"; import deepFreeze from "deep-freeze"; import { - CSSResultGroup, - LitElement, - PropertyValues, css, + CSSResultGroup, html, + LitElement, nothing, + PropertyValues, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import type { HASSDomEvent } from "../../../../common/dom/fire_event"; @@ -20,7 +20,6 @@ import { defaultBadgeConfig, LovelaceBadgeConfig, } from "../../../../data/lovelace/config/badge"; -import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import { getCustomBadgeEntry, @@ -67,9 +66,7 @@ export class HuiDialogEditBadge @state() private _badgeConfig?: LovelaceBadgeConfig; - @state() private _containerConfig!: - | LovelaceViewConfig - | LovelaceSectionConfig; + @state() private _containerConfig!: LovelaceViewConfig; @state() private _saving = false; diff --git a/src/panels/lovelace/editor/lovelace-path.ts b/src/panels/lovelace/editor/lovelace-path.ts index 69e52115904e..c27716d05001 100644 --- a/src/panels/lovelace/editor/lovelace-path.ts +++ b/src/panels/lovelace/editor/lovelace-path.ts @@ -209,5 +209,8 @@ export const findLovelaceItems = ( if (isStrategySection(section)) { throw new Error("Can not find cards in a strategy section"); } - return section[key] as LovelaceItemKeys[T] | undefined; + if (key === "cards") { + return section[key as "cards"] as LovelaceItemKeys[T] | undefined; + } + throw new Error(`${key} is not supported in section`); }; From 84bd453829ed1f884a6ca913f05b4b5fa96d97a0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 19 Jul 2024 10:12:56 +0200 Subject: [PATCH 32/32] Add missing types --- src/panels/lovelace/badges/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/panels/lovelace/badges/types.ts b/src/panels/lovelace/badges/types.ts index 37a35d47734d..682308d39a81 100644 --- a/src/panels/lovelace/badges/types.ts +++ b/src/panels/lovelace/badges/types.ts @@ -33,7 +33,9 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig { name?: string; icon?: string; color?: string; + show_entity_picture?: boolean; display_type?: "minimal" | "standard" | "complete"; + state_content?: string | string[]; tap_action?: ActionConfig; hold_action?: ActionConfig; double_tap_action?: ActionConfig;