From 6cac7eeff008c34d5c03a1697d1bf7122c5570b7 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 21 Feb 2022 09:53:03 -0600 Subject: [PATCH] Lovelace Entity Card Editor to Ha Form - Adds Theme Selector and HaFormColumn (#11731) Co-authored-by: Bram Kragten --- src/components/ha-form/ha-form-grid.ts | 73 ++++++ src/components/ha-form/ha-form.ts | 30 ++- src/components/ha-form/types.ts | 9 +- .../ha-selector/ha-selector-boolean.ts | 5 +- .../ha-selector/ha-selector-icon.ts | 2 + .../ha-selector/ha-selector-theme.ts | 34 +++ src/components/ha-selector/ha-selector.ts | 5 +- src/data/selector.ts | 12 +- .../components/hui-theme-select-editor.ts | 4 +- .../config-elements/hui-entity-card-editor.ts | 238 ++++++------------ 10 files changed, 230 insertions(+), 182 deletions(-) create mode 100644 src/components/ha-form/ha-form-grid.ts create mode 100644 src/components/ha-selector/ha-selector-theme.ts diff --git a/src/components/ha-form/ha-form-grid.ts b/src/components/ha-form/ha-form-grid.ts new file mode 100644 index 0000000000..2c3ed39285 --- /dev/null +++ b/src/components/ha-form/ha-form-grid.ts @@ -0,0 +1,73 @@ +import "./ha-form"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { + HaFormGridSchema, + HaFormDataContainer, + HaFormElement, + HaFormSchema, +} from "./types"; +import type { HomeAssistant } from "../../types"; + +@customElement("ha-form-grid") +export class HaFormGrid extends LitElement implements HaFormElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public data!: HaFormDataContainer; + + @property({ attribute: false }) public schema!: HaFormGridSchema; + + @property({ type: Boolean }) public disabled = false; + + @property() public computeLabel?: ( + schema: HaFormSchema, + data?: HaFormDataContainer + ) => string; + + @property() public computeHelper?: (schema: HaFormSchema) => string; + + protected firstUpdated() { + this.setAttribute("own-margin", ""); + } + + protected render(): TemplateResult { + return html` + ${this.schema.schema.map( + (item) => + html` + + ` + )} + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: grid !important; + grid-template-columns: repeat( + var(--form-grid-column-count, auto-fit), + minmax(var(--form-grid-min-width, 200px), 1fr) + ); + grid-gap: 8px; + } + :host > ha-form { + display: block; + margin-bottom: 24px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-grid": HaFormGrid; + } +} diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 610d2d2de5..c824d8fb89 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -1,10 +1,18 @@ -import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../common/dom/fire_event"; import "../ha-alert"; import "./ha-form-boolean"; import "./ha-form-constant"; +import "./ha-form-grid"; import "./ha-form-float"; import "./ha-form-integer"; import "./ha-form-multi_select"; @@ -14,17 +22,18 @@ import "./ha-form-string"; import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types"; import { HomeAssistant } from "../../types"; -const getValue = (obj, item) => (obj ? obj[item.name] : null); +const getValue = (obj, item) => + obj ? (!item.name ? obj : obj[item.name]) : null; let selectorImported = false; @customElement("ha-form") export class HaForm extends LitElement implements HaFormElement { - @property() public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property() public data!: HaFormDataContainer; + @property({ attribute: false }) public data!: HaFormDataContainer; - @property() public schema!: HaFormSchema[]; + @property({ attribute: false }) public schema!: HaFormSchema[]; @property() public error?: Record; @@ -64,7 +73,7 @@ export class HaForm extends LitElement implements HaFormElement { } } - protected render() { + protected render(): TemplateResult { return html`
${this.error && this.error.base @@ -101,6 +110,9 @@ export class HaForm extends LitElement implements HaFormElement { data: getValue(this.data, item), label: this._computeLabel(item, this.data), disabled: this.disabled, + hass: this.hass, + computeLabel: this.computeLabel, + computeHelper: this.computeHelper, })} `; })} @@ -115,8 +127,12 @@ export class HaForm extends LitElement implements HaFormElement { ev.stopPropagation(); const schema = (ev.target as HaFormElement).schema as HaFormSchema; + const newValue = !schema.name + ? ev.detail.value + : { [schema.name]: ev.detail.value }; + fireEvent(this, "value-changed", { - value: { ...this.data, [schema.name]: ev.detail.value }, + value: { ...this.data, ...newValue }, }); }); return root; diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index f476e759ca..5d9572147a 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -11,7 +11,8 @@ export type HaFormSchema = | HaFormSelectSchema | HaFormMultiSelectSchema | HaFormTimeSchema - | HaFormSelector; + | HaFormSelector + | HaFormGridSchema; export interface HaFormBaseSchema { name: string; @@ -25,6 +26,12 @@ export interface HaFormBaseSchema { }; } +export interface HaFormGridSchema extends HaFormBaseSchema { + type: "grid"; + name: ""; + schema: HaFormSchema[]; +} + export interface HaFormSelector extends HaFormBaseSchema { type?: never; selector: Selector; diff --git a/src/components/ha-selector/ha-selector-boolean.ts b/src/components/ha-selector/ha-selector-boolean.ts index f140ba99e4..4ac5918b4c 100644 --- a/src/components/ha-selector/ha-selector-boolean.ts +++ b/src/components/ha-selector/ha-selector-boolean.ts @@ -35,9 +35,12 @@ export class HaBooleanSelector extends LitElement { static get styles(): CSSResultGroup { return css` + :host { + height: 56px; + display: flex; + } ha-formfield { width: 100%; - margin: 16px 0; --mdc-typography-body2-font-size: 1em; } `; diff --git a/src/components/ha-selector/ha-selector-icon.ts b/src/components/ha-selector/ha-selector-icon.ts index 046a612d5e..0e4a712588 100644 --- a/src/components/ha-selector/ha-selector-icon.ts +++ b/src/components/ha-selector/ha-selector-icon.ts @@ -22,6 +22,8 @@ export class HaIconSelector extends LitElement { `; diff --git a/src/components/ha-selector/ha-selector-theme.ts b/src/components/ha-selector/ha-selector-theme.ts new file mode 100644 index 0000000000..d25539908f --- /dev/null +++ b/src/components/ha-selector/ha-selector-theme.ts @@ -0,0 +1,34 @@ +import "../../panels/lovelace/components/hui-theme-select-editor"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import type { HomeAssistant } from "../../types"; +import type { ThemeSelector } from "../../data/selector"; + +@customElement("ha-selector-theme") +export class HaThemeSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: ThemeSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-theme": HaThemeSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 0181c9c863..5f6a31aebc 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -1,8 +1,8 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; -import { Selector } from "../../data/selector"; -import { HomeAssistant } from "../../types"; +import type { Selector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; import "./ha-selector-action"; import "./ha-selector-addon"; import "./ha-selector-area"; @@ -19,6 +19,7 @@ import "./ha-selector-text"; import "./ha-selector-time"; import "./ha-selector-icon"; import "./ha-selector-media"; +import "./ha-selector-theme"; @customElement("ha-selector") export class HaSelector extends LitElement { diff --git a/src/data/selector.ts b/src/data/selector.ts index e7be0ac147..b51c077b01 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -14,7 +14,8 @@ export type Selector = | ObjectSelector | SelectSelector | IconSelector - | MediaSelector; + | MediaSelector + | ThemeSelector; export interface EntitySelector { entity: { @@ -147,8 +148,15 @@ export interface SelectSelector { } export interface IconSelector { + icon: { + placeholder?: string; + fallbackPath?: string; + }; +} + +export interface ThemeSelector { // eslint-disable-next-line @typescript-eslint/ban-types - icon: {}; + theme: {}; } export interface MediaSelector { diff --git a/src/panels/lovelace/components/hui-theme-select-editor.ts b/src/panels/lovelace/components/hui-theme-select-editor.ts index 47e05ae1bf..30bc0bdef0 100644 --- a/src/panels/lovelace/components/hui-theme-select-editor.ts +++ b/src/panels/lovelace/components/hui-theme-select-editor.ts @@ -39,7 +39,7 @@ export class HuiThemeSelectEditor extends LitElement { .sort() .map( (theme) => - html` ${theme} ` + html`${theme}` )} `; @@ -57,7 +57,7 @@ export class HuiThemeSelectEditor extends LitElement { if (!this.hass || ev.target.value === "") { return; } - this.value = ev.target.value === "remove" ? "" : ev.target.selected; + this.value = ev.target.value === "remove" ? "" : ev.target.value; fireEvent(this, "value-changed", { value: this.value }); } } diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index df9cb0e627..7dcf65a1bc 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -1,22 +1,18 @@ -import "@polymer/paper-input/paper-input"; -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import "../../../../components/ha-form/ha-form"; +import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { assert, assign, boolean, object, optional, string } from "superstruct"; +import type { HassEntity } from "home-assistant-js-websocket/dist/types"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; import { domainIcon } from "../../../../common/entity/domain_icon"; -import "../../../../components/entity/ha-entity-attribute-picker"; -import "../../../../components/ha-icon-picker"; -import { HomeAssistant } from "../../../../types"; -import { EntityCardConfig } from "../../cards/types"; -import "../../components/hui-action-editor"; -import "../../components/hui-entity-editor"; -import "../../components/hui-theme-select-editor"; +import type { HaFormSchema } from "../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../types"; +import type { EntityCardConfig } from "../../cards/types"; import { headerFooterConfigStructs } from "../../header-footer/structs"; -import { LovelaceCardEditor } from "../../types"; +import type { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; -import { EditorTarget, EntitiesEditorEvent } from "../types"; -import { configElementStyle } from "./config-elements-style"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -46,174 +42,82 @@ export class HuiEntityCardEditor this._config = config; } - get _entity(): string { - return this._config!.entity || ""; - } + private _schema = memoizeOne( + (entity: string, icon: string, entityState: HassEntity): HaFormSchema[] => [ + { name: "entity", required: true, selector: { entity: {} } }, + { + type: "grid", + name: "", + schema: [ + { name: "name", selector: { text: {} } }, + { + name: "icon", + selector: { + icon: { + placeholder: icon || entityState?.attributes.icon, + fallbackPath: + !icon && !entityState?.attributes.icon && entityState + ? domainIcon(computeDomain(entity), entityState) + : undefined, + }, + }, + }, - get _name(): string { - return this._config!.name || ""; - } - - get _icon(): string { - return this._config!.icon || ""; - } - - get _attribute(): string { - return this._config!.attribute || ""; - } - - get _unit(): string { - return this._config!.unit || ""; - } - - get _state_color(): boolean { - return this._config!.state_color ?? false; - } - - get _theme(): string { - return this._config!.theme || ""; - } + { + name: "attribute", + selector: { attribute: { entity_id: entity } }, + }, + { name: "unit", selector: { text: {} } }, + { name: "theme", selector: { theme: {} } }, + { name: "state_color", selector: { boolean: {} } }, + ], + }, + ] + ); protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; } - const entityState = this.hass.states[this._entity]; + + const entityState = this.hass.states[this._config.entity]; + + const schema = this._schema( + this._config.entity, + this._config.icon, + entityState + ); return html` -
- -
- - -
-
- - -
-
- - - - - - -
-
+ `; } - private _valueChanged(ev: EntitiesEditorEvent): void { - if (!this._config || !this.hass) { - return; - } - const target = ev.currentTarget! as EditorTarget; - - if ( - this[`_${target.configValue}`] === target.value || - this[`_${target.configValue}`] === target.config - ) { - return; - } - if (target.configValue) { - if (target.value === "") { - this._config = { ...this._config }; - delete this._config[target.configValue!]; - } else { - let newValue: string | undefined; - if ( - target.configValue === "icon_height" && - !isNaN(Number(target.value)) - ) { - newValue = `${String(target.value)}px`; - } - this._config = { - ...this._config, - [target.configValue!]: - target.checked !== undefined - ? target.checked - : newValue !== undefined - ? newValue - : target.value - ? target.value - : target.config, - }; - } - } - fireEvent(this, "config-changed", { config: this._config }); + private _valueChanged(ev: CustomEvent): void { + const config = ev.detail.value; + Object.keys(config).forEach((k) => config[k] === "" && delete config[k]); + fireEvent(this, "config-changed", { config }); } - static get styles(): CSSResultGroup { - return configElementStyle; - } + private _computeLabelCallback = (schema: HaFormSchema) => { + if (schema.name === "entity") { + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.entity" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.required" + )})`; + } + + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + }; } declare global {