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:
Paulus Schoutsen 2019-02-06 11:01:15 -08:00 committed by GitHub
parent ce35416284
commit f00de454d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 294 additions and 298 deletions

17
src/data/automation.ts Normal file
View 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[];
}

View File

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

View 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);