mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add picker for badges (#21436)
This commit is contained in:
parent
345000a0e9
commit
179245e1aa
@ -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<LovelaceBadgeConfig> | 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,
|
||||
};
|
||||
};
|
||||
|
@ -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<LovelaceBadgeConfig>)[]; // Badge can be just an entity_id or without type
|
||||
cards?: LovelaceCardConfig[];
|
||||
sections?: LovelaceSectionRawConfig[];
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"]
|
||||
|
@ -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"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")}
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")}
|
||||
</ha-list-item>
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item graphic="icon" class="warning">
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")}
|
||||
@ -159,18 +171,35 @@ export class HuiBadgeEditMode extends LitElement {
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
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! });
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ export class HuiCardEditMode extends LitElement {
|
||||
public _focused: boolean = false;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
590
src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts
Normal file
590
src/panels/lovelace/editor/badge-editor/hui-badge-picker.ts
Normal file
@ -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<Badge> = {
|
||||
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`
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.search_badgess"
|
||||
)}
|
||||
></search-input>
|
||||
<div
|
||||
id="content"
|
||||
style=${styleMap({
|
||||
width: this._width ? `${this._width}px` : "auto",
|
||||
height: this._height ? `${this._height}px` : "auto",
|
||||
})}
|
||||
>
|
||||
<div class="badges-container">
|
||||
${this._filter
|
||||
? this._filterBadges(this._badges, this._filter).map(
|
||||
(badgeElement: BadgeElement) => badgeElement.element
|
||||
)
|
||||
: html`
|
||||
${suggestedBadges.length > 0
|
||||
? html`
|
||||
<div class="badges-container-header">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.generic.suggested_badges`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${this._renderClipboardBadge()}
|
||||
${suggestedBadges.map(
|
||||
(badgeElement: BadgeElement) => badgeElement.element
|
||||
)}
|
||||
${suggestedBadges.length > 0
|
||||
? html`
|
||||
<div class="badges-container-header">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.generic.other_badges`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${otherBadges.map(
|
||||
(badgeElement: BadgeElement) => badgeElement.element
|
||||
)}
|
||||
${customBadgesItems.length > 0
|
||||
? html`
|
||||
<div class="badges-container-header">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.generic.custom_badges`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${customBadgesItems.map(
|
||||
(badgeElement: BadgeElement) => badgeElement.element
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
<div class="badges-container">
|
||||
<div
|
||||
class="badge manual"
|
||||
@click=${this._badgePicked}
|
||||
.config=${{ type: "" }}
|
||||
>
|
||||
<div class="badge-header">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.generic.manual`
|
||||
)}
|
||||
</div>
|
||||
<div class="preview description">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.generic.manual_description`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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>((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`
|
||||
<div class="badge spinner">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
)}`,
|
||||
}));
|
||||
}
|
||||
|
||||
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`
|
||||
<div class="badge spinner">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
)}`;
|
||||
}
|
||||
|
||||
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<TemplateResult> {
|
||||
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`
|
||||
<div class="badge">
|
||||
<div
|
||||
class="overlay"
|
||||
@click=${this._badgePicked}
|
||||
.config=${badgeConfig}
|
||||
></div>
|
||||
<div class="badge-header">
|
||||
${customBadge
|
||||
? `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.custom_badge"
|
||||
)}: ${customBadge.name || customBadge.type}`
|
||||
: name}
|
||||
</div>
|
||||
<div
|
||||
class="preview ${classMap({
|
||||
description: !element || element.tagName === "HUI-ERROR-BADGE",
|
||||
})}"
|
||||
>
|
||||
${element && element.tagName !== "HUI-ERROR-BADGE"
|
||||
? element
|
||||
: customBadge
|
||||
? customBadge.description ||
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge_picker.no_description`
|
||||
)
|
||||
: description}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<CreateBadgeDialogParams>
|
||||
{
|
||||
@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<void> {
|
||||
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`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._cancel}
|
||||
.heading=${title}
|
||||
class=${classMap({ table: this._currTabIndex === 1 })}
|
||||
>
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title"> ${title} </span>
|
||||
<mwc-tab-bar
|
||||
.activeIndex=${this._currTabIndex}
|
||||
@MDCTabBar:activated=${this._handleTabChanged}
|
||||
>
|
||||
<mwc-tab
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.by_badge"
|
||||
)}
|
||||
dialogInitialFocus
|
||||
></mwc-tab>
|
||||
<mwc-tab
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.by_entity"
|
||||
)}
|
||||
></mwc-tab>
|
||||
</mwc-tab-bar>
|
||||
</ha-dialog-header>
|
||||
${cache(
|
||||
this._currTabIndex === 0
|
||||
? html`
|
||||
<hui-badge-picker
|
||||
.suggestedBadges=${this._params.suggestedBadges}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.hass=${this.hass}
|
||||
@config-changed=${this._handleBadgePicked}
|
||||
></hui-badge-picker>
|
||||
`
|
||||
: html`
|
||||
<hui-entity-picker-table
|
||||
no-label-float
|
||||
.hass=${this.hass}
|
||||
.narrow=${true}
|
||||
.entities=${this._allEntities(this.hass.states)}
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
></hui-entity-picker-table>
|
||||
`
|
||||
)}
|
||||
|
||||
<div slot="primaryAction">
|
||||
<mwc-button @click=${this._cancel}>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
${this._selectedEntities.length
|
||||
? html`
|
||||
<mwc-button @click=${this._suggestBadges}>
|
||||
${this.hass!.localize("ui.common.continue")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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`
|
||||
<div class="element-preview">
|
||||
${this._badgeConfig.map(
|
||||
(badgeConfig) => html`
|
||||
<hui-badge
|
||||
.hass=${this.hass}
|
||||
.config=${badgeConfig}
|
||||
preview
|
||||
></hui-badge>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.suggest_badge.header"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._renderPreview()}
|
||||
${this._params.yaml && this._badgeConfig
|
||||
? html`
|
||||
<div class="editor">
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._badgeConfig}
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this._params.yaml
|
||||
? this.hass!.localize("ui.common.close")
|
||||
: this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
${!this._params.yaml
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._saving}
|
||||
@click=${this._save}
|
||||
>
|
||||
${this._saving
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
aria-label="Saving"
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.suggest_badge.add"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
};
|
@ -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,
|
||||
});
|
||||
};
|
@ -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 {
|
||||
|
@ -45,7 +45,7 @@ export class HuiConditionalCardEditor
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
|
@ -67,7 +67,7 @@ export class HuiStackCardEditor
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
8
src/panels/lovelace/editor/lovelace-badges.ts
Normal file
8
src/panels/lovelace/editor/lovelace-badges.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Badge } from "./types";
|
||||
|
||||
export const coreBadges: Badge[] = [
|
||||
{
|
||||
type: "entity",
|
||||
showElement: true,
|
||||
},
|
||||
];
|
@ -127,7 +127,7 @@ export const updateLovelaceContainer = (
|
||||
|
||||
type LovelaceItemKeys = {
|
||||
cards: LovelaceCardConfig[];
|
||||
badges: LovelaceBadgeConfig[];
|
||||
badges: (Partial<LovelaceBadgeConfig> | string)[];
|
||||
};
|
||||
|
||||
export const updateLovelaceItems = <T extends keyof LovelaceItemKeys>(
|
||||
|
@ -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?:
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user