Convert customize to LitElement + "real" entity picker + option to directly jump to entity (#8180)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Philip Allgaier 2021-01-27 12:34:31 +01:00 committed by GitHub
parent 0a3172dfdb
commit 8bfe583a20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 237 additions and 354 deletions

View File

@ -1,111 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { sortStatesByName } from "../../../common/entity/states_sort_by_name";
import "../../../layouts/hass-tabs-subpage";
import LocalizeMixin from "../../../mixins/localize-mixin";
import "../../../styles/polymer-ha-style";
import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section";
import "../ha-entity-config";
import { configSections } from "../ha-panel-config";
import "./ha-form-customize";
/*
* @appliesMixin LocalizeMixin
*/
class HaConfigCustomize extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style"></style>
<hass-tabs-subpage
hass="[[hass]]"
narrow="[[narrow]]"
route="[[route]]"
back-path="/config"
tabs="[[_computeTabs()]]"
show-advanced="[[showAdvanced]]"
>
<div class$="[[computeClasses(isWide)]]">
<ha-config-section is-wide="[[isWide]]">
<span slot="header">
[[localize('ui.panel.config.customize.picker.header')]]
</span>
<span slot="introduction">
[[localize('ui.panel.config.customize.picker.introduction')]]
<br />
<a
href="[[_computeDocumentationUrl(hass)]]"
target="_blank"
rel="noreferrer"
>
[[localize("ui.panel.config.customize.picker.documentation")]]
</a>
</span>
<ha-entity-config
hass="[[hass]]"
label="[[localize('ui.panel.config.customize.picker.entity')]]"
entities="[[entities]]"
config="[[entityConfig]]"
>
</ha-entity-config>
</ha-config-section>
</div>
</hass-tabs-subpage>
`;
}
static get properties() {
return {
hass: Object,
isWide: Boolean,
narrow: Boolean,
route: Object,
showAdvanced: Boolean,
entities: {
type: Array,
computed: "computeEntities(hass)",
},
entityConfig: {
type: Object,
value: {
component: "ha-form-customize",
computeSelectCaption: (stateObj) =>
computeStateName(stateObj) +
" (" +
computeStateDomain(stateObj) +
")",
},
},
};
}
computeClasses(isWide) {
return isWide ? "content" : "content narrow";
}
_backTapped() {
history.back();
}
_computeTabs() {
return configSections.advanced;
}
computeEntities(hass) {
return Object.keys(hass.states)
.map((key) => hass.states[key])
.sort(sortStatesByName);
}
_computeDocumentationUrl(hass) {
return documentationUrl(
hass,
"/docs/configuration/customizing-devices/#customization-using-the-ui"
);
}
}
customElements.define("ha-config-customize", HaConfigCustomize);

View File

@ -0,0 +1,91 @@
import {
css,
CSSResult,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../components/ha-card";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section";
import "../ha-entity-config";
import { configSections } from "../ha-panel-config";
import "./ha-form-customize";
class HaConfigCustomize extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide?: boolean;
@property() public narrow?: boolean;
@property() public route!: Route;
@property() private _selectedEntityId = "";
protected render(): TemplateResult {
return html`
<style include="ha-style"></style>
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.tabs=${configSections.advanced}
>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize("ui.panel.config.customize.picker.header")}
</span>
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.customize.picker.introduction"
)}
<br />
<a
href=${documentationUrl(
this.hass,
"/docs/configuration/customizing-devices/#customization-using-the-ui"
)}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.customize.picker.documentation"
)}
</a>
</span>
<ha-entity-config
.hass=${this.hass}
.selectedEntityId=${this._selectedEntityId}
>
</ha-entity-config>
</ha-config-section>
</div>
</hass-tabs-subpage>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (!this.route.path.includes("/edit/")) {
return;
}
const routeSegments = this.route.path.split("/edit/");
this._selectedEntityId = routeSegments.length > 1 ? routeSegments[1] : "";
}
static get styles(): CSSResult {
return css`
a {
color: var(--primary-color);
}
`;
}
}
customElements.define("ha-config-customize", HaConfigCustomize);

