Lovelace Entity Card Editor to Ha Form - Adds Theme Selector and HaFormColumn (#11731)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Zack Barett 2022-02-21 09:53:03 -06:00 committed by GitHub
parent 794bc161c8
commit 6cac7eeff0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 230 additions and 182 deletions

View File

@ -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`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
></ha-form>
`
)}
`;
}
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;
}
}

View File

@ -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<string, string>;
@ -64,7 +73,7 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
protected render() {
protected render(): TemplateResult {
return html`
<div class="root">
${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;

View File

@ -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;

View File

@ -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;
}
`;

View File

@ -22,6 +22,8 @@ export class HaIconSelector extends LitElement {
<ha-icon-picker
.label=${this.label}
.value=${this.value}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}
></ha-icon-picker>
`;

View File

@ -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`
<hui-theme-select-editor
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
></hui-theme-select-editor>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-theme": HaThemeSelector;
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -39,7 +39,7 @@ export class HuiThemeSelectEditor extends LitElement {
.sort()
.map(
(theme) =>
html` <mwc-list-item .value=${theme}>${theme}</mwc-list-item> `
html`<mwc-list-item .value=${theme}>${theme}</mwc-list-item>`
)}
</mwc-select>
`;
@ -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 });
}
}

View File

@ -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`
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
@change=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<div class="side-by-side">
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._name}
.configValue=${"name"}
@value-changed=${this._valueChanged}
></paper-input>
<ha-icon-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.icon"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._icon}
.placeholder=${this._icon || entityState?.attributes.icon}
.fallbackPath=${!this._icon &&
!entityState?.attributes.icon &&
entityState
? domainIcon(computeDomain(entityState.entity_id), entityState)
: undefined}
.configValue=${"icon"}
@value-changed=${this._valueChanged}
></ha-icon-picker>
</div>
<div class="side-by-side">
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${this._entity}
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.attribute"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._attribute}
.configValue=${"attribute"}
@value-changed=${this._valueChanged}
></ha-entity-attribute-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.unit"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value=${this._unit}
.configValue=${"unit"}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<div class="side-by-side">
<hui-theme-select-editor
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
>
</hui-theme-select-editor>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.state_color"
)}
>
<ha-switch
.checked=${this._config!.state_color}
.configValue=${"state_color"}
@change=${this._valueChanged}
>
</ha-switch>
</ha-formfield>
</div>
</div>
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
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 {