Add a sub-editor to hui-entity-editor (#27157)

* Add a sub-editor to hui-entity-editor

* item styling
This commit is contained in:
karwosts
2025-10-01 22:19:24 -07:00
committed by GitHub
parent 91258c86c1
commit 552691e200
10 changed files with 302 additions and 26 deletions

View File

@@ -283,7 +283,7 @@ export interface GlanceConfigEntity extends ConfigEntity {
image?: string;
show_state?: boolean;
state_color?: boolean;
format: TimestampRenderingFormat;
format?: TimestampRenderingFormat;
}
export interface GlanceCardConfig extends LovelaceCardConfig {

View File

@@ -1,4 +1,4 @@
import { mdiDrag } from "@mdi/js";
import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
@@ -12,6 +12,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-sortable";
import type { HomeAssistant } from "../../../types";
import type { EntityConfig } from "../entity-rows/types";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("hui-entity-editor")
export class HuiEntityEditor extends LitElement {
@@ -24,6 +25,8 @@ export class HuiEntityEditor extends LitElement {
@property() public label?: string;
@property({ attribute: "can-edit", type: Boolean }) public canEdit?;
private _entityKeys = new WeakMap<EntityConfig, string>();
private _getKey(action: EntityConfig) {
@@ -34,6 +37,70 @@ export class HuiEntityEditor extends LitElement {
return this._entityKeys.get(action)!;
}
private _renderItem(item: EntityConfig, index: number) {
const stateObj = this.hass!.states[item.entity];
const entityName =
stateObj && this.hass!.formatEntityName(stateObj, "entity");
const deviceName =
stateObj && this.hass!.formatEntityName(stateObj, "device");
const areaName = stateObj && this.hass!.formatEntityName(stateObj, "area");
const isRTL = computeRTL(this.hass!);
const primary = item.name || entityName || deviceName || item.entity;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return html`
<ha-md-list-item class="item">
<ha-svg-icon class="handle" .path=${mdiDrag} slot="start"></ha-svg-icon>
<div slot="headline" class="label">${primary}</div>
${secondary
? html`<div slot="supporting-text" class="description">
${secondary}
</div>`
: nothing}
<ha-icon-button
slot="end"
.item=${item}
.index=${index}
.label=${this.hass!.localize("ui.common.edit")}
.path=${mdiPencil}
@click=${this._editItem}
></ha-icon-button>
<ha-icon-button
slot="end"
.index=${index}
.label=${this.hass!.localize("ui.common.delete")}
.path=${mdiClose}
@click=${this._deleteItem}
></ha-icon-button>
</ha-md-list-item>
`;
}
private _editItem(ev) {
const index = (ev.currentTarget as any).index;
fireEvent(this, "edit-detail-element", {
subElementConfig: {
index,
type: "row",
elementConfig: this.entities![index],
},
});
}
private _deleteItem(ev) {
const index = ev.target.index;
const newConfigEntities = this.entities!.slice(0, index).concat(
this.entities!.slice(index + 1)
);
fireEvent(this, "entities-changed", { entities: newConfigEntities });
}
protected render() {
if (!this.entities) {
return nothing;
@@ -47,7 +114,26 @@ export class HuiEntityEditor extends LitElement {
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
")"}
</h3>
<ha-sortable handle-selector=".handle" @item-moved=${this._entityMoved}>
${this.canEdit
? html`
<div class="items-container">
<ha-sortable
handle-selector=".handle"
draggable-selector=".item"
@item-moved=${this._entityMoved}
>
<ha-md-list>
${this.entities.map((item, index) =>
this._renderItem(item, index)
)}
</ha-md-list>
</ha-sortable>
</div>
`
: html` <ha-sortable
handle-selector=".handle"
@item-moved=${this._entityMoved}
>
<div class="entities">
${repeat(
this.entities,
@@ -69,7 +155,7 @@ export class HuiEntityEditor extends LitElement {
`
)}
</div>
</ha-sortable>
</ha-sortable>`}
<ha-entity-picker
class="add-entity"
.hass=${this.hass}
@@ -148,6 +234,35 @@ export class HuiEntityEditor extends LitElement {
.entity ha-entity-picker {
flex-grow: 1;
}
ha-md-list {
gap: 8px;
}
ha-md-list-item {
border: 1px solid var(--divider-color);
border-radius: 8px;
--ha-md-list-item-gap: 0;
--md-list-item-top-space: 0;
--md-list-item-bottom-space: 0;
--md-list-item-leading-space: 12px;
--md-list-item-trailing-space: 4px;
--md-list-item-two-line-container-height: 48px;
--md-list-item-one-line-container-height: 48px;
}
.handle {
cursor: move;
padding: 8px;
margin-inline-start: -8px;
}
label {
margin-bottom: 8px;
display: block;
}
ha-md-list-item .label,
ha-md-list-item .description {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
`;
}

View File

@@ -31,6 +31,8 @@ export class HuiGenericEntityRowEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public schema?;
@state() private _config?: EntitiesCardEntityConfig;
public setConfig(config: EntitiesCardEntityConfig): void {
@@ -87,7 +89,8 @@ export class HuiGenericEntityRowEditor
return nothing;
}
const schema = this._schema(this._config.entity, this.hass.localize);
const schema =
this.schema || this._schema(this._config.entity, this.hass.localize);
return html`
<ha-form

View File

@@ -13,6 +13,9 @@ import {
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import "../hui-sub-element-editor";
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { ConfigEntity, GlanceCardConfig } from "../../cards/types";
@@ -21,6 +24,7 @@ import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entitiesConfigStruct } from "../structs/entities-struct";
import type { EntityConfig } from "../../entity-rows/types";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -36,6 +40,49 @@ const cardConfigStruct = assign(
})
);
const SUB_SCHEMA = [
{ name: "entity", selector: { entity: {} }, required: true },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {},
},
context: {
icon_entity: "entity",
},
},
{ name: "show_last_changed", selector: { boolean: {} } },
{ name: "show_state", selector: { boolean: {} }, default: true },
],
},
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map((action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})),
},
] as const;
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
@@ -68,6 +115,8 @@ export class HuiGlanceCardEditor
@state() private _config?: GlanceCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: ConfigEntity[];
public setConfig(config: GlanceCardConfig): void {
@@ -81,6 +130,19 @@ export class HuiGlanceCardEditor
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.schema=${SUB_SCHEMA}
@go-back=${this._goBack}
@config-changed=${this._handleSubEntityChanged}
>
</hui-sub-element-editor>
`;
}
const data = {
show_name: true,
show_icon: true,
@@ -98,12 +160,42 @@ export class HuiGlanceCardEditor
></ha-form>
<hui-entity-editor
.hass=${this.hass}
can-edit
.entities=${this._configEntities}
@entities-changed=${this._entitiesChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor>
`;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _handleSubEntityChanged(ev: CustomEvent): void {
ev.stopPropagation();
const index = this._subElementEditorConfig!.index!;
const newEntities = this._configEntities!.concat();
const newConfig = ev.detail.config as EntityConfig;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: newConfig,
};
newEntities[index] = newConfig;
let config = this._config!;
config = { ...config, entities: newEntities };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
fireEvent(this, "config-changed", { config });
}
private _valueChanged(ev: CustomEvent): void {
const config = ev.detail.value;
fireEvent(this, "config-changed", { config });

View File

@@ -18,6 +18,9 @@ import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { HistoryGraphCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import "../hui-sub-element-editor";
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { EntityConfig } from "../../entity-rows/types";
import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
@@ -40,6 +43,11 @@ const cardConfigStruct = assign(
})
);
const SUB_SCHEMA = [
{ name: "entity", selector: { entity: {} }, required: true },
{ name: "name", selector: { text: {} } },
] as const;
@customElement("hui-history-graph-card-editor")
export class HuiHistoryGraphCardEditor
extends LitElement
@@ -49,6 +57,8 @@ export class HuiHistoryGraphCardEditor
@state() private _config?: HistoryGraphCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: EntityConfig[];
public setConfig(config: HistoryGraphCardConfig): void {
@@ -110,6 +120,19 @@ export class HuiHistoryGraphCardEditor
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.schema=${SUB_SCHEMA}
@go-back=${this._goBack}
@config-changed=${this._handleSubEntityChanged}
>
</hui-sub-element-editor>
`;
}
const schema = this._schema(
this._config!.min_y_axis !== undefined ||
this._config!.max_y_axis !== undefined
@@ -126,11 +149,41 @@ export class HuiHistoryGraphCardEditor
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
can-edit
@entities-changed=${this._entitiesChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor>
`;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _handleSubEntityChanged(ev: CustomEvent): void {
ev.stopPropagation();
const index = this._subElementEditorConfig!.index!;
const newEntities = this._configEntities!.concat();
const newConfig = ev.detail.config as EntityConfig;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: newConfig,
};
newEntities[index] = newConfig;
let config = this._config!;
config = { ...config, entities: newEntities };
this._config = config;
this._configEntities = processEditorEntities(config.entities);
fireEvent(this, "config-changed", { config });
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}

View File

@@ -57,6 +57,8 @@ export abstract class HuiElementEditor<
@property({ attribute: false }) public context?: C;
@property({ attribute: false }) public schema?;
@state() private _config?: T;
@state() private _configElement?: LovelaceGenericElementEditor;
@@ -312,6 +314,9 @@ export abstract class HuiElementEditor<
if (this._configElement && changedProperties.has("context")) {
this._configElement.context = this.context;
}
if (this._configElement && changedProperties.has("schema")) {
this._configElement.schema = this.schema;
}
}
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
@@ -399,6 +404,7 @@ export abstract class HuiElementEditor<
configElement.lovelace = this.lovelace;
}
configElement.context = this.context;
configElement.schema = this.schema;
configElement.addEventListener("config-changed", (ev) =>
this._handleUIConfigChanged(ev as UIConfigChangedEvent<T>)
);

View File

@@ -27,6 +27,8 @@ export class HuiSubElementEditor extends LitElement {
@property({ attribute: false }) public config!: SubElementEditorConfig;
@property({ attribute: false }) public schema?;
@state() private _guiModeAvailable = true;
@state() private _guiMode = true;
@@ -89,6 +91,7 @@ export class HuiSubElementEditor extends LitElement {
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.config.context}
.schema=${this.schema}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-row-element-editor>

View File

@@ -18,6 +18,8 @@ export const entitiesConfigStruct = union([
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
confirmation: optional(actionConfigStructConfirmation),
show_last_changed: optional(boolean()),
show_state: optional(boolean()),
}),
string(),
]);

View File

@@ -169,6 +169,7 @@ export interface LovelaceGenericElementEditor<C = any> extends HTMLElement {
hass?: HomeAssistant;
lovelace?: LovelaceConfig;
context?: C;
schema?: any;
setConfig(config: any): void;
focusYamlEditor?: () => void;
}

View File

@@ -7720,6 +7720,7 @@
"show_icon": "Show icon",
"show_name": "Show name",
"show_state": "Show state",
"show_last_changed": "Show last changed",
"tap_action": "Tap behavior",
"interactions": "Interactions",
"title": "Title",