Collapse automation/script editor sections by default (#13390)

This commit is contained in:
Paulus Schoutsen 2022-08-18 10:04:35 -04:00 committed by GitHub
parent d7b888f761
commit 47b820d28f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1305 additions and 920 deletions

View File

@ -0,0 +1,5 @@
---
title: Expansion Panel
---
Expansion panel following all the ARIA guidelines.

View File

@ -0,0 +1,157 @@
import { mdiPacMan } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-markdown";
import "../../components/demo-black-white-row";
import { LONG_TEXT } from "../../data/text";
const SHORT_TEXT = LONG_TEXT.substring(0, 113);
const SAMPLES: {
template: (slot: string, leftChevron: boolean) => TemplateResult;
}[] = [
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr header"
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr header"
secondary="Attr secondary"
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
.secondary=${"Prop secondary"}
>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
.header=${"Prop header"}
>
<span slot="secondary">Slot Secondary</span>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel slot=${slot} .leftChevron=${leftChevron}>
<span slot="header">Slot header</span>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel slot=${slot} .leftChevron=${leftChevron}>
<span slot="header">Slot header with actions</span>
<ha-icon-button
slot="icons"
label="Some Action"
.path=${mdiPacMan}
></ha-icon-button>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
{
template(slot, leftChevron) {
return html`
<ha-expansion-panel
slot=${slot}
.leftChevron=${leftChevron}
header="Attr Header with actions"
>
<ha-icon-button
slot="icons"
label="Some Action"
.path=${mdiPacMan}
></ha-icon-button>
${SHORT_TEXT}
</ha-expansion-panel>
`;
},
},
];
@customElement("demo-components-ha-expansion-panel")
export class DemoHaExpansionPanel extends LitElement {
protected render(): TemplateResult {
return html`
${SAMPLES.map(
(sample) => html`
<demo-black-white-row>
${["light", "dark"].map((slot) =>
sample.template(slot, slot === "dark")
)}
</demo-black-white-row>
`
)}
`;
}
static get styles() {
return css`
ha-expansion-panel {
margin: -16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-expansion-panel": DemoHaExpansionPanel;
}
}

View File

@ -172,8 +172,7 @@ export abstract class HaDeviceAutomationPicker<
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-select { ha-select {
width: 100%; display: block;
margin-top: 4px;
} }
`; `;
} }

View File

@ -19,6 +19,8 @@ class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) outlined = false; @property({ type: Boolean, reflect: true }) outlined = false;
@property({ type: Boolean, reflect: true }) leftChevron = false;
@property() header?: string; @property() header?: string;
@property() secondary?: string; @property() secondary?: string;
@ -29,23 +31,42 @@ class HaExpansionPanel extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div <div class="top">
id="summary" <div
@click=${this._toggleContainer} id="summary"
@keydown=${this._toggleContainer} @click=${this._toggleContainer}
role="button" @keydown=${this._toggleContainer}
tabindex="0" @focus=${this._focusChanged}
aria-expanded=${this.expanded} @blur=${this._focusChanged}
aria-controls="sect1" role="button"
> tabindex="0"
<slot class="header" name="header"> aria-expanded=${this.expanded}
${this.header} aria-controls="sect1"
<slot class="secondary" name="secondary">${this.secondary}</slot> >
</slot> ${this.leftChevron
<ha-svg-icon ? html`
.path=${mdiChevronDown} <ha-svg-icon
class="summary-icon ${classMap({ expanded: this.expanded })}" .path=${mdiChevronDown}
></ha-svg-icon> class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
`
: ""}
<slot name="header">
<div class="header">
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</div>
</slot>
${!this.leftChevron
? html`
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
`
: ""}
</div>
<slot name="icons"></slot>
</div> </div>
<div <div
class="container ${classMap({ expanded: this.expanded })}" class="container ${classMap({ expanded: this.expanded })}"
@ -61,6 +82,7 @@ class HaExpansionPanel extends LitElement {
} }
protected willUpdate(changedProps: PropertyValues) { protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("expanded") && this.expanded) { if (changedProps.has("expanded") && this.expanded) {
this._showContent = this.expanded; this._showContent = this.expanded;
} }
@ -72,6 +94,9 @@ class HaExpansionPanel extends LitElement {
} }
private async _toggleContainer(ev): Promise<void> { private async _toggleContainer(ev): Promise<void> {
if (ev.defaultPrevented) {
return;
}
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") { if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return; return;
} }
@ -98,12 +123,28 @@ class HaExpansionPanel extends LitElement {
fireEvent(this, "expanded-changed", { expanded: this.expanded }); fireEvent(this, "expanded-changed", { expanded: this.expanded });
} }
private _focusChanged(ev) {
this.shadowRoot!.querySelector(".top")!.classList.toggle(
"focused",
ev.type === "focus"
);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
display: block; display: block;
} }
.top {
display: flex;
align-items: center;
}
.top.focused {
background: var(--input-fill-color);
}
:host([outlined]) { :host([outlined]) {
box-shadow: none; box-shadow: none;
border-width: 1px; border-width: 1px;
@ -115,7 +156,17 @@ class HaExpansionPanel extends LitElement {
border-radius: var(--ha-card-border-radius, 4px); border-radius: var(--ha-card-border-radius, 4px);
} }
.summary-icon {
margin-left: 8px;
}
:host([leftchevron]) .summary-icon {
margin-left: 0;
margin-right: 8px;
}
#summary { #summary {
flex: 1;
display: flex; display: flex;
padding: var(--expansion-panel-summary-padding, 0 8px); padding: var(--expansion-panel-summary-padding, 0 8px);
min-height: 48px; min-height: 48px;
@ -126,15 +177,8 @@ class HaExpansionPanel extends LitElement {
outline: none; outline: none;
} }
#summary:focus {
background: var(--input-fill-color);
}
.summary-icon { .summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto;
margin-inline-start: auto;
margin-inline-end: initial;
direction: var(--direction); direction: var(--direction);
} }
@ -142,6 +186,11 @@ class HaExpansionPanel extends LitElement {
transform: rotate(180deg); transform: rotate(180deg);
} }
.header,
::slotted([slot="header"]) {
flex: 1;
}
.container { .container {
padding: var(--expansion-panel-content-padding, 0 8px); padding: var(--expansion-panel-content-padding, 0 8px);
overflow: hidden; overflow: hidden;
@ -153,10 +202,6 @@ class HaExpansionPanel extends LitElement {
height: auto; height: auto;
} }
.header {
display: block;
}
.secondary { .secondary {
display: block; display: block;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,4 +1,4 @@
import { html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@ -48,6 +48,14 @@ export class HaTemplateSelector extends LitElement {
} }
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
} }
static get styles() {
return css`
p {
margin-top: 0;
}
`;
}
} }
declare global { declare global {

View File

@ -230,7 +230,9 @@ export class HaServiceControl extends LitElement {
@value-changed=${this._serviceChanged} @value-changed=${this._serviceChanged}
></ha-service-picker> ></ha-service-picker>
<div class="description"> <div class="description">
<p>${serviceData?.description}</p> ${serviceData?.description
? html`<p>${serviceData?.description}</p>`
: ""}
${this._manifest ${this._manifest
? html` <a ? html` <a
href=${this._manifest.is_built_in href=${this._manifest.is_built_in

16
src/data/action.ts Normal file
View File

@ -0,0 +1,16 @@
export const ACTION_TYPES = [
"condition",
"delay",
"event",
"play_media",
"activate_scene",
"service",
"wait_template",
"wait_for_trigger",
"repeat",
"choose",
"if",
"device_id",
"stop",
"parallel",
];

View File

@ -1,7 +1,7 @@
import { Condition, Trigger } from "./automation"; import { Condition, Trigger } from "./automation";
export const describeTrigger = (trigger: Trigger) => export const describeTrigger = (trigger: Trigger) =>
`${trigger.platform} trigger`; `${trigger.platform || "Unknown"} trigger`;
export const describeCondition = (condition: Condition) => { export const describeCondition = (condition: Condition) => {
if (condition.alias) { if (condition.alias) {

15
src/data/condition.ts Normal file
View File

@ -0,0 +1,15 @@
import type { Condition } from "./automation";
export const CONDITION_TYPES: Condition["condition"][] = [
"device",
"and",
"or",
"not",
"state",
"numeric_state",
"sun",
"template",
"time",
"trigger",
"zone",
];

View File

@ -123,7 +123,7 @@ export const describeAction = <T extends ActionType>(
? computeStateName(sceneStateObj) ? computeStateName(sceneStateObj)
: "scene" in config : "scene" in config
? config.scene ? config.scene
: config.target?.entity_id || config.entity_id : config.target?.entity_id || config.entity_id || ""
}`; }`;
} }

19
src/data/trigger.ts Normal file
View File

@ -0,0 +1,19 @@
import type { Trigger } from "./automation";
export const TRIGGER_TYPES: Trigger["platform"][] = [
"calendar",
"device",
"event",
"state",
"geo_location",
"homeassistant",
"mqtt",
"numeric_state",
"sun",
"tag",
"template",
"time",
"time_pattern",
"webhook",
"zone",
];

View File

@ -3,21 +3,19 @@ import "@material/mwc-list/mwc-list-item";
import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js"; import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import { classMap } from "lit/directives/class-map";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-select"; import "../../../../components/ha-expansion-panel";
import type { HaSelect } from "../../../../components/ha-select";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { Action, getActionType } from "../../../../data/script"; import { Action, getActionType } from "../../../../data/script";
import { describeAction } from "../../../../data/script_i18n";
import { callExecuteScript } from "../../../../data/service"; import { callExecuteScript } from "../../../../data/service";
import { import {
showAlertDialog, showAlertDialog,
@ -40,23 +38,7 @@ import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template"; import "./types/ha-automation-action-wait_template";
import { ACTION_TYPES } from "../../../../data/action";
const OPTIONS = [
"condition",
"delay",
"event",
"play_media",
"activate_scene",
"service",
"wait_template",
"wait_for_trigger",
"repeat",
"choose",
"if",
"device_id",
"stop",
"parallel",
];
const getType = (action: Action | undefined) => { const getType = (action: Action | undefined) => {
if (!action) { if (!action) {
@ -68,7 +50,7 @@ const getType = (action: Action | undefined) => {
if (["and", "or", "not"].some((key) => key in action)) { if (["and", "or", "not"].some((key) => key in action)) {
return "condition"; return "condition";
} }
return OPTIONS.find((option) => option in action); return ACTION_TYPES.find((option) => option in action);
}; };
declare global { declare global {
@ -104,6 +86,8 @@ export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => {
fireEvent(element, "value-changed", { value: newAction }); fireEvent(element, "value-changed", { value: newAction });
}; };
const preventDefault = (ev) => ev.preventDefault();
@customElement("ha-automation-action-row") @customElement("ha-automation-action-row")
export default class HaAutomationActionRow extends LitElement { export default class HaAutomationActionRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -124,19 +108,6 @@ export default class HaAutomationActionRow extends LitElement {
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
OPTIONS.map(
(action) =>
[
action,
localize(
`ui.panel.config.automation.editor.actions.type.${action}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
protected willUpdate(changedProperties: PropertyValues) { protected willUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) { if (!changedProperties.has("action")) {
return; return;
@ -172,10 +143,14 @@ export default class HaAutomationActionRow extends LitElement {
)} )}
</div>` </div>`
: ""} : ""}
<div class="card-menu"> <ha-expansion-panel
leftChevron
.header=${describeAction(this.hass, this.action)}
>
${this.index !== 0 ${this.index !== 0
? html` ? html`
<ha-icon-button <ha-icon-button
slot="icons"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up" "ui.panel.config.automation.editor.move_up"
)} )}
@ -187,6 +162,7 @@ export default class HaAutomationActionRow extends LitElement {
${this.index !== this.totalActions - 1 ${this.index !== this.totalActions - 1
? html` ? html`
<ha-icon-button <ha-icon-button
slot="icons"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down" "ui.panel.config.automation.editor.move_down"
)} )}
@ -195,7 +171,13 @@ export default class HaAutomationActionRow extends LitElement {
></ha-icon-button> ></ha-icon-button>
` `
: ""} : ""}
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> <ha-button-menu
slot="icons"
fixed
corner="BOTTOM_START"
@action=${this._handleAction}
@click=${preventDefault}
>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
@ -235,76 +217,65 @@ export default class HaAutomationActionRow extends LitElement {
)} )}
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
</div> <div
<div class=${classMap({
class="card-content ${this.action.enabled === false "card-content": true,
? "disabled" disabled: this.action.enabled === false,
: ""}" })}
> >
${this._warnings ${this._warnings
? html`<ha-alert ? html`<ha-alert
alert-type="warning" alert-type="warning"
.title=${this.hass.localize( .title=${this.hass.localize(
"ui.errors.config.editor_not_supported" "ui.errors.config.editor_not_supported"
)}
>
${this._warnings!.length > 0 && this._warnings![0] !== undefined
? html` <ul>
${this._warnings!.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</ha-alert>`
: ""}
${yamlMode
? html`
${type === undefined
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action",
"action",
type
)}
`
: ""}
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)} )}
</h2>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<ha-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type_select"
)}
.value=${getType(this.action)}
naturalMenuWidth
@selected=${this._typeChanged}
> >
${this._processedTypes(this.hass.localize).map( ${this._warnings!.length > 0 &&
([opt, label]) => html` this._warnings![0] !== undefined
<mwc-list-item .value=${opt}>${label}</mwc-list-item> ? html` <ul>
` ${this._warnings!.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize(
"ui.errors.config.edit_in_yaml_supported"
)} )}
</ha-select> </ha-alert>`
: ""}
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}> ${yamlMode
${dynamicElement(`ha-automation-action-${type}`, { ? html`
hass: this.hass, ${type === undefined
action: this.action, ? html`
narrow: this.narrow, ${this.hass.localize(
})} "ui.panel.config.automation.editor.actions.unsupported_action",
</div> "action",
`} type
</div> )}
`
: ""}
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</h2>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
narrow: this.narrow,
})}
</div>
`}
</div>
</ha-expansion-panel>
</ha-card> </ha-card>
`; `;
} }
@ -319,11 +290,13 @@ export default class HaAutomationActionRow extends LitElement {
} }
} }
private _moveUp() { private _moveUp(ev) {
ev.preventDefault();
fireEvent(this, "move-action", { direction: "up" }); fireEvent(this, "move-action", { direction: "up" });
} }
private _moveDown() { private _moveDown(ev) {
ev.preventDefault();
fireEvent(this, "move-action", { direction: "down" }); fireEvent(this, "move-action", { direction: "down" });
} }
@ -403,31 +376,6 @@ export default class HaAutomationActionRow extends LitElement {
}); });
} }
private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value;
if (!type) {
return;
}
this._uiModeAvailable = OPTIONS.includes(type);
if (!this._uiModeAvailable && !this._yamlMode) {
this._yamlMode = false;
}
if (type !== getType(this.action)) {
const elClass = customElements.get(
`ha-automation-action-${type}`
) as CustomElementConstructor & { defaultConfig: Action };
fireEvent(this, "value-changed", {
value: {
...elClass.defaultConfig,
},
});
}
}
private _onYamlChange(ev: CustomEvent) { private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
if (!ev.detail.isValid) { if (!ev.detail.isValid) {
@ -441,17 +389,30 @@ export default class HaAutomationActionRow extends LitElement {
this._yamlMode = !this._yamlMode; this._yamlMode = !this._yamlMode;
} }
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
css` css`
ha-button-menu,
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled { .disabled {
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
.card-content { .card-content {
padding-top: 16px; padding: 16px;
margin-top: 0;
} }
.disabled-bar { .disabled-bar {
background: var(--divider-color, #e0e0e0); background: var(--divider-color, #e0e0e0);
@ -459,14 +420,7 @@ export default class HaAutomationActionRow extends LitElement {
border-top-right-radius: var(--ha-card-border-radius); border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius); border-top-left-radius: var(--ha-card-border-radius);
} }
.card-menu {
float: var(--float-end, right);
z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
}
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
@ -476,9 +430,6 @@ export default class HaAutomationActionRow extends LitElement {
.warning ul { .warning ul {
margin: 4px 0; margin: 4px 0;
} }
ha-select {
margin-bottom: 24px;
}
`, `,
]; ];
} }

View File

@ -1,13 +1,36 @@
import { repeat } from "lit/directives/repeat";
import { mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import "@material/mwc-button"; import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit"; import type { ActionDetail } from "@material/mwc-list";
import memoizeOne from "memoize-one";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card"; import "../../../../components/ha-svg-icon";
import "../../../../components/ha-button-menu";
import { Action } from "../../../../data/script"; import { Action } from "../../../../data/script";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-action-row"; import "./ha-automation-action-row";
import { HaDeviceAction } from "./types/ha-automation-action-device_id"; import type HaAutomationActionRow from "./ha-automation-action-row";
import "./types/ha-automation-action-activate_scene";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import { ACTION_TYPES } from "../../../../data/action";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select";
@customElement("ha-automation-action") @customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement { export default class HaAutomationAction extends LitElement {
@ -17,9 +40,15 @@ export default class HaAutomationAction extends LitElement {
@property() public actions!: Action[]; @property() public actions!: Action[];
private _focusLastActionOnChange = false;
protected render() { protected render() {
return html` return html`
${this.actions.map( ${repeat(
this.actions,
// Use the action as key, so moving around keeps the same DOM,
// including expand state
(action) => action,
(action, idx) => html` (action, idx) => html`
<ha-automation-action-row <ha-automation-action-row
.index=${idx} .index=${idx}
@ -33,23 +62,51 @@ export default class HaAutomationAction extends LitElement {
></ha-automation-action-row> ></ha-automation-action-row>
` `
)} )}
<ha-card outlined> <ha-button-menu fixed @action=${this._addAction}>
<div class="card-actions add-card"> <mwc-button
<mwc-button @click=${this._addAction}> slot="trigger"
${this.hass.localize( outlined
"ui.panel.config.automation.editor.actions.add" .label=${this.hass.localize(
)} "ui.panel.config.automation.editor.actions.add"
</mwc-button> )}
</div> >
</ha-card> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</mwc-button>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</ha-button-menu>
`; `;
} }
private _addAction() { protected updated(changedProps: PropertyValues) {
const actions = this.actions.concat({ super.updated(changedProps);
...HaDeviceAction.defaultConfig,
});
if (changedProps.has("actions") && this._focusLastActionOnChange) {
this._focusLastActionOnChange = false;
const row = this.shadowRoot!.querySelector<HaAutomationActionRow>(
"ha-automation-action-row:last-of-type"
)!;
row.expand();
row.focus();
}
}
private _addAction(ev: CustomEvent<ActionDetail>) {
const action = (ev.currentTarget as HaSelect).items[ev.detail.index]
.value as typeof ACTION_TYPES[number];
const elClass = customElements.get(
`ha-automation-action-${action}`
) as CustomElementConstructor & { defaultConfig: Action };
const actions = this.actions.concat({
...elClass.defaultConfig,
});
this._focusLastActionOnChange = true;
fireEvent(this, "value-changed", { value: actions }); fireEvent(this, "value-changed", { value: actions });
} }
@ -88,16 +145,27 @@ export default class HaAutomationAction extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
ACTION_TYPES.map(
(action) =>
[
action,
localize(
`ui.panel.config.automation.editor.actions.type.${action}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-automation-action-row, ha-automation-action-row {
ha-card {
display: block; display: block;
margin-top: 16px; margin-bottom: 16px;
} }
.add-card mwc-button { ha-svg-icon {
display: block; height: 20px;
text-align: center;
} }
`; `;
} }

