mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
Convert automation editor to Lit (#2687)
* Convert automation editor to Lit * Apply suggestions from code review Co-Authored-By: balloob <paulus@home-assistant.io>
This commit is contained in:
parent
ce35416284
commit
f00de454d1
17
src/data/automation.ts
Normal file
17
src/data/automation.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {
|
||||
HassEntityBase,
|
||||
HassEntityAttributeBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export interface AutomationEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AutomationConfig {
|
||||
alias: string;
|
||||
trigger: any[];
|
||||
condition?: any[];
|
||||
action: any[];
|
||||
}
|
@ -1,298 +0,0 @@
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-fab/paper-fab";
|
||||
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { h, render } from "preact";
|
||||
|
||||
import "../../../layouts/ha-app-layout";
|
||||
|
||||
import Automation from "../js/automation";
|
||||
import unmountPreact from "../../../common/preact/unmount";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
import NavigateMixin from "../../../mixins/navigate-mixin";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
|
||||
function AutomationEditor(mountEl, props, mergeEl) {
|
||||
return render(h(Automation, props), mountEl, mergeEl);
|
||||
}
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
*/
|
||||
class HaAutomationEditor extends LocalizeMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
.errors {
|
||||
padding: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
.triggers,
|
||||
.script {
|
||||
margin-top: -16px;
|
||||
}
|
||||
.triggers paper-card,
|
||||
.script paper-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.add-card paper-button {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-menu paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
span[slot="introduction"] a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
margin-bottom: -80px;
|
||||
transition: margin-bottom 0.3s;
|
||||
}
|
||||
|
||||
paper-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
paper-fab[dirty] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-app-layout has-scrolling-region="">
|
||||
<app-header slot="header" fixed="">
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon="hass:arrow-left"
|
||||
on-click="backTapped"
|
||||
></paper-icon-button>
|
||||
<div main-title="">[[computeName(automation, localize)]]</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class="content">
|
||||
<template is="dom-if" if="[[errors]]">
|
||||
<div class="errors">[[errors]]</div>
|
||||
</template>
|
||||
<div id="root"></div>
|
||||
</div>
|
||||
<paper-fab
|
||||
slot="fab"
|
||||
is-wide$="[[isWide]]"
|
||||
dirty$="[[dirty]]"
|
||||
icon="hass:content-save"
|
||||
title="[[localize('ui.panel.config.automation.editor.save')]]"
|
||||
on-click="saveAutomation"
|
||||
></paper-fab>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
observer: "_updateComponent",
|
||||
},
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
|
||||
dirty: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
config: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
|
||||
automation: {
|
||||
type: Object,
|
||||
observer: "automationChanged",
|
||||
},
|
||||
|
||||
creatingNew: {
|
||||
type: Boolean,
|
||||
observer: "creatingNewChanged",
|
||||
},
|
||||
|
||||
isWide: {
|
||||
type: Boolean,
|
||||
observer: "_updateComponent",
|
||||
},
|
||||
|
||||
_rendered: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
|
||||
_renderScheduled: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
this.configChanged = this.configChanged.bind(this);
|
||||
super.ready(); // This call will initialize preact.
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._rendered) {
|
||||
unmountPreact(this._rendered);
|
||||
this._rendered = null;
|
||||
}
|
||||
}
|
||||
|
||||
configChanged(config) {
|
||||
// onChange gets called a lot during initial rendering causing recursing calls.
|
||||
if (this._rendered === null) return;
|
||||
this.config = config;
|
||||
this.errors = null;
|
||||
this.dirty = true;
|
||||
this._updateComponent();
|
||||
}
|
||||
|
||||
automationChanged(newVal, oldVal) {
|
||||
if (!newVal) return;
|
||||
if (!this.hass) {
|
||||
setTimeout(() => this.automationChanged(newVal, oldVal), 0);
|
||||
return;
|
||||
}
|
||||
if (oldVal && oldVal.attributes.id === newVal.attributes.id) {
|
||||
return;
|
||||
}
|
||||
this.hass
|
||||
.callApi("get", "config/automation/config/" + newVal.attributes.id)
|
||||
.then(
|
||||
function(config) {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
["trigger", "condition", "action"].forEach(function(key) {
|
||||
var value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
});
|
||||
this.dirty = false;
|
||||
this.config = config;
|
||||
this._updateComponent();
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
creatingNewChanged(newVal) {
|
||||
if (!newVal) {
|
||||
return;
|
||||
}
|
||||
this.dirty = false;
|
||||
this.config = {
|
||||
alias: this.localize("ui.panel.config.automation.editor.default_name"),
|
||||
trigger: [{ platform: "state" }],
|
||||
condition: [],
|
||||
action: [{ service: "" }],
|
||||
};
|
||||
this._updateComponent();
|
||||
}
|
||||
|
||||
backTapped() {
|
||||
if (
|
||||
this.dirty &&
|
||||
// eslint-disable-next-line
|
||||
!confirm(
|
||||
this.localize("ui.panel.config.automation.editor.unsaved_confirm")
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
history.back();
|
||||
}
|
||||
|
||||
async _updateComponent() {
|
||||
if (this._renderScheduled || !this.hass || !this.config) return;
|
||||
this._renderScheduled = true;
|
||||
|
||||
await 0;
|
||||
|
||||
if (!this._renderScheduled) return;
|
||||
|
||||
this._renderScheduled = false;
|
||||
|
||||
this._rendered = AutomationEditor(
|
||||
this.$.root,
|
||||
{
|
||||
automation: this.config,
|
||||
onChange: this.configChanged,
|
||||
isWide: this.isWide,
|
||||
hass: this.hass,
|
||||
localize: this.localize,
|
||||
},
|
||||
this._rendered
|
||||
);
|
||||
}
|
||||
|
||||
saveAutomation() {
|
||||
var id = this.creatingNew ? "" + Date.now() : this.automation.attributes.id;
|
||||
this.hass
|
||||
.callApi("post", "config/automation/config/" + id, this.config)
|
||||
.then(
|
||||
function() {
|
||||
this.dirty = false;
|
||||
|
||||
if (this.creatingNew) {
|
||||
this.navigate(`/config/automation/edit/${id}`, true);
|
||||
}
|
||||
}.bind(this),
|
||||
function(errors) {
|
||||
this.errors = errors.body.message;
|
||||
throw errors;
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
computeName(automation, localize) {
|
||||
return automation
|
||||
? computeStateName(automation)
|
||||
: localize("ui.panel.config.automation.editor.default_name");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-automation-editor", HaAutomationEditor);
|
277
src/panels/config/automation/ha-automation-editor.ts
Normal file
277
src/panels/config/automation/ha-automation-editor.ts
Normal file
@ -0,0 +1,277 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
CSSResult,
|
||||
css,
|
||||
PropertyDeclarations,
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-fab/paper-fab";
|
||||
|
||||
import { h, render } from "preact";
|
||||
|
||||
import "../../../layouts/ha-app-layout";
|
||||
|
||||
import Automation from "../js/automation";
|
||||
import unmountPreact from "../../../common/preact/unmount";
|
||||
import computeStateName from "../../../common/entity/compute_state_name";
|
||||
|
||||
import { haStyle } from "../../../resources/ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { AutomationEntity, AutomationConfig } from "../../../data/automation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
|
||||
function AutomationEditor(mountEl, props, mergeEl) {
|
||||
return render(h(Automation, props), mountEl, mergeEl);
|
||||
}
|
||||
|
||||
class HaAutomationEditor extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public automation?: AutomationEntity;
|
||||
public isWide?: boolean;
|
||||
public creatingNew?: boolean;
|
||||
private _config?: AutomationConfig;
|
||||
private _dirty?: boolean;
|
||||
private _rendered?: unknown;
|
||||
private _errors?: string;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
automation: {},
|
||||
creatingNew: {},
|
||||
isWide: {},
|
||||
_errors: {},
|
||||
_dirty: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._configChanged = this._configChanged.bind(this);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._rendered) {
|
||||
unmountPreact(this._rendered);
|
||||
this._rendered = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
return html`
|
||||
<ha-app-layout has-scrolling-region>
|
||||
<app-header slot="header" fixed>
|
||||
<app-toolbar>
|
||||
<paper-icon-button
|
||||
icon="hass:arrow-left"
|
||||
@click=${this._backTapped}
|
||||
></paper-icon-button>
|
||||
<div main-title>
|
||||
${this.automation
|
||||
? computeStateName(this.automation)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.default_name"
|
||||
)}
|
||||
</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
|
||||
<div class="content">
|
||||
${this._errors
|
||||
? html`
|
||||
<div class="errors">${this._errors}</div>
|
||||
`
|
||||
: ""}
|
||||
<div id="root"></div>
|
||||
</div>
|
||||
<paper-fab
|
||||
slot="fab"
|
||||
?is-wide="${this.isWide}"
|
||||
?dirty="${this._dirty}"
|
||||
icon="hass:content-save"
|
||||
.title="${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.save"
|
||||
)}"
|
||||
@click=${this._saveAutomation}
|
||||
></paper-fab>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
const oldAutomation = changedProps.get("automation") as AutomationEntity;
|
||||
if (
|
||||
changedProps.has("automation") &&
|
||||
this.automation &&
|
||||
this.hass &&
|
||||
// Only refresh config if we picked a new automation. If same ID, don't fetch it.
|
||||
(!oldAutomation ||
|
||||
oldAutomation.attributes.id !== this.automation.attributes.id)
|
||||
) {
|
||||
this.hass
|
||||
.callApi<AutomationConfig>(
|
||||
"GET",
|
||||
`config/automation/config/${this.automation.attributes.id}`
|
||||
)
|
||||
.then((config) => {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
this._dirty = false;
|
||||
this._config = config;
|
||||
});
|
||||
}
|
||||
|
||||
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
|
||||
this._dirty = false;
|
||||
this._config = {
|
||||
alias: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.default_name"
|
||||
),
|
||||
trigger: [{ platform: "state" }],
|
||||
condition: [],
|
||||
action: [{ service: "" }],
|
||||
};
|
||||
}
|
||||
|
||||
if (changedProps.has("_config") && this.hass) {
|
||||
this._rendered = AutomationEditor(
|
||||
this.shadowRoot!.querySelector("#root"),
|
||||
{
|
||||
automation: this._config,
|
||||
onChange: this._configChanged,
|
||||
isWide: this.isWide,
|
||||
hass: this.hass,
|
||||
localize: this.hass.localize,
|
||||
},
|
||||
this._rendered
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(config: AutomationConfig): void {
|
||||
// onChange gets called a lot during initial rendering causing recursing calls.
|
||||
if (!this._rendered) {
|
||||
return;
|
||||
}
|
||||
this._config = config;
|
||||
this._errors = undefined;
|
||||
this._dirty = true;
|
||||
// this._updateComponent();
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
if (
|
||||
this._dirty &&
|
||||
!confirm(
|
||||
this.hass!.localize("ui.panel.config.automation.editor.unsaved_confirm")
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
history.back();
|
||||
}
|
||||
|
||||
private _saveAutomation(): void {
|
||||
const id = this.creatingNew
|
||||
? "" + Date.now()
|
||||
: this.automation!.attributes.id;
|
||||
this.hass!.callApi(
|
||||
"POST",
|
||||
"config/automation/config/" + id,
|
||||
this._config
|
||||
).then(
|
||||
() => {
|
||||
this._dirty = false;
|
||||
|
||||
if (this.creatingNew) {
|
||||
navigate(this, `/config/automation/edit/${id}`, true);
|
||||
}
|
||||
},
|
||||
(errors) => {
|
||||
this._errors = errors.body.message;
|
||||
throw errors;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.errors {
|
||||
padding: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
.triggers,
|
||||
.script {
|
||||
margin-top: -16px;
|
||||
}
|
||||
.triggers paper-card,
|
||||
.script paper-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.add-card paper-button {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.card-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-menu paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
span[slot="introduction"] a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
margin-bottom: -80px;
|
||||
transition: margin-bottom 0.3s;
|
||||
}
|
||||
|
||||
paper-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
paper-fab[dirty] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-automation-editor", HaAutomationEditor);
|
Loading…
x
Reference in New Issue
Block a user