diff --git a/gallery/src/pages/components/ha-expansion-panel.markdown b/gallery/src/pages/components/ha-expansion-panel.markdown new file mode 100644 index 0000000000..275627e6a2 --- /dev/null +++ b/gallery/src/pages/components/ha-expansion-panel.markdown @@ -0,0 +1,5 @@ +--- +title: Expansion Panel +--- + +Expansion panel following all the ARIA guidelines. diff --git a/gallery/src/pages/components/ha-expansion-panel.ts b/gallery/src/pages/components/ha-expansion-panel.ts new file mode 100644 index 0000000000..781aa063bb --- /dev/null +++ b/gallery/src/pages/components/ha-expansion-panel.ts @@ -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` + + ${SHORT_TEXT} + + `; + }, + }, + { + template(slot, leftChevron) { + return html` + + ${SHORT_TEXT} + + `; + }, + }, + { + template(slot, leftChevron) { + return html` + + ${SHORT_TEXT} + + `; + }, + }, + { + template(slot, leftChevron) { + return html` + + ${SHORT_TEXT} + + `; + }, + }, + { + template(slot, leftChevron) { + return html` + + Slot Secondary + ${SHORT_TEXT} + + `; + }, + }, + { + template(slot, leftChevron) { + return html` + + Slot header + ${SHORT_TEXT} + + `; + }, + }, + { + template(slot, leftChevron) { + return html` + + Slot header with actions + + ${SHORT_TEXT} + + `; + }, + }, + { + template(slot, leftChevron) { + return html` + + + ${SHORT_TEXT} + + `; + }, + }, +]; + +@customElement("demo-components-ha-expansion-panel") +export class DemoHaExpansionPanel extends LitElement { + protected render(): TemplateResult { + return html` + ${SAMPLES.map( + (sample) => html` + + ${["light", "dark"].map((slot) => + sample.template(slot, slot === "dark") + )} + + ` + )} + `; + } + + static get styles() { + return css` + ha-expansion-panel { + margin: -16px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-components-ha-expansion-panel": DemoHaExpansionPanel; + } +} diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts index 9e12fe6e8e..d14833ca9e 100644 --- a/src/components/device/ha-device-automation-picker.ts +++ b/src/components/device/ha-device-automation-picker.ts @@ -172,8 +172,7 @@ export abstract class HaDeviceAutomationPicker< static get styles(): CSSResultGroup { return css` ha-select { - width: 100%; - margin-top: 4px; + display: block; } `; } diff --git a/src/components/ha-expansion-panel.ts b/src/components/ha-expansion-panel.ts index a4f3817508..33c93d352a 100644 --- a/src/components/ha-expansion-panel.ts +++ b/src/components/ha-expansion-panel.ts @@ -19,6 +19,8 @@ class HaExpansionPanel extends LitElement { @property({ type: Boolean, reflect: true }) outlined = false; + @property({ type: Boolean, reflect: true }) leftChevron = false; + @property() header?: string; @property() secondary?: string; @@ -29,23 +31,42 @@ class HaExpansionPanel extends LitElement { protected render(): TemplateResult { return html` -
- - ${this.header} - ${this.secondary} - - +
+
+ ${this.leftChevron + ? html` + + ` + : ""} + +
+ ${this.header} + ${this.secondary} +
+
+ ${!this.leftChevron + ? html` + + ` + : ""} +
+
{ + if (ev.defaultPrevented) { + return; + } if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") { return; } @@ -98,12 +123,28 @@ class HaExpansionPanel extends LitElement { fireEvent(this, "expanded-changed", { expanded: this.expanded }); } + private _focusChanged(ev) { + this.shadowRoot!.querySelector(".top")!.classList.toggle( + "focused", + ev.type === "focus" + ); + } + static get styles(): CSSResultGroup { return css` :host { display: block; } + .top { + display: flex; + align-items: center; + } + + .top.focused { + background: var(--input-fill-color); + } + :host([outlined]) { box-shadow: none; border-width: 1px; @@ -115,7 +156,17 @@ class HaExpansionPanel extends LitElement { border-radius: var(--ha-card-border-radius, 4px); } + .summary-icon { + margin-left: 8px; + } + + :host([leftchevron]) .summary-icon { + margin-left: 0; + margin-right: 8px; + } + #summary { + flex: 1; display: flex; padding: var(--expansion-panel-summary-padding, 0 8px); min-height: 48px; @@ -126,15 +177,8 @@ class HaExpansionPanel extends LitElement { outline: none; } - #summary:focus { - background: var(--input-fill-color); - } - .summary-icon { 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); } @@ -142,6 +186,11 @@ class HaExpansionPanel extends LitElement { transform: rotate(180deg); } + .header, + ::slotted([slot="header"]) { + flex: 1; + } + .container { padding: var(--expansion-panel-content-padding, 0 8px); overflow: hidden; @@ -153,10 +202,6 @@ class HaExpansionPanel extends LitElement { height: auto; } - .header { - display: block; - } - .secondary { display: block; color: var(--secondary-text-color); diff --git a/src/components/ha-selector/ha-selector-template.ts b/src/components/ha-selector/ha-selector-template.ts index 0761473dd8..e571f4c2ba 100644 --- a/src/components/ha-selector/ha-selector-template.ts +++ b/src/components/ha-selector/ha-selector-template.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { HomeAssistant } from "../../types"; @@ -48,6 +48,14 @@ export class HaTemplateSelector extends LitElement { } fireEvent(this, "value-changed", { value }); } + + static get styles() { + return css` + p { + margin-top: 0; + } + `; + } } declare global { diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index 54cca6a765..a282f9b5f5 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -230,7 +230,9 @@ export class HaServiceControl extends LitElement { @value-changed=${this._serviceChanged} >
-

${serviceData?.description}

+ ${serviceData?.description + ? html`

${serviceData?.description}

` + : ""} ${this._manifest ? html` - `${trigger.platform} trigger`; + `${trigger.platform || "Unknown"} trigger`; export const describeCondition = (condition: Condition) => { if (condition.alias) { diff --git a/src/data/condition.ts b/src/data/condition.ts new file mode 100644 index 0000000000..2d064a61ff --- /dev/null +++ b/src/data/condition.ts @@ -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", +]; diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index a66c9529e0..6e3fcdc36b 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -123,7 +123,7 @@ export const describeAction = ( ? computeStateName(sceneStateObj) : "scene" in config ? config.scene - : config.target?.entity_id || config.entity_id + : config.target?.entity_id || config.entity_id || "" }`; } diff --git a/src/data/trigger.ts b/src/data/trigger.ts new file mode 100644 index 0000000000..6f2d5e1271 --- /dev/null +++ b/src/data/trigger.ts @@ -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", +]; diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 7d7d9bbd86..2e2f80c29d 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -3,21 +3,19 @@ import "@material/mwc-list/mwc-list-item"; import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; 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 { fireEvent } from "../../../../common/dom/fire_event"; -import { stringCompare } from "../../../../common/string/compare"; import { handleStructError } from "../../../../common/structs/handle-errors"; -import { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-alert"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; import "../../../../components/ha-icon-button"; -import "../../../../components/ha-select"; -import type { HaSelect } from "../../../../components/ha-select"; +import "../../../../components/ha-expansion-panel"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import { validateConfig } from "../../../../data/config"; import { Action, getActionType } from "../../../../data/script"; +import { describeAction } from "../../../../data/script_i18n"; import { callExecuteScript } from "../../../../data/service"; import { showAlertDialog, @@ -40,23 +38,7 @@ 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"; - -const OPTIONS = [ - "condition", - "delay", - "event", - "play_media", - "activate_scene", - "service", - "wait_template", - "wait_for_trigger", - "repeat", - "choose", - "if", - "device_id", - "stop", - "parallel", -]; +import { ACTION_TYPES } from "../../../../data/action"; const getType = (action: Action | undefined) => { if (!action) { @@ -68,7 +50,7 @@ const getType = (action: Action | undefined) => { if (["and", "or", "not"].some((key) => key in action)) { return "condition"; } - return OPTIONS.find((option) => option in action); + return ACTION_TYPES.find((option) => option in action); }; declare global { @@ -104,6 +86,8 @@ export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => { fireEvent(element, "value-changed", { value: newAction }); }; +const preventDefault = (ev) => ev.preventDefault(); + @customElement("ha-automation-action-row") export default class HaAutomationActionRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -124,19 +108,6 @@ export default class HaAutomationActionRow extends LitElement { @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) { if (!changedProperties.has("action")) { return; @@ -172,10 +143,14 @@ export default class HaAutomationActionRow extends LitElement { )}
` : ""} -
+ ${this.index !== 0 ? html` ` : ""} - + -
-
- ${this._warnings - ? html` - ${this._warnings!.length > 0 && this._warnings![0] !== undefined - ? html`
    - ${this._warnings!.map( - (warning) => html`
  • ${warning}
  • ` - )} -
` - : ""} - ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} -
` - : ""} - ${yamlMode - ? html` - ${type === undefined - ? html` - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.unsupported_action", - "action", - type - )} - ` - : ""} -

- ${this.hass.localize( - "ui.panel.config.automation.editor.edit_yaml" +
+ ${this._warnings + ? html` - - ` - : html` - - ${this._processedTypes(this.hass.localize).map( - ([opt, label]) => html` - ${label} - ` + ${this._warnings!.length > 0 && + this._warnings![0] !== undefined + ? html`
    + ${this._warnings!.map( + (warning) => html`
  • ${warning}
  • ` + )} +
` + : ""} + ${this.hass.localize( + "ui.errors.config.edit_in_yaml_supported" )} -
- -
- ${dynamicElement(`ha-automation-action-${type}`, { - hass: this.hass, - action: this.action, - narrow: this.narrow, - })} -
- `} -
+ ` + : ""} + ${yamlMode + ? html` + ${type === undefined + ? html` + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.unsupported_action", + "action", + type + )} + ` + : ""} +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} +

+ + ` + : html` +
+ ${dynamicElement(`ha-automation-action-${type}`, { + hass: this.hass, + action: this.action, + narrow: this.narrow, + })} +
+ `} +
+ `; } @@ -319,11 +290,13 @@ export default class HaAutomationActionRow extends LitElement { } } - private _moveUp() { + private _moveUp(ev) { + ev.preventDefault(); fireEvent(this, "move-action", { direction: "up" }); } - private _moveDown() { + private _moveDown(ev) { + ev.preventDefault(); 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) { ev.stopPropagation(); if (!ev.detail.isValid) { @@ -441,17 +389,30 @@ export default class HaAutomationActionRow extends LitElement { this._yamlMode = !this._yamlMode; } + public expand() { + this.updateComplete.then(() => { + this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true; + }); + } + static get styles(): CSSResultGroup { return [ haStyle, css` + ha-button-menu, + ha-icon-button { + --mdc-theme-text-primary-on-background: var(--primary-text-color); + } .disabled { opacity: 0.5; pointer-events: none; } + ha-expansion-panel { + --expansion-panel-summary-padding: 0 0 0 8px; + --expansion-panel-content-padding: 0; + } .card-content { - padding-top: 16px; - margin-top: 0; + padding: 16px; } .disabled-bar { 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-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] { --mdc-theme-text-primary-on-background: var(--disabled-text-color); } @@ -476,9 +430,6 @@ export default class HaAutomationActionRow extends LitElement { .warning ul { margin: 4px 0; } - ha-select { - margin-bottom: 24px; - } `, ]; } diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index 3abbbd7d6e..7e27cdac76 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -1,13 +1,36 @@ +import { repeat } from "lit/directives/repeat"; +import { mdiPlus } from "@mdi/js"; import deepClone from "deep-clone-simple"; 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 { 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 { HomeAssistant } from "../../../../types"; 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") export default class HaAutomationAction extends LitElement { @@ -17,9 +40,15 @@ export default class HaAutomationAction extends LitElement { @property() public actions!: Action[]; + private _focusLastActionOnChange = false; + protected render() { 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` ` )} - -
- - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.add" - )} - -
-
+ + + + + ${this._processedTypes(this.hass.localize).map( + ([opt, label]) => html` + ${label} + ` + )} + `; } - private _addAction() { - const actions = this.actions.concat({ - ...HaDeviceAction.defaultConfig, - }); + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("actions") && this._focusLastActionOnChange) { + this._focusLastActionOnChange = false; + + const row = this.shadowRoot!.querySelector( + "ha-automation-action-row:last-of-type" + )!; + row.expand(); + row.focus(); + } + } + + private _addAction(ev: CustomEvent) { + 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 }); } @@ -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 { return css` - ha-automation-action-row, - ha-card { + ha-automation-action-row { display: block; - margin-top: 16px; + margin-bottom: 16px; } - .add-card mwc-button { - display: block; - text-align: center; + ha-svg-icon { + height: 20px; } `; } diff --git a/src/panels/config/automation/action/types/ha-automation-action-activate_scene.ts b/src/panels/config/automation/action/types/ha-automation-action-activate_scene.ts index 0ce146589a..aa3199c4ef 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-activate_scene.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-activate_scene.ts @@ -37,6 +37,9 @@ export class HaSceneAction extends LitElement implements ActionElement { return html` ` )} - -
- - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.choose.add_option" - )} - -
-
+ + +

${this.hass.localize( "ui.panel.config.automation.editor.actions.type.choose.default" @@ -154,7 +154,7 @@ export class HaChooseAction extends LitElement implements ActionElement { haStyle, css` ha-card { - margin-top: 16px; + margin: 16px 0; } .add-card mwc-button { display: block; @@ -168,6 +168,9 @@ export class HaChooseAction extends LitElement implements ActionElement { ha-form::part(root) { overflow: visible; } + ha-svg-icon { + height: 20px; + } `, ]; } diff --git a/src/panels/config/automation/action/types/ha-automation-action-condition.ts b/src/panels/config/automation/action/types/ha-automation-action-condition.ts index 6509d71c6c..23048b0983 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-condition.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-condition.ts @@ -1,10 +1,16 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; 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 "../../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") export class HaConditionAction extends LitElement implements ActionElement { @@ -18,6 +24,21 @@ export class HaConditionAction extends LitElement implements ActionElement { protected render() { return html` + + ${this._processedTypes(this.hass.localize).map( + ([opt, label]) => html` + ${label} + ` + )} + + 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) { ev.stopPropagation(); @@ -33,6 +67,37 @@ export class HaConditionAction extends LitElement implements ActionElement { 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; + }; + + 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 { diff --git a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts index 6aaf19f8f1..6b78e6c0cf 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts @@ -52,42 +52,42 @@ export class HaRepeatAction extends LitElement implements ActionElement { ` )} - ${type === "count" - ? html` - - ` - : ""} - ${type === "while" - ? html`

- ${this.hass.localize( - `ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions` - )}: -

- ` - : ""} - ${type === "until" - ? html`

- ${this.hass.localize( - `ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions` - )}: -

- ` - : ""} +
+ ${type === "count" + ? html` + + ` + : type === "while" + ? html`

+ ${this.hass.localize( + `ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions` + )}: +

+ ` + : type === "until" + ? html`

+ ${this.hass.localize( + `ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions` + )}: +

+ ` + : ""} +

${this.hass.localize( "ui.panel.config.automation.editor.actions.type.repeat.sequence" diff --git a/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts index e0f4554fa4..bee68da94c 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts @@ -68,6 +68,10 @@ export class HaWaitForTriggerAction display: block; margin-bottom: 24px; } + ha-automation-trigger { + display: block; + margin-top: 24px; + } `; } } diff --git a/src/panels/config/automation/condition/ha-automation-condition-editor.ts b/src/panels/config/automation/condition/ha-automation-condition-editor.ts index b694b3536c..45b026695f 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-editor.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-editor.ts @@ -1,12 +1,8 @@ -import { css, html, LitElement } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; 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 type { Condition } 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-zone"; -const OPTIONS = [ - "device", - "and", - "or", - "not", - "state", - "numeric_state", - "sun", - "template", - "time", - "trigger", - "zone", -] as const; - @customElement("ha-automation-condition-editor") export default class HaAutomationConditionEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -50,27 +32,16 @@ export default class HaAutomationConditionEditor extends LitElement { 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() { const condition = this._processedCondition(this.condition); - const selected = OPTIONS.indexOf(condition.condition); - const yamlMode = this.yamlMode || selected === -1; + const supported = + customElements.get(`ha-automation-condition-${condition.condition}`) !== + undefined; + const yamlMode = this.yamlMode || !supported; return html` ${yamlMode ? html` - ${selected === -1 + ${!supported ? html` ${this.hass.localize( "ui.panel.config.automation.editor.conditions.unsupported_condition", @@ -91,21 +62,6 @@ export default class HaAutomationConditionEditor extends LitElement { > ` : html` - - ${this._processedTypes(this.hass.localize).map( - ([opt, label]) => html` - ${label} - ` - )} - -
${dynamicElement( `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; - }; - - if (type !== this._processedCondition(this.condition).condition) { - fireEvent(this, "value-changed", { - value: { - condition: type, - ...elClass.defaultConfig, - }, - }); - } - } - private _onYamlChange(ev: CustomEvent) { ev.stopPropagation(); if (!ev.detail.isValid) { @@ -148,14 +81,7 @@ export default class HaAutomationConditionEditor extends LitElement { fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true }); } - static styles = [ - haStyle, - css` - ha-select { - margin-bottom: 24px; - } - `, - ]; + static styles = haStyle; } declare global { diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index f5450196e0..4ccf7fedd8 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -3,6 +3,7 @@ import "@material/mwc-list/mwc-list-item"; import { mdiDotsVertical } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../../common/dom/fire_event"; import { handleStructError } from "../../../../common/structs/handle-errors"; import "../../../../components/ha-button-menu"; @@ -10,6 +11,7 @@ import "../../../../components/ha-card"; import "../../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button"; import "../../../../components/ha-icon-button"; +import "../../../../components/ha-expansion-panel"; import { Condition, testCondition } from "../../../../data/automation"; import { showAlertDialog, @@ -20,11 +22,14 @@ import { HomeAssistant } from "../../../../types"; import "./ha-automation-condition-editor"; import { validateConfig } from "../../../../data/config"; import { HaYamlEditor } from "../../../../components/ha-yaml-editor"; +import { describeCondition } from "../../../../data/automation_i18n"; export interface ConditionElement extends LitElement { condition: Condition; } +const preventDefault = (ev) => ev.preventDefault(); + export const handleChangeEvent = ( element: ConditionElement, ev: CustomEvent @@ -75,13 +80,23 @@ export default class HaAutomationConditionRow extends LitElement { )}
` : ""} -
- + + + ${this.hass.localize( "ui.panel.config.automation.editor.conditions.test" )} - + -
-
- ${this._warnings - ? html` - ${this._warnings!.length > 0 && this._warnings![0] !== undefined - ? html`
    - ${this._warnings!.map( - (warning) => html`
  • ${warning}
  • ` - )} -
` - : ""} - ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} -
` - : ""} - -
+ +
+ ${this._warnings + ? html` + ${this._warnings!.length > 0 && + this._warnings![0] !== undefined + ? html`
    + ${this._warnings!.map( + (warning) => html`
  • ${warning}
  • ` + )} +
` + : ""} + ${this.hass.localize( + "ui.errors.config.edit_in_yaml_supported" + )} +
` + : ""} + +
+ `; } @@ -212,6 +232,7 @@ export default class HaAutomationConditionRow extends LitElement { } private async _testCondition(ev) { + ev.preventDefault(); const condition = this.condition; const button = ev.target as HaProgressButton; 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 { return [ haStyle, css` + ha-button-menu, + ha-progress-button { + --mdc-theme-text-primary-on-background: var(--primary-text-color); + } .disabled { opacity: 0.5; pointer-events: none; } + ha-expansion-panel { + --expansion-panel-summary-padding: 0 0 0 8px; + --expansion-panel-content-padding: 0; + } .card-content { - padding-top: 16px; - margin-top: 0; + padding: 16px; } .disabled-bar { 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-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] { --mdc-theme-text-primary-on-background: var(--disabled-text-color); } diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index de5972e164..9229fa5657 100644 --- a/src/panels/config/automation/condition/ha-automation-condition.ts +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -1,13 +1,34 @@ +import { mdiPlus } from "@mdi/js"; +import { repeat } from "lit/directives/repeat"; import deepClone from "deep-clone-simple"; import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; 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 "../../../../components/ha-card"; -import { Condition } from "../../../../data/automation"; -import { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-button-menu"; +import type { Condition } from "../../../../data/automation"; +import type { HomeAssistant } from "../../../../types"; 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") export default class HaAutomationCondition extends LitElement { @@ -15,10 +36,13 @@ export default class HaAutomationCondition extends LitElement { @property() public conditions!: Condition[]; + private _focusLastConditionOnChange = false; + protected updated(changedProperties: PropertyValues) { if (!changedProperties.has("conditions")) { return; } + let updatedConditions: Condition[] | undefined; if (!Array.isArray(this.conditions)) { updatedConditions = [this.conditions]; @@ -38,6 +62,13 @@ export default class HaAutomationCondition extends LitElement { fireEvent(this, "value-changed", { value: updatedConditions, }); + } else if (this._focusLastConditionOnChange) { + this._focusLastConditionOnChange = false; + const row = this.shadowRoot!.querySelector( + "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` - ${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` ` )} - -
- - ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.add" - )} - -
-
+ + + + + ${this._processedTypes(this.hass.localize).map( + ([opt, label]) => html` + ${label} + ` + )} + `; } - private _addCondition() { - const conditions = this.conditions.concat({ - condition: "device", - ...HaDeviceCondition.defaultConfig, - }); + private _addCondition(ev: CustomEvent) { + const condition = (ev.currentTarget as HaSelect).items[ev.detail.index] + .value as Condition["condition"]; + const elClass = customElements.get( + `ha-automation-condition-${condition}` + ) as CustomElementConstructor & { + defaultConfig: Omit; + }; + + const conditions = this.conditions.concat({ + condition: condition as any, + ...elClass.defaultConfig, + }); + this._focusLastConditionOnChange = true; 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 { return css` - ha-automation-condition-row, - ha-card { + ha-automation-condition-row { display: block; - margin-top: 16px; + margin-bottom: 16px; } - .add-card mwc-button { - display: block; - text-align: center; + ha-svg-icon { + height: 20px; } `; } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts index 18933eb6df..628c050663 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts @@ -1,14 +1,10 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import type { - Condition, - LogicalCondition, -} from "../../../../../data/automation"; +import type { LogicalCondition } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; import "../ha-automation-condition"; import type { ConditionElement } from "../ha-automation-condition-row"; -import { HaStateCondition } from "./ha-automation-condition-state"; @customElement("ha-automation-condition-logical") export class HaLogicalCondition extends LitElement implements ConditionElement { @@ -18,12 +14,7 @@ export class HaLogicalCondition extends LitElement implements ConditionElement { public static get defaultConfig() { return { - conditions: [ - { - condition: "state", - ...HaStateCondition.defaultConfig, - }, - ] as Condition[], + conditions: [], }; } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-template.ts b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts index ed9bc32e82..2d21860834 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-template.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import "../../../../../components/ha-textarea"; import type { TemplateCondition } from "../../../../../data/automation"; @@ -39,6 +39,14 @@ export class HaTemplateCondition extends LitElement { private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } + + static get styles() { + return css` + p { + margin-top: 0; + } + `; + } } declare global { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts index f1825750c1..def76b97d8 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-zone.ts @@ -73,7 +73,7 @@ export class HaZoneCondition extends LitElement { } static styles = css` - ha-entity-picker { + ha-entity-picker:first-child { display: block; margin-bottom: 24px; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index d2ceff6b61..6c0c087d4a 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -50,10 +50,8 @@ import { HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; -import { HaDeviceAction } from "./action/types/ha-automation-action-device_id"; import "./blueprint-automation-editor"; import "./manual-automation-editor"; -import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device"; declare global { interface HTMLElementTagNameMap { @@ -329,9 +327,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { baseConfig = { ...baseConfig, mode: "single", - trigger: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }], + trigger: [], condition: [], - action: [{ ...HaDeviceAction.defaultConfig }], + action: [], }; } this._config = { @@ -570,6 +568,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { flex-direction: column; padding-bottom: 0; } + manual-automation-editor { + margin: 0 auto; + max-width: 1040px; + padding: 28px 20px 0; + } ha-yaml-editor { flex-grow: 1; --code-mirror-height: 100%; diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 4ed456a86e..eb8b7ef3b1 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -1,4 +1,5 @@ import "@material/mwc-button/mwc-button"; +import { mdiHelpCircle } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -7,6 +8,7 @@ import "../../../components/entity/ha-entity-toggle"; import "../../../components/ha-card"; import "../../../components/ha-textarea"; import "../../../components/ha-textfield"; +import "../../../components/ha-icon-button"; import { AUTOMATION_DEFAULT_MODE, Condition, @@ -18,7 +20,6 @@ import { Action, isMaxMode, MODES } from "../../../data/script"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; -import "../ha-config-section"; import "./action/ha-automation-action"; import "./condition/ha-automation-condition"; import "./trigger/ha-automation-trigger"; @@ -38,218 +39,198 @@ export class HaManualAutomationEditor extends LitElement { @state() private _showDescription = false; protected render() { - return html` - ${!this.narrow - ? html`${this.config.alias}` - : ""} - - ${this.hass.localize( - "ui.panel.config.automation.editor.introduction" - )} - - -
- ${this.stateObj + return html` + +
+ + + ${this._showDescription ? html` -
-
- - ${this.hass.localize( - "ui.panel.config.automation.editor.enable_disable" - )} -
-
- - - ${this.hass.localize( - "ui.panel.config.automation.editor.show_trace" - )} - - - - ${this.hass.localize("ui.card.automation.trigger")} - -
-
+ ` - : ""} - - + : html` + + `} + ${this.hass.localize( + "ui.panel.config.automation.editor.modes.learn_more" + )} + `} + > + ${MODES.map( + (mode) => html` + + ${this.hass.localize( + `ui.panel.config.automation.editor.modes.${mode}` + ) || mode} + + ` + )} + + ${this.config.mode && isMaxMode(this.config.mode) + ? html` +
+ + ` + : html``} +
+ ${this.stateObj + ? html` +
+
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.enable_disable" + )} +
+
+ + + ${this.hass.localize( + "ui.panel.config.automation.editor.show_trace" + )} + + + + ${this.hass.localize("ui.card.automation.trigger")} + +
+
+ ` + : ""} +
- - +
+
${this.hass.localize( "ui.panel.config.automation.editor.triggers.header" )} - - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.introduction" - )} -

- - ${this.hass.localize( +
+ + - - - + > + +
- - + + +
+
${this.hass.localize( "ui.panel.config.automation.editor.conditions.header" )} - - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.introduction" - )} -

- - ${this.hass.localize( +
+ + - - - + > + +
- - + + +
+
${this.hass.localize( "ui.panel.config.automation.editor.actions.header" )} - - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.actions.introduction" - )} -

- - ${this.hass.localize( +
+ + - - - `; + > + +
+ + + `; } protected willUpdate(changedProps: PropertyValues): void { @@ -341,6 +322,9 @@ export class HaManualAutomationEditor extends LitElement { return [ haStyle, css` + :host { + display: block; + } ha-card { overflow: hidden; } @@ -351,9 +335,7 @@ export class HaManualAutomationEditor extends LitElement { ha-textfield { display: block; } - span[slot="introduction"] a { - color: var(--primary-color); - } + p { margin-bottom: 0; } @@ -365,6 +347,19 @@ export class HaManualAutomationEditor extends LitElement { margin-top: 16px; 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); + } `, ]; } diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 0894164036..a565bcae4f 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -5,20 +5,16 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import memoizeOne from "memoize-one"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stringCompare } from "../../../../common/string/compare"; import { handleStructError } from "../../../../common/structs/handle-errors"; -import { LocalizeFunc } from "../../../../common/translations/localize"; import { debounce } from "../../../../common/util/debounce"; import "../../../../components/ha-alert"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-icon-button"; import { HaYamlEditor } from "../../../../components/ha-yaml-editor"; -import "../../../../components/ha-select"; -import type { HaSelect } from "../../../../components/ha-select"; import "../../../../components/ha-textfield"; import { subscribeTrigger, Trigger } from "../../../../data/automation"; 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-webhook"; import "./types/ha-automation-trigger-zone"; - -const OPTIONS = [ - "calendar", - "device", - "event", - "state", - "geo_location", - "homeassistant", - "mqtt", - "numeric_state", - "sun", - "tag", - "template", - "time", - "time_pattern", - "webhook", - "zone", -] as const; +import { describeTrigger } from "../../../../data/automation_i18n"; export interface TriggerElement extends LitElement { trigger: Trigger; @@ -88,6 +67,8 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => { fireEvent(element, "value-changed", { value: newTrigger }); }; +const preventDefault = (ev) => ev.preventDefault(); + @customElement("ha-automation-trigger-row") export default class HaAutomationTriggerRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -108,35 +89,36 @@ export default class HaAutomationTriggerRow extends LitElement { private _triggerUnsub?: Promise; - 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() { - const selected = OPTIONS.indexOf(this.trigger.platform); - const yamlMode = this._yamlMode || selected === -1; + const supported = + customElements.get(`ha-automation-trigger-${this.trigger.platform}`) !== + undefined; + const yamlMode = this._yamlMode || !supported; const showId = "id" in this.trigger || this._requestShowId; return html` ${this.trigger.enabled === false - ? html`
- ${this.hass.localize( - "ui.panel.config.automation.editor.actions.disabled" - )} -
` + ? html` +
+ ${this.hass.localize( + "ui.panel.config.automation.editor.actions.disabled" + )} +
+ ` : ""} -
- + + + - + ${yamlMode ? this.hass.localize( "ui.panel.config.automation.editor.edit_ui" @@ -176,86 +158,77 @@ export default class HaAutomationTriggerRow extends LitElement { )} -
-
- ${this._warnings - ? html` - ${this._warnings.length && this._warnings[0] !== undefined - ? html`
    - ${this._warnings.map( - (warning) => html`
  • ${warning}
  • ` - )} -
` - : ""} - ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} -
` - : ""} - ${yamlMode - ? html` - ${selected === -1 - ? html` - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.unsupported_platform", - "platform", - this.trigger.platform - )} - ` - : ""} -

- ${this.hass.localize( - "ui.panel.config.automation.editor.edit_yaml" + +
+ ${this._warnings + ? html` - - ` - : html` - - ${this._processedTypes(this.hass.localize).map( - ([opt, label]) => html` - ${label} - ` - )} - - ${showId - ? html` - + ${this._warnings.map( + (warning) => html`
  • ${warning}
  • ` )} - .value=${this.trigger.id || ""} - @change=${this._idChanged} - > -
    - ` - : ""} -
    - ${dynamicElement( - `ha-automation-trigger-${this.trigger.platform}`, - { hass: this.hass, trigger: this.trigger } + ` + : ""} + ${this.hass.localize( + "ui.errors.config.edit_in_yaml_supported" )} -
    - `} -
    + ` + : ""} + ${yamlMode + ? html` + ${!supported + ? html` + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.unsupported_platform", + "platform", + this.trigger.platform + )} + ` + : ""} +

    + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} +

    + + ` + : html` + ${showId + ? html` + + + ` + : ""} +
    + ${dynamicElement( + `ha-automation-trigger-${this.trigger.platform}`, + { hass: this.hass, trigger: this.trigger } + )} +
    + `} +
    + +
    ; - }; - - 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) { const newId = (ev.target as any).value; 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 { return [ haStyle, css` + ha-button-menu { + --mdc-theme-text-primary-on-background: var(--primary-text-color); + } .disabled { opacity: 0.5; pointer-events: none; } + ha-expansion-panel { + --expansion-panel-summary-padding: 0 0 0 8px; + --expansion-panel-content-padding: 0; + } .card-content { - padding-top: 16px; - margin-top: 0; + padding: 16px; } .disabled-bar { 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-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 { cursor: pointer; position: absolute; @@ -525,9 +476,6 @@ export default class HaAutomationTriggerRow extends LitElement { mwc-list-item[disabled] { --mdc-theme-text-primary-on-background: var(--disabled-text-color); } - ha-select { - margin-bottom: 24px; - } ha-textfield { display: block; margin-bottom: 24px; diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index 1b359dd83e..9480f49220 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -1,13 +1,37 @@ +import { repeat } from "lit/directives/repeat"; +import { mdiPlus } from "@mdi/js"; import deepClone from "deep-clone-simple"; +import memoizeOne from "memoize-one"; 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 type { ActionDetail } from "@material/mwc-list"; 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_TYPES } from "../../../../data/trigger"; import { HomeAssistant } from "../../../../types"; 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") export default class HaAutomationTrigger extends LitElement { @@ -15,9 +39,15 @@ export default class HaAutomationTrigger extends LitElement { @property() public triggers!: Trigger[]; + private _focusLastTriggerOnChange = false; + protected render() { 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` ` )} - -
    - - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.add" - )} - -
    -
    + + + + + ${this._processedTypes(this.hass.localize).map( + ([opt, label]) => html` + ${label} + ` + )} + `; } - private _addTrigger() { - const triggers = this.triggers.concat({ - platform: "device", - ...HaDeviceTrigger.defaultConfig, - }); + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("triggers") && this._focusLastTriggerOnChange) { + this._focusLastTriggerOnChange = false; + + const row = this.shadowRoot!.querySelector( + "ha-automation-trigger-row:last-of-type" + )!; + row.expand(); + row.focus(); + } + } + + private _addTrigger(ev: CustomEvent) { + 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; + }; + + const triggers = this.triggers.concat({ + platform: platform as any, + ...elClass.defaultConfig, + }); + this._focusLastTriggerOnChange = true; 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 { return css` - ha-automation-trigger-row, - ha-card { + ha-automation-trigger-row { display: block; - margin-top: 16px; + margin-bottom: 16px; } - .add-card mwc-button { - display: block; - text-align: center; + ha-svg-icon { + height: 20px; } `; } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts index 9565c87347..ec80254f35 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts @@ -1,5 +1,5 @@ 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 { fireEvent } from "../../../../../common/dom/fire_event"; 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 { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts index eeb78b986a..063aece21c 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-template.ts @@ -1,5 +1,5 @@ import "../../../../../components/ha-textarea"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import type { TemplateTrigger } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; @@ -39,6 +39,14 @@ export class HaTemplateTrigger extends LitElement { private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } + + static get styles() { + return css` + p { + margin-top: 0; + } + `; + } } declare global { diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index f57d5feb2a..d0309111f9 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -6,6 +6,7 @@ import { mdiContentSave, mdiDelete, mdiDotsVertical, + mdiHelpCircle, } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; 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 { showToast } from "../../../util/toast"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; -import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./blueprint-script-editor"; @@ -276,59 +276,47 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { > ${this._config ? html` - - ${!this.narrow + +
    + +
    + ${this.scriptEntityId ? html` - ${this._config.alias} - ` - : ""} - - ${this.hass.localize( - "ui.panel.config.script.editor.introduction" - )} - - -
    - -
    - ${this.scriptEntityId - ? html` -
    + - - - ${this.hass.localize( - "ui.panel.config.script.editor.show_trace" - )} - - - + ${this.hass.localize( - "ui.panel.config.script.picker.run_script" + "ui.panel.config.script.editor.show_trace" )} -
    - ` - : ``} -
    -
    + + + ${this.hass.localize( + "ui.panel.config.script.picker.run_script" + )} + +
    + ` + : ``} +
    ${"use_blueprint" in this._config ? html` @@ -341,40 +329,34 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { > ` : html` - - +
    +
    ${this.hass.localize( "ui.panel.config.script.editor.sequence" )} - - -

    - ${this.hass.localize( - "ui.panel.config.script.editor.sequence_sentence" - )} -

    - - ${this.hass.localize( +
    + + - - - + > + +
    + + `} ` : ""} @@ -525,25 +507,22 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _computeHelperCallback = ( schema: SchemaUnion> - ): string | undefined => { + ): string | undefined | TemplateResult => { if (schema.name === "mode") { - return this.hass.localize( - "ui.panel.config.script.editor.modes.description", - "documentation_link", - html` - ${this.hass.localize( - "ui.panel.config.script.editor.modes.documentation" - )} - ` - ); + return html` + ${this.hass.localize( + "ui.panel.config.script.editor.modes.learn_more" + )} + `; } return undefined; }; @@ -806,7 +785,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { color: var(--error-color); } .content { - padding-bottom: 20px; + padding: 16px 16px 20px; } .yaml-mode { height: 100%; @@ -841,6 +820,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { li[role="separator"] { 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; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 107ab50288..2a44fbd3fc 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1833,9 +1833,8 @@ }, "modes": { "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.", - "documentation": "automation documentation", - "single": "Single (default)", + "learn_more": "Learn about modes", + "single": "Single", "restart": "Restart", "queued": "Queued", "parallel": "Parallel" @@ -1848,10 +1847,7 @@ "edit_ui": "Edit in visual editor", "copy_to_clipboard": "Copy to Clipboard", "triggers": { - "name": "Trigger", "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", "add": "Add trigger", "id": "Trigger ID", @@ -1965,10 +1961,7 @@ } }, "conditions": { - "name": "Condition", "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", "test": "Test", "invalid_condition": "Invalid condition configuration", @@ -2054,10 +2047,7 @@ } }, "actions": { - "name": "Action", "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", "invalid_action": "Invalid action", "run_action": "Run action", @@ -2117,7 +2107,8 @@ } }, "activate_scene": { - "label": "Activate scene" + "label": "Activate scene", + "scene": "Scene" }, "repeat": { "label": "Repeat", @@ -2261,13 +2252,12 @@ "header": "Script: {name}", "default_name": "New Script", "modes": { - "label": "Mode", - "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.", - "documentation": "script documentation", - "single": "Single (default)", - "restart": "Restart", - "queued": "Queued", - "parallel": "Parallel" + "label": "[%key:ui::panel::config::automation::editor::modes::label%]", + "learn_more": "[%key:ui::panel::config::automation::editor::modes::learn_more%]", + "single": "[%key:ui::panel::config::automation::editor::modes::single%]", + "restart": "[%key:ui::panel::config::automation::editor::modes::restart%]", + "queued": "[%key:ui::panel::config::automation::editor::modes::queued%]", + "parallel": "[%key:ui::panel::config::automation::editor::modes::parallel%]" }, "max": { "queued": "Queue length",