mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-19 14:52:28 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12ea6c6c58 |
@@ -1,6 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
|
||||
import type { LovelaceBadgeConfig } from "./lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "./lovelace/config/card";
|
||||
|
||||
export interface CustomCardSuggestion<
|
||||
@@ -10,6 +11,13 @@ export interface CustomCardSuggestion<
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CustomBadgeSuggestion<
|
||||
T extends LovelaceBadgeConfig = LovelaceBadgeConfig,
|
||||
> {
|
||||
label?: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CustomCardEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
@@ -28,6 +36,10 @@ export interface CustomBadgeEntry {
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
documentationURL?: string;
|
||||
getEntitySuggestion?: (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) => CustomBadgeSuggestion | CustomBadgeSuggestion[] | null;
|
||||
}
|
||||
|
||||
export interface CustomCardFeatureEntry {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { EntityBadgeConfig } from "../badges/types";
|
||||
import type { BadgeSuggestion, BadgeSuggestionProvider } from "./types";
|
||||
|
||||
export const entityBadgeSuggestions: BadgeSuggestionProvider<EntityBadgeConfig> =
|
||||
{
|
||||
getEntitySuggestion(hass, entityId) {
|
||||
const suggestions: BadgeSuggestion<EntityBadgeConfig>[] = [
|
||||
{
|
||||
config: { type: "entity", entity: entityId },
|
||||
},
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.with_name"
|
||||
),
|
||||
config: { type: "entity", entity: entityId, show_name: true },
|
||||
},
|
||||
];
|
||||
return suggestions;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { customBadges } from "../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { BADGE_SUGGESTION_PROVIDERS } from "./registry";
|
||||
import type { BadgeSuggestion } from "./types";
|
||||
|
||||
export type { BadgeSuggestion, BadgeSuggestionProvider } from "./types";
|
||||
export { BADGE_SUGGESTION_PROVIDERS } from "./registry";
|
||||
|
||||
export interface BadgeSuggestions {
|
||||
core: BadgeSuggestion[];
|
||||
custom: BadgeSuggestion[];
|
||||
}
|
||||
|
||||
export const generateBadgeSuggestions = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string | undefined
|
||||
): BadgeSuggestions => {
|
||||
if (!entityId || hass.states[entityId] === undefined) {
|
||||
return { core: [], custom: [] };
|
||||
}
|
||||
const core = Object.values(BADGE_SUGGESTION_PROVIDERS).flatMap((provider) => {
|
||||
try {
|
||||
return ensureArray(provider.getEntitySuggestion(hass, entityId)) ?? [];
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Badge suggestion provider threw:", err);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const custom = customBadges.flatMap((badge) => {
|
||||
if (!badge.getEntitySuggestion) return [];
|
||||
try {
|
||||
return ensureArray(badge.getEntitySuggestion(hass, entityId)) ?? [];
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Custom badge "${badge.type}" getEntitySuggestion threw:`,
|
||||
err
|
||||
);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
return { core, custom };
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { entityBadgeSuggestions } from "./hui-entity-badge-suggestions";
|
||||
import type { BadgeSuggestionProvider } from "./types";
|
||||
|
||||
export const BADGE_SUGGESTION_PROVIDERS: Record<
|
||||
string,
|
||||
BadgeSuggestionProvider
|
||||
> = {
|
||||
entity: entityBadgeSuggestions,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
export interface BadgeSuggestion<
|
||||
T extends LovelaceBadgeConfig = LovelaceBadgeConfig,
|
||||
> {
|
||||
label?: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface BadgeSuggestionProvider<
|
||||
T extends LovelaceBadgeConfig = LovelaceBadgeConfig,
|
||||
> {
|
||||
getEntitySuggestion(
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): BadgeSuggestion<T> | BadgeSuggestion<T>[] | null;
|
||||
}
|
||||
@@ -13,14 +13,12 @@ import type {
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import { computeUserInitials } from "../../../data/user";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HELPER_DOMAINS } from "../../config/helpers/const";
|
||||
import type { EntityBadgeConfig } from "../badges/types";
|
||||
import type {
|
||||
AlarmPanelCardConfig,
|
||||
EntitiesCardConfig,
|
||||
@@ -315,23 +313,6 @@ 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"]
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
import { mdiClose, mdiViewGridPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeEntityPickerDisplay } from "../../../../common/entity/compute_entity_name_display";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-ripple";
|
||||
import "../../../../components/ha-section-title";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import { haStyleScrollbar } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
generateBadgeSuggestions,
|
||||
type BadgeSuggestions,
|
||||
} from "../../badge-suggestions";
|
||||
import type { BadgeSuggestion } from "../../badge-suggestions/types";
|
||||
import "../card-editor/hui-suggestion-entity-tree";
|
||||
import type { HuiSuggestionEntityTree } from "../card-editor/hui-suggestion-entity-tree";
|
||||
import "./hui-suggestion-badge";
|
||||
|
||||
@customElement("hui-badge-suggestion-picker")
|
||||
export class HuiBadgeSuggestionPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Array, attribute: false })
|
||||
public prioritizedBadgeTypes?: string[];
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
private _narrowMql?: MediaQueryList;
|
||||
|
||||
@query("hui-suggestion-entity-tree")
|
||||
private _entityTree?: HuiSuggestionEntityTree;
|
||||
|
||||
public async focus(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._entityTree?.focus();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._narrowMql = matchMedia("(max-width: 600px)");
|
||||
this._narrow = this._narrowMql.matches;
|
||||
this._narrowMql.addEventListener("change", this._handleNarrowChange);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._narrowMql?.removeEventListener("change", this._handleNarrowChange);
|
||||
this._narrowMql = undefined;
|
||||
}
|
||||
|
||||
private _handleNarrowChange = (ev: MediaQueryListEvent) => {
|
||||
this._narrow = ev.matches;
|
||||
};
|
||||
|
||||
// Memoize on scalars so the result stays stable when only hass changes.
|
||||
// Keeps hui-badge previews from re-rendering on every state tick.
|
||||
private _computeSuggestions = memoizeOne(
|
||||
(
|
||||
entityId: string | undefined,
|
||||
priorityTypesKey: string
|
||||
): BadgeSuggestions => {
|
||||
const { core, custom } = generateBadgeSuggestions(this.hass, entityId);
|
||||
const priorityTypes = priorityTypesKey
|
||||
? priorityTypesKey.split("|")
|
||||
: undefined;
|
||||
if (!priorityTypes?.length) return { core, custom };
|
||||
const isPrioritized = (s: BadgeSuggestion) =>
|
||||
priorityTypes.includes(s.config.type);
|
||||
return {
|
||||
core: [
|
||||
...core.filter(isPrioritized),
|
||||
...core.filter((s) => !isPrioritized(s)),
|
||||
],
|
||||
custom,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const hasEntity = !!this._entityId;
|
||||
// Tree is rendered unconditionally so its state (filter, expanded
|
||||
// branches, fuse index) survives the desktop/mobile and tree/suggestions
|
||||
// switches.
|
||||
const showTree = !this._narrow || !hasEntity;
|
||||
const showMain = !this._narrow || hasEntity;
|
||||
return html`
|
||||
<div class=${classMap({ sidebar: true, hidden: !showTree })}>
|
||||
<hui-suggestion-entity-tree
|
||||
class="tree"
|
||||
.hass=${this.hass}
|
||||
.selectedEntityId=${this._entityId}
|
||||
@entity-picked=${this._handleEntityPicked}
|
||||
></hui-suggestion-entity-tree>
|
||||
</div>
|
||||
<div class=${classMap({ main: true, hidden: !showMain })}>
|
||||
<div class="content ha-scrollbar">
|
||||
${this._renderMainContent(hasEntity)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderMainContent(
|
||||
hasEntity: boolean
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!hasEntity) return this._renderEmptyState();
|
||||
const { core, custom } = this._suggestions();
|
||||
return html`
|
||||
${this._narrow ? this._renderSelectedEntity() : nothing}
|
||||
<ha-section-title>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.suggestions_title"
|
||||
)}
|
||||
</ha-section-title>
|
||||
${this._renderSuggestionsGrid(core)}
|
||||
${custom.length
|
||||
? html`
|
||||
<ha-section-title>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.community_title"
|
||||
)}
|
||||
</ha-section-title>
|
||||
${this._renderSuggestionsGrid(custom)}
|
||||
`
|
||||
: nothing}
|
||||
${this._renderBrowseBadge()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderBrowseBadge(): TemplateResult {
|
||||
return html`
|
||||
<div class="browse-badge">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.not_found"
|
||||
)}
|
||||
</p>
|
||||
<ha-button appearance="plain" @click=${this._browseBadges}>
|
||||
<ha-svg-icon slot="start" .path=${mdiViewGridPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.browse_badges"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSelectedEntity(): TemplateResult {
|
||||
const stateObj = this.hass.states[this._entityId!];
|
||||
const { primary, secondary } = stateObj
|
||||
? computeEntityPickerDisplay(this.hass, stateObj)
|
||||
: { primary: this._entityId!, secondary: undefined };
|
||||
return html`
|
||||
<ha-section-title>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.selected_entity"
|
||||
)}
|
||||
</ha-section-title>
|
||||
<ha-combo-box-item compact class="selected-entity">
|
||||
${stateObj
|
||||
? html`<state-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>`
|
||||
: nothing}
|
||||
<span slot="headline">${primary}</span>
|
||||
${secondary
|
||||
? html`<span slot="supporting-text">${secondary}</span>`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.label=${this.hass.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._clearEntity}
|
||||
></ha-icon-button>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEmptyState(): TemplateResult {
|
||||
return html`
|
||||
<div class="content-empty">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.content_empty_title"
|
||||
)}
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.content_empty_description"
|
||||
)}
|
||||
</p>
|
||||
<ha-button appearance="plain" @click=${this._browseBadges}>
|
||||
<ha-svg-icon slot="start" .path=${mdiViewGridPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.browse_badges"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _suggestionKeys = new WeakMap<BadgeSuggestion, string>();
|
||||
|
||||
private _suggestionKey = (s: BadgeSuggestion): string => {
|
||||
let key = this._suggestionKeys.get(s);
|
||||
if (key === undefined) {
|
||||
key = JSON.stringify(s.config);
|
||||
this._suggestionKeys.set(s, key);
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
private _renderSuggestionsGrid(
|
||||
suggestions: BadgeSuggestion[]
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<div class="suggestions" @pick-badge-suggestion=${this._pickSuggestion}>
|
||||
${repeat(
|
||||
suggestions,
|
||||
this._suggestionKey,
|
||||
(s: BadgeSuggestion) => html`
|
||||
<hui-suggestion-badge
|
||||
.hass=${this.hass}
|
||||
.suggestion=${s}
|
||||
></hui-suggestion-badge>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _suggestions(): BadgeSuggestions {
|
||||
return this._computeSuggestions(
|
||||
this._entityId,
|
||||
(this.prioritizedBadgeTypes ?? []).join("|")
|
||||
);
|
||||
}
|
||||
|
||||
private _browseBadges(): void {
|
||||
fireEvent(this, "browse-badges", undefined);
|
||||
}
|
||||
|
||||
private _handleEntityPicked(ev: CustomEvent<{ entityId: string }>): void {
|
||||
this._entityId = ev.detail.entityId;
|
||||
}
|
||||
|
||||
private _clearEntity(): void {
|
||||
this._entityId = undefined;
|
||||
}
|
||||
|
||||
private _pickSuggestion(
|
||||
ev: CustomEvent<{ suggestion: BadgeSuggestion }>
|
||||
): void {
|
||||
fireEvent(this, "badge-suggestion-picked", {
|
||||
config: ev.detail.suggestion.config,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex: 0 0 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-inline-end: var(--ha-border-width-sm) solid
|
||||
var(--divider-color);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tree {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.suggestions {
|
||||
display: grid;
|
||||
gap: var(--ha-space-3);
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.content-empty {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-3);
|
||||
padding: var(--ha-space-8) var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
.content-empty h2 {
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.content-empty p {
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
color: var(--ha-color-text-secondary);
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
}
|
||||
.browse-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-6) var(--ha-space-4);
|
||||
}
|
||||
.browse-badge p {
|
||||
margin: 0;
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
|
||||
/* Mobile master/detail: sidebar OR main is visible, never both. */
|
||||
@media (max-width: 600px) {
|
||||
:host {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sidebar {
|
||||
flex: 1;
|
||||
border-inline-end: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-badge-suggestion-picker": HuiBadgeSuggestionPicker;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"browse-badges": undefined;
|
||||
"badge-suggestion-picked": { config: LovelaceBadgeConfig };
|
||||
}
|
||||
}
|
||||
@@ -3,35 +3,24 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, 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 { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
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 { addBadge } from "../config-util";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import "./hui-badge-picker";
|
||||
import "./hui-badge-suggestion-picker";
|
||||
import type { 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
|
||||
@@ -46,13 +35,17 @@ export class HuiCreateDialogBadge
|
||||
|
||||
@state() private _containerConfig!: LovelaceViewConfig;
|
||||
|
||||
@state() private _selectedEntities: string[] = [];
|
||||
@state() private _currTab: "badge" | "entity" = "entity";
|
||||
|
||||
@state() private _currTab: "badge" | "entity" = "badge";
|
||||
@state() private _narrow = false;
|
||||
|
||||
public async showDialog(params: CreateBadgeDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
@@ -74,8 +67,7 @@ export class HuiCreateDialogBadge
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._currTab = "badge";
|
||||
this._selectedEntities = [];
|
||||
this._currTab = "entity";
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -98,7 +90,6 @@ export class HuiCreateDialogBadge
|
||||
width="large"
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${classMap({ table: this._currTab === "entity" })}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
@@ -109,6 +100,15 @@ export class HuiCreateDialogBadge
|
||||
></ha-icon-button>
|
||||
<span slot="title">${title}</span>
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "entity"}
|
||||
panel="entity"
|
||||
?autofocus=${this._narrow}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.by_entity"
|
||||
)}</ha-tab-group-tab
|
||||
>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "badge"}
|
||||
@@ -118,35 +118,31 @@ export class HuiCreateDialogBadge
|
||||
"ui.panel.lovelace.editor.badge_picker.by_badge"
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "entity"}
|
||||
panel="entity"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.badge_picker.by_entity"
|
||||
)}</ha-tab-group-tab
|
||||
>
|
||||
</ha-tab-group>
|
||||
</ha-dialog-header>
|
||||
${cache(
|
||||
this._currTab === "badge"
|
||||
? html`
|
||||
<hui-badge-picker
|
||||
autofocus
|
||||
.suggestedBadges=${this._params.suggestedBadges}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.hass=${this.hass}
|
||||
@config-changed=${this._handleBadgePicked}
|
||||
></hui-badge-picker>
|
||||
`
|
||||
: html`
|
||||
<hui-entity-picker-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${true}
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
></hui-entity-picker-table>
|
||||
`
|
||||
)}
|
||||
<div class="body">
|
||||
${cache(
|
||||
this._currTab === "entity"
|
||||
? html`
|
||||
<hui-badge-suggestion-picker
|
||||
?autofocus=${!this._narrow}
|
||||
.hass=${this.hass}
|
||||
.prioritizedBadgeTypes=${this._params.suggestedBadges}
|
||||
@badge-suggestion-picked=${this._handleSuggestionPicked}
|
||||
@browse-badges=${this._handleBrowseBadges}
|
||||
></hui-badge-suggestion-picker>
|
||||
`
|
||||
: html`
|
||||
<hui-badge-picker
|
||||
?autofocus=${!this._narrow}
|
||||
.suggestedBadges=${this._params.suggestedBadges}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.hass=${this.hass}
|
||||
@config-changed=${this._handleBadgePicked}
|
||||
></hui-badge-picker>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -156,13 +152,6 @@ export class HuiCreateDialogBadge
|
||||
>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
${this._selectedEntities.length
|
||||
? html`
|
||||
<ha-button slot="primaryAction" @click=${this._suggestBadges}>
|
||||
${this.hass!.localize("ui.common.continue")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -181,13 +170,19 @@ export class HuiCreateDialogBadge
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
|
||||
ha-dialog.table {
|
||||
--dialog-content-padding: 0;
|
||||
@media (min-width: 451px) and (min-height: 501px) {
|
||||
ha-dialog {
|
||||
--ha-dialog-min-height: min(900px, 80vh);
|
||||
--ha-dialog-max-height: var(--ha-dialog-min-height);
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog::part(body) {
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-dialog-footer {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
@@ -196,34 +191,41 @@ export class HuiCreateDialogBadge
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
hui-badge-picker,
|
||||
hui-badge-suggestion-picker {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
hui-badge-picker {
|
||||
--badge-picker-search-shape: 0;
|
||||
--badge-picker-search-margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
hui-badge-picker,
|
||||
hui-entity-picker-table {
|
||||
height: calc(100vh - 198px);
|
||||
}
|
||||
|
||||
hui-entity-picker-table {
|
||||
display: block;
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
hui-badge-picker,
|
||||
hui-entity-picker-table {
|
||||
height: calc(100vh - 158px);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _handleBrowseBadges(): void {
|
||||
this._currTab = "badge";
|
||||
}
|
||||
|
||||
private async _handleSuggestionPicked(
|
||||
ev: CustomEvent<{ config: LovelaceBadgeConfig }>
|
||||
): Promise<void> {
|
||||
const config = ev.detail.config;
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
const newConfig = addBadge(lovelaceConfig, containerPath, config);
|
||||
await saveConfig(newConfig);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleBadgePicked(ev) {
|
||||
const config = ev.detail.config;
|
||||
if (this._params!.entities && this._params!.entities.length) {
|
||||
@@ -249,13 +251,7 @@ export class HuiCreateDialogBadge
|
||||
if (newTab === this._currTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currTab = newTab;
|
||||
this._selectedEntities = [];
|
||||
}
|
||||
|
||||
private _handleSelectedChanged(ev: CustomEvent): void {
|
||||
this._selectedEntities = ev.detail.selectedEntities;
|
||||
}
|
||||
|
||||
private _cancel(ev?: Event) {
|
||||
@@ -264,20 +260,6 @@ export class HuiCreateDialogBadge
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import deepFreeze from "deep-freeze";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, 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 "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../badges/hui-badge";
|
||||
import { addBadges } from "../config-util";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
import { parseLovelaceContainerPath } from "../lovelace-path";
|
||||
import type { 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 _open = false;
|
||||
|
||||
@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;
|
||||
this._open = true;
|
||||
if (!Object.isFrozen(this._badgeConfig)) {
|
||||
this._badgeConfig = deepFreeze(this._badgeConfig);
|
||||
}
|
||||
if (this._yamlEditor) {
|
||||
this._yamlEditor.setValue(this._badgeConfig);
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
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=${this._open}
|
||||
header-title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.suggest_badge.header"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._renderPreview()}
|
||||
${this._params.yaml && this._badgeConfig
|
||||
? html`
|
||||
<div class="editor">
|
||||
<ha-yaml-editor
|
||||
.defaultValue=${this._badgeConfig}
|
||||
in-dialog
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
autofocus
|
||||
>
|
||||
${this._params.yaml
|
||||
? this.hass!.localize("ui.common.close")
|
||||
: this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
${!this._params.yaml
|
||||
? html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.loading=${this._saving}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.suggest_badge.add"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-2);
|
||||
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,124 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-ripple";
|
||||
import {
|
||||
getCustomBadgeEntry,
|
||||
isCustomType,
|
||||
stripCustomPrefix,
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { BadgeSuggestion } from "../../badge-suggestions/types";
|
||||
import "../../badges/hui-badge";
|
||||
|
||||
@customElement("hui-suggestion-badge")
|
||||
export class HuiSuggestionBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public suggestion!: BadgeSuggestion;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { suggestion } = this;
|
||||
const type = suggestion.config.type;
|
||||
let badgeName: string;
|
||||
if (isCustomType(type)) {
|
||||
const customType = stripCustomPrefix(type);
|
||||
badgeName = getCustomBadgeEntry(customType)?.name ?? customType;
|
||||
} else {
|
||||
badgeName =
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.badge.${type}.name` as any
|
||||
) || type;
|
||||
}
|
||||
const label = suggestion.label
|
||||
? `${badgeName} - ${suggestion.label}`
|
||||
: badgeName;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="badge"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label=${label}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
<div class="overlay" @click=${this._handleClick}></div>
|
||||
<div class="badge-header">${label}</div>
|
||||
<div class="preview">
|
||||
<hui-badge
|
||||
.hass=${this.hass}
|
||||
.config=${suggestion.config}
|
||||
preview
|
||||
></hui-badge>
|
||||
</div>
|
||||
<ha-ripple></ha-ripple>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
fireEvent(this, "pick-badge-suggestion", { suggestion: this.suggestion });
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent): void {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleClick();
|
||||
}
|
||||
}
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.badge {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
background: var(--primary-background-color);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: var(--ha-card-border-width, var(--ha-border-width-sm)) solid
|
||||
var(--ha-card-border-color, var(--divider-color));
|
||||
}
|
||||
.badge:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.badge-header {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
.preview {
|
||||
pointer-events: none;
|
||||
margin: var(--ha-space-4);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-suggestion-badge": HuiSuggestionBadge;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"pick-badge-suggestion": { suggestion: BadgeSuggestion };
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { 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,
|
||||
});
|
||||
};
|
||||
@@ -261,4 +261,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entity-picker-table": HuiEntityPickerTable;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"selected-changed": { selectedEntities: string[] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10475,7 +10475,15 @@
|
||||
"domain": "Domain",
|
||||
"entity": "Entity",
|
||||
"by_entity": "By entity",
|
||||
"by_badge": "By badge"
|
||||
"by_badge": "By badge",
|
||||
"with_name": "With name",
|
||||
"suggestions_title": "[%key:ui::panel::lovelace::editor::cardpicker::suggestions_title%]",
|
||||
"community_title": "[%key:ui::panel::lovelace::editor::cardpicker::community_title%]",
|
||||
"selected_entity": "[%key:ui::panel::lovelace::editor::cardpicker::selected_entity%]",
|
||||
"content_empty_title": "[%key:ui::panel::lovelace::editor::cardpicker::content_empty_title%]",
|
||||
"content_empty_description": "Or browse all badge types.",
|
||||
"browse_badges": "Browse all badges",
|
||||
"not_found": "Can't find the badge you want?"
|
||||
},
|
||||
"header-footer": {
|
||||
"header": "Header",
|
||||
|
||||
Reference in New Issue
Block a user