Move sub element editor inside hui-element-editor. (#22079)

* Move sub element editor into hui-element-editor

* Migrate feature editor

* Migrate feature editor

* Simplify context
This commit is contained in:
Paul Bottein 2024-09-25 16:05:41 +02:00 committed by GitHub
parent 765812331b
commit cd631e8693
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 156 additions and 287 deletions

View File

@ -0,0 +1,17 @@
export function updateNestedObject<T = any>(
obj: T,
path: (number | string)[],
value: any
): T {
if (path.length === 0) return value;
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as T;
const key = path[0];
newObj[key] = updateNestedObject(obj[key], path.slice(1), value);
return newObj;
}
export function findNestedObject(obj: any, path: (number | string)[]) {
if (path.length === 0) return obj;
const key = path[0];
return findNestedObject(obj[key], path.slice(1));
}

View File

@ -20,7 +20,7 @@ import {
import "../../conditions/ha-card-conditions-editor";
import "../../hui-picture-elements-card-row-editor";
import { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
import { EditSubElementEvent, SubElementEditorConfig } from "../../types";
import { EditDetailElementEvent, SubElementEditorConfig } from "../../types";
import "../../hui-sub-element-editor";
import { SchemaUnion } from "../../../../../components/ha-form/types";
@ -154,7 +154,7 @@ export class HuiConditionalElementEditor
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}

View File

@ -41,7 +41,7 @@ import { buttonEntityConfigStruct } from "../structs/button-entity-struct";
import { entitiesConfigStruct } from "../structs/entities-struct";
import {
EditorTarget,
EditSubElementEvent,
EditDetailElementEvent,
SubElementEditorConfig,
} from "../types";
import { configElementStyle } from "./config-elements-style";
@ -401,7 +401,7 @@ export class HuiEntitiesCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}

View File

@ -1,7 +1,6 @@
import { mdiGestureTap, mdiListBox } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
@ -26,11 +25,9 @@ import type { HomeAssistant } from "../../../../types";
import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
import { UiAction } from "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { processEditorEntities } from "../process-editor-entities";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-entities-editor";
@ -56,9 +53,6 @@ export class HuiHeadingCardEditor
@state() private _config?: HeadingCardConfig;
@state()
private _subElementEditorConfig?: SubElementEditorConfig;
public setConfig(config: HeadingCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
@ -108,35 +102,15 @@ export class HuiHeadingCardEditor
] as const satisfies readonly HaFormSchema[]
);
private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
processEditorEntities(entities || [])
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return cache(
this._subElementEditorConfig
? this._renderEntityForm()
: this._renderForm()
);
}
private _renderEntityForm() {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
@go-back=${this._goBack}
@config-changed=${this._subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
processEditorEntities(entities || [])
);
private _renderForm() {
const data = {
...this._config!,
};
@ -200,46 +174,12 @@ export class HuiHeadingCardEditor
fireEvent(this, "config-changed", { config });
}
private _subElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const value = ev.detail.config;
const newConfigEntities = this._config!.entities
? [...this._config!.entities]
: [];
if (!value) {
newConfigEntities.splice(this._subElementEditorConfig!.index!, 1);
this._goBack();
} else {
newConfigEntities[this._subElementEditorConfig!.index!] = value;
}
this._config = { ...this._config!, entities: newConfigEntities };
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
const entities = this._entities(this._config!.entities);
this._subElementEditorConfig = {
elementConfig: entities[ev.detail.index],
index: ev.detail.index,
ev.stopPropagation();
fireEvent(this, "edit-sub-element", {
path: ["entities", ev.detail.index],
type: "heading-entity",
};
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
});
}
private _computeLabelCallback = (

View File

@ -1,8 +1,6 @@
import { mdiListBox } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
array,
@ -28,9 +26,8 @@ import {
} from "../../card-features/types";
import type { HumidifierCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { EditDetailElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor";
@ -82,44 +79,16 @@ export class HuiHumidifierCardEditor
@state() private _config?: HumidifierCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
public setConfig(config: HumidifierCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
private _context = memoizeOne(
(entity_id?: string): LovelaceCardFeatureContext => ({ entity_id })
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return cache(
this._subElementEditorConfig
? this._renderFeatureForm()
: this._renderForm()
);
}
private _renderFeatureForm() {
const entityId = this._config!.entity;
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(entityId)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
@ -175,41 +144,14 @@ export class HuiHumidifierCardEditor
fireEvent(this, "config-changed", { config });
}
private subElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const value = ev.detail.config;
const newConfigFeatures = this._config!.features
? [...this._config!.features]
: [];
if (!value) {
newConfigFeatures.splice(this._subElementEditorConfig!.index!, 1);
this._goBack();
} else {
newConfigFeatures[this._subElementEditorConfig!.index!] = value;
}
this._config = { ...this._config!, features: newConfigFeatures };
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
fireEvent(this, "edit-sub-element", {
path: ["features", ev.detail.subElementConfig.index!],
context: {
entity_id: this._config!.entity,
} as LovelaceCardFeatureContext,
type: "feature",
});
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {

View File

@ -21,7 +21,7 @@ import type { PictureElementsCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { EditDetailElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "../hui-picture-elements-card-row-editor";
import { LovelaceElementConfig } from "../../elements/types";
@ -186,7 +186,7 @@ export class HuiPictureElementsCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}

View File

@ -1,8 +1,6 @@
import { mdiListBox } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
array,
@ -28,9 +26,8 @@ import {
} from "../../card-features/types";
import type { ThermostatCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { EditDetailElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor";
@ -80,43 +77,16 @@ export class HuiThermostatCardEditor
@state() private _config?: ThermostatCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
public setConfig(config: ThermostatCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
private _context = memoizeOne(
(entity_id?: string): LovelaceCardFeatureContext => ({ entity_id })
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return cache(
this._subElementEditorConfig
? this._renderFeatureForm()
: this._renderForm()
);
}
private _renderFeatureForm() {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(this._config!.entity)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
@ -172,41 +142,14 @@ export class HuiThermostatCardEditor
fireEvent(this, "config-changed", { config });
}
private subElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const value = ev.detail.config;
const newConfigFeatures = this._config!.features
? [...this._config!.features]
: [];
if (!value) {
newConfigFeatures.splice(this._subElementEditorConfig!.index!, 1);
this._goBack();
} else {
newConfigFeatures[this._subElementEditorConfig!.index!] = value;
}
this._config = { ...this._config!, features: newConfigFeatures };
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
fireEvent(this, "edit-sub-element", {
path: ["features", ev.detail.subElementConfig.index!],
context: {
entity_id: this._config!.entity,
} as LovelaceCardFeatureContext,
type: "feature",
});
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {

View File

@ -1,7 +1,6 @@
import { mdiGestureTap, mdiListBox, mdiPalette } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
@ -30,10 +29,9 @@ import {
import { getEntityDefaultTileIconAction } from "../../cards/hui-tile-card";
import type { TileCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { EditDetailElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
@ -63,8 +61,6 @@ export class HuiTileCardEditor
@state() private _config?: TileCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
public setConfig(config: TileCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
@ -165,36 +161,11 @@ export class HuiTileCardEditor
] as const satisfies readonly HaFormSchema[]
);
private _context = memoizeOne(
(entity_id?: string): LovelaceCardFeatureContext => ({ entity_id })
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return cache(
this._subElementEditorConfig
? this._renderFeatureForm()
: this._renderForm()
);
}
private _renderFeatureForm() {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(this._config!.entity)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
@ -274,41 +245,14 @@ export class HuiTileCardEditor
fireEvent(this, "config-changed", { config });
}
private subElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const value = ev.detail.config;
const newConfigFeatures = this._config!.features
? [...this._config!.features]
: [];
if (!value) {
newConfigFeatures.splice(this._subElementEditorConfig!.index!, 1);
this._goBack();
} else {
newConfigFeatures[this._subElementEditorConfig!.index!] = value;
}
this._config = { ...this._config!, features: newConfigFeatures };
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
fireEvent(this, "edit-sub-element", {
path: ["features", ev.detail.subElementConfig.index!],
context: {
entity_id: this._config!.entity,
} as LovelaceCardFeatureContext,
type: "feature",
});
}
private _computeLabelCallback = (

View File

@ -1,16 +1,21 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { cache } from "lit/directives/cache";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { handleStructError } from "../../../common/structs/handle-errors";
import { deepEqual } from "../../../common/util/deep-equal";
import {
findNestedObject,
updateNestedObject,
} from "../../../common/util/nested-object";
import "../../../components/ha-alert";
import "../../../components/ha-circular-progress";
import "../../../components/ha-yaml-editor";
@ -23,7 +28,12 @@ import type {
} from "../types";
import type { HuiFormEditor } from "./config-elements/hui-form-editor";
import { GUISupportError } from "./gui-support-error";
import { EditSubElementEvent, GUIModeChangedEvent } from "./types";
import {
EditDetailElementEvent,
EditSubElementEvent,
GUIModeChangedEvent,
SubElementEditorConfig,
} from "./types";
export interface ConfigChangedEvent<T extends object = object> {
config: T;
@ -35,7 +45,8 @@ declare global {
interface HASSDomEvents {
"config-changed": ConfigChangedEvent;
"GUImode-changed": GUIModeChangedEvent;
"edit-detail-element": EditSubElementEvent;
"edit-detail-element": EditDetailElementEvent;
"edit-sub-element": EditSubElementEvent;
}
}
@ -59,6 +70,8 @@ export abstract class HuiElementEditor<
@state() private _configElement?: LovelaceGenericElementEditor;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _guiMode = true;
// Error: Configuration broken - do not save
@ -162,12 +175,71 @@ export abstract class HuiElementEditor<
return html`${this._configElement}`;
}
private _renderSubElement() {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
@go-back=${this._goBack}
@config-changed=${this._subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _subElementChanged(ev: CustomEvent): void {
ev.stopPropagation();
const value = ev.detail.config;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: value,
};
const config = updateNestedObject(
this._config,
this._subElementEditorConfig.path!,
value
);
this._config = config;
this._setConfig();
}
private _goBack(ev): void {
ev.stopPropagation();
this._subElementEditorConfig = undefined;
}
private async _editSubElement(
ev: HASSDomEvent<EditSubElementEvent>
): Promise<void> {
if (!ev.detail.path) {
return;
}
ev.stopPropagation();
const config = findNestedObject(this._config, ev.detail.path);
if (!config) {
throw new Error("Failed to edit config");
}
await import("./hui-sub-element-editor");
this._subElementEditorConfig = {
type: ev.detail.type,
path: ev.detail.path,
elementConfig: config,
};
}
protected render(): TemplateResult {
return html`
<div class="wrapper">
${this.GUImode
? html`
<div class="gui-editor">
<div class="gui-editor" @edit-sub-element=${this._editSubElement}>
${this._loading
? html`
<ha-circular-progress
@ -175,7 +247,11 @@ export abstract class HuiElementEditor<
class="center margin-bot"
></ha-circular-progress>
`
: this.renderConfigElement()}
: cache(
this._subElementEditorConfig
? this._renderSubElement()
: this.renderConfigElement()
)}
</div>
`
: html`

View File

@ -32,8 +32,6 @@ export class HuiSubElementEditor extends LitElement {
@property({ attribute: false }) public config!: SubElementEditorConfig;
@property({ attribute: false }) public context?: any;
@state() private _guiModeAvailable = true;
@state() private _guiMode = true;
@ -87,6 +85,7 @@ export class HuiSubElementEditor extends LitElement {
private _renderEditor() {
const type = this.config.type;
switch (type) {
case "row":
return html`
@ -94,7 +93,7 @@ export class HuiSubElementEditor extends LitElement {
class="editor"
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.context}
.context=${this.config.context}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-row-element-editor>
@ -106,7 +105,7 @@ export class HuiSubElementEditor extends LitElement {
class="editor"
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.context}
.context=${this.config.context}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-headerfooter-element-editor>
@ -117,7 +116,7 @@ export class HuiSubElementEditor extends LitElement {
class="editor"
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.context}
.context=${this.config.context}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-picture-element-element-editor>
@ -128,7 +127,7 @@ export class HuiSubElementEditor extends LitElement {
class="editor"
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.context}
.context=${this.config.context}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-card-feature-element-editor>
@ -139,7 +138,7 @@ export class HuiSubElementEditor extends LitElement {
class="editor"
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.context}
.context=${this.config.context}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-heading-entity-element-editor>

View File

@ -92,15 +92,23 @@ export interface BadgePickTarget extends EventTarget {
export interface SubElementEditorConfig {
index?: number;
path?: (string | number)[];
elementConfig?:
| LovelaceRowConfig
| LovelaceHeaderFooterConfig
| LovelaceCardFeatureConfig
| LovelaceElementConfig
| HeadingEntityConfig;
context?: any;
type: "header" | "footer" | "row" | "feature" | "element" | "heading-entity";
}
export interface EditSubElementEvent {
path: (string | number)[];
type: SubElementEditorConfig["type"];
context?: any;
}
export interface EditDetailElementEvent {
subElementConfig: SubElementEditorConfig;
}