From 179245e1aaaccee95085887370bd34e5ad1bc29f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 19 Jul 2024 20:56:29 +0200 Subject: [PATCH] Add picker for badges (#21436) --- src/data/lovelace/config/badge.ts | 23 +- src/data/lovelace/config/view.ts | 2 +- .../badges/hui-entity-filter-badge.ts | 5 +- .../common/generate-lovelace-config.ts | 19 + .../components/hui-badge-edit-mode.ts | 41 +- .../lovelace/components/hui-card-edit-mode.ts | 2 +- .../lovelace/components/hui-card-options.ts | 2 +- .../create-element/create-badge-element.ts | 12 + .../editor/badge-editor/hui-badge-picker.ts | 590 ++++++++++++++++++ .../badge-editor/hui-dialog-create-badge.ts | 289 +++++++++ .../badge-editor/hui-dialog-edit-badge.ts | 5 +- .../badge-editor/hui-dialog-suggest-badge.ts | 204 ++++++ .../badge-editor/show-create-badge-dialog.ts | 25 + .../badge-editor/show-suggest-badge-dialog.ts | 26 + .../editor/card-editor/hui-card-picker.ts | 5 +- .../hui-conditional-card-editor.ts | 2 +- .../config-elements/hui-stack-card-editor.ts | 2 +- src/panels/lovelace/editor/config-util.ts | 18 +- .../lovelace/editor/hui-element-editor.ts | 4 +- src/panels/lovelace/editor/lovelace-badges.ts | 8 + src/panels/lovelace/editor/lovelace-path.ts | 2 +- src/panels/lovelace/editor/types.ts | 14 + src/panels/lovelace/views/hui-view.ts | 17 +- src/translations/en.json | 23 +- 24 files changed, 1300 insertions(+), 40 deletions(-) create mode 100644 src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts create mode 100644 src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts create mode 100644 src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts create mode 100644 src/panels/lovelace/editor/badge-editor/show-create-badge-dialog.ts create mode 100644 src/panels/lovelace/editor/badge-editor/show-suggest-badge-dialog.ts create mode 100644 src/panels/lovelace/editor/lovelace-badges.ts diff --git a/src/data/lovelace/config/badge.ts b/src/data/lovelace/config/badge.ts index 7226adc982d1..b6b5d7c207f4 100644 --- a/src/data/lovelace/config/badge.ts +++ b/src/data/lovelace/config/badge.ts @@ -1,12 +1,25 @@ import { Condition } from "../../../panels/lovelace/common/validate-condition"; export interface LovelaceBadgeConfig { - type?: string; + type: string; [key: string]: any; visibility?: Condition[]; } -export const defaultBadgeConfig = (entity_id: string): LovelaceBadgeConfig => ({ - type: "entity", - entity: entity_id, -}); +export const ensureBadgeConfig = ( + config: Partial | string +): LovelaceBadgeConfig => { + if (typeof config === "string") { + return { + type: "entity", + entity: config, + }; + } + if ("type" in config && config.type) { + return config as LovelaceBadgeConfig; + } + return { + type: "entity", + ...config, + }; +}; diff --git a/src/data/lovelace/config/view.ts b/src/data/lovelace/config/view.ts index 708d307ab581..4bbf67c5a6cd 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?: (string | LovelaceBadgeConfig)[]; // Badge can be just an entity_id + badges?: (string | Partial)[]; // Badge can be just an entity_id or without type cards?: LovelaceCardConfig[]; sections?: LovelaceSectionRawConfig[]; } diff --git a/src/panels/lovelace/badges/hui-entity-filter-badge.ts b/src/panels/lovelace/badges/hui-entity-filter-badge.ts index 5cba77434c91..3b5de69b3845 100644 --- a/src/panels/lovelace/badges/hui-entity-filter-badge.ts +++ b/src/panels/lovelace/badges/hui-entity-filter-badge.ts @@ -127,7 +127,10 @@ export class HuiEntityFilterBadge const element = document.createElement("hui-badge"); element.hass = this.hass; element.preview = this.preview; - element.config = badgeConfig; + element.config = { + type: "entity", + ...badgeConfig, + }; element.load(); this._elements.push(element); } diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 6f75cf3559c5..f065215ce959 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -31,6 +31,8 @@ import { } from "../cards/types"; import { EntityConfig } from "../entity-rows/types"; import { ButtonsHeaderFooterConfig } from "../header-footer/types"; +import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; +import { EntityBadgeConfig } from "../badges/types"; const HIDE_DOMAIN = new Set([ "automation", @@ -310,6 +312,23 @@ export const computeCards = ( ]; }; +export const computeBadges = ( + _states: HassEntities, + entityIds: string[] +): LovelaceBadgeConfig[] => { + const badges: LovelaceBadgeConfig[] = []; + + for (const entityId of entityIds) { + const config: EntityBadgeConfig = { + type: "entity", + entity: entityId, + }; + + badges.push(config); + } + return badges; +}; + const computeDefaultViewStates = ( entities: HassEntities, entityEntries: HomeAssistant["entities"] diff --git a/src/panels/lovelace/components/hui-badge-edit-mode.ts b/src/panels/lovelace/components/hui-badge-edit-mode.ts index 5ccee7a24a89..bf57aae9b6b1 100644 --- a/src/panels/lovelace/components/hui-badge-edit-mode.ts +++ b/src/panels/lovelace/components/hui-badge-edit-mode.ts @@ -1,10 +1,13 @@ 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"; @@ -25,6 +28,7 @@ import { parseLovelaceCardPath, } from "../editor/lovelace-path"; import { Lovelace } from "../types"; +import { ensureBadgeConfig } from "../../../data/lovelace/config/badge"; @customElement("hui-badge-edit-mode") export class HuiBadgeEditMode extends LitElement { @@ -46,7 +50,7 @@ export class HuiBadgeEditMode extends LitElement { public _focused: boolean = false; @storage({ - key: "lovelaceClipboard", + key: "dashboardBadgeClipboard", state: false, subscribe: false, storage: "sessionStorage", @@ -134,6 +138,14 @@ export class HuiBadgeEditMode 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")} @@ -159,18 +171,35 @@ export class HuiBadgeEditMode extends LitElement { private _handleAction(ev: CustomEvent) { switch (ev.detail.index) { case 0: - this._duplicateCard(); + this._duplicateBadge(); break; case 1: - this._deleteCard(); + this._copyBadge(); + break; + case 2: + this._cutBadge(); + break; + case 3: + this._deleteBadge(); break; } } - private _duplicateCard(): void { + private _cutBadge(): void { + this._copyBadge(); + this._deleteBadge(); + } + + private _copyBadge(): void { + const { cardIndex } = parseLovelaceCardPath(this.path!); + const cardConfig = this._badges[cardIndex]; + this._clipboard = deepClone(cardConfig); + } + + private _duplicateBadge(): void { const { cardIndex } = parseLovelaceCardPath(this.path!); const containerPath = getLovelaceContainerPath(this.path!); - const badgeConfig = this._badges![cardIndex]; + const badgeConfig = ensureBadgeConfig(this._badges![cardIndex]); showEditBadgeDialog(this, { lovelaceConfig: this.lovelace!.config, saveConfig: this.lovelace!.saveConfig, @@ -191,7 +220,7 @@ export class HuiBadgeEditMode extends LitElement { fireEvent(this, "ll-edit-badge", { path: this.path! }); } - private _deleteCard(): void { + private _deleteBadge(): void { fireEvent(this, "ll-delete-badge", { path: this.path! }); } diff --git a/src/panels/lovelace/components/hui-card-edit-mode.ts b/src/panels/lovelace/components/hui-card-edit-mode.ts index f66e6c5a5f1b..3fd2e1c0574f 100644 --- a/src/panels/lovelace/components/hui-card-edit-mode.ts +++ b/src/panels/lovelace/components/hui-card-edit-mode.ts @@ -50,7 +50,7 @@ export class HuiCardEditMode extends LitElement { public _focused: boolean = false; @storage({ - key: "lovelaceClipboard", + key: "dashboardCardClipboard", state: false, subscribe: false, storage: "sessionStorage", diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 163f32589571..261c95775865 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -67,7 +67,7 @@ export class HuiCardOptions extends LitElement { @property({ type: Boolean }) public hidePosition = false; @storage({ - key: "lovelaceClipboard", + key: "dashboardCardClipboard", state: false, subscribe: false, storage: "sessionStorage", diff --git a/src/panels/lovelace/create-element/create-badge-element.ts b/src/panels/lovelace/create-element/create-badge-element.ts index a6f8efeb6383..220fd598d4f4 100644 --- a/src/panels/lovelace/create-element/create-badge-element.ts +++ b/src/panels/lovelace/create-element/create-badge-element.ts @@ -4,6 +4,7 @@ import "../badges/hui-state-label-badge"; import { createLovelaceElement, getLovelaceElementClass, + tryCreateLovelaceElement, } from "./create-element-base"; const ALWAYS_LOADED_TYPES = new Set(["error", "state-label", "entity"]); @@ -11,6 +12,17 @@ const LAZY_LOAD_TYPES = { "entity-filter": () => import("../badges/hui-entity-filter-badge"), }; +// This will not return an error card but will throw the error +export const tryCreateBadgeElement = (config: LovelaceBadgeConfig) => + tryCreateLovelaceElement( + "badge", + config, + ALWAYS_LOADED_TYPES, + LAZY_LOAD_TYPES, + undefined, + undefined + ); + export const createBadgeElement = (config: LovelaceBadgeConfig) => createLovelaceElement( "badge", diff --git a/src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts b/src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts new file mode 100644 index 000000000000..81edb16c9f3e --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts @@ -0,0 +1,590 @@ +import Fuse, { IFuseOptions } from "fuse.js"; +import { + CSSResultGroup, + LitElement, + PropertyValues, + TemplateResult, + css, + html, + nothing, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import { until } from "lit/directives/until"; +import memoizeOne from "memoize-one"; +import { storage } from "../../../../common/decorators/storage"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { stringCompare } from "../../../../common/string/compare"; +import { stripDiacritics } from "../../../../common/string/strip-diacritics"; +import "../../../../components/ha-circular-progress"; +import "../../../../components/search-input"; +import { isUnavailableState } from "../../../../data/entity"; +import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; +import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; +import { + CUSTOM_TYPE_PREFIX, + CustomBadgeEntry, + customBadges, + getCustomBadgeEntry, +} from "../../../../data/lovelace_custom_cards"; +import type { HomeAssistant } from "../../../../types"; +import { getStripDiacriticsFn } from "../../../../util/fuse"; +import { + calcUnusedEntities, + computeUsedEntities, +} from "../../common/compute-unused-entities"; +import { tryCreateBadgeElement } from "../../create-element/create-badge-element"; +import type { LovelaceBadge } from "../../types"; +import { getBadgeStubConfig } from "../get-badge-stub-config"; +import { coreBadges } from "../lovelace-badges"; +import type { Badge, BadgePickTarget } from "../types"; + +interface BadgeElement { + badge: Badge; + element: TemplateResult; +} + +@customElement("hui-badge-picker") +export class HuiBadgePicker extends LitElement { + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public suggestedBadges?: string[]; + + @storage({ + key: "dashboardBadgeClipboard", + state: true, + subscribe: true, + storage: "sessionStorage", + }) + private _clipboard?: LovelaceBadgeConfig; + + @state() private _badges: BadgeElement[] = []; + + public lovelace?: LovelaceConfig; + + public badgePicked?: (badgeConf: LovelaceBadgeConfig) => void; + + @state() private _filter = ""; + + @state() private _width?: number; + + @state() private _height?: number; + + private _unusedEntities?: string[]; + + private _usedEntities?: string[]; + + private _filterBadges = memoizeOne( + (badgeElements: BadgeElement[], filter?: string): BadgeElement[] => { + if (!filter) { + return badgeElements; + } + let badges = badgeElements.map( + (badgeElement: BadgeElement) => badgeElement.badge + ); + const options: IFuseOptions = { + keys: ["type", "name", "description"], + isCaseSensitive: false, + minMatchCharLength: Math.min(filter.length, 2), + threshold: 0.2, + getFn: getStripDiacriticsFn, + }; + const fuse = new Fuse(badges, options); + badges = fuse + .search(stripDiacritics(filter)) + .map((result) => result.item); + return badgeElements.filter((badgeElement: BadgeElement) => + badges.includes(badgeElement.badge) + ); + } + ); + + private _suggestedBadges = memoizeOne( + (badgeElements: BadgeElement[]): BadgeElement[] => + badgeElements.filter( + (badgeElement: BadgeElement) => badgeElement.badge.isSuggested + ) + ); + + private _customBadges = memoizeOne( + (badgeElements: BadgeElement[]): BadgeElement[] => + badgeElements.filter( + (badgeElement: BadgeElement) => + badgeElement.badge.isCustom && !badgeElement.badge.isSuggested + ) + ); + + private _otherBadges = memoizeOne( + (badgeElements: BadgeElement[]): BadgeElement[] => + badgeElements.filter( + (badgeElement: BadgeElement) => + !badgeElement.badge.isSuggested && !badgeElement.badge.isCustom + ) + ); + + protected render() { + if ( + !this.hass || + !this.lovelace || + !this._unusedEntities || + !this._usedEntities + ) { + return nothing; + } + + const suggestedBadges = this._suggestedBadges(this._badges); + const otherBadges = this._otherBadges(this._badges); + const customBadgesItems = this._customBadges(this._badges); + + return html` + +
    +
    + ${this._filter + ? this._filterBadges(this._badges, this._filter).map( + (badgeElement: BadgeElement) => badgeElement.element + ) + : html` + ${suggestedBadges.length > 0 + ? html` +
    + ${this.hass!.localize( + `ui.panel.lovelace.editor.badge.generic.suggested_badges` + )} +
    + ` + : nothing} + ${this._renderClipboardBadge()} + ${suggestedBadges.map( + (badgeElement: BadgeElement) => badgeElement.element + )} + ${suggestedBadges.length > 0 + ? html` +
    + ${this.hass!.localize( + `ui.panel.lovelace.editor.badge.generic.other_badges` + )} +
    + ` + : nothing} + ${otherBadges.map( + (badgeElement: BadgeElement) => badgeElement.element + )} + ${customBadgesItems.length > 0 + ? html` +
    + ${this.hass!.localize( + `ui.panel.lovelace.editor.badge.generic.custom_badges` + )} +
    + ` + : nothing} + ${customBadgesItems.map( + (badgeElement: BadgeElement) => badgeElement.element + )} + `} +
    +
    +
    +
    + ${this.hass!.localize( + `ui.panel.lovelace.editor.badge.generic.manual` + )} +
    +
    + ${this.hass!.localize( + `ui.panel.lovelace.editor.badge.generic.manual_description` + )} +
    +
    +
    +
    + `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass) { + return true; + } + + if (oldHass.locale !== this.hass!.locale) { + return true; + } + + return false; + } + + protected firstUpdated(): void { + if (!this.hass || !this.lovelace) { + return; + } + + const usedEntities = computeUsedEntities(this.lovelace); + const unusedEntities = calcUnusedEntities(this.hass, usedEntities); + + this._usedEntities = [...usedEntities].filter( + (eid) => + this.hass!.states[eid] && + !isUnavailableState(this.hass!.states[eid].state) + ); + this._unusedEntities = [...unusedEntities].filter( + (eid) => + this.hass!.states[eid] && + !isUnavailableState(this.hass!.states[eid].state) + ); + + this._loadBages(); + } + + private _loadBages() { + let badges = coreBadges.map((badge) => ({ + name: this.hass!.localize( + `ui.panel.lovelace.editor.badge.${badge.type}.name` + ), + description: this.hass!.localize( + `ui.panel.lovelace.editor.badge.${badge.type}.description` + ), + isSuggested: this.suggestedBadges?.includes(badge.type) || false, + ...badge, + })); + + badges = badges.sort((a, b) => { + if (a.isSuggested && !b.isSuggested) { + return -1; + } + if (!a.isSuggested && b.isSuggested) { + return 1; + } + return stringCompare( + a.name || a.type, + b.name || b.type, + this.hass?.language + ); + }); + + if (customBadges.length > 0) { + badges = badges.concat( + customBadges + .map((cbadge: CustomBadgeEntry) => ({ + type: cbadge.type, + name: cbadge.name, + description: cbadge.description, + showElement: cbadge.preview, + isCustom: true, + })) + .sort((a, b) => + stringCompare( + a.name || a.type, + b.name || b.type, + this.hass?.language + ) + ) + ); + } + this._badges = badges.map((badge) => ({ + badge: badge, + element: html`${until( + this._renderBadgeElement(badge), + html` +
    + +
    + ` + )}`, + })); + } + + private _renderClipboardBadge() { + if (!this._clipboard) { + return nothing; + } + + return html` ${until( + this._renderBadgeElement( + { + type: this._clipboard.type, + showElement: true, + isCustom: false, + name: this.hass!.localize( + "ui.panel.lovelace.editor.badge.generic.paste" + ), + description: `${this.hass!.localize( + "ui.panel.lovelace.editor.badge.generic.paste_description", + { + type: this._clipboard.type, + } + )}`, + }, + this._clipboard + ), + html` +
    + +
    + ` + )}`; + } + + private _handleSearchChange(ev: CustomEvent) { + const value = ev.detail.value; + + if (!value) { + // Reset when we no longer filter + this._width = undefined; + this._height = undefined; + } else if (!this._width || !this._height) { + // Save height and width so the dialog doesn't jump while searching + const div = this.shadowRoot!.getElementById("content"); + if (div && !this._width) { + const width = div.clientWidth; + if (width) { + this._width = width; + } + } + if (div && !this._height) { + const height = div.clientHeight; + if (height) { + this._height = height; + } + } + } + + this._filter = value; + } + + private _badgePicked(ev: Event): void { + const config: LovelaceBadgeConfig = (ev.currentTarget! as BadgePickTarget) + .config; + + fireEvent(this, "config-changed", { config }); + } + + private _tryCreateBadgeElement(badge: LovelaceBadgeConfig) { + const element = tryCreateBadgeElement(badge) as LovelaceBadge; + element.hass = this.hass; + element.addEventListener( + "ll-rebuild", + (ev) => { + ev.stopPropagation(); + this._rebuildBadge(element, badge); + }, + { once: true } + ); + return element; + } + + private _rebuildBadge( + badgeElToReplace: LovelaceBadge, + config: LovelaceBadgeConfig + ): void { + let newBadgeEl: LovelaceBadge; + try { + newBadgeEl = this._tryCreateBadgeElement(config); + } catch (err: any) { + return; + } + if (badgeElToReplace.parentElement) { + badgeElToReplace.parentElement!.replaceChild( + newBadgeEl, + badgeElToReplace + ); + } + } + + private async _renderBadgeElement( + badge: Badge, + config?: LovelaceBadgeConfig + ): Promise { + let { type } = badge; + const { showElement, isCustom, name, description } = badge; + const customBadge = isCustom ? getCustomBadgeEntry(type) : undefined; + if (isCustom) { + type = `${CUSTOM_TYPE_PREFIX}${type}`; + } + + let element: LovelaceBadge | undefined; + let badgeConfig: LovelaceBadgeConfig = config ?? { type }; + + if (this.hass && this.lovelace) { + if (!config) { + badgeConfig = await getBadgeStubConfig( + this.hass, + type, + this._unusedEntities!, + this._usedEntities! + ); + } + + if (showElement) { + try { + element = this._tryCreateBadgeElement(badgeConfig); + } catch (err: any) { + element = undefined; + } + } + } + + return html` +
    +
    +
    + ${customBadge + ? `${this.hass!.localize( + "ui.panel.lovelace.editor.badge_picker.custom_badge" + )}: ${customBadge.name || customBadge.type}` + : name} +
    +
    + ${element && element.tagName !== "HUI-ERROR-BADGE" + ? element + : customBadge + ? customBadge.description || + this.hass!.localize( + `ui.panel.lovelace.editor.badge_picker.no_description` + ) + : description} +
    +
    + `; + } + + static get styles(): CSSResultGroup { + return [ + css` + search-input { + display: block; + --mdc-shape-small: var(--badge-picker-search-shape); + margin: var(--badge-picker-search-margin); + } + + .badges-container-header { + font-size: 16px; + font-weight: 500; + padding: 12px 8px 4px 8px; + margin: 0; + grid-column: 1 / -1; + } + + .badges-container { + display: grid; + grid-gap: 8px 8px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + margin-top: 20px; + } + + .badge { + height: 100%; + max-width: 500px; + display: flex; + flex-direction: column; + border-radius: var(--ha-card-border-radius, 12px); + background: var(--primary-background-color, #fafafa); + cursor: pointer; + position: relative; + overflow: hidden; + border: var(--ha-card-border-width, 1px) solid + var(--ha-card-border-color, var(--divider-color)); + } + + .badge-header { + color: var(--ha-card-header-color, --primary-text-color); + font-family: var(--ha-card-header-font-family, inherit); + font-size: 16px; + font-weight: bold; + letter-spacing: -0.012em; + line-height: 20px; + padding: 12px 16px; + display: block; + text-align: center; + background: var( + --ha-card-background, + var(--card-background-color, white) + ); + border-bottom: 1px solid var(--divider-color); + } + + .preview { + pointer-events: none; + margin: 20px; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .description { + text-align: center; + } + + .spinner { + align-items: center; + justify-content: center; + } + + .overlay { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + box-sizing: border-box; + border-radius: var(--ha-card-border-radius, 12px); + } + + .manual { + grid-column: 1 / -1; + max-width: none; + } + + .icon { + position: absolute; + top: 8px; + right: 8px; + inset-inline-start: 8px; + inset-inline-end: 8px; + border-radius: 50%; + --mdc-icon-size: 16px; + line-height: 16px; + box-sizing: border-box; + color: var(--text-primary-color); + padding: 4px; + } + .icon.custom { + background: var(--warning-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-badge-picker": HuiBadgePicker; + } +} diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts new file mode 100644 index 000000000000..77bbfaead836 --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-create-badge.ts @@ -0,0 +1,289 @@ +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; +import { mdiClose } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import { classMap } from "lit/directives/class-map"; +import memoize from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import { DataTableRowData } from "../../../../components/data-table/ha-data-table"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-dialog-header"; +import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { computeBadges } from "../../common/generate-lovelace-config"; +import "../card-editor/hui-entity-picker-table"; +import { findLovelaceContainer } from "../lovelace-path"; +import "./hui-badge-picker"; +import { CreateBadgeDialogParams } from "./show-create-badge-dialog"; +import { showEditBadgeDialog } from "./show-edit-badge-dialog"; +import { showSuggestBadgeDialog } from "./show-suggest-badge-dialog"; + +declare global { + interface HASSDomEvents { + "selected-changed": SelectedChangedEvent; + } +} + +interface SelectedChangedEvent { + selectedEntities: string[]; +} + +@customElement("hui-dialog-create-badge") +export class HuiCreateDialogBadge + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: CreateBadgeDialogParams; + + @state() private _containerConfig!: LovelaceViewConfig; + + @state() private _selectedEntities: string[] = []; + + @state() private _currTabIndex = 0; + + public async showDialog(params: CreateBadgeDialogParams): Promise { + this._params = params; + + const containerConfig = findLovelaceContainer( + params.lovelaceConfig, + params.path + ); + + if ("strategy" in containerConfig) { + throw new Error("Can't edit strategy"); + } + + this._containerConfig = containerConfig; + } + + public closeDialog(): boolean { + this._params = undefined; + this._currTabIndex = 0; + this._selectedEntities = []; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + return true; + } + + protected render() { + if (!this._params) { + return nothing; + } + + const title = this._containerConfig.title + ? this.hass!.localize( + "ui.panel.lovelace.editor.edit_badge.pick_badge_title", + { name: `"${this._containerConfig.title}"` } + ) + : this.hass!.localize("ui.panel.lovelace.editor.edit_badge.pick_badge"); + + return html` + + + + ${title} + + + + + + ${cache( + this._currTabIndex === 0 + ? html` + + ` + : html` + + ` + )} + +
    + + ${this.hass!.localize("ui.common.cancel")} + + ${this._selectedEntities.length + ? html` + + ${this.hass!.localize("ui.common.continue")} + + ` + : ""} +
    +
    + `; + } + + private _ignoreKeydown(ev: KeyboardEvent) { + ev.stopPropagation(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + --mdc-dialog-max-height: 100%; + height: 100%; + } + } + + @media all and (min-width: 850px) { + ha-dialog { + --mdc-dialog-min-width: 845px; + } + } + + ha-dialog { + --mdc-dialog-max-width: 845px; + --dialog-content-padding: 2px 24px 20px 24px; + --dialog-z-index: 6; + } + + ha-dialog.table { + --dialog-content-padding: 0; + } + + @media (min-width: 1200px) { + ha-dialog { + --mdc-dialog-max-width: calc(100vw - 32px); + --mdc-dialog-min-width: 1000px; + } + } + + hui-badge-picker { + --badge-picker-search-shape: 0; + --badge-picker-search-margin: -2px -24px 0; + } + hui-entity-picker-table { + display: block; + height: calc(100vh - 198px); + --mdc-shape-small: 0; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + hui-entity-picker-table { + height: calc(100vh - 158px); + } + } + `, + ]; + } + + private _handleBadgePicked(ev) { + const config = ev.detail.config; + if (this._params!.entities && this._params!.entities.length) { + if (Object.keys(config).includes("entities")) { + config.entities = this._params!.entities; + } else if (Object.keys(config).includes("entity")) { + config.entity = this._params!.entities[0]; + } + } + + showEditBadgeDialog(this, { + lovelaceConfig: this._params!.lovelaceConfig, + saveConfig: this._params!.saveConfig, + path: this._params!.path, + badgeConfig: config, + }); + + this.closeDialog(); + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = ev.detail.index; + if (newTab === this._currTabIndex) { + return; + } + + this._currTabIndex = ev.detail.index; + this._selectedEntities = []; + } + + private _handleSelectedChanged(ev: CustomEvent): void { + this._selectedEntities = ev.detail.selectedEntities; + } + + private _cancel(ev?: Event) { + if (ev) { + ev.stopPropagation(); + } + this.closeDialog(); + } + + private _suggestBadges(): void { + const badgeConfig = computeBadges(this.hass.states, this._selectedEntities); + + showSuggestBadgeDialog(this, { + lovelaceConfig: this._params!.lovelaceConfig, + saveConfig: this._params!.saveConfig, + path: this._params!.path as [number], + entities: this._selectedEntities, + badgeConfig, + }); + + this.closeDialog(); + } + + private _allEntities = memoize((entities) => + Object.keys(entities).map((entity) => { + const stateObj = this.hass.states[entity]; + return { + icon: "", + entity_id: entity, + stateObj, + name: computeStateName(stateObj), + domain: computeDomain(entity), + last_changed: stateObj!.last_changed, + } as DataTableRowData; + }) + ); +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-create-badge": HuiCreateDialogBadge; + } +} 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 19ae477d5e23..b469ae9d189b 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,7 @@ import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; import "../../../../components/ha-icon-button"; import { - defaultBadgeConfig, + ensureBadgeConfig, LovelaceBadgeConfig, } from "../../../../data/lovelace/config/badge"; import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; @@ -106,8 +106,7 @@ export class HuiDialogEditBadge this._dirty = true; } else { const badge = this._containerConfig.badges?.[params.badgeIndex]; - this._badgeConfig = - typeof badge === "string" ? defaultBadgeConfig(badge) : badge; + this._badgeConfig = badge != null ? ensureBadgeConfig(badge) : badge; } this.large = false; diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts new file mode 100644 index 000000000000..c02195022258 --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-suggest-badge.ts @@ -0,0 +1,204 @@ +import deepFreeze from "deep-freeze"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-yaml-editor"; + +import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; +import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; +import { LovelaceConfig } from "../../../../data/lovelace/config/types"; +import { haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; +import "../../badges/hui-badge"; +import { addBadges } from "../config-util"; +import { + LovelaceContainerPath, + parseLovelaceContainerPath, +} from "../lovelace-path"; +import { SuggestBadgeDialogParams } from "./show-suggest-badge-dialog"; + +@customElement("hui-dialog-suggest-badge") +export class HuiDialogSuggestBadge extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: SuggestBadgeDialogParams; + + @state() private _badgeConfig?: LovelaceBadgeConfig[]; + + @state() private _saving = false; + + @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; + + public showDialog(params: SuggestBadgeDialogParams): void { + this._params = params; + this._badgeConfig = params.badgeConfig; + if (!Object.isFrozen(this._badgeConfig)) { + this._badgeConfig = deepFreeze(this._badgeConfig); + } + if (this._yamlEditor) { + this._yamlEditor.setValue(this._badgeConfig); + } + } + + public closeDialog(): void { + this._params = undefined; + this._badgeConfig = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _renderPreview() { + if (this._badgeConfig) { + return html` +
    + ${this._badgeConfig.map( + (badgeConfig) => html` + + ` + )} +
    + `; + } + return nothing; + } + + protected render() { + if (!this._params) { + return nothing; + } + return html` + +
    + ${this._renderPreview()} + ${this._params.yaml && this._badgeConfig + ? html` +
    + +
    + ` + : nothing} +
    + + ${this._params.yaml + ? this.hass!.localize("ui.common.close") + : this.hass!.localize("ui.common.cancel")} + + ${!this._params.yaml + ? html` + + ${this._saving + ? html` + + ` + : this.hass!.localize( + "ui.panel.lovelace.editor.suggest_badge.add" + )} + + ` + : nothing} +
    + `; + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + max-height: 100%; + height: 100%; + } + } + @media all and (min-width: 850px) { + ha-dialog { + width: 845px; + } + } + ha-dialog { + max-width: 845px; + --dialog-z-index: 6; + } + .hidden { + display: none; + } + .element-preview { + position: relative; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + margin: 0; + } + .editor { + padding-top: 16px; + } + `, + ]; + } + + private _computeNewConfig( + config: LovelaceConfig, + path: LovelaceContainerPath + ): LovelaceConfig { + const { viewIndex } = parseLovelaceContainerPath(path); + + const newBadges = this._badgeConfig!; + return addBadges(config, [viewIndex], newBadges); + } + + private async _save(): Promise { + if ( + !this._params?.lovelaceConfig || + !this._params?.path || + !this._params?.saveConfig || + !this._badgeConfig + ) { + return; + } + this._saving = true; + + const newConfig = this._computeNewConfig( + this._params.lovelaceConfig, + this._params.path + ); + await this._params!.saveConfig(newConfig); + this._saving = false; + showSaveSuccessToast(this, this.hass); + this.closeDialog(); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-suggest-badge": HuiDialogSuggestBadge; + } +} diff --git a/src/panels/lovelace/editor/badge-editor/show-create-badge-dialog.ts b/src/panels/lovelace/editor/badge-editor/show-create-badge-dialog.ts new file mode 100644 index 000000000000..ab2313d10bd7 --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/show-create-badge-dialog.ts @@ -0,0 +1,25 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; +import { LovelaceContainerPath } from "../lovelace-path"; + +export interface CreateBadgeDialogParams { + lovelaceConfig: LovelaceConfig; + saveConfig: (config: LovelaceConfig) => void; + path: LovelaceContainerPath; + suggestedBadges?: string[]; + entities?: string[]; // We can pass entity id's that will be added to the config when a badge is picked +} + +export const importCreateBadgeDialog = () => + import("./hui-dialog-create-badge"); + +export const showCreateBadgeDialog = ( + element: HTMLElement, + createBadgeDialogParams: CreateBadgeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-create-badge", + dialogImport: importCreateBadgeDialog, + dialogParams: createBadgeDialogParams, + }); +}; diff --git a/src/panels/lovelace/editor/badge-editor/show-suggest-badge-dialog.ts b/src/panels/lovelace/editor/badge-editor/show-suggest-badge-dialog.ts new file mode 100644 index 000000000000..06bb94e1863f --- /dev/null +++ b/src/panels/lovelace/editor/badge-editor/show-suggest-badge-dialog.ts @@ -0,0 +1,26 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge"; +import { LovelaceConfig } from "../../../../data/lovelace/config/types"; +import { LovelaceContainerPath } from "../lovelace-path"; + +export interface SuggestBadgeDialogParams { + lovelaceConfig?: LovelaceConfig; + yaml?: boolean; + saveConfig?: (config: LovelaceConfig) => void; + path?: LovelaceContainerPath; + entities?: string[]; // We pass this to create dialog when user chooses "Pick own" + badgeConfig: LovelaceBadgeConfig[]; // We can pass a suggested config +} + +const importSuggestBadgeDialog = () => import("./hui-dialog-suggest-badge"); + +export const showSuggestBadgeDialog = ( + element: HTMLElement, + suggestBadgeDialogParams: SuggestBadgeDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-suggest-badge", + dialogImport: importSuggestBadgeDialog, + dialogParams: suggestBadgeDialogParams, + }); +}; diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index 3537955479a5..c8686a4dfe10 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -52,7 +52,7 @@ export class HuiCardPicker extends LitElement { @property({ attribute: false }) public suggestedCards?: string[]; @storage({ - key: "lovelaceClipboard", + key: "dashboardCardClipboard", state: true, subscribe: true, storage: "sessionStorage", @@ -490,7 +490,7 @@ export class HuiCardPicker extends LitElement { .cards-container { display: grid; grid-gap: 8px 8px; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); margin-top: 20px; } @@ -560,6 +560,7 @@ export class HuiCardPicker extends LitElement { .manual { max-width: none; + grid-column: 1 / -1; } .icon { diff --git a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts index b217bdc57702..dbc5d2fb2616 100644 --- a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts @@ -45,7 +45,7 @@ export class HuiConditionalCardEditor @property({ attribute: false }) public lovelace?: LovelaceConfig; @storage({ - key: "lovelaceClipboard", + key: "dashboardCardClipboard", state: false, subscribe: false, storage: "sessionStorage", diff --git a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts index c5e81164e134..896ea2129685 100644 --- a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts @@ -67,7 +67,7 @@ export class HuiStackCardEditor @property({ attribute: false }) public lovelace?: LovelaceConfig; @storage({ - key: "lovelaceClipboard", + key: "dashboardCardClipboard", state: false, subscribe: false, storage: "sessionStorage", diff --git a/src/panels/lovelace/editor/config-util.ts b/src/panels/lovelace/editor/config-util.ts index 7eb5d5f105e2..7d467d9aaa4b 100644 --- a/src/panels/lovelace/editor/config-util.ts +++ b/src/panels/lovelace/editor/config-util.ts @@ -1,4 +1,7 @@ -import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; +import { + ensureBadgeConfig, + 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"; @@ -331,6 +334,17 @@ export const addBadge = ( return newConfig; }; +export const addBadges = ( + 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, @@ -410,7 +424,7 @@ export const moveBadge = ( const badge = badges![fromCardIndex]; let newConfig = deleteBadge(config, fromPath); - newConfig = insertBadge(newConfig, toPath, badge); + newConfig = insertBadge(newConfig, toPath, ensureBadgeConfig(badge)); return newConfig; }; diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts index ab18788d9951..58c03373345a 100644 --- a/src/panels/lovelace/editor/hui-element-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -32,6 +32,7 @@ import type { HuiFormEditor } from "./config-elements/hui-form-editor"; import "./config-elements/hui-generic-entity-row-editor"; import { GUISupportError } from "./gui-support-error"; import { EditSubElementEvent, GUIModeChangedEvent } from "./types"; +import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; export interface ConfigChangedEvent { config: @@ -39,7 +40,8 @@ export interface ConfigChangedEvent { | LovelaceRowConfig | LovelaceHeaderFooterConfig | LovelaceCardFeatureConfig - | LovelaceStrategyConfig; + | LovelaceStrategyConfig + | LovelaceBadgeConfig; error?: string; guiModeAvailable?: boolean; } diff --git a/src/panels/lovelace/editor/lovelace-badges.ts b/src/panels/lovelace/editor/lovelace-badges.ts new file mode 100644 index 000000000000..c4b17856bf56 --- /dev/null +++ b/src/panels/lovelace/editor/lovelace-badges.ts @@ -0,0 +1,8 @@ +import { Badge } from "./types"; + +export const coreBadges: Badge[] = [ + { + type: "entity", + showElement: true, + }, +]; diff --git a/src/panels/lovelace/editor/lovelace-path.ts b/src/panels/lovelace/editor/lovelace-path.ts index c27716d05001..22f58dfbd5ea 100644 --- a/src/panels/lovelace/editor/lovelace-path.ts +++ b/src/panels/lovelace/editor/lovelace-path.ts @@ -127,7 +127,7 @@ export const updateLovelaceContainer = ( type LovelaceItemKeys = { cards: LovelaceCardConfig[]; - badges: LovelaceBadgeConfig[]; + badges: (Partial | string)[]; }; export const updateLovelaceItems = ( diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index 44c42beac8b9..aaa8f4225f0b 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -7,6 +7,7 @@ import { import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types"; import { LovelaceCardFeatureConfig } from "../card-features/types"; +import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; export interface YamlChangedEvent extends Event { detail: { @@ -65,6 +66,15 @@ export interface Card { isSuggested?: boolean; } +export interface Badge { + type: string; + name?: string; + description?: string; + showElement?: boolean; + isCustom?: boolean; + isSuggested?: boolean; +} + export interface HeaderFooter { type: LovelaceHeaderFooterConfig["type"]; icon?: string; @@ -74,6 +84,10 @@ export interface CardPickTarget extends EventTarget { config: LovelaceCardConfig; } +export interface BadgePickTarget extends EventTarget { + config: LovelaceBadgeConfig; +} + export interface SubElementEditorConfig { index?: number; elementConfig?: diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 6c9cdf3381f2..5d0feb7fb4fe 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -6,7 +6,7 @@ import "../../../components/entity/ha-state-label-badge"; import "../../../components/ha-svg-icon"; import type { LovelaceViewElement } from "../../../data/lovelace"; import { - defaultBadgeConfig, + ensureBadgeConfig, LovelaceBadgeConfig, } from "../../../data/lovelace/config/badge"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; @@ -26,7 +26,6 @@ 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, @@ -37,6 +36,7 @@ import type { HuiSection } from "../sections/hui-section"; import { generateLovelaceViewStrategy } from "../strategies/get-strategy"; import type { Lovelace } from "../types"; import { DEFAULT_VIEW_LAYOUT, PANEL_VIEW_LAYOUT } from "./const"; +import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-dialog"; declare global { // for fire event @@ -332,18 +332,10 @@ export class HUIView extends ReactiveElement { } }); this._layoutElement.addEventListener("ll-create-badge", async () => { - const defaultConfig = await getBadgeStubConfig( - this.hass, - "entity", - Object.keys(this.hass.entities), - [] - ); - - showEditBadgeDialog(this, { + showCreateBadgeDialog(this, { lovelaceConfig: this.lovelace.config, saveConfig: this.lovelace.saveConfig, path: [this.index], - badgeConfig: defaultConfig, }); }); this._layoutElement.addEventListener("ll-edit-badge", (ev) => { @@ -368,8 +360,7 @@ export class HUIView extends ReactiveElement { } this._badges = config.badges.map((badge) => { - const badgeConfig = - typeof badge === "string" ? defaultBadgeConfig(badge) : badge; + const badgeConfig = ensureBadgeConfig(badge); const element = this._createBadgeElement(badgeConfig); return element; }); diff --git a/src/translations/en.json b/src/translations/en.json index 7fa5f0253d37..5eb543eb0cf5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5586,6 +5586,10 @@ "explanation": "The badge will be shown when ALL conditions below are fulfilled. If no conditions are set, the badge will always be shown." } }, + "suggest_badge": { + "header": "[%key:ui::panel::lovelace::editor::suggest_card::header%]", + "add": "[%key:ui::panel::lovelace::editor::suggest_card::add%]" + }, "move_card": { "header": "Choose a view to move the card to", "error_title": "Impossible to move the card", @@ -6047,6 +6051,15 @@ "standard": "Standard (icon and state)", "complete": "Complete (icon, name and state)" } + }, + "generic": { + "manual": "Manual", + "manual_description": "Need to add a custom badge or just want to manually write the YAML?", + "paste": "Paste from clipboard", + "paste_description": "Paste a {type} badge from the clipboard", + "suggested_badges": "Suggested badges", + "other_badges": "Other badges", + "custom_badges": "Custom badges" } }, "features": { @@ -6236,7 +6249,15 @@ "domain": "Domain", "entity": "Entity", "by_entity": "By entity", - "by_card": "By Card" + "by_card": "By card" + }, + "badge_picker": { + "no_description": "No description available.", + "custom_badge": "Custom", + "domain": "Domain", + "entity": "Entity", + "by_entity": "By entity", + "by_badge": "By badge" }, "header-footer": { "header": "Header",