diff --git a/src/data/automation.ts b/src/data/automation.ts new file mode 100644 index 0000000000..02032b06c4 --- /dev/null +++ b/src/data/automation.ts @@ -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[]; +} diff --git a/src/panels/config/automation/ha-automation-editor.js b/src/panels/config/automation/ha-automation-editor.js deleted file mode 100644 index 45e010facb..0000000000 --- a/src/panels/config/automation/ha-automation-editor.js +++ /dev/null @@ -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` - - - - - - - [[computeName(automation, localize)]] - - - - - - [[errors]] - - - - - - `; - } - - 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); diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts new file mode 100644 index 0000000000..4d00ececa8 --- /dev/null +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -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` + + + + + + ${this.automation + ? computeStateName(this.automation) + : this.hass.localize( + "ui.panel.config.automation.editor.default_name" + )} + + + + + + ${this._errors + ? html` + ${this._errors} + ` + : ""} + + + + + `; + } + + 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( + "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);