mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-29 12:16:39 +00:00
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:
parent
0a3172dfdb
commit
8bfe583a20
@ -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);
|
91
src/panels/config/customize/ha-config-customize.ts
Normal file
91
src/panels/config/customize/ha-config-customize.ts
Normal 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);
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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);
|
124
src/panels/config/ha-entity-config.ts
Normal file
124
src/panels/config/ha-entity-config.ts
Normal 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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user