Convert script and automation editor to lit (#4327)

* Convert script and automation editor to lit

* Update yarn.lock
This commit is contained in:
Bram Kragten 2019-12-09 10:59:52 +01:00 committed by GitHub
parent 43393d1647
commit cbba1849e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 521 additions and 744 deletions

View File

@ -91,7 +91,7 @@ const createWebpackConfig = ({
),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json", ".tsx"],
extensions: [".ts", ".js", ".json"],
alias: {
react: "preact-compat",
"react-dom": "preact-compat",

View File

@ -8,7 +8,7 @@
"version": "1.0.0",
"scripts": {
"build": "script/build_frontend",
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'src/**/*.tsx' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "npm run lint && npm run mocha",
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
@ -76,6 +76,7 @@
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.49.0",
"copy-to-clipboard": "^1.0.9",
"cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0",
@ -99,7 +100,6 @@
"regenerator-runtime": "^0.13.2",
"roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1",
"copy-to-clipboard": "^1.0.9",
"tslib": "^1.10.0",
"unfetch": "^4.1.0",
"web-animations-js": "^2.3.1",

View File

@ -1,28 +0,0 @@
// interface OnChangeComponent {
// props: {
// index: number;
// onChange(index: number, data: object);
// };
// }
// export function onChangeEvent(this: OnChangeComponent, prop, ev) {
export function onChangeEvent(this: any, prop, ev) {
if (!this.initialized) {
return;
}
const origData = this.props[prop];
if (ev.target.value === origData[ev.target.name]) {
return;
}
const data = { ...origData };
if (ev.target.value) {
data[ev.target.name] = ev.target.value;
} else {
delete data[ev.target.name];
}
this.props.onChange(this.props.index, data);
}

View File

@ -1,9 +0,0 @@
import { render } from "preact";
export default function unmount(mountEl) {
render(
// @ts-ignore
() => null,
mountEl
);
}

View File

@ -1,6 +1,21 @@
import { HomeAssistant } from "../types";
import { computeObjectId } from "../common/entity/compute_object_id";
import { Condition } from "./automation";
import {
HassEntityBase,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
export interface ScriptEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
last_triggered: string;
};
}
export interface ScriptConfig {
alias: string;
sequence: Action[];
}
export interface EventAction {
event: string;

View File

@ -1,42 +1,37 @@
import {
LitElement,
TemplateResult,
html,
CSSResult,
css,
PropertyValues,
property,
} 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 { classMap } from "lit-html/directives/class-map";
import { h, render } from "preact";
import "../../../components/ha-fab";
import "../../../components/ha-paper-icon-button-arrow-prev";
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/styles";
import { HomeAssistant } from "../../../types";
import {
AutomationEntity,
AutomationConfig,
deleteAutomation,
getAutomationEditorInitData,
} from "../../../data/automation";
css,
CSSResult,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-fab";
import "../../../components/ha-paper-icon-button-arrow-prev";
import {
AutomationConfig,
AutomationEntity,
Condition,
deleteAutomation,
getAutomationEditorInitData,
Trigger,
} from "../../../data/automation";
import { Action } from "../../../data/script";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
function AutomationEditor(mountEl, props, mergeEl) {
return render(h(Automation, props), mountEl, mergeEl);
}
import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
export class HaAutomationEditor extends LitElement {
@property() public hass!: HomeAssistant;
@ -45,26 +40,9 @@ export class HaAutomationEditor extends LitElement {
@property() public creatingNew?: boolean;
@property() private _config?: AutomationConfig;
@property() private _dirty?: boolean;
private _rendered?: unknown;
@property() private _errors?: string;
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>
@ -100,11 +78,131 @@ export class HaAutomationEditor extends LitElement {
`
: ""}
<div
id="root"
class="${classMap({
rtl: computeRTL(this.hass),
})}"
></div>
>
${this._config
? html`
<ha-config-section .isWide=${this.isWide}>
<span slot="header">${this._config.alias}</span>
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></ha-textarea>
</div>
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/trigger/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/scripts/conditions/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/action/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`
: ""}
</div>
</div>
<ha-fab
slot="fab"
@ -184,28 +282,40 @@ export class HaAutomationEditor extends LitElement {
...initData,
};
}
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) {
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const name = (ev.target as any)?.name;
if (!name) {
return;
}
this._config = config;
const newVal = ev.detail.value;
if ((this._config![name] || "") === newVal) {
return;
}
this._config = { ...this._config!, [name]: newVal };
this._dirty = true;
}
private _triggerChanged(ev: CustomEvent): void {
this._config = { ...this._config!, trigger: ev.detail.value as Trigger[] };
this._errors = undefined;
this._dirty = true;
}
private _conditionChanged(ev: CustomEvent): void {
this._config = {
...this._config!,
condition: ev.detail.value as Condition[],
};
this._errors = undefined;
this._dirty = true;
}
private _actionChanged(ev: CustomEvent): void {
this._config = { ...this._config!, action: ev.detail.value as Action[] };
this._errors = undefined;
this._dirty = true;
}
@ -275,32 +385,6 @@ export class HaAutomationEditor extends LitElement {
.content {
padding-bottom: 20px;
}
.triggers,
.script {
margin-top: -16px;
}
.triggers ha-card,
.script ha-card {
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
.card-menu {
position: absolute;
top: 0;
right: 0;
z-index: 1;
color: var(--primary-text-color);
}
.rtl .card-menu {
right: auto;
left: 0;
}
.card-menu paper-item {
cursor: pointer;
}
span[slot="introduction"] a {
color: var(--primary-color);
}

View File

@ -1,155 +0,0 @@
import { h, Component } from "preact";
import "@polymer/paper-input/paper-input";
import "../ha-config-section";
import "../../../components/ha-card";
import "../../../components/ha-textarea";
import "../automation/trigger/ha-automation-trigger";
import "../automation/condition/ha-automation-condition";
import "../automation/action/ha-automation-action";
export default class Automation extends Component<any> {
constructor() {
super();
this.onChange = this.onChange.bind(this);
this.triggerChanged = this.triggerChanged.bind(this);
this.conditionChanged = this.conditionChanged.bind(this);
this.actionChanged = this.actionChanged.bind(this);
}
public onChange(ev) {
this.props.onChange({
...this.props.automation,
[ev.target.name]: ev.target.value,
});
}
public triggerChanged(ev: CustomEvent) {
this.props.onChange({ ...this.props.automation, trigger: ev.detail.value });
}
public conditionChanged(ev: CustomEvent) {
this.props.onChange({
...this.props.automation,
condition: ev.detail.value,
});
}
public actionChanged(ev: CustomEvent) {
this.props.onChange({ ...this.props.automation, action: ev.detail.value });
}
public render({ automation, isWide, hass, localize }) {
const { alias, description, trigger, condition, action } = automation;
return (
<div>
<ha-config-section is-wide={isWide}>
<span slot="header">{alias}</span>
<span slot="introduction">
{localize("ui.panel.config.automation.editor.introduction")}
</span>
<ha-card>
<div class="card-content">
<paper-input
label={localize("ui.panel.config.automation.editor.alias")}
name="alias"
value={alias}
onvalue-changed={this.onChange}
/>
<ha-textarea
label={localize(
"ui.panel.config.automation.editor.description.label"
)}
placeholder={localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
value={description}
onvalue-changed={this.onChange}
/>
</div>
</ha-card>
</ha-config-section>
<ha-config-section is-wide={isWide}>
<span slot="header">
{localize("ui.panel.config.automation.editor.triggers.header")}
</span>
<span slot="introduction">
<p>
{localize(
"ui.panel.config.automation.editor.triggers.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/trigger/"
target="_blank"
>
{localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
triggers={trigger}
onvalue-changed={this.triggerChanged}
hass={hass}
/>
</ha-config-section>
<ha-config-section is-wide={isWide}>
<span slot="header">
{localize("ui.panel.config.automation.editor.conditions.header")}
</span>
<span slot="introduction">
<p>
{localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/scripts/conditions/"
target="_blank"
>
{localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
conditions={condition || []}
onvalue-changed={this.conditionChanged}
hass={hass}
/>
</ha-config-section>
<ha-config-section is-wide={isWide}>
<span slot="header">
{localize("ui.panel.config.automation.editor.actions.header")}
</span>
<span slot="introduction">
<p>
{localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/action/"
target="_blank"
>
{localize("ui.panel.config.automation.editor.actions.learn_more")}
</a>
</span>
<ha-automation-action
actions={action}
onvalue-changed={this.actionChanged}
hass={hass}
/>
</ha-config-section>
</div>
);
}
}

View File

@ -1,35 +0,0 @@
import { PaperInputElement } from "@polymer/paper-input/paper-input";
// Force file to be a module to augment global scope.
export {};
declare global {
namespace JSX {
interface IntrinsicElements {
"paper-input": Partial<PaperInputElement>;
"ha-config-section": any;
"ha-card": any;
"paper-radio-button": any;
"paper-radio-group": any;
"ha-entity-picker": any;
"paper-listbox": any;
"paper-item": any;
"paper-menu-button": any;
"paper-dropdown-menu-light": any;
"paper-icon-button": any;
"ha-device-picker": any;
"ha-device-condition-picker": any;
"ha-textarea": any;
"ha-code-editor": any;
"ha-service-picker": any;
"mwc-button": any;
"ha-automation-trigger": any;
"ha-automation-condition": any;
"ha-automation-condition-editor": any;
"ha-automation-action": any;
"ha-device-trigger-picker": any;
"ha-device-action-picker": any;
"ha-form": any;
}
}
}

View File

@ -1,80 +0,0 @@
import { h, Component } from "preact";
import "@polymer/paper-input/paper-input";
import "../ha-config-section";
import "../../../components/ha-card";
import "../automation/action/ha-automation-action";
export default class ScriptEditor extends Component<{
onChange: (...args: any[]) => any;
script: any;
isWide: any;
hass: any;
localize: any;
}> {
constructor() {
super();
this.onChange = this.onChange.bind(this);
this.sequenceChanged = this.sequenceChanged.bind(this);
}
public onChange(ev) {
this.props.onChange({
...this.props.script,
[ev.target.name]: ev.target.value,
});
}
public sequenceChanged(ev: CustomEvent) {
this.props.onChange({ ...this.props.script, sequence: ev.detail.value });
}
// @ts-ignore
public render({ script, isWide, hass, localize }) {
const { alias, sequence } = script;
return (
<div>
<ha-config-section is-wide={isWide}>
<span slot="header">{alias}</span>
<span slot="introduction">
{localize("ui.panel.config.script.editor.introduction")}
</span>
<ha-card>
<div class="card-content">
<paper-input
label="Name"
name="alias"
value={alias}
onvalue-changed={this.onChange}
/>
</div>
</ha-card>
</ha-config-section>
<ha-config-section is-wide={isWide}>
<span slot="header">
{localize("ui.panel.config.script.editor.sequence")}
</span>
<span slot="introduction">
{localize("ui.panel.config.script.editor.sequence_sentence")}
<p>
<a href="https://home-assistant.io/docs/scripts/" target="_blank">
{localize(
"ui.panel.config.script.editor.link_available_actions"
)}
</a>
</p>
</span>
<ha-automation-action
actions={sequence}
onvalue-changed={this.sequenceChanged}
hass={hass}
/>
</ha-config-section>
</div>
);
}
}

View File

@ -1,336 +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 { 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 "../../../components/ha-paper-icon-button-arrow-prev";
import "../../../components/ha-fab";
import Script from "../js/script";
import unmountPreact from "../../../common/preact/unmount";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { computeStateName } from "../../../common/entity/compute_state_name";
import NavigateMixin from "../../../mixins/navigate-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
import { computeRTL } from "../../../common/util/compute_rtl";
import { deleteScript } from "../../../data/script";
function ScriptEditor(mountEl, props, mergeEl) {
return render(h(Script, props), mountEl, mergeEl);
}
/*
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
*/
class HaScriptEditor 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;
}
.triggers,
.script {
margin-top: -16px;
}
.triggers ha-card,
.script ha-card {
margin-top: 16px;
}
.add-card mwc-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);
}
ha-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
}
ha-fab[is-wide] {
bottom: 24px;
right: 24px;
}
ha-fab[dirty] {
margin-bottom: 0;
}
ha-fab[rtl] {
right: auto;
left: 16px;
}
ha-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
</style>
<ha-app-layout has-scrolling-region="">
<app-header slot="header" fixed="">
<app-toolbar>
<ha-paper-icon-button-arrow-prev
on-click="backTapped"
></ha-paper-icon-button-arrow-prev>
<div main-title>[[computeHeader(script)]]</div>
<template is="dom-if" if="[[!creatingNew]]">
<paper-icon-button
icon="hass:delete"
title="[[localize('ui.panel.config.script.editor.delete_script')]]"
on-click="_delete"
></paper-icon-button>
</template>
</app-toolbar>
</app-header>
<div class="content">
<template is="dom-if" if="[[errors]]">
<div class="errors">[[errors]]</div>
</template>
<div id="root"></div>
</div>
<ha-fab
slot="fab"
is-wide$="[[isWide]]"
dirty$="[[dirty]]"
icon="hass:content-save"
title="[[localize('ui.common.save')]]"
on-click="saveScript"
rtl$="[[rtl]]"
></ha-fab>
</ha-app-layout>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
errors: {
type: Object,
value: null,
},
dirty: {
type: Boolean,
value: false,
},
config: {
type: Object,
value: null,
},
script: {
type: Object,
observer: "scriptChanged",
},
creatingNew: {
type: Boolean,
observer: "creatingNewChanged",
},
isWide: {
type: Boolean,
observer: "_updateComponent",
},
_rendered: {
type: Object,
value: null,
},
_renderScheduled: {
type: Boolean,
value: false,
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
};
}
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();
}
scriptChanged(newVal, oldVal) {
if (!newVal) return;
if (!this.hass) {
setTimeout(() => this.scriptChanged(newVal, oldVal), 0);
return;
}
if (oldVal && oldVal.entity_id === newVal.entity_id) {
return;
}
this.hass
.callApi(
"get",
"config/script/config/" + computeObjectId(newVal.entity_id)
)
.then(
(config) => {
// Normalize data: ensure sequence is a list
// Happens when people copy paste their scripts into the config
var value = config.sequence;
if (value && !Array.isArray(value)) {
config.sequence = [value];
}
this.dirty = false;
this.config = config;
this._updateComponent();
},
() => {
alert(
this.hass.localize(
"ui.panel.config.script.editor.load_error_not_editable"
)
);
history.back();
}
);
}
creatingNewChanged(newVal) {
if (!newVal) {
return;
}
this.dirty = false;
this.config = {
alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
sequence: [{ service: "", data: {} }],
};
this._updateComponent();
}
backTapped() {
if (
this.dirty &&
// eslint-disable-next-line
!confirm(
this.hass.localize("ui.panel.config.common.editor.confirm_unsaved")
)
) {
return;
}
history.back();
}
_updateComponent() {
if (this._renderScheduled || !this.hass || !this.config) return;
this._renderScheduled = true;
Promise.resolve().then(() => {
this._rendered = ScriptEditor(
this.$.root,
{
script: this.config,
onChange: this.configChanged,
isWide: this.isWide,
hass: this.hass,
localize: this.localize,
},
this._rendered
);
this._renderScheduled = false;
});
}
async _delete() {
if (
!confirm(
this.hass.localize("ui.panel.config.script.editor.delete_confirm")
)
) {
return;
}
await deleteScript(this.hass, computeObjectId(this.script.entity_id));
history.back();
}
saveScript() {
var id = this.creatingNew
? "" + Date.now()
: computeObjectId(this.script.entity_id);
this.hass.callApi("post", "config/script/config/" + id, this.config).then(
() => {
this.dirty = false;
if (this.creatingNew) {
this.navigate(`/config/script/edit/${id}`, true);
}
},
(errors) => {
this.errors = errors.body.message;
throw errors;
}
);
}
computeHeader(script) {
return script
? this.hass.localize(
"ui.panel.config.script.editor.header",
"name",
computeStateName(script)
)
: this.hass.localize("ui.panel.config.script.editor.default_name");
}
_computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("ha-script-editor", HaScriptEditor);

View File

@ -0,0 +1,322 @@
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 {
css,
CSSResult,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-fab";
import "../../../components/ha-paper-icon-button-arrow-prev";
import {
Action,
ScriptEntity,
ScriptConfig,
deleteScript,
} from "../../../data/script";
import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation";
import "../../../layouts/ha-app-layout";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../automation/action/ha-automation-action";
import { computeObjectId } from "../../../common/entity/compute_object_id";
export class HaScriptEditor extends LitElement {
@property() public hass!: HomeAssistant;
@property() public script!: ScriptEntity;
@property() public isWide?: boolean;
@property() public creatingNew?: boolean;
@property() private _config?: ScriptConfig;
@property() private _dirty?: boolean;
@property() private _errors?: string;
protected render(): TemplateResult | void {
return html`
<ha-app-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<ha-paper-icon-button-arrow-prev
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
<div main-title>
${this.script
? computeStateName(this.script)
: this.hass.localize(
"ui.panel.config.script.editor.default_name"
)}
</div>
${this.creatingNew
? ""
: html`
<paper-icon-button
title="${this.hass.localize(
"ui.panel.config.script.editor.delete_script"
)}"
icon="hass:delete"
@click=${this._delete}
></paper-icon-button>
`}
</app-toolbar>
</app-header>
<div class="content">
${this._errors
? html`
<div class="errors">${this._errors}</div>
`
: ""}
<div
class="${classMap({
rtl: computeRTL(this.hass),
})}"
>
${this._config
? html`
<ha-config-section .isWide=${this.isWide}>
<span slot="header">${this._config.alias}</span>
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.script.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.script.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
</div>
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.script.editor.sequence"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.script.editor.sequence_sentence"
)}
</p>
<a
href="https://home-assistant.io/docs/scripts/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.sequence}
@value-changed=${this._sequenceChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`
: ""}
</div>
</div>
<ha-fab
slot="fab"
?is-wide="${this.isWide}"
?dirty="${this._dirty}"
icon="hass:content-save"
.title="${this.hass.localize("ui.common.save")}"
@click=${this._saveScript}
class="${classMap({
rtl: computeRTL(this.hass),
})}"
></ha-fab>
</ha-app-layout>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
const oldScript = changedProps.get("script") as ScriptEntity;
if (
changedProps.has("script") &&
this.script &&
this.hass &&
// Only refresh config if we picked a new script. If same ID, don't fetch it.
(!oldScript || oldScript.entity_id !== this.script.entity_id)
) {
this.hass
.callApi<ScriptConfig>(
"GET",
`config/script/config/${computeObjectId(this.script.entity_id)}`
)
.then(
(config) => {
// Normalize data: ensure sequence is a list
// Happens when people copy paste their scripts into the config
const value = config.sequence;
if (value && !Array.isArray(value)) {
config.sequence = [value];
}
this._dirty = false;
this._config = config;
},
(resp) => {
alert(
resp.status_code === 404
? this.hass.localize(
"ui.panel.config.script.editor.load_error_not_editable"
)
: this.hass.localize(
"ui.panel.config.script.editor.load_error_unknown",
"err_no",
resp.status_code
)
);
history.back();
}
);
}
if (changedProps.has("creatingNew") && this.creatingNew && this.hass) {
this._dirty = false;
this._config = {
alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
sequence: [{ service: "" }],
};
}
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const name = (ev.target as any)?.name;
if (!name) {
return;
}
const newVal = ev.detail.value;
if ((this._config![name] || "") === newVal) {
return;
}
this._config = { ...this._config!, [name]: newVal };
this._dirty = true;
}
private _sequenceChanged(ev: CustomEvent): void {
this._config = { ...this._config!, sequence: ev.detail.value as Action[] };
this._errors = undefined;
this._dirty = true;
}
private _backTapped(): void {
if (this._dirty) {
showConfirmationDialog(this, {
text: this.hass!.localize(
"ui.panel.config.common.editor.confirm_unsaved"
),
confirmBtnText: this.hass!.localize("ui.common.yes"),
cancelBtnText: this.hass!.localize("ui.common.no"),
confirm: () => history.back(),
});
} else {
history.back();
}
}
private async _delete() {
if (
!confirm(
this.hass.localize("ui.panel.config.script.editor.delete_confirm")
)
) {
return;
}
await deleteScript(this.hass, computeObjectId(this.script.entity_id));
history.back();
}
private _saveScript(): void {
const id = this.creatingNew
? "" + Date.now()
: computeObjectId(this.script.entity_id);
this.hass!.callApi("POST", "config/script/config/" + id, this._config).then(
() => {
this._dirty = false;
if (this.creatingNew) {
navigate(this, `/config/script/edit/${id}`, true);
}
},
(errors) => {
this._errors = errors.body.message;
throw errors;
}
);
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
overflow: hidden;
}
.errors {
padding: 20px;
font-weight: bold;
color: var(--google-red-500);
}
.content {
padding-bottom: 20px;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
}
ha-fab[is-wide] {
bottom: 24px;
right: 24px;
}
ha-fab[dirty] {
margin-bottom: 0;
}
ha-fab.rtl {
right: auto;
left: 16px;
}
ha-fab[is-wide].rtl {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
}
}
customElements.define("ha-script-editor", HaScriptEditor);

View File

@ -999,6 +999,7 @@
"edit_script": "Edit script"
},
"editor": {
"alias": "Name",
"introduction": "Use scripts to execute a sequence of actions.",
"header": "Script: {name}",
"default_name": "New Script",

View File

@ -1,7 +1,5 @@
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"target": "es2017",
"module": "esnext",
"moduleResolution": "node",