View File

@ -37,6 +37,9 @@ export class HaSceneAction extends LitElement implements ActionElement {
return html` return html`
<ha-entity-picker <ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.activate_scene.scene"
)}
.value=${scene} .value=${scene}
@value-changed=${this._entityPicked} @value-changed=${this._entityPicked}
.includeDomains=${includeDomains} .includeDomains=${includeDomains}

View File

@ -1,4 +1,4 @@
import { mdiDelete } from "@mdi/js"; import { mdiDelete, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
@ -69,15 +69,15 @@ export class HaChooseAction extends LitElement implements ActionElement {
</div> </div>
</ha-card>` </ha-card>`
)} )}
<ha-card outlined> <mwc-button
<div class="card-actions add-card"> outlined
<mwc-button @click=${this._addOption}> .label=${this.hass.localize(
${this.hass.localize( "ui.panel.config.automation.editor.actions.type.choose.add_option"
"ui.panel.config.automation.editor.actions.type.choose.add_option" )}
)} @click=${this._addOption}
</mwc-button> >
</div> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-card> </mwc-button>
<h2> <h2>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.default" "ui.panel.config.automation.editor.actions.type.choose.default"
@ -154,7 +154,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
haStyle, haStyle,
css` css`
ha-card { ha-card {
margin-top: 16px; margin: 16px 0;
} }
.add-card mwc-button { .add-card mwc-button {
display: block; display: block;
@ -168,6 +168,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
ha-form::part(root) { ha-form::part(root) {
overflow: visible; overflow: visible;
} }
ha-svg-icon {
height: 20px;
}
`, `,
]; ];
} }

