mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Convert script and automation editor to lit (#4327)
* Convert script and automation editor to lit * Update yarn.lock
This commit is contained in:
parent
43393d1647
commit
cbba1849e2
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { render } from "preact";
|
||||
|
||||
export default function unmount(mountEl) {
|
||||
render(
|
||||
// @ts-ignore
|
||||
() => null,
|
||||
mountEl
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
322
src/panels/config/script/ha-script-editor.ts
Normal file
322
src/panels/config/script/ha-script-editor.ts
Normal 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);
|
@ -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",
|
||||
|
@ -1,7 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
"target": "es2017",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
|
Loading…
x
Reference in New Issue
Block a user