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; image?: string;
show_state?: boolean; show_state?: boolean;
state_color?: boolean; state_color?: boolean;
format: TimestampRenderingFormat; format?: TimestampRenderingFormat;
} }
export interface GlanceCardConfig extends LovelaceCardConfig { 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 { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@@ -12,6 +12,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-sortable"; import "../../../components/ha-sortable";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { EntityConfig } from "../entity-rows/types"; import type { EntityConfig } from "../entity-rows/types";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("hui-entity-editor") @customElement("hui-entity-editor")
export class HuiEntityEditor extends LitElement { export class HuiEntityEditor extends LitElement {
@@ -24,6 +25,8 @@ export class HuiEntityEditor extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ attribute: "can-edit", type: Boolean }) public canEdit?;
private _entityKeys = new WeakMap<EntityConfig, string>(); private _entityKeys = new WeakMap<EntityConfig, string>();
private _getKey(action: EntityConfig) { private _getKey(action: EntityConfig) {
@@ -34,6 +37,70 @@ export class HuiEntityEditor extends LitElement {
return this._entityKeys.get(action)!; 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() { protected render() {
if (!this.entities) { if (!this.entities) {
return nothing; return nothing;
@@ -47,7 +114,26 @@ export class HuiEntityEditor extends LitElement {
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") + this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
")"} ")"}
</h3> </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"> <div class="entities">
${repeat( ${repeat(
this.entities, this.entities,
@@ -69,7 +155,7 @@ export class HuiEntityEditor extends LitElement {
` `
)} )}
</div> </div>
</ha-sortable> </ha-sortable>`}
<ha-entity-picker <ha-entity-picker
class="add-entity" class="add-entity"
.hass=${this.hass} .hass=${this.hass}
@@ -148,6 +234,35 @@ export class HuiEntityEditor extends LitElement {
.entity ha-entity-picker { .entity ha-entity-picker {
flex-grow: 1; 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 hass?: HomeAssistant;
@property({ attribute: false }) public schema?;
@state() private _config?: EntitiesCardEntityConfig; @state() private _config?: EntitiesCardEntityConfig;
public setConfig(config: EntitiesCardEntityConfig): void { public setConfig(config: EntitiesCardEntityConfig): void {
@@ -87,7 +89,8 @@ export class HuiGenericEntityRowEditor
return nothing; 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` return html`
<ha-form <ha-form

View File

@@ -13,6 +13,9 @@ import {
} from "superstruct"; } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form"; 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 { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { ConfigEntity, GlanceCardConfig } from "../../cards/types"; import type { ConfigEntity, GlanceCardConfig } from "../../cards/types";
@@ -21,6 +24,7 @@ import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities"; import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entitiesConfigStruct } from "../structs/entities-struct"; import { entitiesConfigStruct } from "../structs/entities-struct";
import type { EntityConfig } from "../../entity-rows/types";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, 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 = [ const SCHEMA = [
{ name: "title", selector: { text: {} } }, { name: "title", selector: { text: {} } },
{ {
@@ -68,6 +115,8 @@ export class HuiGlanceCardEditor
@state() private _config?: GlanceCardConfig; @state() private _config?: GlanceCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: ConfigEntity[]; @state() private _configEntities?: ConfigEntity[];
public setConfig(config: GlanceCardConfig): void { public setConfig(config: GlanceCardConfig): void {
@@ -81,6 +130,19 @@ export class HuiGlanceCardEditor
return nothing; 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 = { const data = {
show_name: true, show_name: true,
show_icon: true, show_icon: true,
@@ -98,12 +160,42 @@ export class HuiGlanceCardEditor
></ha-form> ></ha-form>
<hui-entity-editor <hui-entity-editor
.hass=${this.hass} .hass=${this.hass}
can-edit
.entities=${this._configEntities} .entities=${this._configEntities}
@entities-changed=${this._entitiesChanged} @entities-changed=${this._entitiesChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor> ></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 { private _valueChanged(ev: CustomEvent): void {
const config = ev.detail.value; const config = ev.detail.value;
fireEvent(this, "config-changed", { config }); 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 { HomeAssistant } from "../../../../types";
import type { HistoryGraphCardConfig } from "../../cards/types"; import type { HistoryGraphCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor"; 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 { EntityConfig } from "../../entity-rows/types";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities"; 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") @customElement("hui-history-graph-card-editor")
export class HuiHistoryGraphCardEditor export class HuiHistoryGraphCardEditor
extends LitElement extends LitElement
@@ -49,6 +57,8 @@ export class HuiHistoryGraphCardEditor
@state() private _config?: HistoryGraphCardConfig; @state() private _config?: HistoryGraphCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: EntityConfig[]; @state() private _configEntities?: EntityConfig[];
public setConfig(config: HistoryGraphCardConfig): void { public setConfig(config: HistoryGraphCardConfig): void {
@@ -110,6 +120,19 @@ export class HuiHistoryGraphCardEditor
return nothing; 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( const schema = this._schema(
this._config!.min_y_axis !== undefined || this._config!.min_y_axis !== undefined ||
this._config!.max_y_axis !== undefined this._config!.max_y_axis !== undefined
@@ -126,11 +149,41 @@ export class HuiHistoryGraphCardEditor
<hui-entity-editor <hui-entity-editor
.hass=${this.hass} .hass=${this.hass}
.entities=${this._configEntities} .entities=${this._configEntities}
can-edit
@entities-changed=${this._entitiesChanged} @entities-changed=${this._entitiesChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor> ></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 { private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value }); 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 context?: C;
@property({ attribute: false }) public schema?;
@state() private _config?: T; @state() private _config?: T;
@state() private _configElement?: LovelaceGenericElementEditor; @state() private _configElement?: LovelaceGenericElementEditor;
@@ -312,6 +314,9 @@ export abstract class HuiElementEditor<
if (this._configElement && changedProperties.has("context")) { if (this._configElement && changedProperties.has("context")) {
this._configElement.context = this.context; this._configElement.context = this.context;
} }
if (this._configElement && changedProperties.has("schema")) {
this._configElement.schema = this.schema;
}
} }
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) { private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
@@ -399,6 +404,7 @@ export abstract class HuiElementEditor<
configElement.lovelace = this.lovelace; configElement.lovelace = this.lovelace;
} }
configElement.context = this.context; configElement.context = this.context;
configElement.schema = this.schema;
configElement.addEventListener("config-changed", (ev) => configElement.addEventListener("config-changed", (ev) =>
this._handleUIConfigChanged(ev as UIConfigChangedEvent<T>) 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 config!: SubElementEditorConfig;
@property({ attribute: false }) public schema?;
@state() private _guiModeAvailable = true; @state() private _guiModeAvailable = true;
@state() private _guiMode = true; @state() private _guiMode = true;
@@ -89,6 +91,7 @@ export class HuiSubElementEditor extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this.config.elementConfig} .value=${this.config.elementConfig}
.context=${this.config.context} .context=${this.config.context}
.schema=${this.schema}
@config-changed=${this._handleConfigChanged} @config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged} @GUImode-changed=${this._handleGUIModeChanged}
></hui-row-element-editor> ></hui-row-element-editor>

View File

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

View File

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

View File

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