View File

@ -1,10 +1,16 @@
import { html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { Condition } from "../../../../../data/automation"; import { stringCompare } from "../../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation";
import { CONDITION_TYPES } from "../../../../../data/condition";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor"; import "../../condition/ha-automation-condition-editor";
import { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
@customElement("ha-automation-action-condition") @customElement("ha-automation-action-condition")
export class HaConditionAction extends LitElement implements ActionElement { export class HaConditionAction extends LitElement implements ActionElement {
@ -18,6 +24,21 @@ export class HaConditionAction extends LitElement implements ActionElement {
protected render() { protected render() {
return html` return html`
<ha-select
fixedMenuPosition
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
.value=${this.action.condition}
naturalMenuWidth
@selected=${this._typeChanged}
>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</ha-select>
<ha-automation-condition-editor <ha-automation-condition-editor
.condition=${this.action} .condition=${this.action}
.hass=${this.hass} .hass=${this.hass}
@ -26,6 +47,19 @@ export class HaConditionAction extends LitElement implements ActionElement {
`; `;
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
CONDITION_TYPES.map(
(condition) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
private _conditionChanged(ev: CustomEvent) { private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
@ -33,6 +67,37 @@ export class HaConditionAction extends LitElement implements ActionElement {
value: ev.detail.value, value: ev.detail.value,
}); });
} }
private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value;
if (!type) {
return;
}
const elClass = customElements.get(
`ha-automation-condition-${type}`
) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">;
};
if (type !== this.action.condition) {
fireEvent(this, "value-changed", {
value: {
condition: type,
...elClass.defaultConfig,
},
});
}
}
static get styles() {
return css`
ha-select {
margin-bottom: 24px;
}
`;
}
} }
declare global { declare global {

View File

@ -52,42 +52,42 @@ export class HaRepeatAction extends LitElement implements ActionElement {
` `
)} )}
</ha-select> </ha-select>
${type === "count" <div>
? html` ${type === "count"
<ha-textfield ? html`
.label=${this.hass.localize( <ha-textfield
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label" .label=${this.hass.localize(
)} "ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
name="count" )}
.value=${(action as CountRepeat).count || "0"} name="count"
@change=${this._countChanged} .value=${(action as CountRepeat).count || "0"}
></ha-textfield> @change=${this._countChanged}
` ></ha-textfield>
: ""} `
${type === "while" : type === "while"
? html` <h3> ? html` <h3>
${this.hass.localize( ${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions` `ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions`
)}: )}:
</h3> </h3>
<ha-automation-condition <ha-automation-condition
.conditions=${(action as WhileRepeat).while || []} .conditions=${(action as WhileRepeat).while || []}
.hass=${this.hass} .hass=${this.hass}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
></ha-automation-condition>` ></ha-automation-condition>`
: ""} : type === "until"
${type === "until" ? html` <h3>
? html` <h3> ${this.hass.localize(
${this.hass.localize( `ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions`
`ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions` )}:
)}: </h3>
</h3> <ha-automation-condition
<ha-automation-condition .conditions=${(action as UntilRepeat).until || []}
.conditions=${(action as UntilRepeat).until || []} .hass=${this.hass}
.hass=${this.hass} @value-changed=${this._conditionChanged}
@value-changed=${this._conditionChanged} ></ha-automation-condition>`
></ha-automation-condition>` : ""}
: ""} </div>
<h3> <h3>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.sequence" "ui.panel.config.automation.editor.actions.type.repeat.sequence"

View File

@ -68,6 +68,10 @@ export class HaWaitForTriggerAction
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }
ha-automation-trigger {
display: block;
margin-top: 24px;
}
`; `;
} }
} }

View File

@ -1,12 +1,8 @@
import { css, html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-yaml-editor";
import type { Condition } from "../../../../data/automation"; import type { Condition } from "../../../../data/automation";
import { expandConditionWithShorthand } from "../../../../data/automation"; import { expandConditionWithShorthand } from "../../../../data/automation";
@ -24,20 +20,6 @@ import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger"; import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone"; import "./types/ha-automation-condition-zone";
const OPTIONS = [
"device",
"and",
"or",
"not",
"state",
"numeric_state",
"sun",
"template",
"time",
"trigger",
"zone",
] as const;
@customElement("ha-automation-condition-editor") @customElement("ha-automation-condition-editor")
export default class HaAutomationConditionEditor extends LitElement { export default class HaAutomationConditionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -50,27 +32,16 @@ export default class HaAutomationConditionEditor extends LitElement {
expandConditionWithShorthand(condition) expandConditionWithShorthand(condition)
); );
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
OPTIONS.map(
(condition) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
protected render() { protected render() {
const condition = this._processedCondition(this.condition); const condition = this._processedCondition(this.condition);
const selected = OPTIONS.indexOf(condition.condition); const supported =
const yamlMode = this.yamlMode || selected === -1; customElements.get(`ha-automation-condition-${condition.condition}`) !==
undefined;
const yamlMode = this.yamlMode || !supported;
return html` return html`
${yamlMode ${yamlMode
? html` ? html`
${selected === -1 ${!supported
? html` ? html`
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition", "ui.panel.config.automation.editor.conditions.unsupported_condition",
@ -91,21 +62,6 @@ export default class HaAutomationConditionEditor extends LitElement {
></ha-yaml-editor> ></ha-yaml-editor>
` `
: html` : html`
<ha-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
.value=${condition.condition}
naturalMenuWidth
@selected=${this._typeChanged}
>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</ha-select>
<div> <div>
${dynamicElement( ${dynamicElement(
`ha-automation-condition-${condition.condition}`, `ha-automation-condition-${condition.condition}`,
@ -116,29 +72,6 @@ export default class HaAutomationConditionEditor extends LitElement {
`; `;
} }
private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value;
if (!type) {
return;
}
const elClass = customElements.get(
`ha-automation-condition-${type}`
) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">;
};
if (type !== this._processedCondition(this.condition).condition) {
fireEvent(this, "value-changed", {
value: {
condition: type,
...elClass.defaultConfig,
},
});
}
}
private _onYamlChange(ev: CustomEvent) { private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
if (!ev.detail.isValid) { if (!ev.detail.isValid) {
@ -148,14 +81,7 @@ export default class HaAutomationConditionEditor extends LitElement {
fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true }); fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true });
} }
static styles = [ static styles = haStyle;
haStyle,
css`
ha-select {
margin-bottom: 24px;
}
`,
];
} }
declare global { declare global {

View File

@ -3,6 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
@ -10,6 +11,7 @@ import "../../../../components/ha-card";
import "../../../../components/buttons/ha-progress-button"; import "../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-expansion-panel";
import { Condition, testCondition } from "../../../../data/automation"; import { Condition, testCondition } from "../../../../data/automation";
import { import {
showAlertDialog, showAlertDialog,
@ -20,11 +22,14 @@ import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor"; import "./ha-automation-condition-editor";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { describeCondition } from "../../../../data/automation_i18n";
export interface ConditionElement extends LitElement { export interface ConditionElement extends LitElement {
condition: Condition; condition: Condition;
} }
const preventDefault = (ev) => ev.preventDefault();
export const handleChangeEvent = ( export const handleChangeEvent = (
element: ConditionElement, element: ConditionElement,
ev: CustomEvent ev: CustomEvent
@ -75,13 +80,23 @@ export default class HaAutomationConditionRow extends LitElement {
)} )}
</div>` </div>`
: ""} : ""}
<div class="card-menu">
<ha-progress-button @click=${this._testCondition}> <ha-expansion-panel
leftChevron
.header=${describeCondition(this.condition)}
>
<ha-progress-button slot="icons" @click=${this._testCondition}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test" "ui.panel.config.automation.editor.conditions.test"
)} )}
</ha-progress-button> </ha-progress-button>
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> <ha-button-menu
slot="icons"
fixed
corner="BOTTOM_START"
@action=${this._handleAction}
@click=${preventDefault}
>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
@ -117,37 +132,42 @@ export default class HaAutomationConditionRow extends LitElement {
)} )}
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
</div>
<div <div
class="card-content ${this.condition.enabled === false class=${classMap({
? "disabled" "card-content": true,
: ""}" disabled: this.condition.enabled === false,
> })}
${this._warnings >
? html`<ha-alert ${this._warnings
alert-type="warning" ? html`<ha-alert
.title=${this.hass.localize( alert-type="warning"
"ui.errors.config.editor_not_supported" .title=${this.hass.localize(
)} "ui.errors.config.editor_not_supported"
> )}
${this._warnings!.length > 0 && this._warnings![0] !== undefined >
? html` <ul> ${this._warnings!.length > 0 &&
${this._warnings!.map( this._warnings![0] !== undefined
(warning) => html`<li>${warning}</li>` ? html` <ul>
)} ${this._warnings!.map(
</ul>` (warning) => html`<li>${warning}</li>`
: ""} )}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} </ul>`
</ha-alert>` : ""}
: ""} ${this.hass.localize(
<ha-automation-condition-editor "ui.errors.config.edit_in_yaml_supported"
@ui-mode-not-available=${this._handleUiModeNotAvailable} )}
@value-changed=${this._handleChangeEvent} </ha-alert>`
.yamlMode=${this._yamlMode} : ""}
.hass=${this.hass} <ha-automation-condition-editor
.condition=${this.condition} @ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor> @value-changed=${this._handleChangeEvent}
</div> .yamlMode=${this._yamlMode}
.hass=${this.hass}
.condition=${this.condition}
></ha-automation-condition-editor>
</div>
</ha-expansion-panel>
</ha-card> </ha-card>
`; `;
} }
@ -212,6 +232,7 @@ export default class HaAutomationConditionRow extends LitElement {
} }
private async _testCondition(ev) { private async _testCondition(ev) {
ev.preventDefault();
const condition = this.condition; const condition = this.condition;
const button = ev.target as HaProgressButton; const button = ev.target as HaProgressButton;
if (button.progress) { if (button.progress) {
@ -269,17 +290,30 @@ export default class HaAutomationConditionRow extends LitElement {
} }
} }
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
css` css`
ha-button-menu,
ha-progress-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled { .disabled {
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
.card-content { .card-content {
padding-top: 16px; padding: 16px;
margin-top: 0;
} }
.disabled-bar { .disabled-bar {
background: var(--divider-color, #e0e0e0); background: var(--divider-color, #e0e0e0);
@ -287,14 +321,6 @@ export default class HaAutomationConditionRow extends LitElement {
border-top-right-radius: var(--ha-card-border-radius); border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius); border-top-left-radius: var(--ha-card-border-radius);
} }
.card-menu {
float: var(--float-end, right);
z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
}
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }

View File

@ -1,13 +1,34 @@
import { mdiPlus } from "@mdi/js";
import { repeat } from "lit/directives/repeat";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import "@material/mwc-button"; import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { ActionDetail } from "@material/mwc-list";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card"; import "../../../../components/ha-svg-icon";
import { Condition } from "../../../../data/automation"; import "../../../../components/ha-button-menu";
import { HomeAssistant } from "../../../../types"; import type { Condition } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-row"; import "./ha-automation-condition-row";
import { HaDeviceCondition } from "./types/ha-automation-condition-device"; import type HaAutomationConditionRow from "./ha-automation-condition-row";
// Uncommenting these and this element doesn't load
// import "./types/ha-automation-condition-not";
// import "./types/ha-automation-condition-or";
import "./types/ha-automation-condition-and";
import "./types/ha-automation-condition-device";
import "./types/ha-automation-condition-numeric_state";
import "./types/ha-automation-condition-state";
import "./types/ha-automation-condition-sun";
import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
import { CONDITION_TYPES } from "../../../../data/condition";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select";
@customElement("ha-automation-condition") @customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement { export default class HaAutomationCondition extends LitElement {
@ -15,10 +36,13 @@ export default class HaAutomationCondition extends LitElement {
@property() public conditions!: Condition[]; @property() public conditions!: Condition[];
private _focusLastConditionOnChange = false;
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("conditions")) { if (!changedProperties.has("conditions")) {
return; return;
} }
let updatedConditions: Condition[] | undefined; let updatedConditions: Condition[] | undefined;
if (!Array.isArray(this.conditions)) { if (!Array.isArray(this.conditions)) {
updatedConditions = [this.conditions]; updatedConditions = [this.conditions];
@ -38,6 +62,13 @@ export default class HaAutomationCondition extends LitElement {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: updatedConditions, value: updatedConditions,
}); });
} else if (this._focusLastConditionOnChange) {
this._focusLastConditionOnChange = false;
const row = this.shadowRoot!.querySelector<HaAutomationConditionRow>(
"ha-automation-condition-row:last-of-type"
)!;
row.expand();
row.focus();
} }
} }
@ -46,7 +77,11 @@ export default class HaAutomationCondition extends LitElement {
return html``; return html``;
} }
return html` return html`
${this.conditions.map( ${repeat(
this.conditions,
// Use the condition as key, so moving around keeps the same DOM,
// including expand state
(condition) => condition,
(cond, idx) => html` (cond, idx) => html`
<ha-automation-condition-row <ha-automation-condition-row
.index=${idx} .index=${idx}
@ -57,24 +92,40 @@ export default class HaAutomationCondition extends LitElement {
></ha-automation-condition-row> ></ha-automation-condition-row>
` `
)} )}
<ha-card outlined> <ha-button-menu fixed @action=${this._addCondition}>
<div class="card-actions add-card"> <mwc-button
<mwc-button @click=${this._addCondition}> slot="trigger"
${this.hass.localize( outlined
"ui.panel.config.automation.editor.conditions.add" .label=${this.hass.localize(
)} "ui.panel.config.automation.editor.conditions.add"
</mwc-button> )}
</div> >
</ha-card> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</mwc-button>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</ha-button-menu>
`; `;
} }
private _addCondition() { private _addCondition(ev: CustomEvent<ActionDetail>) {
const conditions = this.conditions.concat({ const condition = (ev.currentTarget as HaSelect).items[ev.detail.index]
condition: "device", .value as Condition["condition"];
...HaDeviceCondition.defaultConfig,
});
const elClass = customElements.get(
`ha-automation-condition-${condition}`
) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">;
};
const conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig,
});
this._focusLastConditionOnChange = true;
fireEvent(this, "value-changed", { value: conditions }); fireEvent(this, "value-changed", { value: conditions });
} }
@ -101,16 +152,27 @@ export default class HaAutomationCondition extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
CONDITION_TYPES.map(
(condition) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-automation-condition-row, ha-automation-condition-row {
ha-card {
display: block; display: block;
margin-top: 16px; margin-bottom: 16px;
} }
.add-card mwc-button { ha-svg-icon {
display: block; height: 20px;
text-align: center;
} }
`; `;
} }

View File

@ -1,14 +1,10 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import type { import type { LogicalCondition } from "../../../../../data/automation";
Condition,
LogicalCondition,
} from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-condition"; import "../ha-automation-condition";
import type { ConditionElement } from "../ha-automation-condition-row"; import type { ConditionElement } from "../ha-automation-condition-row";
import { HaStateCondition } from "./ha-automation-condition-state";
@customElement("ha-automation-condition-logical") @customElement("ha-automation-condition-logical")
export class HaLogicalCondition extends LitElement implements ConditionElement { export class HaLogicalCondition extends LitElement implements ConditionElement {
@ -18,12 +14,7 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
public static get defaultConfig() { public static get defaultConfig() {
return { return {
conditions: [ conditions: [],
{
condition: "state",
...HaStateCondition.defaultConfig,
},
] as Condition[],
}; };
} }

View File

@ -1,4 +1,4 @@
import { html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../../components/ha-textarea"; import "../../../../../components/ha-textarea";
import type { TemplateCondition } from "../../../../../data/automation"; import type { TemplateCondition } from "../../../../../data/automation";
@ -39,6 +39,14 @@ export class HaTemplateCondition extends LitElement {
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev); handleChangeEvent(this, ev);
} }
static get styles() {
return css`
p {
margin-top: 0;
}
`;
}
} }
declare global { declare global {

View File

@ -73,7 +73,7 @@ export class HaZoneCondition extends LitElement {
} }
static styles = css` static styles = css`
ha-entity-picker { ha-entity-picker:first-child {
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }

View File

@ -50,10 +50,8 @@ import { HomeAssistant, Route } from "../../../types";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { HaDeviceAction } from "./action/types/ha-automation-action-device_id";
import "./blueprint-automation-editor"; import "./blueprint-automation-editor";
import "./manual-automation-editor"; import "./manual-automation-editor";
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -329,9 +327,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
baseConfig = { baseConfig = {
...baseConfig, ...baseConfig,
mode: "single", mode: "single",
trigger: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }], trigger: [],
condition: [], condition: [],
action: [{ ...HaDeviceAction.defaultConfig }], action: [],
}; };
} }
this._config = { this._config = {
@ -570,6 +568,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
flex-direction: column; flex-direction: column;
padding-bottom: 0; padding-bottom: 0;
} }
manual-automation-editor {
margin: 0 auto;
max-width: 1040px;
padding: 28px 20px 0;
}
ha-yaml-editor { ha-yaml-editor {
flex-grow: 1; flex-grow: 1;
--code-mirror-height: 100%; --code-mirror-height: 100%;

View File

@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -7,6 +8,7 @@ import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-textarea"; import "../../../components/ha-textarea";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import "../../../components/ha-icon-button";
import { import {
AUTOMATION_DEFAULT_MODE, AUTOMATION_DEFAULT_MODE,
Condition, Condition,
@ -18,7 +20,6 @@ import { Action, isMaxMode, MODES } from "../../../data/script";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section";
import "./action/ha-automation-action"; import "./action/ha-automation-action";
import "./condition/ha-automation-condition"; import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger"; import "./trigger/ha-automation-trigger";
@ -38,218 +39,198 @@ export class HaManualAutomationEditor extends LitElement {
@state() private _showDescription = false; @state() private _showDescription = false;
protected render() { protected render() {
return html`<ha-config-section vertical .isWide=${this.isWide}> return html`
${!this.narrow <ha-card outlined>
? html`<span slot="header">${this.config.alias}</span>` <div class="card-content">
: ""} <ha-textfield
<span slot="introduction"> .label=${this.hass.localize(
${this.hass.localize( "ui.panel.config.automation.editor.alias"
"ui.panel.config.automation.editor.introduction" )}
)} name="alias"
</span> .value=${this.config.alias || ""}
<ha-card outlined> @change=${this._valueChanged}
<div class="card-content"> >
<ha-textfield </ha-textfield>
.label=${this.hass.localize( ${this._showDescription
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this.config.alias || ""}
@change=${this._valueChanged}
>
</ha-textfield>
${this._showDescription
? html`
<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"
autogrow
.value=${this.config.description || ""}
@change=${this._valueChanged}
></ha-textarea>
`
: html`
<div class="link-button-row">
<button class="link" @click=${this._addDescription}>
${this.hass.localize(
"ui.panel.config.automation.editor.description.add"
)}
</button>
</div>
`}
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.modes.description",
"documentation_link",
html`<a
href=${documentationUrl(this.hass, "/docs/automation/modes/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.automation.editor.modes.documentation"
)}</a
>`
)}
</p>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
.value=${this.config.mode || AUTOMATION_DEFAULT_MODE}
@selected=${this._modeChanged}
fixedMenuPosition
>
${MODES.map(
(mode) => html`
<mwc-list-item .value=${mode}>
${this.hass.localize(
`ui.panel.config.automation.editor.modes.${mode}`
) || mode}
</mwc-list-item>
`
)}
</ha-select>
${this.config.mode && isMaxMode(this.config.mode)
? html`
<br /><ha-textfield
.label=${this.hass.localize(
`ui.panel.config.automation.editor.max.${this.config.mode}`
)}
type="number"
name="max"
.value=${this.config.max || "10"}
@change=${this._valueChanged}
class="max"
>
</ha-textfield>
`
: html``}
</div>
${this.stateObj
? html` ? html`
<div class="card-actions layout horizontal justified center"> <ha-textarea
<div class="layout horizontal center"> .label=${this.hass.localize(
<ha-entity-toggle "ui.panel.config.automation.editor.description.label"
.hass=${this.hass} )}
.stateObj=${this.stateObj!} .placeholder=${this.hass.localize(
></ha-entity-toggle> "ui.panel.config.automation.editor.description.placeholder"
${this.hass.localize( )}
"ui.panel.config.automation.editor.enable_disable" name="description"
)} autogrow
</div> .value=${this.config.description || ""}
<div> @change=${this._valueChanged}
<a href="/config/automation/trace/${this.config.id}"> ></ha-textarea>
<mwc-button>
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</mwc-button>
</a>
<mwc-button
@click=${this._runActions}
.stateObj=${this.stateObj}
>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
</div>
` `
: ""} : html`
</ha-card> <div class="link-button-row">
</ha-config-section> <button class="link" @click=${this._addDescription}>
${this.hass.localize(
"ui.panel.config.automation.editor.description.add"
)}
</button>
</div>
`}
<ha-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
.value=${this.config.mode || AUTOMATION_DEFAULT_MODE}
@selected=${this._modeChanged}
fixedMenuPosition
.helper=${html`
<a
style="color: var(--secondary-text-color)"
href=${documentationUrl(this.hass, "/docs/automation/modes/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.automation.editor.modes.learn_more"
)}</a
>
`}
>
${MODES.map(
(mode) => html`
<mwc-list-item .value=${mode}>
${this.hass.localize(
`ui.panel.config.automation.editor.modes.${mode}`
) || mode}
</mwc-list-item>
`
)}
</ha-select>
${this.config.mode && isMaxMode(this.config.mode)
? html`
<br /><ha-textfield
.label=${this.hass.localize(
`ui.panel.config.automation.editor.max.${this.config.mode}`
)}
type="number"
name="max"
.value=${this.config.max || "10"}
@change=${this._valueChanged}
class="max"
>
</ha-textfield>
`
: html``}
</div>
${this.stateObj
? html`
<div class="card-actions layout horizontal justified center">
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this.stateObj!}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<div>
<a href="/config/automation/trace/${this.config.id}">
<mwc-button>
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</mwc-button>
</a>
<mwc-button
@click=${this._runActions}
.stateObj=${this.stateObj}
>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
</div>
`
: ""}
</ha-card>
<ha-config-section vertical .isWide=${this.isWide}> <div class="header">
<span slot="header"> <div class="name">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header" "ui.panel.config.automation.editor.triggers.header"
)} )}
</span> </div>
<span slot="introduction"> <a
<p> href=${documentationUrl(this.hass, "/docs/automation/trigger/")}
${this.hass.localize( target="_blank"
"ui.panel.config.automation.editor.triggers.introduction" rel="noreferrer"
)} >
</p> <ha-icon-button
<a .path=${mdiHelpCircle}
href=${documentationUrl(this.hass, "/docs/automation/trigger/")} .label=${this.hass.localize(
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more" "ui.panel.config.automation.editor.triggers.learn_more"
)} )}
</a> ></ha-icon-button>
</span> </a>
<ha-automation-trigger </div>
.triggers=${this.config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section vertical .isWide=${this.isWide}> <ha-automation-trigger
<span slot="header"> .triggers=${this.config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
<div class="header">
<div class="name">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header" "ui.panel.config.automation.editor.conditions.header"
)} )}
</span> </div>
<span slot="introduction"> <a
<p> href=${documentationUrl(this.hass, "/docs/automation/condition/")}
${this.hass.localize( target="_blank"
"ui.panel.config.automation.editor.conditions.introduction" rel="noreferrer"
)} >
</p> <ha-icon-button
<a .path=${mdiHelpCircle}
href=${documentationUrl(this.hass, "/docs/scripts/conditions/")} .label=${this.hass.localize(
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more" "ui.panel.config.automation.editor.conditions.learn_more"
)} )}
</a> ></ha-icon-button>
</span> </a>
<ha-automation-condition </div>
.conditions=${this.config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section vertical .isWide=${this.isWide}> <ha-automation-condition
<span slot="header"> .conditions=${this.config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
<div class="header">
<div class="name">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.header" "ui.panel.config.automation.editor.actions.header"
)} )}
</span> </div>
<span slot="introduction"> <a
<p> href=${documentationUrl(this.hass, "/docs/automation/action/")}
${this.hass.localize( target="_blank"
"ui.panel.config.automation.editor.actions.introduction" rel="noreferrer"
)} >
</p> <ha-icon-button
<a .path=${mdiHelpCircle}
href=${documentationUrl(this.hass, "/docs/automation/action/")} .label=${this.hass.localize(
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more" "ui.panel.config.automation.editor.actions.learn_more"
)} )}
</a> ></ha-icon-button>
</span> </a>
<ha-automation-action </div>
.actions=${this.config.action}
@value-changed=${this._actionChanged} <ha-automation-action
.hass=${this.hass} .actions=${this.config.action}
.narrow=${this.narrow} @value-changed=${this._actionChanged}
></ha-automation-action> .hass=${this.hass}
</ha-config-section>`; .narrow=${this.narrow}
></ha-automation-action>
`;
} }
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
@ -341,6 +322,9 @@ export class HaManualAutomationEditor extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
:host {
display: block;
}
ha-card { ha-card {
overflow: hidden; overflow: hidden;
} }
@ -351,9 +335,7 @@ export class HaManualAutomationEditor extends LitElement {
ha-textfield { ha-textfield {
display: block; display: block;
} }
span[slot="introduction"] a {
color: var(--primary-color);
}
p { p {
margin-bottom: 0; margin-bottom: 0;
} }
@ -365,6 +347,19 @@ export class HaManualAutomationEditor extends LitElement {
margin-top: 16px; margin-top: 16px;
width: 200px; width: 200px;
} }
.header {
display: flex;
margin: 16px 0;
align-items: center;
}
.header .name {
font-size: 20px;
font-weight: 400;
flex: 1;
}
.header a {
color: var(--secondary-text-color);
}
`, `,
]; ];
} }

View File

@ -5,20 +5,16 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { debounce } from "../../../../common/util/debounce"; import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
import { subscribeTrigger, Trigger } from "../../../../data/automation"; import { subscribeTrigger, Trigger } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
@ -43,24 +39,7 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone"; import "./types/ha-automation-trigger-zone";
import { describeTrigger } from "../../../../data/automation_i18n";
const OPTIONS = [
"calendar",
"device",
"event",
"state",
"geo_location",
"homeassistant",
"mqtt",
"numeric_state",
"sun",
"tag",
"template",
"time",
"time_pattern",
"webhook",
"zone",
] as const;
export interface TriggerElement extends LitElement { export interface TriggerElement extends LitElement {
trigger: Trigger; trigger: Trigger;
@ -88,6 +67,8 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
fireEvent(element, "value-changed", { value: newTrigger }); fireEvent(element, "value-changed", { value: newTrigger });
}; };
const preventDefault = (ev) => ev.preventDefault();
@customElement("ha-automation-trigger-row") @customElement("ha-automation-trigger-row")
export default class HaAutomationTriggerRow extends LitElement { export default class HaAutomationTriggerRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -108,35 +89,36 @@ export default class HaAutomationTriggerRow extends LitElement {
private _triggerUnsub?: Promise<UnsubscribeFunc>; private _triggerUnsub?: Promise<UnsubscribeFunc>;
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
OPTIONS.map(
(action) =>
[
action,
localize(
`ui.panel.config.automation.editor.triggers.type.${action}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
protected render() { protected render() {
const selected = OPTIONS.indexOf(this.trigger.platform); const supported =
const yamlMode = this._yamlMode || selected === -1; customElements.get(`ha-automation-trigger-${this.trigger.platform}`) !==
undefined;
const yamlMode = this._yamlMode || !supported;
const showId = "id" in this.trigger || this._requestShowId; const showId = "id" in this.trigger || this._requestShowId;
return html` return html`
<ha-card outlined> <ha-card outlined>
${this.trigger.enabled === false ${this.trigger.enabled === false
? html`<div class="disabled-bar"> ? html`
${this.hass.localize( <div class="disabled-bar">
"ui.panel.config.automation.editor.actions.disabled" ${this.hass.localize(
)} "ui.panel.config.automation.editor.actions.disabled"
</div>` )}
</div>
`
: ""} : ""}
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> <ha-expansion-panel
leftChevron
.header=${describeTrigger(this.trigger)}
>
<ha-button-menu
slot="icons"
fixed
corner="BOTTOM_START"
@action=${this._handleAction}
@click=${preventDefault}
>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
@ -147,7 +129,7 @@ export default class HaAutomationTriggerRow extends LitElement {
"ui.panel.config.automation.editor.triggers.edit_id" "ui.panel.config.automation.editor.triggers.edit_id"
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item .disabled=${selected === -1}> <mwc-list-item .disabled=${!supported}>
${yamlMode ${yamlMode
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui" "ui.panel.config.automation.editor.edit_ui"
@ -176,86 +158,77 @@ export default class HaAutomationTriggerRow extends LitElement {
)} )}
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
</div>
<div <div
class="card-content ${this.trigger.enabled === false class=${classMap({
? "disabled" "card-content": true,
: ""}" disabled: this.trigger.enabled === false,
> })}
${this._warnings >
? html`<ha-alert ${this._warnings
alert-type="warning" ? html`<ha-alert
.title=${this.hass.localize( alert-type="warning"
"ui.errors.config.editor_not_supported" .title=${this.hass.localize(
)} "ui.errors.config.editor_not_supported"
>
${this._warnings.length && this._warnings[0] !== undefined
? html` <ul>
${this._warnings.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</ha-alert>`
: ""}
${yamlMode
? html`
${selected === -1
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
"platform",
this.trigger.platform
)}
`
: ""}
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)} )}
</h2>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<ha-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type_select"
)}
.value=${this.trigger.platform}
naturalMenuWidth
@selected=${this._typeChanged}
> >
${this._processedTypes(this.hass.localize).map( ${this._warnings.length && this._warnings[0] !== undefined
([opt, label]) => html` ? html` <ul>
<mwc-list-item .value=${opt}>${label}</mwc-list-item> ${this._warnings.map(
` (warning) => html`<li>${warning}</li>`
)}
</ha-select>
${showId
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)} )}
.value=${this.trigger.id || ""} </ul>`
@change=${this._idChanged} : ""}
> ${this.hass.localize(
</ha-textfield> "ui.errors.config.edit_in_yaml_supported"
`
: ""}
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${dynamicElement(
`ha-automation-trigger-${this.trigger.platform}`,
{ hass: this.hass, trigger: this.trigger }
)} )}
</div> </ha-alert>`
`} : ""}
</div> ${yamlMode
? html`
${!supported
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
"platform",
this.trigger.platform
)}
`
: ""}
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</h2>
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${showId
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id || ""}
@change=${this._idChanged}
>
</ha-textfield>
`
: ""}
<div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${dynamicElement(
`ha-automation-trigger-${this.trigger.platform}`,
{ hass: this.hass, trigger: this.trigger }
)}
</div>
`}
</div>
</ha-expansion-panel>
<div <div
class="triggered ${classMap({ class="triggered ${classMap({
active: this._triggered !== undefined, active: this._triggered !== undefined,
@ -360,6 +333,7 @@ export default class HaAutomationTriggerRow extends LitElement {
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:
this._requestShowId = true; this._requestShowId = true;
this.expand();
break; break;
case 1: case 1:
this._switchYamlMode(); this._switchYamlMode();
@ -398,33 +372,6 @@ export default class HaAutomationTriggerRow extends LitElement {
} }
} }
private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value;
if (!type) {
return;
}
const elClass = customElements.get(
`ha-automation-trigger-${type}`
) as CustomElementConstructor & {
defaultConfig: Omit<Trigger, "platform">;
};
if (type !== this.trigger.platform) {
const value = {
platform: type,
...elClass.defaultConfig,
};
if (this.trigger.id) {
value.id = this.trigger.id;
}
fireEvent(this, "value-changed", {
value,
});
}
}
private _idChanged(ev: CustomEvent) { private _idChanged(ev: CustomEvent) {
const newId = (ev.target as any).value; const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) { if (newId === (this.trigger.id ?? "")) {
@ -468,17 +415,29 @@ export default class HaAutomationTriggerRow extends LitElement {
}); });
} }
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
css` css`
ha-button-menu {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled { .disabled {
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
.card-content { .card-content {
padding-top: 16px; padding: 16px;
margin-top: 0;
} }
.disabled-bar { .disabled-bar {
background: var(--divider-color, #e0e0e0); background: var(--divider-color, #e0e0e0);
@ -486,14 +445,6 @@ export default class HaAutomationTriggerRow extends LitElement {
border-top-right-radius: var(--ha-card-border-radius); border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius); border-top-left-radius: var(--ha-card-border-radius);
} }
.card-menu {
float: var(--float-end, right);
z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
}
.triggered { .triggered {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
@ -525,9 +476,6 @@ export default class HaAutomationTriggerRow extends LitElement {
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
ha-select {
margin-bottom: 24px;
}
ha-textfield { ha-textfield {
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;

View File

@ -1,13 +1,37 @@
import { repeat } from "lit/directives/repeat";
import { mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import memoizeOne from "memoize-one";
import "@material/mwc-button"; import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { ActionDetail } from "@material/mwc-list";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card"; import "../../../../components/ha-svg-icon";
import "../../../../components/ha-button-menu";
import { Trigger } from "../../../../data/automation"; import { Trigger } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row"; import "./ha-automation-trigger-row";
import { HaDeviceTrigger } from "./types/ha-automation-trigger-device"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { stringCompare } from "../../../../common/string/compare";
import type { HaSelect } from "../../../../components/ha-select";
import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag";
import "./types/ha-automation-trigger-template";
import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
@customElement("ha-automation-trigger") @customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement { export default class HaAutomationTrigger extends LitElement {
@ -15,9 +39,15 @@ export default class HaAutomationTrigger extends LitElement {
@property() public triggers!: Trigger[]; @property() public triggers!: Trigger[];
private _focusLastTriggerOnChange = false;
protected render() { protected render() {
return html` return html`
${this.triggers.map( ${repeat(
this.triggers,
// Use the trigger as key, so moving around keeps the same DOM,
// including expand state
(trigger) => trigger,
(trg, idx) => html` (trg, idx) => html`
<ha-automation-trigger-row <ha-automation-trigger-row
.index=${idx} .index=${idx}
@ -28,24 +58,54 @@ export default class HaAutomationTrigger extends LitElement {
></ha-automation-trigger-row> ></ha-automation-trigger-row>
` `
)} )}
<ha-card outlined> <ha-button-menu @action=${this._addTrigger}>
<div class="card-actions add-card"> <mwc-button
<mwc-button @click=${this._addTrigger}> slot="trigger"
${this.hass.localize( outlined
"ui.panel.config.automation.editor.triggers.add" .label=${this.hass.localize(
)} "ui.panel.config.automation.editor.triggers.add"
</mwc-button> )}
</div> >
</ha-card> <ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</mwc-button>
${this._processedTypes(this.hass.localize).map(
([opt, label]) => html`
<mwc-list-item .value=${opt}>${label}</mwc-list-item>
`
)}
</ha-button-menu>
`; `;
} }
private _addTrigger() { protected updated(changedProps: PropertyValues) {
const triggers = this.triggers.concat({ super.updated(changedProps);
platform: "device",
...HaDeviceTrigger.defaultConfig,
});
if (changedProps.has("triggers") && this._focusLastTriggerOnChange) {
this._focusLastTriggerOnChange = false;
const row = this.shadowRoot!.querySelector<HaAutomationTriggerRow>(
"ha-automation-trigger-row:last-of-type"
)!;
row.expand();
row.focus();
}
}
private _addTrigger(ev: CustomEvent<ActionDetail>) {
const platform = (ev.currentTarget as HaSelect).items[ev.detail.index]
.value as Trigger["platform"];
const elClass = customElements.get(
`ha-automation-trigger-${platform}`
) as CustomElementConstructor & {
defaultConfig: Omit<Trigger, "platform">;
};
const triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig,
});
this._focusLastTriggerOnChange = true;
fireEvent(this, "value-changed", { value: triggers }); fireEvent(this, "value-changed", { value: triggers });
} }
@ -72,16 +132,27 @@ export default class HaAutomationTrigger extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
TRIGGER_TYPES.map(
(action) =>
[
action,
localize(
`ui.panel.config.automation.editor.triggers.type.${action}.label`
),
] as [string, string]
).sort((a, b) => stringCompare(a[1], b[1]))
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-automation-trigger-row, ha-automation-trigger-row {
ha-card {
display: block; display: block;
margin-top: 16px; margin-bottom: 16px;
} }
.add-card mwc-button { ha-svg-icon {
display: block; height: 20px;
text-align: center;
} }
`; `;
} }

View File

@ -1,5 +1,5 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { html, LitElement, PropertyValues } from "lit"; import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
@ -63,6 +63,14 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
}, },
}); });
} }
static get styles() {
return css`
ha-select {
display: block;
}
`;
}
} }
declare global { declare global {

View File

@ -1,5 +1,5 @@
import "../../../../../components/ha-textarea"; import "../../../../../components/ha-textarea";
import { html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { TemplateTrigger } from "../../../../../data/automation"; import type { TemplateTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
@ -39,6 +39,14 @@ export class HaTemplateTrigger extends LitElement {
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev); handleChangeEvent(this, ev);
} }
static get styles() {
return css`
p {
margin-top: 0;
}
`;
}
} }
declare global { declare global {

View File

@ -6,6 +6,7 @@ import {
mdiContentSave, mdiContentSave,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiHelpCircle,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
@ -56,7 +57,6 @@ import type { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id";
import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "./blueprint-script-editor"; import "./blueprint-script-editor";
@ -276,59 +276,47 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
> >
${this._config ${this._config
? html` ? html`
<ha-config-section vertical .isWide=${this.isWide}> <ha-card outlined>
${!this.narrow <div class="card-content">
<ha-form
.schema=${schema}
.data=${data}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
${this.scriptEntityId
? html` ? html`
<span slot="header">${this._config.alias}</span> <div
` class="card-actions layout horizontal justified center"
: ""} >
<span slot="introduction"> <a
${this.hass.localize( href="/config/script/trace/${this
"ui.panel.config.script.editor.introduction" .scriptEntityId}"
)}
</span>
<ha-card outlined>
<div class="card-content">
<ha-form
.schema=${schema}
.data=${data}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
${this.scriptEntityId
? html`
<div
class="card-actions layout horizontal justified center"
> >
<a <mwc-button>
href="/config/script/trace/${this
.scriptEntityId}"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.script.editor.show_trace"
)}
</mwc-button>
</a>
<mwc-button
@click=${this._runScript}
title=${this.hass.localize(
"ui.panel.config.script.picker.run_script"
)}
?disabled=${this._dirty}
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.script.picker.run_script" "ui.panel.config.script.editor.show_trace"
)} )}
</mwc-button> </mwc-button>
</div> </a>
` <mwc-button
: ``} @click=${this._runScript}
</ha-card> title=${this.hass.localize(
</ha-config-section> "ui.panel.config.script.picker.run_script"
)}
?disabled=${this._dirty}
>
${this.hass.localize(
"ui.panel.config.script.picker.run_script"
)}
</mwc-button>
</div>
`
: ``}
</ha-card>
${"use_blueprint" in this._config ${"use_blueprint" in this._config
? html` ? html`
@ -341,40 +329,34 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
></blueprint-script-editor> ></blueprint-script-editor>
` `
: html` : html`
<ha-config-section <div class="header">
vertical <div class="name">
.isWide=${this.isWide}
>
<span slot="header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.script.editor.sequence" "ui.panel.config.script.editor.sequence"
)} )}
</span> </div>
<span slot="introduction"> <a
<p> href=${documentationUrl(
${this.hass.localize( this.hass,
"ui.panel.config.script.editor.sequence_sentence" "/docs/scripts/"
)} )}
</p> target="_blank"
<a rel="noreferrer"
href=${documentationUrl( >
this.hass, <ha-icon-button
"/docs/scripts/" .path=${mdiHelpCircle}
)} .label=${this.hass.localize(
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions" "ui.panel.config.script.editor.link_available_actions"
)} )}
</a> ></ha-icon-button>
</span> </a>
<ha-automation-action </div>
.actions=${this._config.sequence}
@value-changed=${this._sequenceChanged} <ha-automation-action
.hass=${this.hass} .actions=${this._config.sequence}
></ha-automation-action> @value-changed=${this._sequenceChanged}
</ha-config-section> .hass=${this.hass}
></ha-automation-action>
`} `}
` `
: ""} : ""}
@ -525,25 +507,22 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
private _computeHelperCallback = ( private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>> schema: SchemaUnion<ReturnType<typeof this._schema>>
): string | undefined => { ): string | undefined | TemplateResult => {
if (schema.name === "mode") { if (schema.name === "mode") {
return this.hass.localize( return html`
"ui.panel.config.script.editor.modes.description", <a
"documentation_link", style="color: var(--secondary-text-color)"
html` href=${documentationUrl(
<a this.hass,
href=${documentationUrl( "/integrations/script/#script-modes"
this.hass, )}
"/integrations/script/#script-modes" target="_blank"
)} rel="noreferrer"
target="_blank" >${this.hass.localize(
rel="noreferrer" "ui.panel.config.script.editor.modes.learn_more"
>${this.hass.localize( )}</a
"ui.panel.config.script.editor.modes.documentation" >
)}</a `;
>
`
);
} }
return undefined; return undefined;
}; };
@ -806,7 +785,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
color: var(--error-color); color: var(--error-color);
} }
.content { .content {
padding-bottom: 20px; padding: 16px 16px 20px;
} }
.yaml-mode { .yaml-mode {
height: 100%; height: 100%;
@ -841,6 +820,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
li[role="separator"] { li[role="separator"] {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }
.header {
display: flex;
margin: 16px 0;
align-items: center;
}
.header .name {
font-size: 20px;
font-weight: 400;
flex: 1;
}
`, `,
]; ];
} }

View File

@ -1833,9 +1833,8 @@
}, },
"modes": { "modes": {
"label": "Mode", "label": "Mode",
"description": "The mode controls what happens when the automation is triggered while the actions are still running from a previous trigger. Check the {documentation_link} for more info.", "learn_more": "Learn about modes",
"documentation": "automation documentation", "single": "Single",
"single": "Single (default)",
"restart": "Restart", "restart": "Restart",
"queued": "Queued", "queued": "Queued",
"parallel": "Parallel" "parallel": "Parallel"
@ -1848,10 +1847,7 @@
"edit_ui": "Edit in visual editor", "edit_ui": "Edit in visual editor",
"copy_to_clipboard": "Copy to Clipboard", "copy_to_clipboard": "Copy to Clipboard",
"triggers": { "triggers": {
"name": "Trigger",
"header": "Triggers", "header": "Triggers",
"introduction": "Triggers are what starts the processing of an automation rule. It is possible to specify multiple triggers for the same rule. Once a trigger starts, Home Assistant will validate the conditions, if any, and call the action.",
"learn_more": "Learn more about triggers",
"triggered": "Triggered", "triggered": "Triggered",
"add": "Add trigger", "add": "Add trigger",
"id": "Trigger ID", "id": "Trigger ID",
@ -1965,10 +1961,7 @@
} }
}, },
"conditions": { "conditions": {
"name": "Condition",
"header": "Conditions", "header": "Conditions",
"introduction": "Conditions are optional and will prevent the automation from running unless all conditions are satisfied.",
"learn_more": "Learn more about conditions",
"add": "Add condition", "add": "Add condition",
"test": "Test", "test": "Test",
"invalid_condition": "Invalid condition configuration", "invalid_condition": "Invalid condition configuration",
@ -2054,10 +2047,7 @@
} }
}, },
"actions": { "actions": {
"name": "Action",
"header": "Actions", "header": "Actions",
"introduction": "The actions are what Home Assistant will do when the automation is triggered.",
"learn_more": "Learn more about actions",
"add": "Add action", "add": "Add action",
"invalid_action": "Invalid action", "invalid_action": "Invalid action",
"run_action": "Run action", "run_action": "Run action",
@ -2117,7 +2107,8 @@
} }
}, },
"activate_scene": { "activate_scene": {
"label": "Activate scene" "label": "Activate scene",
"scene": "Scene"
}, },
"repeat": { "repeat": {
"label": "Repeat", "label": "Repeat",
@ -2261,13 +2252,12 @@
"header": "Script: {name}", "header": "Script: {name}",
"default_name": "New Script", "default_name": "New Script",
"modes": { "modes": {
"label": "Mode", "label": "[%key:ui::panel::config::automation::editor::modes::label%]",
"description": "The mode controls what happens when script is invoked while it is still running from one or more previous invocations. Check the {documentation_link} for more info.", "learn_more": "[%key:ui::panel::config::automation::editor::modes::learn_more%]",
"documentation": "script documentation", "single": "[%key:ui::panel::config::automation::editor::modes::single%]",
"single": "Single (default)", "restart": "[%key:ui::panel::config::automation::editor::modes::restart%]",
"restart": "Restart", "queued": "[%key:ui::panel::config::automation::editor::modes::queued%]",
"queued": "Queued", "parallel": "[%key:ui::panel::config::automation::editor::modes::parallel%]"
"parallel": "Parallel"
}, },
"max": { "max": {
"queued": "Queue length", "queued": "Queue length",