View File

@ -12,7 +12,7 @@ import hassAttributeUtil from "../../../util/hass-attributes-util";
import "../ha-form-style";
import "./ha-form-customize-attributes";
class HaFormCustomize extends LocalizeMixin(PolymerElement) {
export class HaFormCustomize extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex ha-style ha-form-style">
@ -73,26 +73,28 @@ class HaFormCustomize extends LocalizeMixin(PolymerElement) {
attributes="{{newAttributes}}"
></ha-form-customize-attributes>
</template>
<div class="form-group">
<paper-dropdown-menu
label="[[localize('ui.panel.config.customize.pick_attribute')]]"
class="flex"
dynamic-align=""
>
<paper-listbox
slot="dropdown-content"
selected="{{selectedNewAttribute}}"
<template is="dom-if" if="[[entity]]">
<div class="form-group">
<paper-dropdown-menu
label="[[localize('ui.panel.config.customize.pick_attribute')]]"
class="flex"
dynamic-align=""
>
<template
is="dom-repeat"
items="[[newAttributesOptions]]"
as="option"
<paper-listbox
slot="dropdown-content"
selected="{{selectedNewAttribute}}"
>
<paper-item>[[option]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<template
is="dom-repeat"
items="[[newAttributesOptions]]"
as="option"
>
<paper-item>[[option]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
</template>
`;
}

View File

@ -1,222 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "../../components/ha-circular-progress";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../../common/entity/compute_state_name";
import "../../components/ha-card";
import "../../styles/polymer-ha-style";
class HaEntityConfig extends PolymerElement {
static get template() {
return html`
<style include="iron-flex ha-style">
ha-card {
direction: ltr;
}
.device-picker {
@apply --layout-horizontal;
padding-bottom: 24px;
}
.form-placeholder {
@apply --layout-vertical;
@apply --layout-center-center;
height: 96px;
}
[hidden]: {
display: none;
}
.card-actions {
@apply --layout-horizontal;
@apply --layout-justified;
}
</style>
<ha-card>
<div class="card-content">
<div class="device-picker">
<paper-dropdown-menu
label="[[label]]"
class="flex"
disabled="[[!entities.length]]"
>
<paper-listbox
slot="dropdown-content"
selected="{{selectedEntity}}"
>
<template is="dom-repeat" items="[[entities]]" as="state">
<paper-item>[[computeSelectCaption(state)]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="form-container">
<template is="dom-if" if="[[computeShowPlaceholder(formState)]]">
<div class="form-placeholder">
<template is="dom-if" if="[[computeShowNoDevices(formState)]]">
No entities found! :-(
</template>
<template is="dom-if" if="[[computeShowSpinner(formState)]]">
<ha-circular-progress
active=""
alt="[[formState]]"
></ha-circular-progress>
[[formState]]
</template>
</div>
</template>
<div hidden$="[[!computeShowForm(formState)]]" id="form"></div>
</div>
</div>
<div class="card-actions">
<mwc-button
on-click="saveEntity"
disabled="[[computeShowPlaceholder(formState)]]"
>SAVE</mwc-button
>
<template is="dom-if" if="[[allowDelete]]">
<mwc-button
class="warning"
on-click="deleteEntity"
disabled="[[computeShowPlaceholder(formState)]]"
>DELETE</mwc-button
>
</template>
</div>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "hassChanged",
},
label: {
type: String,
value: "Device",
},
entities: {
type: Array,
observer: "entitiesChanged",
},
allowDelete: {
type: Boolean,
value: false,
},
selectedEntity: {
type: Number,
value: -1,
observer: "entityChanged",
},
formState: {
type: String,
// no-devices, loading, saving, editing
value: "no-devices",
},
config: {
type: Object,
},
};
}
connectedCallback() {
super.connectedCallback();
this.formEl = document.createElement(this.config.component);
this.formEl.hass = this.hass;
this.$.form.appendChild(this.formEl);
this.entityChanged(this.selectedEntity);
}
computeSelectCaption(stateObj) {
return this.config.computeSelectCaption
? this.config.computeSelectCaption(stateObj)
: computeStateName(stateObj);
}
computeShowNoDevices(formState) {
return formState === "no-devices";
}
computeShowSpinner(formState) {
return formState === "loading" || formState === "saving";
}
computeShowPlaceholder(formState) {
return formState !== "editing";
}
computeShowForm(formState) {
return formState === "editing";
}
hassChanged(hass) {
if (this.formEl) {
this.formEl.hass = hass;
}
}
entitiesChanged(entities, oldEntities) {
if (entities.length === 0) {
this.formState = "no-devices";
return;
}
if (!oldEntities) {
this.selectedEntity = 0;
return;
}
const oldEntityId = oldEntities[this.selectedEntity].entity_id;
const newIndex = entities.findIndex(function (ent) {
return ent.entity_id === oldEntityId;
});
if (newIndex === -1) {
this.selectedEntity = 0;
} else if (newIndex !== this.selectedEntity) {
// Entity moved index
this.selectedEntity = newIndex;
}
}
entityChanged(index) {
if (!this.entities || !this.formEl) return;
const entity = this.entities[index];
if (!entity) return;
this.formState = "loading";
// eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this;
this.formEl.loadEntity(entity).then(function () {
el.formState = "editing";
});
}
saveEntity() {
this.formState = "saving";
// eslint-disable-next-line @typescript-eslint/no-this-alias
const el = this;
this.formEl.saveEntity().then(function () {
el.formState = "editing";
});
}
}
customElements.define("ha-entity-config", HaEntityConfig);

View File

@ -0,0 +1,124 @@
import "@material/mwc-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import "../../components/buttons/ha-progress-button";
import "../../components/entity/ha-entity-picker";
import "../../components/ha-card";
import "../../components/ha-circular-progress";
import { haStyle } from "../../resources/styles";
import "../../styles/polymer-ha-style";
import type { HomeAssistant } from "../../types";
import { HaFormCustomize } from "./customize/ha-form-customize";
@customElement("ha-entity-config")
export class HaEntityConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public selectedEntityId!: string;
// False if no entity is selected or currently saving or loading
@property() private _formEditState = false;
@query("#form") private _form!: HaFormCustomize;
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
<ha-entity-picker
.hass=${this.hass}
.value=${this.selectedEntityId}
.configValue=${"entity"}
@change=${this._selectedEntityChanged}
allow-custom-entity
hideClearIcon
>
</ha-entity-picker>
<div class="form-container">
<ha-form-customize .hass=${this.hass} .id=${"form"}>
</ha-form-customize>
</div>
</div>
<div class="card-actions">
<ha-progress-button
@click=${this._saveEntity}
.disabled=${!this._formEditState}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (
changedProps.has("selectedEntityId") &&
changedProps.get("selectedEntityId") !== this.selectedEntityId
) {
this._selectEntity(this.selectedEntityId);
this.requestUpdate();
}
}
private _selectedEntityChanged(ev) {
this._selectEntity(ev.target.value);
}
private async _selectEntity(entityId?: string) {
if (!this._form || !entityId) return;
const entity = this.hass.states[entityId];
if (!entity) return;
this._formEditState = false;
await this._form.loadEntity(entity);
this._formEditState = true;
}
private async _saveEntity(ev) {
if (!this._formEditState) return;
this._formEditState = false;
const button = ev.target;
button.progress = true;
try {
await this._form.saveEntity();
this._formEditState = true;
button.actionSuccess();
} catch {
button.actionError();
} finally {
button.progress = false;
}
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
direction: ltr;
}
.form-placeholder {
height: 96px;
}
.hidden {
display: none;
}
`,
];
}
}

View File

@ -1108,8 +1108,7 @@
"picker": {
"header": "Customizations",
"introduction": "Tweak per-entity attributes. Added/edited customizations will take effect immediately. Removed customizations will take effect when the entity is updated.",
"documentation": "Customization documentation",
"entity": "Entity"
"documentation": "Customization documentation"
},
"warning": {
"include_sentence": "It seems that your configuration.yaml doesn't properly",