Automation editor sidebar (#26413)

This commit is contained in:
Wendelin
2025-08-14 10:06:16 +02:00
committed by GitHub
parent 8c65876413
commit 3bf25f125b
37 changed files with 2941 additions and 1429 deletions

View File

@@ -0,0 +1,4 @@
export const preventDefaultStopPropagation = (ev) => {
ev.preventDefault();
ev.stopPropagation();
};

View File

@@ -10,8 +10,8 @@ import {
} from "../../data/device_automation"; } from "../../data/device_automation";
import type { EntityRegistryEntry } from "../../data/entity_registry"; import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-list-item"; import "../ha-md-select-option";
import "../ha-select"; import "../ha-md-select";
import { stopPropagation } from "../../common/dom/stop_propagation"; import { stopPropagation } from "../../common/dom/stop_propagation";
const NO_AUTOMATION_KEY = "NO_AUTOMATION"; const NO_AUTOMATION_KEY = "NO_AUTOMATION";
@@ -100,35 +100,35 @@ export abstract class HaDeviceAutomationPicker<
} }
const value = this._value; const value = this._value;
return html` return html`
<ha-select <ha-md-select
.label=${this.label} .label=${this.label}
.value=${value} .value=${value}
@selected=${this._automationChanged} @change=${this._automationChanged}
@closed=${stopPropagation} @closed=${stopPropagation}
.disabled=${this._automations.length === 0} .disabled=${this._automations.length === 0}
> >
${value === NO_AUTOMATION_KEY ${value === NO_AUTOMATION_KEY
? html`<ha-list-item .value=${NO_AUTOMATION_KEY}> ? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
${this.NO_AUTOMATION_TEXT} ${this.NO_AUTOMATION_TEXT}
</ha-list-item>` </ha-md-select-option>`
: ""} : nothing}
${value === UNKNOWN_AUTOMATION_KEY ${value === UNKNOWN_AUTOMATION_KEY
? html`<ha-list-item .value=${UNKNOWN_AUTOMATION_KEY}> ? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
${this.UNKNOWN_AUTOMATION_TEXT} ${this.UNKNOWN_AUTOMATION_TEXT}
</ha-list-item>` </ha-md-select-option>`
: ""} : nothing}
${this._automations.map( ${this._automations.map(
(automation, idx) => html` (automation, idx) => html`
<ha-list-item .value=${`${automation.device_id}_${idx}`}> <ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
${this._localizeDeviceAutomation( ${this._localizeDeviceAutomation(
this.hass, this.hass,
this._entityReg, this._entityReg,
automation automation
)} )}
</ha-list-item> </ha-md-select-option>
` `
)} )}
</ha-select> </ha-md-select>
`; `;
} }

View File

@@ -0,0 +1,148 @@
import { mdiChevronUp } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
@customElement("ha-automation-row")
export class HaAutomationRow extends LitElement {
@property({ attribute: "left-chevron", type: Boolean })
public leftChevron = false;
@property({ type: Boolean, reflect: true })
public collapsed = false;
@property({ type: Boolean, reflect: true })
public selected = false;
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Boolean, reflect: true, attribute: "building-block" })
public buildingBlock = false;
protected render(): TemplateResult {
return html`
<div
class="row"
tabindex="0"
role="button"
@keydown=${this._handleKeydown}
>
${this.leftChevron
? html`
<ha-icon-button
class="expand-button"
.path=${mdiChevronUp}
@click=${this._handleExpand}
@keydown=${this._handleExpand}
></ha-icon-button>
`
: nothing}
<div class="leading-icon-wrapper">
<slot name="leading-icon"></slot>
</div>
<slot class="header" name="header"></slot>
<slot name="icons"></slot>
</div>
`;
}
private async _handleExpand(ev) {
if (ev.defaultPrevented) {
return;
}
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.stopPropagation();
ev.preventDefault();
fireEvent(this, "toggle-collapsed");
}
private async _handleKeydown(ev: KeyboardEvent): Promise<void> {
if (ev.defaultPrevented) {
return;
}
if (ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.preventDefault();
ev.stopPropagation();
this.click();
}
static styles = css`
:host {
display: block;
}
.row {
display: flex;
padding: 0 8px;
min-height: 48px;
align-items: center;
cursor: pointer;
overflow: hidden;
font-weight: var(--ha-font-weight-medium);
outline: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
}
.row:focus {
outline: var(--wa-focus-ring);
outline-offset: -2px;
}
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md);
padding: 4px;
display: flex;
justify-content: center;
align-items: center;
transform: rotate(45deg);
}
::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: 20px;
color: var(--white-color);
transform: rotate(-45deg);
}
:host([collapsed]) .expand-button {
transform: rotate(180deg);
}
:host([selected]) .row,
:host([selected]) .row:focus {
outline: solid;
outline-color: var(--primary-color);
outline-offset: -2px;
outline-width: 2px;
}
:host([disabled]) .row {
border-top-right-radius: var(--ha-border-radius-square);
border-top-left-radius: var(--ha-border-radius-square);
}
::slotted([slot="header"]) {
flex: 1;
overflow-wrap: anywhere;
margin: 0 12px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-row": HaAutomationRow;
}
interface HASSDomEvents {
"toggle-collapsed": undefined;
}
}

View File

@@ -1,19 +1,19 @@
import { ContextProvider, consume } from "@lit/context"; import { consume, ContextProvider } from "@lit/context";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { fullEntitiesContext } from "../../data/context"; import { fullEntitiesContext } from "../../data/context";
import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script";
import type { ActionSelector } from "../../data/selector";
import "../../panels/config/automation/action/ha-automation-action";
import type { HomeAssistant } from "../../types";
import { import {
subscribeEntityRegistry, subscribeEntityRegistry,
type EntityRegistryEntry, type EntityRegistryEntry,
} from "../../data/entity_registry"; } from "../../data/entity_registry";
import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script";
import type { ActionSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import "../../panels/config/automation/action/ha-automation-action";
import type { HomeAssistant } from "../../types";
@customElement("ha-selector-action") @customElement("ha-selector-action")
export class HaActionSelector extends SubscribeMixin(LitElement) { export class HaActionSelector extends SubscribeMixin(LitElement) {
@@ -69,6 +69,7 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
.actions=${this._actions(this.value)} .actions=${this._actions(this.value)}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.optionsInSidebar=${!!this.selector.action?.optionsInSidebar}
></ha-automation-action> ></ha-automation-action>
`; `;
} }

View File

@@ -27,6 +27,7 @@ export class HaConditionSelector extends LitElement {
.conditions=${this.value || []} .conditions=${this.value || []}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.optionsInSidebar=${!!this.selector.condition?.optionsInSidebar}
></ha-automation-condition> ></ha-automation-condition>
`; `;
} }

View File

@@ -90,3 +90,19 @@ export const isService = (key: string | undefined): boolean | undefined =>
export const getService = (key: string): string => export const getService = (key: string): string =>
key.substring(SERVICE_PREFIX.length); key.substring(SERVICE_PREFIX.length);
export const ACTION_BUILDING_BLOCKS = [
"choose",
"if",
"parallel",
"sequence",
"repeat_while",
"repeat_until",
];
// Building blocks that have options in the sidebar
export const ACTION_COMBINED_BLOCKS = [
"repeat_count", // virtual repeat variant
"repeat_for_each", // virtual repeat variant
"wait_for_trigger",
];

View File

@@ -2,14 +2,15 @@ import type {
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { navigate } from "../common/navigate";
import { createSearchParam } from "../common/url/search-params";
import type { Context, HomeAssistant } from "../types"; import type { Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint"; import type { BlueprintInput } from "./blueprint";
import type { DeviceCondition, DeviceTrigger } from "./device_automation"; import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, MODES } from "./script"; import type { Action, MODES } from "./script";
import { migrateAutomationAction } from "./script"; import { migrateAutomationAction } from "./script";
import { createSearchParam } from "../common/url/search-params"; import { CONDITION_BUILDING_BLOCKS } from "./condition";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single"; export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10; export const AUTOMATION_DEFAULT_MAX = 10;
@@ -325,7 +326,7 @@ export const expandConditionWithShorthand = (
}; };
} }
for (const condition of ["and", "or", "not"]) { for (const condition of CONDITION_BUILDING_BLOCKS) {
if (condition in cond) { if (condition in cond) {
return { return {
condition, condition,

View File

@@ -50,3 +50,5 @@ export const CONDITION_GROUPS: AutomationElementGroup = {
}, },
}, },
} as const; } as const;
export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];

View File

@@ -74,7 +74,9 @@ export type Selector =
| BackupLocationSelector; | BackupLocationSelector;
export interface ActionSelector { export interface ActionSelector {
action: {} | null; action: {
optionsInSidebar?: boolean;
} | null;
} }
export interface AddonSelector { export interface AddonSelector {
@@ -130,7 +132,9 @@ export interface ColorTempSelector {
} }
export interface ConditionSelector { export interface ConditionSelector {
condition: {} | null; condition: {
optionsInSidebar?: boolean;
} | null;
} }
export interface ConversationAgentSelector { export interface ConversationAgentSelector {

View File

@@ -0,0 +1,113 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { migrateAutomationAction, type Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { editorStyles } from "../styles";
import { getAutomationActionType } from "./ha-automation-action-row";
@customElement("ha-automation-action-editor")
export default class HaAutomationActionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) action!: Action;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public yamlMode = false;
@property({ type: Boolean }) public indent = false;
@property({ type: Boolean, reflect: true }) public selected = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
const yamlMode = this.yamlMode || !this.uiSupported;
const type = getAutomationActionType(this.action);
return html`
<div
class=${classMap({
"card-content": true,
disabled:
this.disabled || (this.action.enabled === false && !this.yamlMode),
yaml: yamlMode,
indent: this.indent,
})}
>
${yamlMode
? html`
${!this.uiSupported
? html`
<ha-automation-editor-warning
.alertTitle=${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action"
)}
.localize=${this.hass.localize}
></ha-automation-editor-warning>
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
></ha-yaml-editor>
`
: html`
<div @value-changed=${this._onUiChanged}>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
disabled: this.disabled,
narrow: this.narrow,
optionsInSidebar: this.indent,
indent: this.indent,
inSidebar: this.inSidebar,
})}
</div>
`}
</div>
`;
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
value: migrateAutomationAction(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = {
...(this.action.alias ? { alias: this.action.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
static styles = editorStyles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-editor": HaAutomationActionEditor;
}
}

View File

@@ -15,28 +15,35 @@ import {
mdiStopCircleOutline, mdiStopCircleOutline,
} from "@mdi/js"; } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, html, nothing } 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 memoizeOne from "memoize-one";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert"; import "../../../../components/ha-automation-row";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-service-icon"; import "../../../../components/ha-service-icon";
import "../../../../components/ha-tooltip"; import "../../../../components/ha-tooltip";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import {
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action"; ACTION_BUILDING_BLOCKS,
import type { AutomationClipboard } from "../../../../data/automation"; ACTION_COMBINED_BLOCKS,
ACTION_ICONS,
YAML_ONLY_ACTION_TYPES,
} from "../../../../data/action";
import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { import {
floorsContext, floorsContext,
@@ -46,11 +53,12 @@ import {
import type { EntityRegistryEntry } from "../../../../data/entity_registry"; import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { FloorRegistryEntry } from "../../../../data/floor_registry"; import type { FloorRegistryEntry } from "../../../../data/floor_registry";
import type { LabelRegistryEntry } from "../../../../data/label_registry"; import type { LabelRegistryEntry } from "../../../../data/label_registry";
import type { Action, NonConditionAction } from "../../../../data/script"; import type {
import { Action,
getActionType, NonConditionAction,
migrateAutomationAction, RepeatAction,
} from "../../../../data/script"; } from "../../../../data/script";
import { getActionType } from "../../../../data/script";
import { describeAction } from "../../../../data/script_i18n"; import { describeAction } from "../../../../data/script_i18n";
import { callExecuteScript } from "../../../../data/service"; import { callExecuteScript } from "../../../../data/service";
import { import {
@@ -58,9 +66,12 @@ import {
showConfirmationDialog, showConfirmationDialog,
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-action-editor";
import type HaAutomationActionEditor from "./ha-automation-action-editor";
import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition"; import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay"; import "./types/ha-automation-action-delay";
@@ -69,28 +80,31 @@ import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if"; import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel"; import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media"; import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat"; import { getRepeatType } from "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-sequence"; import "./types/ha-automation-action-sequence";
import "./types/ha-automation-action-service"; import "./types/ha-automation-action-service";
import "./types/ha-automation-action-set_conversation_response"; import "./types/ha-automation-action-set_conversation_response";
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 { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
export const getType = (action: Action | undefined) => { export const getAutomationActionType = memoizeOne(
if (!action) { (action: Action | undefined) => {
return undefined; if (!action) {
return undefined;
}
if ("action" in action) {
return getActionType(action) as "action" | "play_media";
}
if (CONDITION_BUILDING_BLOCKS.some((key) => key in action)) {
return "condition" as const;
}
return Object.keys(ACTION_ICONS).find(
(option) => option in action
) as keyof typeof ACTION_ICONS;
} }
if ("action" in action) { );
return getActionType(action) as "action" | "play_media";
}
if (["and", "or", "not"].some((key) => key in action)) {
return "condition" as const;
}
return Object.keys(ACTION_ICONS).find(
(option) => option in action
) as keyof typeof ACTION_ICONS;
};
export interface ActionElement extends LitElement { export interface ActionElement extends LitElement {
action: Action; action: Action;
@@ -118,8 +132,6 @@ 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;
@@ -134,6 +146,9 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public last?: boolean; @property({ type: Boolean }) public last?: boolean;
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
state: false, state: false,
@@ -154,19 +169,27 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: floorsContext, subscribe: true }) @consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>; _floorReg!: Record<string, FloorRegistryEntry>;
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true; @state() private _uiModeAvailable = true;
@state() private _yamlMode = false; @state() private _yamlMode = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @state() private _selected = false;
@state() private _collapsed = false;
@state() private _warnings?: string[];
@query("ha-automation-action-editor")
private actionEditor?: HaAutomationActionEditor;
protected willUpdate(changedProperties: PropertyValues) { protected willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("yamlMode")) {
this._warnings = undefined;
}
if (!changedProperties.has("action")) { if (!changedProperties.has("action")) {
return; return;
} }
const type = getType(this.action); const type = getAutomationActionType(this.action);
this._uiModeAvailable = this._uiModeAvailable =
type !== undefined && !YAML_ONLY_ACTION_TYPES.has(type as any); type !== undefined && !YAML_ONLY_ACTION_TYPES.has(type as any);
if (!this._uiModeAvailable && !this._yamlMode) { if (!this._uiModeAvailable && !this._yamlMode) {
@@ -174,23 +197,207 @@ export default class HaAutomationActionRow extends LitElement {
} }
} }
protected updated(changedProperties: PropertyValues) { private _renderRow() {
if (!changedProperties.has("action")) { const type = getAutomationActionType(this.action);
return;
} return html`
if (this._yamlMode) { ${type === "service" && "action" in this.action && this.action.action
const yamlEditor = this._yamlEditor; ? html`
if (yamlEditor && yamlEditor.value !== this.action) { <ha-service-icon
yamlEditor.setValue(this.action); slot="leading-icon"
} class="action-icon"
} .hass=${this.hass}
.service=${this.action.action}
></ha-service-icon>
`
: html`
<ha-svg-icon
slot="leading-icon"
class="action-icon"
.path=${ACTION_ICONS[type!]}
></ha-svg-icon>
`}
<h3 slot="header">
${capitalizeFirstLetter(
describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
</h3>
<slot name="icons" slot="icons"></slot>
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<ha-tooltip
slot="icons"
.content=${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
>
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
</ha-tooltip>`
: nothing}
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._runAction}>
${this.hass.localize("ui.panel.config.automation.editor.actions.run")}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html` <ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || !!this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || !!this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<ha-automation-action-editor
.hass=${this.hass}
.action=${this.action}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.narrow=${this.narrow}
.uiSupported=${this._uiSupported(type!)}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>`
: nothing}
`;
} }
protected render() { protected render() {
if (!this.action) return nothing; if (!this.action) return nothing;
const type = getType(this.action); const type = getAutomationActionType(this.action);
const yamlMode = this._yamlMode;
const blockType =
type === "repeat"
? `repeat_${getRepeatType((this.action as RepeatAction).repeat)}`
: type;
return html` return html`
<ha-card outlined> <ha-card outlined>
@@ -203,245 +410,57 @@ export default class HaAutomationActionRow extends LitElement {
</div> </div>
` `
: nothing} : nothing}
<ha-expansion-panel left-chevron> ${this.optionsInSidebar
${type === "service" && "action" in this.action && this.action.action ? html`<ha-automation-row
? html` .disabled=${this.action.enabled === false}
<ha-service-icon @click=${this._toggleSidebar}
slot="leading-icon" .leftChevron=${[
class="action-icon" ...ACTION_BUILDING_BLOCKS,
.hass=${this.hass} ...ACTION_COMBINED_BLOCKS,
.service=${this.action.action} ].includes(blockType!)}
></ha-service-icon> .collapsed=${this._collapsed}
` .selected=${this._selected}
: html` @toggle-collapsed=${this._toggleCollapse}
<ha-svg-icon .buildingBlock=${[
slot="leading-icon" ...ACTION_BUILDING_BLOCKS,
class="action-icon" ...ACTION_COMBINED_BLOCKS,
.path=${ACTION_ICONS[type!]} ].includes(blockType!)}
></ha-svg-icon> >${this._renderRow()}</ha-automation-row
`} >`
<h3 slot="header"> : html`
${capitalizeFirstLetter( <ha-expansion-panel left-chevron>
describeAction( ${this._renderRow()}
this.hass, </ha-expansion-panel>
this._entityReg, `}
this._labelReg,
this._floorReg,
this.action
)
)}
</h3>
<slot name="icons" slot="icons"></slot>
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<ha-tooltip
slot="icons"
.content=${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
>
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
</ha-tooltip>`
: nothing}
<ha-md-button-menu
slot="icons"
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._runAction}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run"
)}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
"card-content": true,
disabled: this.action.enabled === false,
})}
>
${this._warnings
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"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"
)}
`
: ""}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<div
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._onUiChanged}
>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
narrow: this.narrow,
disabled: this.disabled,
})}
</div>
`}
</div>
</ha-expansion-panel>
</ha-card> </ha-card>
${this.optionsInSidebar &&
([...ACTION_BUILDING_BLOCKS, ...ACTION_COMBINED_BLOCKS].includes(
blockType!
) ||
(blockType === "condition" &&
CONDITION_BUILDING_BLOCKS.includes(
(this.action as Condition).condition
))) &&
!this._collapsed
? html`<ha-automation-action-editor
.hass=${this.hass}
.action=${this.action}
.narrow=${this.narrow}
.disabled=${this.disabled}
.uiSupported=${this._uiSupported(type!)}
indent
.selected=${this._selected}
@value-changed=${this._onValueChange}
></ha-automation-action-editor>`
: nothing}
`; `;
} }
private _handleUiModeNotAvailable(ev: CustomEvent) { private _onValueChange(event: CustomEvent) {
// Prevent possible parent action-row from switching to yamlMode // reload sidebar if sort, deleted,... happend
ev.stopPropagation(); if (this._selected && this.optionsInSidebar) {
this.openSidebar(event.detail.value);
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
} }
} }
@@ -456,8 +475,10 @@ export default class HaAutomationActionRow extends LitElement {
const enabled = !(this.action.enabled ?? true); const enabled = !(this.action.enabled ?? true);
const value = { ...this.action, enabled }; const value = { ...this.action, enabled };
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
if (this._yamlMode) { this.openSidebar(value); // refresh sidebar
this._yamlEditor?.setValue(value);
if (this._yamlMode && !this.optionsInSidebar) {
this.actionEditor?.yamlEditor?.setValue(value);
} }
}; };
@@ -508,36 +529,18 @@ export default class HaAutomationActionRow extends LitElement {
destructive: true, destructive: true,
confirm: () => { confirm: () => {
fireEvent(this, "value-changed", { value: null }); fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
}, },
}); });
}; };
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
value: migrateAutomationAction(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = {
...(this.action.alias ? { alias: this.action.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
private _switchUiMode() { private _switchUiMode() {
this._warnings = undefined;
this._yamlMode = false; this._yamlMode = false;
} }
private _switchYamlMode() { private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = true; this._yamlMode = true;
} }
@@ -574,8 +577,11 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,
}); });
if (this._yamlMode) {
this._yamlEditor?.setValue(value); if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this.actionEditor?.yamlEditor?.setValue(value);
} }
} }
}; };
@@ -591,6 +597,9 @@ export default class HaAutomationActionRow extends LitElement {
private _cutAction = () => { private _cutAction = () => {
this._setClipboard(); this._setClipboard();
fireEvent(this, "value-changed", { value: null }); fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
}; };
private _moveUp = () => { private _moveUp = () => {
@@ -607,82 +616,78 @@ export default class HaAutomationActionRow extends LitElement {
} else { } else {
this._switchYamlMode(); this._switchYamlMode();
} }
this.expand();
if (!this.optionsInSidebar) {
this.expand();
}
}; };
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(action?: Action): void {
if (this.narrow) {
this.scrollIntoView();
}
const sidebarAction = action ?? this.action;
const actionType = getAutomationActionType(sidebarAction);
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameAction();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
},
disable: this._onDisable,
delete: this._onDelete,
config: sidebarAction,
type: "action",
uiSupported: actionType ? this._uiSupported(actionType) : false,
yamlMode: this._yamlMode,
});
this._selected = true;
}
public expand() { public expand() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true; this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
}); });
} }
static get styles(): CSSResultGroup { private _uiSupported = memoizeOne(
return [ (type: string) =>
haStyle, customElements.get(`ha-automation-action-${type}`) !== undefined
css` );
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color); private _toggleCollapse() {
} this._collapsed = !this._collapsed;
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.action-icon {
display: none;
}
@media (min-width: 870px) {
.action-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
}
}
.card-content {
padding: 16px;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`,
];
} }
static styles = rowStyles;
} }
declare global { declare global {

View File

@@ -11,7 +11,11 @@ import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-sortable"; import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { getService, isService } from "../../../../data/action"; import {
ACTION_BUILDING_BLOCKS,
getService,
isService,
} from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import type { Action } from "../../../../data/script"; import type { Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
@@ -21,7 +25,7 @@ import {
showAddAutomationElementDialog, showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog"; } from "../show-add-automation-element-dialog";
import type HaAutomationActionRow from "./ha-automation-action-row"; import type HaAutomationActionRow from "./ha-automation-action-row";
import { getType } from "./ha-automation-action-row"; import { getAutomationActionType } from "./ha-automation-action-row";
@customElement("ha-automation-action") @customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement { export default class HaAutomationAction extends LitElement {
@@ -37,6 +41,9 @@ export default class HaAutomationAction extends LitElement {
@property({ attribute: false }) public highlightedActions?: Action[]; @property({ attribute: false }) public highlightedActions?: Action[];
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false; @state() private _showReorder = false;
@state() @state()
@@ -98,6 +105,7 @@ export default class HaAutomationAction extends LitElement {
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
.hass=${this.hass} .hass=${this.hass}
?highlight=${this.highlightedActions?.includes(action)} ?highlight=${this.highlightedActions?.includes(action)}
.optionsInSidebar=${this.optionsInSidebar}
> >
${this._showReorder && !this.disabled ${this._showReorder && !this.disabled
? html` ? html`
@@ -148,7 +156,17 @@ export default class HaAutomationAction extends LitElement {
"ha-automation-action-row:last-of-type" "ha-automation-action-row:last-of-type"
)!; )!;
row.updateComplete.then(() => { row.updateComplete.then(() => {
row.expand(); // on new condition open the settings in the sidebar, except for building blocks
const type = getAutomationActionType(row.action);
if (
type &&
this.optionsInSidebar &&
!ACTION_BUILDING_BLOCKS.includes(type)
) {
row.openSidebar();
} else if (!this.optionsInSidebar) {
row.expand();
}
row.scrollIntoView(); row.scrollIntoView();
row.focus(); row.focus();
}); });
@@ -168,7 +186,7 @@ export default class HaAutomationAction extends LitElement {
showAddAutomationElementDialog(this, { showAddAutomationElementDialog(this, {
type: "action", type: "action",
add: this._addAction, add: this._addAction,
clipboardItem: getType(this._clipboard?.action), clipboardItem: getAutomationActionType(this._clipboard?.action),
}); });
} }
@@ -176,7 +194,7 @@ export default class HaAutomationAction extends LitElement {
showAddAutomationElementDialog(this, { showAddAutomationElementDialog(this, {
type: "action", type: "action",
add: this._addAction, add: this._addAction,
clipboardItem: getType(this._clipboard?.action), clipboardItem: getAutomationActionType(this._clipboard?.action),
group: "building_blocks", group: "building_blocks",
}); });
} }
@@ -272,6 +290,7 @@ export default class HaAutomationAction extends LitElement {
// Ensure action is removed even after update // Ensure action is removed even after update
const actions = this.actions.filter((a) => a !== action); const actions = this.actions.filter((a) => a !== action);
fireEvent(this, "value-changed", { value: actions }); fireEvent(this, "value-changed", { value: actions });
fireEvent(this, "close-sidebar");
} }
private _actionChanged(ev: CustomEvent) { private _actionChanged(ev: CustomEvent) {
@@ -303,15 +322,18 @@ export default class HaAutomationAction extends LitElement {
static styles = css` static styles = css`
.actions { .actions {
padding: 16px; padding: 16px 0 16px 16px;
margin: -16px; margin: -16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
:host([root]) .actions {
padding-right: 8px;
}
.sortable-ghost { .sortable-ghost {
background: none; background: none;
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
} }
.sortable-drag { .sortable-drag {
background: none; background: none;

View File

@@ -7,8 +7,8 @@ import type { Action, ChooseAction, Option } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../../option/ha-automation-option"; import "../../option/ha-automation-option";
import type { ActionElement } from "../ha-automation-action-row";
import "../ha-automation-action"; import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
@customElement("ha-automation-action-choose") @customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement { export class HaChooseAction extends LitElement implements ActionElement {
@@ -20,6 +20,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public indent = false;
@state() private _showDefault = false; @state() private _showDefault = false;
public static get defaultConfig(): ChooseAction { public static get defaultConfig(): ChooseAction {
@@ -38,6 +40,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
@value-changed=${this._optionsChanged} @value-changed=${this._optionsChanged}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-option> ></ha-automation-option>
${this._showDefault || action.default ${this._showDefault || action.default
@@ -53,6 +56,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
@value-changed=${this._defaultChanged} @value-changed=${this._defaultChanged}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-action> ></ha-automation-action>
` `
: html` : html`

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement } from "lit"; import { css, html, LitElement, nothing } 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 { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -8,10 +8,24 @@ import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select"; import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select"; import type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation"; import type { Condition } from "../../../../../data/automation";
import { CONDITION_ICONS } from "../../../../../data/condition"; import {
CONDITION_BUILDING_BLOCKS,
CONDITION_ICONS,
} from "../../../../../data/condition";
import type { Entries, HomeAssistant } from "../../../../../types"; import type { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor"; import "../../condition/ha-automation-condition-editor";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
import "../../condition/types/ha-automation-condition-and";
import "../../condition/types/ha-automation-condition-device";
import "../../condition/types/ha-automation-condition-not";
import "../../condition/types/ha-automation-condition-numeric_state";
import "../../condition/types/ha-automation-condition-or";
import "../../condition/types/ha-automation-condition-state";
import "../../condition/types/ha-automation-condition-sun";
import "../../condition/types/ha-automation-condition-template";
import "../../condition/types/ha-automation-condition-time";
import "../../condition/types/ha-automation-condition-trigger";
import "../../condition/types/ha-automation-condition-zone";
@customElement("ha-automation-action-condition") @customElement("ha-automation-action-condition")
export class HaConditionAction extends LitElement implements ActionElement { export class HaConditionAction extends LitElement implements ActionElement {
@@ -21,36 +35,63 @@ export class HaConditionAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: Condition; @property({ attribute: false }) public action!: Condition;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "indent" }) public indent = false;
public static get defaultConfig(): Omit<Condition, "state" | "entity_id"> { public static get defaultConfig(): Omit<Condition, "state" | "entity_id"> {
return { condition: "state" }; return { condition: "state" };
} }
protected render() { protected render() {
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
this.action.condition
);
return html` return html`
<ha-select ${this.inSidebar || (!this.inSidebar && !this.indent)
fixedMenuPosition ? html`
.label=${this.hass.localize( <ha-select
"ui.panel.config.automation.editor.conditions.type_select" fixedMenuPosition
)} .label=${this.hass.localize(
.disabled=${this.disabled} "ui.panel.config.automation.editor.conditions.type_select"
.value=${this.action.condition} )}
naturalMenuWidth .disabled=${this.disabled}
@selected=${this._typeChanged} .value=${this.action.condition}
> naturalMenuWidth
${this._processedTypes(this.hass.localize).map( @selected=${this._typeChanged}
([opt, label, icon]) => html` >
<ha-list-item .value=${opt} graphic="icon"> ${this._processedTypes(this.hass.localize).map(
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon ([opt, label, icon]) => html`
></ha-list-item> <ha-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon
slot="graphic"
.path=${icon}
></ha-svg-icon
></ha-list-item>
`
)}
</ha-select>
` `
)} : nothing}
</ha-select> ${(this.indent && buildingBlock) ||
<ha-automation-condition-editor (this.inSidebar && !buildingBlock) ||
.condition=${this.action} (!this.indent && !this.inSidebar)
.disabled=${this.disabled} ? html`
.hass=${this.hass} <ha-automation-condition-editor
@value-changed=${this._conditionChanged} .condition=${this.action}
></ha-automation-condition-editor> .disabled=${this.disabled}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
.narrow=${this.narrow}
.uiSupported=${this._uiSupported(this.action.condition)}
.indent=${this.indent}
action
></ha-automation-condition-editor>
`
: nothing}
`; `;
} }
@@ -100,6 +141,11 @@ export class HaConditionAction extends LitElement implements ActionElement {
} }
} }
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-condition-${type}`) !== undefined
);
static styles = css` static styles = css`
ha-select { ha-select {
margin-bottom: 24px; margin-bottom: 24px;

View File

@@ -20,6 +20,8 @@ export class HaIfAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public indent = false;
@state() private _showElse = false; @state() private _showElse = false;
public static get defaultConfig(): IfAction { public static get defaultConfig(): IfAction {
@@ -44,6 +46,7 @@ export class HaIfAction extends LitElement implements ActionElement {
@value-changed=${this._ifChanged} @value-changed=${this._ifChanged}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-condition> ></ha-automation-condition>
<h3> <h3>
@@ -57,6 +60,7 @@ export class HaIfAction extends LitElement implements ActionElement {
@value-changed=${this._thenChanged} @value-changed=${this._thenChanged}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-action> ></ha-automation-action>
${this._showElse || action.else ${this._showElse || action.else
? html` ? html`
@@ -71,9 +75,10 @@ export class HaIfAction extends LitElement implements ActionElement {
@value-changed=${this._elseChanged} @value-changed=${this._elseChanged}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-action> ></ha-automation-action>
` `
: html` <div class="link-button-row"> : html`<div class="link-button-row">
<button <button
class="link" class="link"
@click=${this._addElse} @click=${this._addElse}

View File

@@ -19,6 +19,8 @@ export class HaParallelAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: ParallelAction; @property({ attribute: false }) public action!: ParallelAction;
@property({ type: Boolean }) public indent = false;
public static get defaultConfig(): ParallelAction { public static get defaultConfig(): ParallelAction {
return { return {
parallel: [], parallel: [],
@@ -35,6 +37,7 @@ export class HaParallelAction extends LitElement implements ActionElement {
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._actionsChanged} @value-changed=${this._actionsChanged}
.hass=${this.hass} .hass=${this.hass}
.optionsInSidebar=${this.indent}
></ha-automation-action> ></ha-automation-action>
`; `;
} }

View File

@@ -18,8 +18,10 @@ import type {
} from "../../../../../components/ha-form/types"; } from "../../../../../components/ha-form/types";
const OPTIONS = ["count", "while", "until", "for_each"] as const; const OPTIONS = ["count", "while", "until", "for_each"] as const;
type RepeatType = (typeof OPTIONS)[number];
const getType = (action) => OPTIONS.find((option) => option in action); export const getRepeatType = (action: RepeatAction["repeat"]) =>
OPTIONS.find((option) => option in action);
@customElement("ha-automation-action-repeat") @customElement("ha-automation-action-repeat")
export class HaRepeatAction extends LitElement implements ActionElement { export class HaRepeatAction extends LitElement implements ActionElement {
@@ -27,16 +29,27 @@ export class HaRepeatAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public action!: RepeatAction; @property({ attribute: false }) public action!: RepeatAction;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "indent" }) public indent = false;
public static get defaultConfig(): RepeatAction { public static get defaultConfig(): RepeatAction {
return { repeat: { count: 2, sequence: [] } }; return { repeat: { count: 2, sequence: [] } };
} }
private _schema = memoizeOne( private _schema = memoizeOne(
(type: string, template: boolean) => (
type: RepeatType,
template: boolean,
inSidebar: boolean,
indent: boolean
) =>
[ [
...(type === "count" ...(type === "count" && (inSidebar || (!inSidebar && !indent))
? ([ ? ([
{ {
name: "count", name: "count",
@@ -47,17 +60,20 @@ export class HaRepeatAction extends LitElement implements ActionElement {
}, },
] as const satisfies readonly HaFormSchema[]) ] as const satisfies readonly HaFormSchema[])
: []), : []),
...(type === "until" || type === "while" ...((type === "until" || type === "while") &&
(indent || (!inSidebar && !indent))
? ([ ? ([
{ {
name: type, name: type,
selector: { selector: {
condition: {}, condition: {
optionsInSidebar: indent,
},
}, },
}, },
] as const satisfies readonly HaFormSchema[]) ] as const satisfies readonly HaFormSchema[])
: []), : []),
...(type === "for_each" ...(type === "for_each" && (inSidebar || (!inSidebar && !indent))
? ([ ? ([
{ {
name: "for_each", name: "for_each",
@@ -66,23 +82,31 @@ export class HaRepeatAction extends LitElement implements ActionElement {
}, },
] as const satisfies readonly HaFormSchema[]) ] as const satisfies readonly HaFormSchema[])
: []), : []),
{ ...(indent || (!inSidebar && !indent)
name: "sequence", ? ([
selector: { {
action: {}, name: "sequence",
}, selector: {
}, action: {
optionsInSidebar: indent,
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const satisfies readonly HaFormSchema[] ] as const satisfies readonly HaFormSchema[]
); );
protected render() { protected render() {
const action = this.action.repeat; const action = this.action.repeat;
const type = getType(action); const type = getRepeatType(action);
const schema = this._schema( const schema = this._schema(
type ?? "count", type ?? "count",
"count" in action && typeof action.count === "string" "count" in action && typeof action.count === "string"
? isTemplate(action.count) ? isTemplate(action.count)
: false : false,
this.inSidebar,
this.indent
); );
const data = { ...action, type }; const data = { ...action, type };
@@ -93,6 +117,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
.narrow=${this.narrow}
></ha-form>`; ></ha-form>`;
} }
@@ -102,7 +127,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
const newType = newVal.type; const newType = newVal.type;
delete newVal.type; delete newVal.type;
const oldType = getType(this.action.repeat); const oldType = getRepeatType(this.action.repeat);
if (newType !== oldType) { if (newType !== oldType) {
if (newType === "count") { if (newType === "count") {

View File

@@ -19,6 +19,8 @@ export class HaSequenceAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: SequenceAction; @property({ attribute: false }) public action!: SequenceAction;
@property({ type: Boolean }) public indent = false;
public static get defaultConfig(): SequenceAction { public static get defaultConfig(): SequenceAction {
return { return {
sequence: [], sequence: [],
@@ -35,6 +37,7 @@ export class HaSequenceAction extends LitElement implements ActionElement {
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._actionsChanged} @value-changed=${this._actionsChanged}
.hass=${this.hass} .hass=${this.hass}
.optionsInSidebar=${this.indent}
></ha-automation-action> ></ha-automation-action>
`; `;
} }

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../../../common/array/ensure-array"; import { ensureArray } from "../../../../../common/array/ensure-array";
import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import { createDurationData } from "../../../../../common/datetime/create_duration_data";
@@ -24,6 +24,12 @@ export class HaWaitForTriggerAction
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "indent" }) public indent = false;
public static get defaultConfig(): WaitForTriggerAction { public static get defaultConfig(): WaitForTriggerAction {
return { wait_for_trigger: [] }; return { wait_for_trigger: [] };
} }
@@ -32,34 +38,43 @@ export class HaWaitForTriggerAction
const timeData = createDurationData(this.action.timeout); const timeData = createDurationData(this.action.timeout);
return html` return html`
<ha-duration-input ${this.inSidebar || (!this.inSidebar && !this.indent)
.label=${this.hass.localize( ? html`
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout" <ha-duration-input
)} .label=${this.hass.localize(
.data=${timeData} "ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
.disabled=${this.disabled} )}
enable-millisecond .data=${timeData}
@value-changed=${this._timeoutChanged} .disabled=${this.disabled}
></ha-duration-input> enable-millisecond
<ha-formfield @value-changed=${this._timeoutChanged}
.disabled=${this.disabled} ></ha-duration-input>
.label=${this.hass.localize( <ha-formfield
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout" .disabled=${this.disabled}
)} .label=${this.hass.localize(
> "ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout"
<ha-switch )}
.checked=${this.action.continue_on_timeout ?? true} >
.disabled=${this.disabled} <ha-switch
@change=${this._continueChanged} .checked=${this.action.continue_on_timeout ?? true}
></ha-switch> .disabled=${this.disabled}
</ha-formfield> @change=${this._continueChanged}
<ha-automation-trigger ></ha-switch>
.triggers=${ensureArray(this.action.wait_for_trigger)} </ha-formfield>
.hass=${this.hass} `
.disabled=${this.disabled} : nothing}
.name=${"wait_for_trigger"} ${this.indent || (!this.inSidebar && !this.indent)
@value-changed=${this._valueChanged} ? html`<ha-automation-trigger
></ha-automation-trigger> class=${!this.inSidebar && !this.indent ? "expansion-panel" : ""}
.triggers=${ensureArray(this.action.wait_for_trigger)}
.hass=${this.hass}
.disabled=${this.disabled}
.name=${"wait_for_trigger"}
@value-changed=${this._valueChanged}
.optionsInSidebar=${this.indent}
.narrow=${this.narrow}
></ha-automation-trigger>`
: nothing}
`; `;
} }
@@ -86,7 +101,7 @@ export class HaWaitForTriggerAction
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }
ha-automation-trigger { ha-automation-trigger.expansion-panel {
display: block; display: block;
margin-top: 24px; margin-top: 24px;
} }

View File

@@ -652,6 +652,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
ha-dialog { ha-dialog {
--dialog-content-padding: 0; --dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh; --mdc-dialog-max-height: 60vh;
--mdc-dialog-max-height: 60dvh;
} }
@media all and (min-width: 550px) { @media all and (min-width: 550px) {
ha-dialog { ha-dialog {

View File

@@ -1,12 +1,15 @@
import { mdiContentSave } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { html, nothing } from "lit"; import { css, html, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import type { BlueprintAutomationConfig } from "../../../data/automation"; import type { BlueprintAutomationConfig } from "../../../data/automation";
import { fetchBlueprints } from "../../../data/blueprint"; import { fetchBlueprints } from "../../../data/blueprint";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor"; import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
import { saveFabStyles } from "./styles";
@customElement("blueprint-automation-editor") @customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor { export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
@@ -14,6 +17,10 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public stateObj?: HassEntity;
@property({ type: Boolean }) public saving = false;
@property({ type: Boolean }) public dirty = false;
protected get _config(): BlueprintAutomationConfig { protected get _config(): BlueprintAutomationConfig {
return this.config; return this.config;
} }
@@ -47,9 +54,24 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
></ha-markdown>` ></ha-markdown>`
: nothing} : nothing}
${this.renderCard()} ${this.renderCard()}
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
`; `;
} }
private _saveAutomation() {
fireEvent(this, "save-automation");
}
protected async _getBlueprints() { protected async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "automation"); this._blueprints = await fetchBlueprints(this.hass, "automation");
} }
@@ -62,6 +84,24 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
entity_id: this.stateObj.entity_id, entity_id: this.stateObj.entity_id,
}); });
} }
static get styles(): CSSResultGroup {
return [
HaBlueprintGenericEditor.styles,
saveFabStyles,
css`
:host {
position: relative;
height: 100%;
min-height: calc(100vh - 85px);
min-height: calc(100dvh - 85px);
}
ha-fab {
position: fixed;
}
`,
];
}
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -1,24 +1,16 @@
import { html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
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 "../../../../components/ha-yaml-editor"; import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../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";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-condition-and"; import "../ha-automation-editor-warning";
import "./types/ha-automation-condition-device"; import { editorStyles } from "../styles";
import "./types/ha-automation-condition-not";
import "./types/ha-automation-condition-numeric_state";
import "./types/ha-automation-condition-or";
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";
@customElement("ha-automation-condition-editor") @customElement("ha-automation-condition-editor")
export default class HaAutomationConditionEditor extends LitElement { export default class HaAutomationConditionEditor extends LitElement {
@@ -30,46 +22,71 @@ export default class HaAutomationConditionEditor extends LitElement {
@property({ attribute: false }) public yamlMode = false; @property({ attribute: false }) public yamlMode = false;
@property({ type: Boolean }) public indent = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public selected = false;
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
private _processedCondition = memoizeOne((condition) => private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition) expandConditionWithShorthand(condition)
); );
protected render() { protected render() {
const condition = this._processedCondition(this.condition); const condition = this._processedCondition(this.condition);
const supported = const yamlMode = this.yamlMode || !this.uiSupported;
customElements.get(`ha-automation-condition-${condition.condition}`) !==
undefined;
const yamlMode = this.yamlMode || !supported;
return html` return html`
${yamlMode <div
? html` class=${classMap({
${!supported "card-content": true,
? html` disabled:
${this.hass.localize( this.disabled ||
"ui.panel.config.automation.editor.conditions.unsupported_condition", (this.condition.enabled === false && !this.yamlMode),
{ condition: condition.condition } yaml: yamlMode,
)} indent: this.indent,
` })}
: ""} >
<ha-yaml-editor ${yamlMode
.hass=${this.hass} ? html`
.defaultValue=${this.condition} ${!this.uiSupported
@value-changed=${this._onYamlChange} ? html`
.readOnly=${this.disabled} <ha-automation-editor-warning
></ha-yaml-editor> .alertTitle=${this.hass.localize(
` "ui.panel.config.automation.editor.conditions.unsupported_condition",
: html` { condition: condition.condition }
<div @value-changed=${this._onUiChanged}> )}
${dynamicElement( .localize=${this.hass.localize}
`ha-automation-condition-${condition.condition}`, ></ha-automation-editor-warning>
{ `
hass: this.hass, : nothing}
condition: condition, <ha-yaml-editor
disabled: this.disabled, .hass=${this.hass}
} .defaultValue=${this.condition}
)} @value-changed=${this._onYamlChange}
</div> .readOnly=${this.disabled}
`} ></ha-yaml-editor>
`
: html`
<div @value-changed=${this._onUiChanged}>
${dynamicElement(
`ha-automation-condition-${condition.condition}`,
{
hass: this.hass,
condition: condition,
disabled: this.disabled,
optionsInSidebar: this.indent,
narrow: this.narrow,
}
)}
</div>
`}
</div>
`; `;
} }
@@ -91,7 +108,20 @@ export default class HaAutomationConditionEditor extends LitElement {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
} }
static styles = haStyle; static styles = [
editorStyles,
css`
:host([action]) .card-content {
padding: 0;
}
:host([action]) .card-content.indent {
margin-left: 0;
margin-right: 0;
padding: 0;
border-left: none;
}
`,
];
} }
declare global { declare global {

View File

@@ -16,26 +16,32 @@ import {
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, 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 { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-md-button-menu"; import "../../../../components/ha-automation-row";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type { import type {
AutomationClipboard, AutomationClipboard,
Condition, Condition,
} from "../../../../data/automation"; } from "../../../../data/automation";
import { testCondition } from "../../../../data/automation"; import { testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n"; import { describeCondition } from "../../../../data/automation_i18n";
import { CONDITION_ICONS } from "../../../../data/condition"; import {
CONDITION_BUILDING_BLOCKS,
CONDITION_ICONS,
} from "../../../../data/condition";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry"; import type { EntityRegistryEntry } from "../../../../data/entity_registry";
@@ -44,16 +50,27 @@ import {
showConfirmationDialog, showConfirmationDialog,
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-condition-editor"; import "./ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
import "./types/ha-automation-condition-and";
import "./types/ha-automation-condition-device";
import "./types/ha-automation-condition-not";
import "./types/ha-automation-condition-numeric_state";
import "./types/ha-automation-condition-or";
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";
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
@@ -91,6 +108,15 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public last?: boolean; @property({ type: Boolean }) public last?: boolean;
@property({ type: Boolean }) public narrow = false;
@state() private _collapsed = false;
@state() private _warnings?: string[];
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
state: false, state: false,
@@ -101,23 +127,202 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _yamlMode = false; @state() private _yamlMode = false;
@state() private _warnings?: string[];
@state() private _testing = false; @state() private _testing = false;
@state() private _testingResult?: boolean; @state() private _testingResult?: boolean;
@state() private _selected = false;
@state() @state()
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@query("ha-automation-condition-editor")
public conditionEditor?: HaAutomationConditionEditor;
private _renderRow() {
return html`
<ha-svg-icon
slot="leading-icon"
class="condition-icon"
.path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon>
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<ha-md-menu-item .clickAction=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._uiSupported(this.condition.condition) ||
!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<ha-automation-condition-editor
.hass=${this.hass}
.condition=${this.condition}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.uiSupported=${this._uiSupported(this.condition.condition)}
.narrow=${this.narrow}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor>`
: nothing}
`;
}
protected render() { protected render() {
if (!this.condition) { if (!this.condition) {
return nothing; return nothing;
} }
return html` return html`
<ha-card outlined> <ha-card
outlined
class=${classMap({
selected: this._selected,
"building-block":
this.optionsInSidebar &&
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition) &&
!this._collapsed,
})}
>
${this.condition.enabled === false ${this.condition.enabled === false
? html` ? html`
<div class="disabled-bar"> <div class="disabled-bar">
@@ -126,187 +331,27 @@ export default class HaAutomationConditionRow extends LitElement {
)} )}
</div> </div>
` `
: ""} : nothing}
${this.optionsInSidebar
<ha-expansion-panel left-chevron> ? html`<ha-automation-row
<ha-svg-icon .disabled=${this.condition.enabled === false}
slot="leading-icon" .leftChevron=${CONDITION_BUILDING_BLOCKS.includes(
class="condition-icon" this.condition.condition
.path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon>
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<ha-md-menu-item .clickAction=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)} )}
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon> .collapsed=${this._collapsed}
</ha-md-menu-item> .selected=${this._selected}
<ha-md-menu-item @click=${this._toggleSidebar}
.clickAction=${this._renameCondition} @toggle-collapsed=${this._toggleCollapse}
.disabled=${this.disabled} .buildingBlock=${CONDITION_BUILDING_BLOCKS.includes(
> this.condition.condition
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)} )}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon> >${this._renderRow()}</ha-automation-row
</ha-md-menu-item> >`
: html`
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider> <ha-expansion-panel left-chevron>
${this._renderRow()}
<ha-md-menu-item </ha-expansion-panel>
.clickAction=${this._duplicateCondition} `}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
"card-content": true,
disabled: this.condition.enabled === false,
})}
>
${this._warnings
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"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>`
: ""}
<ha-automation-condition-editor
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._handleChangeEvent}
.yamlMode=${this._yamlMode}
.disabled=${this.disabled}
.hass=${this.hass}
.condition=${this.condition}
></ha-automation-condition-editor>
</div>
</ha-expansion-panel>
<div <div
class="testing ${classMap({ class="testing ${classMap({
active: this._testing, active: this._testing,
@@ -323,21 +368,35 @@ export default class HaAutomationConditionRow extends LitElement {
)} )}
</div> </div>
</ha-card> </ha-card>
${this.optionsInSidebar &&
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition) &&
!this._collapsed
? html`<ha-automation-condition-editor
.hass=${this.hass}
.condition=${this.condition}
.disabled=${this.disabled}
.uiSupported=${this._uiSupported(this.condition.condition)}
indent
.selected=${this._selected}
.narrow=${this.narrow}
@value-changed=${this._onValueChange}
></ha-automation-condition-editor>`
: nothing}
`; `;
} }
private _handleUiModeNotAvailable(ev: CustomEvent) { protected willUpdate(changedProperties) {
// Prevent possible parent action-row from switching to yamlMode // on yaml toggle --> clear warnings
ev.stopPropagation(); if (changedProperties.has("yamlMode")) {
this._warnings = handleStructError(this.hass, ev.detail).warnings; this._warnings = undefined;
if (!this._yamlMode) {
this._yamlMode = true;
} }
} }
private _handleChangeEvent(ev: CustomEvent) { private _onValueChange(event: CustomEvent) {
if (ev.detail.yaml) { // reload sidebar if sort, deleted,... happend
this._warnings = undefined; if (this._selected && this.optionsInSidebar) {
this.openSidebar(event.detail.value);
} }
} }
@@ -352,6 +411,11 @@ export default class HaAutomationConditionRow extends LitElement {
const enabled = !(this.condition.enabled ?? true); const enabled = !(this.condition.enabled ?? true);
const value = { ...this.condition, enabled }; const value = { ...this.condition, enabled };
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
this.openSidebar(value); // refresh sidebar
if (this._yamlMode && !this.optionsInSidebar) {
this.conditionEditor?.yamlEditor?.setValue(value);
}
}; };
private _onDelete = () => { private _onDelete = () => {
@@ -367,17 +431,18 @@ export default class HaAutomationConditionRow extends LitElement {
destructive: true, destructive: true,
confirm: () => { confirm: () => {
fireEvent(this, "value-changed", { value: null }); fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
}, },
}); });
}; };
private _switchUiMode() { private _switchUiMode() {
this._warnings = undefined;
this._yamlMode = false; this._yamlMode = false;
} }
private _switchYamlMode() { private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = true; this._yamlMode = true;
} }
@@ -463,6 +528,12 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,
}); });
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this.conditionEditor?.yamlEditor?.setValue(value);
}
} }
}; };
@@ -477,6 +548,9 @@ export default class HaAutomationConditionRow extends LitElement {
private _cutCondition = () => { private _cutCondition = () => {
this._setClipboard(); this._setClipboard();
fireEvent(this, "value-changed", { value: null }); fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
}; };
private _moveUp = () => { private _moveUp = () => {
@@ -493,7 +567,10 @@ export default class HaAutomationConditionRow extends LitElement {
} else { } else {
this._switchYamlMode(); this._switchYamlMode();
} }
this.expand();
if (!this.optionsInSidebar) {
this.expand();
}
}; };
public expand() { public expand() {
@@ -502,52 +579,68 @@ export default class HaAutomationConditionRow extends LitElement {
}); });
} }
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(condition?: Condition): void {
if (this.narrow) {
this.scrollIntoView();
}
const sidebarCondition = condition || this.condition;
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameCondition();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
},
disable: this._onDisable,
delete: this._onDelete,
config: sidebarCondition,
type: "condition",
uiSupported: this._uiSupported(sidebarCondition.condition),
yamlMode: this._yamlMode,
});
this._selected = true;
}
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-condition-${type}`) !== undefined
);
private _toggleCollapse() {
this._collapsed = !this._collapsed;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, rowStyles,
css` css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.condition-icon {
display: none;
}
@media (min-width: 870px) {
.condition-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
}
}
.card-content {
padding: 16px;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.testing { .testing {
position: absolute; position: absolute;
top: 0px; top: 0px;
@@ -562,17 +655,8 @@ export default class HaAutomationConditionRow extends LitElement {
overflow: hidden; overflow: hidden;
transition: max-height 0.3s; transition: max-height 0.3s;
text-align: center; text-align: center;
border-top-right-radius: calc( border-top-right-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
var(--ha-card-border-radius, 12px) - var( border-top-left-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
); );
} }
.testing.active { .testing.active {
@@ -584,15 +668,6 @@ export default class HaAutomationConditionRow extends LitElement {
.testing.pass { .testing.pass {
background-color: var(--success-color); background-color: var(--success-color);
} }
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`, `,
]; ];
} }

View File

@@ -23,6 +23,7 @@ import {
} from "../show-add-automation-element-dialog"; } from "../show-add-automation-element-dialog";
import "./ha-automation-condition-row"; import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row"; import type HaAutomationConditionRow from "./ha-automation-condition-row";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
@customElement("ha-automation-condition") @customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement { export default class HaAutomationCondition extends LitElement {
@@ -34,8 +35,13 @@ export default class HaAutomationCondition extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public root = false; @property({ type: Boolean }) public root = false;
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false; @state() private _showReorder = false;
@state() @state()
@@ -96,7 +102,15 @@ export default class HaAutomationCondition extends LitElement {
"ha-automation-condition-row:last-of-type" "ha-automation-condition-row:last-of-type"
)!; )!;
row.updateComplete.then(() => { row.updateComplete.then(() => {
row.expand(); // on new condition open the settings in the sidebar, except for building blocks
if (
this.optionsInSidebar &&
!CONDITION_BUILDING_BLOCKS.includes(row.condition.condition)
) {
row.openSidebar();
} else if (!this.optionsInSidebar) {
row.expand();
}
row.scrollIntoView(); row.scrollIntoView();
row.focus(); row.focus();
}); });
@@ -140,12 +154,14 @@ export default class HaAutomationCondition extends LitElement {
.totalConditions=${this.conditions.length} .totalConditions=${this.conditions.length}
.condition=${cond} .condition=${cond}
.disabled=${this.disabled} .disabled=${this.disabled}
.narrow=${this.narrow}
@duplicate=${this._duplicateCondition} @duplicate=${this._duplicateCondition}
@move-down=${this._moveDown} @move-down=${this._moveDown}
@move-up=${this._moveUp} @move-up=${this._moveUp}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
.hass=${this.hass} .hass=${this.hass}
?highlight=${this.highlightedConditions?.includes(cond)} ?highlight=${this.highlightedConditions?.includes(cond)}
.optionsInSidebar=${this.optionsInSidebar}
> >
${this._showReorder && !this.disabled ${this._showReorder && !this.disabled
? html` ? html`
@@ -292,6 +308,7 @@ export default class HaAutomationCondition extends LitElement {
// Ensure condition is removed even after update // Ensure condition is removed even after update
const conditions = this.conditions.filter((c) => c !== condition); const conditions = this.conditions.filter((c) => c !== condition);
fireEvent(this, "value-changed", { value: conditions }); fireEvent(this, "value-changed", { value: conditions });
fireEvent(this, "close-sidebar");
} }
private _conditionChanged(ev: CustomEvent) { private _conditionChanged(ev: CustomEvent) {
@@ -325,15 +342,18 @@ export default class HaAutomationCondition extends LitElement {
static styles = css` static styles = css`
.conditions { .conditions {
padding: 16px; padding: 16px 0 16px 16px;
margin: -16px; margin: -16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
:host([root]) .conditions {
padding-right: 8px;
}
.sortable-ghost { .sortable-ghost {
background: none; background: none;
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
} }
.sortable-drag { .sortable-drag {
background: none; background: none;
@@ -342,9 +362,6 @@ export default class HaAutomationCondition extends LitElement {
display: block; display: block;
scroll-margin-top: 48px; scroll-margin-top: 48px;
} }
.buttons {
order: 1;
}
.handle { .handle {
padding: 12px; padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */ cursor: move; /* fallback if grab cursor is unsupported */
@@ -358,6 +375,7 @@ export default class HaAutomationCondition extends LitElement {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
order: 1;
} }
`; `;
} }

View File

@@ -17,6 +17,11 @@ export abstract class HaLogicalCondition
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
protected render() { protected render() {
return html` return html`
<ha-automation-condition <ha-automation-condition
@@ -24,6 +29,8 @@ export abstract class HaLogicalCondition
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
.optionsInSidebar=${this.optionsInSidebar}
.narrow=${this.narrow}
></ha-automation-condition> ></ha-automation-condition>
`; `;
} }

View File

@@ -232,6 +232,7 @@ class DialogNewAutomation extends LitElement implements HassDialog {
ha-dialog { ha-dialog {
--dialog-content-padding: 0; --dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh; --mdc-dialog-max-height: 60vh;
--mdc-dialog-max-height: 60dvh;
} }
@media all and (min-width: 550px) { @media all and (min-width: 550px) {
ha-dialog { ha-dialog {

View File

@@ -0,0 +1,36 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-alert";
@customElement("ha-automation-editor-warning")
export class HaAutomationEditorWarning extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: "alert-title" }) public alertTitle?: string;
@property({ attribute: false }) public warnings: string[] = [];
protected render() {
return html`
<ha-alert
alert-type="warning"
.title=${this.alertTitle ||
this.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>`
: nothing}
${this.localize("ui.errors.config.edit_in_yaml_supported")}
</ha-alert>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-editor-warning": HaAutomationEditorWarning;
}
}

View File

@@ -28,9 +28,9 @@ import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl"; import { computeRTL } from "../../../common/util/compute_rtl";
import { promiseTimeout } from "../../../common/util/promise-timeout"; import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status"; import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-fade-in"; import "../../../components/ha-fade-in";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
@@ -97,6 +97,7 @@ declare global {
"move-down": undefined; "move-down": undefined;
"move-up": undefined; "move-up": undefined;
duplicate: undefined; duplicate: undefined;
"save-automation": undefined;
} }
} }
@@ -403,61 +404,65 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</ha-list-item> </ha-list-item>
</ha-button-menu> </ha-button-menu>
<div <div
class="content ${classMap({ class=${this._mode === "yaml" ? "yaml-mode" : ""}
"yaml-mode": this._mode === "yaml",
})}"
@subscribe-automation-config=${this._subscribeAutomationConfig} @subscribe-automation-config=${this._subscribeAutomationConfig}
> >
${this._errors || stateObj?.state === UNAVAILABLE <div class="error-wrapper">
? html`<ha-alert ${this._errors || stateObj?.state === UNAVAILABLE
alert-type="error" ? html`<ha-alert
.title=${stateObj?.state === UNAVAILABLE alert-type="error"
? this.hass.localize( .title=${stateObj?.state === UNAVAILABLE
"ui.panel.config.automation.editor.unavailable" ? this.hass.localize(
) "ui.panel.config.automation.editor.unavailable"
: undefined} )
> : undefined}
${this._errors || this._validationErrors} >
${stateObj?.state === UNAVAILABLE ${this._errors || this._validationErrors}
? html`<ha-svg-icon ${stateObj?.state === UNAVAILABLE
slot="icon" ? html`<ha-svg-icon
.path=${mdiRobotConfused} slot="icon"
></ha-svg-icon>` .path=${mdiRobotConfused}
: nothing} ></ha-svg-icon>`
</ha-alert>` : nothing}
: ""}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
)}
<div slot="action" style="display: flex;">
<ha-button appearance="plain" @click=${this._takeControlSave}
>${this.hass.localize("ui.common.yes")}</ha-button
>
<ha-button appearance="plain" @click=${this._revertBlueprint}
>${this.hass.localize("ui.common.no")}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert alert-type="warning" dismissable
>${this.hass.localize(
"ui.panel.config.automation.editor.read_only"
)}
<ha-button
appearance="filled"
size="small"
variant="warning"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.automation.editor.migrate"
)}
</ha-button>
</ha-alert>` </ha-alert>`
: nothing} : ""}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
)}
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
>${this.hass.localize("ui.common.yes")}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
>${this.hass.localize("ui.common.no")}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert alert-type="warning" dismissable
>${this.hass.localize(
"ui.panel.config.automation.editor.read_only"
)}
<ha-button
appearance="filled"
size="small"
variant="warning"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.automation.editor.migrate"
)}
</ha-button>
</ha-alert>`
: nothing}
</div>
${this._mode === "gui" ${this._mode === "gui"
? html` ? html`
<div <div
@@ -474,7 +479,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.stateObj=${stateObj} .stateObj=${stateObj}
.config=${this._config} .config=${this._config}
.disabled=${Boolean(this._readOnly)} .disabled=${Boolean(this._readOnly)}
.saving=${this._saving}
.dirty=${this._dirty}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
></blueprint-automation-editor> ></blueprint-automation-editor>
` `
: html` : html`
@@ -486,7 +494,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.config=${this._config} .config=${this._config}
.disabled=${Boolean(this._readOnly)} .disabled=${Boolean(this._readOnly)}
.dirty=${this._dirty} .dirty=${this._dirty}
.saving=${this._saving}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation} @editor-save=${this._handleSaveAutomation}
></manual-automation-editor> ></manual-automation-editor>
`} `}
@@ -521,21 +531,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
@editor-save=${this._handleSaveAutomation} @editor-save=${this._handleSaveAutomation}
.showErrors=${false} .showErrors=${false}
disable-fullscreen disable-fullscreen
></ha-yaml-editor>` ></ha-yaml-editor>
<ha-fab
slot="fab"
class=${this._dirty ? "dirty" : ""}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}
.disabled=${this._saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon
slot="icon"
.path=${mdiContentSave}
></ha-svg-icon>
</ha-fab>`
: nothing} : nothing}
</div> </div>
<ha-fab
slot="fab"
class=${classMap({
dirty: !this._readOnly && this._dirty,
})}
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
.disabled=${this._saving}
extended
@click=${this._handleSaveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
</hass-subpage> </hass-subpage>
`; `;
} }
@@ -1102,9 +1115,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
align-items: center; align-items: center;
height: 100%; height: 100%;
} }
.content {
padding-bottom: 20px;
}
.yaml-mode { .yaml-mode {
height: 100%; height: 100%;
display: flex; display: flex;
@@ -1112,13 +1122,34 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
padding-bottom: 0; padding-bottom: 0;
} }
manual-automation-editor, manual-automation-editor,
blueprint-automation-editor, blueprint-automation-editor {
:not(.yaml-mode) > ha-alert {
margin: 0 auto; margin: 0 auto;
max-width: 1040px; max-width: 1040px;
padding: 28px 20px 0; padding: 28px 20px 0;
display: block; display: block;
} }
:not(.yaml-mode) > .error-wrapper {
position: absolute;
top: 4px;
z-index: 3;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
:not(.yaml-mode) > .error-wrapper ha-alert {
background-color: var(--card-background-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border-radius: var(--ha-border-radius-sm);
}
manual-automation-editor {
max-width: 1540px;
padding: 0 12px;
}
ha-yaml-editor { ha-yaml-editor {
flex-grow: 1; flex-grow: 1;
--actions-border-radius: 0; --actions-border-radius: 0;
@@ -1135,14 +1166,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
margin-inline-end: 8px; margin-inline-end: 8px;
margin-inline-start: initial; margin-inline-start: initial;
} }
ha-fab {
position: relative;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: 0;
}
li[role="separator"] { li[role="separator"] {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }
@@ -1160,6 +1183,15 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
max-width: 1040px; max-width: 1040px;
padding: 28px 20px 0; padding: 28px 20px 0;
} }
ha-fab {
position: fixed;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: 16px;
}
`, `,
]; ];
} }

View File

@@ -0,0 +1,412 @@
import {
mdiClose,
mdiDelete,
mdiDotsVertical,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { css, html, LitElement, nothing } 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 { stopPropagation } from "../../../common/dom/stop_propagation";
import { handleStructError } from "../../../common/structs/handle-errors";
import type { LocalizeKeys } from "../../../common/translations/localize";
import "../../../components/ha-card";
import "../../../components/ha-dialog-header";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import type { Condition, Trigger } from "../../../data/automation";
import type { Action, RepeatAction } from "../../../data/script";
import { isTriggerList } from "../../../data/trigger";
import type { HomeAssistant } from "../../../types";
import "./action/ha-automation-action-editor";
import { getAutomationActionType } from "./action/ha-automation-action-row";
import { getRepeatType } from "./action/types/ha-automation-action-repeat";
import "./condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./condition/ha-automation-condition-editor";
import "./ha-automation-editor-warning";
import "./trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./trigger/ha-automation-trigger-editor";
import { ACTION_BUILDING_BLOCKS } from "../../../data/action";
import { CONDITION_BUILDING_BLOCKS } from "../../../data/condition";
export interface OpenSidebarConfig {
save: (config: Trigger | Condition | Action) => void;
close: () => void;
rename: () => void;
toggleYamlMode: () => boolean;
disable: () => void;
delete: () => void;
config: Trigger | Condition | Action;
type: "trigger" | "condition" | "action" | "option";
uiSupported: boolean;
yamlMode: boolean;
}
@customElement("ha-automation-sidebar")
export default class HaAutomationSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config?: OpenSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@state() private _yamlMode = false;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
public editor?: HaAutomationTriggerEditor | HaAutomationConditionEditor;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined;
if (this.config) {
this._yamlMode = this.config.yamlMode;
if (this._yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
if (!this.config) {
return nothing;
}
const disabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
let type = isTriggerList(this.config.config as Trigger)
? "list"
: this.config.type === "action"
? getAutomationActionType(this.config.config as Action)
: this.config.config[this.config.type];
if (this.config.type === "action" && type === "repeat") {
type = `repeat_${getRepeatType((this.config.config as RepeatAction).repeat)}`;
}
const isBuildingBlock = [
...CONDITION_BUILDING_BLOCKS,
...ACTION_BUILDING_BLOCKS,
].includes(type);
const subtitle = this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.label"
: `ui.panel.config.automation.editor.${this.config.type}s.${this.config.type}`) as LocalizeKeys
);
const title =
this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.option_label"
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.label`) as LocalizeKeys
) || type;
const description =
isBuildingBlock || this.config.type === "option"
? this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.option_description"
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.description.picker`) as LocalizeKeys
)
: "";
return html`
<ha-card
outlined
class=${classMap({
mobile: !this.isWide,
yaml: this._yamlMode,
})}
>
<ha-dialog-header>
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this._closeSidebar}
></ha-icon-button>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-button-menu
slot="actionItems"
@click=${this._openOverflowMenu}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
.clickAction=${this.config.rename}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
${this.config.type === "trigger" &&
!this._yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon
slot="start"
.path=${mdiIdentifier}
></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
${this.config.type !== "option"
? html`
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
${this.config.type !== "option"
? html`
<ha-md-menu-item
.clickAction=${this.config.disable}
.disabled=${this.disabled || type === "list"}
>
${disabled
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${disabled
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
.clickAction=${this.config.delete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${this.config.type !== "option" ? "delete" : "type.choose.remove_option"}`
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dialog-header>
${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<div class="card-content">
${this.config.type === "trigger"
? html`<ha-automation-trigger-editor
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config as Trigger}
@value-changed=${this._valueChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this._yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>`
: this.config.type === "condition" &&
(this._yamlMode || !CONDITION_BUILDING_BLOCKS.includes(type))
? html`
<ha-automation-condition-editor
class="sidebar-editor"
.hass=${this.hass}
.condition=${this.config.config as Condition}
.yamlMode=${this._yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor>
`
: this.config.type === "action" &&
(this._yamlMode || !ACTION_BUILDING_BLOCKS.includes(type))
? html`
<ha-automation-action-editor
class="sidebar-editor"
.hass=${this.hass}
.action=${this.config.config as Action}
.yamlMode=${this._yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
sidebar
narrow
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>
`
: description || nothing}
</div>
</ha-card>
`;
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save(ev.detail.value);
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: ev.detail.value,
},
});
}
}
private _closeSidebar() {
this.config?.close();
}
private _openOverflowMenu(ev: MouseEvent) {
ev.stopPropagation();
ev.preventDefault();
}
private _toggleYamlMode = () => {
this._yamlMode = this.config!.toggleYamlMode();
fireEvent(this, "value-changed", {
value: {
...this.config,
yamlMode: this._yamlMode,
},
});
};
private _showTriggerId = () => {
this._requestShowId = true;
};
static styles = css`
:host {
height: 100%;
--ha-card-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
border-radius: var(--ha-card-border-radius);
}
ha-card {
height: 100%;
width: 100%;
border-color: var(--primary-color);
border-width: 2px;
display: block;
}
ha-card.mobile {
border-bottom-right-radius: var(--ha-border-radius-square);
border-bottom-left-radius: var(--ha-border-radius-square);
}
@media all and (max-width: 870px) {
ha-card.mobile {
max-height: 70vh;
max-height: 70dvh;
border-width: 2px 2px 0;
}
ha-card.mobile.yaml {
height: 70vh;
height: 70dvh;
}
}
ha-dialog-header {
border-radius: var(--ha-card-border-radius);
}
.sidebar-editor {
padding-top: 64px;
}
.card-content {
max-height: calc(100% - 80px);
overflow: auto;
}
@media (min-width: 450px) and (min-height: 500px) {
.card-content {
max-height: calc(100% - 104px);
overflow: auto;
}
}
@media all and (max-width: 870px) {
ha-card.mobile .card-content {
max-height: calc(
70vh - 88px - max(var(--safe-area-inset-bottom), 16px)
);
max-height: calc(
70dvh - 88px - max(var(--safe-area-inset-bottom), 16px)
);
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar": HaAutomationSidebar;
}
}

View File

@@ -1,9 +1,10 @@
import { mdiHelpCircle } from "@mdi/js"; import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { load } from "js-yaml"; import { load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit"; import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { import {
any, any,
array, array,
@@ -23,7 +24,7 @@ import {
removeSearchParam, removeSearchParam,
} from "../../../common/url/search-params"; } from "../../../common/url/search-params";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-card"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import type { import type {
@@ -38,7 +39,6 @@ import {
normalizeAutomationConfig, normalizeAutomationConfig,
} from "../../../data/automation"; } from "../../../data/automation";
import { getActionType, type Action } from "../../../data/script"; import { getActionType, type Action } from "../../../data/script";
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 { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
@@ -46,7 +46,10 @@ import "./action/ha-automation-action";
import type HaAutomationAction from "./action/ha-automation-action"; import type HaAutomationAction from "./action/ha-automation-action";
import "./condition/ha-automation-condition"; import "./condition/ha-automation-condition";
import type HaAutomationCondition from "./condition/ha-automation-condition"; import type HaAutomationCondition from "./condition/ha-automation-condition";
import "./ha-automation-sidebar";
import type { OpenSidebarConfig } from "./ha-automation-sidebar";
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace"; import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
import { saveFabStyles } from "./styles";
import "./trigger/ha-automation-trigger"; import "./trigger/ha-automation-trigger";
import type HaAutomationTrigger from "./trigger/ha-automation-trigger"; import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
@@ -77,6 +80,8 @@ export class HaManualAutomationEditor extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public saving = false;
@property({ attribute: false }) public config!: ManualAutomationConfig; @property({ attribute: false }) public config!: ManualAutomationConfig;
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public stateObj?: HassEntity;
@@ -85,6 +90,8 @@ export class HaManualAutomationEditor extends LitElement {
@state() private _pastedConfig?: ManualAutomationConfig; @state() private _pastedConfig?: ManualAutomationConfig;
@state() private _sidebarConfig?: OpenSidebarConfig;
private _previousConfig?: ManualAutomationConfig; private _previousConfig?: ManualAutomationConfig;
public connectedCallback() { public connectedCallback() {
@@ -122,7 +129,7 @@ export class HaManualAutomationEditor extends LitElement {
); );
} }
protected render() { private _renderContent() {
return html` return html`
${this.stateObj?.state === "off" ${this.stateObj?.state === "off"
? html` ? html`
@@ -130,12 +137,7 @@ export class HaManualAutomationEditor extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.disabled" "ui.panel.config.automation.editor.disabled"
)} )}
<ha-button <ha-button size="small" slot="action" @click=${this._enable}>
size="small"
appearance="filled"
slot="action"
@click=${this._enable}
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.enable" "ui.panel.config.automation.editor.enable"
)} )}
@@ -182,10 +184,14 @@ export class HaManualAutomationEditor extends LitElement {
aria-labelledby="triggers-heading" aria-labelledby="triggers-heading"
.triggers=${this.config.triggers || []} .triggers=${this.config.triggers || []}
.highlightedTriggers=${this._pastedConfig?.triggers || []} .highlightedTriggers=${this._pastedConfig?.triggers || []}
.path=${["triggers"]}
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
root
sidebar
></ha-automation-trigger> ></ha-automation-trigger>
<div class="header"> <div class="header">
@@ -224,11 +230,14 @@ export class HaManualAutomationEditor extends LitElement {
aria-labelledby="conditions-heading" aria-labelledby="conditions-heading"
.conditions=${this.config.conditions || []} .conditions=${this.config.conditions || []}
.highlightedConditions=${this._pastedConfig?.conditions || []} .highlightedConditions=${this._pastedConfig?.conditions || []}
.path=${["conditions"]}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
root root
sidebar
></ha-automation-condition> ></ha-automation-condition>
<div class="header"> <div class="header">
@@ -265,16 +274,82 @@ export class HaManualAutomationEditor extends LitElement {
aria-labelledby="actions-heading" aria-labelledby="actions-heading"
.actions=${this.config.actions || []} .actions=${this.config.actions || []}
.highlightedActions=${this._pastedConfig?.actions || []} .highlightedActions=${this._pastedConfig?.actions || []}
.path=${["actions"]}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.disabled=${this.disabled} .disabled=${this.disabled || this.saving}
root root
sidebar
></ha-automation-action> ></ha-automation-action>
`; `;
} }
protected render() {
return html`
<div class="split-view">
<div class="content-wrapper">
<div class="content">${this._renderContent()}</div>
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
</div>
<ha-automation-sidebar
class=${classMap({
sidebar: true,
hidden: !this._sidebarConfig,
overlay: !this.isWide,
})}
.isWide=${this.isWide}
.hass=${this.hass}
.config=${this._sidebarConfig}
@value-changed=${this._sidebarConfigChanged}
.disabled=${this.disabled}
></ha-automation-sidebar>
</div>
`;
}
private _openSidebar(ev: CustomEvent<OpenSidebarConfig>) {
// deselect previous selected row
this._sidebarConfig?.close?.();
this._sidebarConfig = ev.detail;
}
private _sidebarConfigChanged(ev: CustomEvent<{ value: OpenSidebarConfig }>) {
ev.stopPropagation();
if (!this._sidebarConfig) {
return;
}
this._sidebarConfig = {
...this._sidebarConfig,
...ev.detail.value,
};
}
private _closeSidebar() {
if (this._sidebarConfig) {
const closeRow = this._sidebarConfig?.close;
this._sidebarConfig = undefined;
closeRow?.();
}
}
private _handleCloseSidebar() {
this._sidebarConfig = undefined;
}
private _triggerChanged(ev: CustomEvent): void { private _triggerChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
this.resetPastedConfig(); this.resetPastedConfig();
@@ -311,6 +386,11 @@ export class HaManualAutomationEditor extends LitElement {
}); });
} }
private _saveAutomation() {
this._closeSidebar();
fireEvent(this, "save-automation");
}
private _handlePaste = async (ev: ClipboardEvent) => { private _handlePaste = async (ev: ClipboardEvent) => {
if (!canOverrideAlphanumericInput(ev.composedPath())) { if (!canOverrideAlphanumericInput(ev.composedPath())) {
return; return;
@@ -523,14 +603,77 @@ export class HaManualAutomationEditor extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, saveFabStyles,
css` css`
:host { :host {
display: block; display: block;
} }
ha-card {
overflow: hidden; .split-view {
display: flex;
flex-direction: row;
height: 100%;
position: relative;
gap: 16px;
} }
.content-wrapper {
position: relative;
flex: 6;
}
.content {
padding: 32px 16px 64px 0;
height: calc(100vh - 153px);
height: calc(100dvh - 153px);
overflow-y: auto;
overflow-x: hidden;
}
.sidebar {
padding: 12px 0;
flex: 4;
height: calc(100vh - 81px);
height: calc(100dvh - 81px);
width: 40%;
}
.sidebar.hidden {
border-color: transparent;
border-width: 0;
overflow: hidden;
flex: 0;
visibility: hidden;
}
.sidebar.overlay {
position: fixed;
bottom: 0;
right: 0;
height: calc(100% - 64px);
padding: 0;
z-index: 5;
}
@media all and (max-width: 870px) {
.sidebar.overlay {
max-height: 70vh;
max-height: 70dvh;
height: auto;
width: 100%;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
}
}
@media all and (max-width: 870px) {
.sidebar.overlay.hidden {
height: 0;
}
}
.sidebar.overlay.hidden {
width: 0;
}
.description { .description {
margin: 0; margin: 0;
} }
@@ -559,6 +702,11 @@ export class HaManualAutomationEditor extends LitElement {
font-weight: var(--ha-font-weight-normal); font-weight: var(--ha-font-weight-normal);
line-height: 0; line-height: 0;
} }
ha-alert {
display: block;
margin-bottom: 16px;
}
`, `,
]; ];
} }
@@ -568,4 +716,9 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"manual-automation-editor": HaManualAutomationEditor; "manual-automation-editor": HaManualAutomationEditor;
} }
interface HASSDomEvents {
"open-sidebar": OpenSidebarConfig;
"close-sidebar": undefined;
}
} }

View File

@@ -1,6 +1,4 @@
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { import {
mdiArrowDown, mdiArrowDown,
mdiArrowUp, mdiArrowUp,
@@ -12,16 +10,19 @@ import {
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ensureArray } from "../../../../common/array/ensure-array"; import { ensureArray } from "../../../../common/array/ensure-array";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefault } from "../../../../common/dom/prevent_default"; import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-automation-row";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item"; import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import type { Condition } from "../../../../data/automation"; import type { Condition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n"; import { describeCondition } from "../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
@@ -31,10 +32,10 @@ import {
showConfirmationDialog, showConfirmationDialog,
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../action/ha-automation-action"; import "../action/ha-automation-action";
import "../condition/ha-automation-condition"; import "../condition/ha-automation-condition";
import { editorStyles, rowStyles } from "../styles";
@customElement("ha-automation-option-row") @customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement { export default class HaAutomationOptionRow extends LitElement {
@@ -52,8 +53,15 @@ export default class HaAutomationOptionRow extends LitElement {
@property({ type: Boolean }) public last = false; @property({ type: Boolean }) public last = false;
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@state() private _expanded = false; @state() private _expanded = false;
@state() private _selected = false;
@state() private _collapsed = false;
@state() @state()
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@@ -87,144 +95,175 @@ export default class HaAutomationOptionRow extends LitElement {
return str; return str;
} }
private _renderRow() {
return html`
<h3 slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: this.index + 1 }
)}:
${this.option.alias || (this._expanded ? "" : this._getDescription())}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@closed=${stopPropagation}
@keydown=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
@click=${this._renameOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
@click=${this._duplicateOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._removeOption}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar ? this._renderContent() : nothing}
`;
}
private _renderContent() {
return html`<div
class=${classMap({
"card-content": true,
indent: this.optionsInSidebar,
selected: this._selected,
})}
>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h4>
<ha-automation-condition
.conditions=${ensureArray<string | Condition>(this.option.conditions)}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._conditionChanged}
.optionsInSidebar=${this.optionsInSidebar}
></ha-automation-condition>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h4>
<ha-automation-action
.actions=${ensureArray(this.option.sequence) || []}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._actionChanged}
.optionsInSidebar=${this.optionsInSidebar}
></ha-automation-action>
</div>`;
}
protected render() { protected render() {
if (!this.option) return nothing; if (!this.option) return nothing;
return html` return html`
<ha-card outlined> <ha-card outlined class=${this._selected ? "selected" : ""}>
<ha-expansion-panel ${this.optionsInSidebar
left-chevron ? html`<ha-automation-row
@expanded-changed=${this._expandedChanged} left-chevron
id="option" .collapsed=${this._collapsed}
> .selected=${this._selected}
<h3 slot="header"> @click=${this._toggleSidebar}
${this.hass.localize( @toggle-collapsed=${this._toggleCollapse}
"ui.panel.config.automation.editor.actions.type.choose.option", >${this._renderRow()}</ha-automation-row
{ number: this.index + 1 } >`
)}: : html`
${this.option.alias || <ha-expansion-panel
(this._expanded ? "" : this._getDescription())} left-chevron
</h3> @expanded-changed=${this._expandedChanged}
id="option"
<slot name="icons" slot="icons"></slot> >
${this._renderRow()}
<ha-button-menu </ha-expansion-panel>
slot="icons" `}
@action=${this._handleAction}
@click=${preventDefault}
@closed=${stopPropagation}
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
</ha-list-item>
<ha-list-item
class="warning"
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="card-content">
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h4>
<ha-automation-condition
.conditions=${ensureArray<string | Condition>(
this.option.conditions
)}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h4>
<ha-automation-action
.actions=${ensureArray(this.option.sequence) || []}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-expansion-panel>
</ha-card> </ha-card>
${this.optionsInSidebar && !this._collapsed
? this._renderContent()
: nothing}
`; `;
} }
private async _handleAction(ev: CustomEvent<ActionDetail>) { private _duplicateOption() {
switch (ev.detail.index) { fireEvent(this, "duplicate");
case 0:
await this._renameOption();
break;
case 1:
fireEvent(this, "duplicate");
break;
case 2:
fireEvent(this, "move-up");
break;
case 3:
fireEvent(this, "move-down");
break;
case 4:
this._removeOption();
break;
}
} }
private _removeOption() { private _moveUp() {
fireEvent(this, "move-up");
}
private _moveDown() {
fireEvent(this, "move-down");
}
private _removeOption = () => {
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title" "ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
@@ -235,14 +274,18 @@ export default class HaAutomationOptionRow extends LitElement {
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"), confirmText: this.hass.localize("ui.common.delete"),
destructive: true, destructive: true,
confirm: () => confirm: () => {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: null, value: null,
}), });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
},
}); });
} };
private async _renameOption(): Promise<void> { private _renameOption = async () => {
const alias = await showPromptDialog(this, { const alias = await showPromptDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.change_alias" "ui.panel.config.automation.editor.actions.type.choose.change_alias"
@@ -266,7 +309,7 @@ export default class HaAutomationOptionRow extends LitElement {
value, value,
}); });
} }
} };
private _conditionChanged(ev: CustomEvent) { private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
@@ -286,46 +329,61 @@ export default class HaAutomationOptionRow extends LitElement {
}); });
} }
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(): void {
if (this.narrow) {
this.scrollIntoView();
}
fireEvent(this, "open-sidebar", {
save: () => {
// nothing to save for an option in the sidebar
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameOption();
},
toggleYamlMode: () => false, // no yaml mode for options
disable: () => {
// option cannot be disabled
},
delete: this._removeOption,
config: {},
type: "option",
uiSupported: true,
yamlMode: false,
});
this._selected = true;
}
public expand() { public expand() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true; this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
}); });
} }
private _toggleCollapse() {
this._collapsed = !this._collapsed;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, rowStyles,
editorStyles,
css` 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;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.card-content {
padding: 16px;
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-list-item.hidden {
display: none;
}
.warning ul {
margin: 4px 0;
}
li[role="separator"] { li[role="separator"] {
border-bottom-color: var(--divider-color); border-bottom-color: var(--divider-color);
} }

View File

@@ -27,6 +27,9 @@ export default class HaAutomationOption extends LitElement {
@property({ attribute: false }) public options!: Option[]; @property({ attribute: false }) public options!: Option[];
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false; @state() private _showReorder = false;
@state() @state()
@@ -87,6 +90,7 @@ export default class HaAutomationOption extends LitElement {
@move-up=${this._moveUp} @move-up=${this._moveUp}
@value-changed=${this._optionChanged} @value-changed=${this._optionChanged}
.hass=${this.hass} .hass=${this.hass}
.optionsInSidebar=${this.optionsInSidebar}
> >
${this._showReorder && !this.disabled ${this._showReorder && !this.disabled
? html` ? html`
@@ -101,6 +105,7 @@ export default class HaAutomationOption extends LitElement {
<div class="buttons"> <div class="buttons">
<ha-button <ha-button
appearance="filled" appearance="filled"
size="small"
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._addOption} @click=${this._addOption}
> >
@@ -125,7 +130,9 @@ export default class HaAutomationOption extends LitElement {
"ha-automation-option-row:last-of-type" "ha-automation-option-row:last-of-type"
)!; )!;
row.updateComplete.then(() => { row.updateComplete.then(() => {
row.expand(); if (!this.optionsInSidebar) {
row.expand();
}
row.scrollIntoView(); row.scrollIntoView();
row.focus(); row.focus();
}); });
@@ -238,7 +245,7 @@ export default class HaAutomationOption extends LitElement {
static styles = css` static styles = css`
.options { .options {
padding: 16px; padding: 16px 0 16px 16px;
margin: -16px; margin: -16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -246,7 +253,7 @@ export default class HaAutomationOption extends LitElement {
} }
.sortable-ghost { .sortable-ghost {
background: none; background: none;
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
} }
.sortable-drag { .sortable-drag {
background: none; background: none;

View File

@@ -0,0 +1,90 @@
import { css } from "lit";
export const rowStyles = css`
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
font-size: inherit;
font-weight: inherit;
}
ha-card {
transition: outline 0.2s;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
}
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`;
export const editorStyles = css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
.card-content {
padding: 16px;
}
.card-content.yaml {
padding: 0 1px;
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
}
.card-content.indent {
margin-left: 12px;
margin-right: -4px;
padding: 12px 24px 16px 16px;
border-left: 2px solid var(--ha-color-border-neutral-quiet);
}
.card-content.indent.selected,
:host([selected]) .card-content.indent {
border-color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
border-top-right-radius: var(--ha-border-radius-xl);
border-bottom-right-radius: var(--ha-border-radius-xl);
}
`;
export const saveFabStyles = css`
:host {
overflow: hidden;
}
ha-fab {
position: absolute;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: 16px;
}
`;

View File

@@ -0,0 +1,166 @@
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-textfield";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { Trigger } from "../../../../data/automation";
import { migrateAutomationTrigger } from "../../../../data/automation";
import { isTriggerList } from "../../../../data/trigger";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
@customElement("ha-automation-trigger-editor")
export default class HaAutomationTriggerEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: Trigger;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml" }) public yamlMode = false;
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
const yamlMode = this.yamlMode || !this.uiSupported;
const showId = "id" in this.trigger || this.showId;
return html`
<div
class=${classMap({
"card-content": true,
disabled:
this.disabled ||
("enabled" in this.trigger &&
this.trigger.enabled === false &&
!this.yamlMode),
yaml: yamlMode,
})}
>
${yamlMode
? html`
${!this.uiSupported
? html`
<ha-automation-editor-warning
.alertTitle=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
{ platform: type }
)}
.localize=${this.hass.localize}
></ha-automation-editor-warning>
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${showId && !isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id || ""}
.disabled=${this.disabled}
@change=${this._idChanged}
>
</ha-textfield>
`
: ""}
<div @value-changed=${this._onUiChanged}>
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
})}
</div>
`}
</div>
`;
}
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
const value = { ...this.trigger };
if (!newId) {
delete value.id;
} else {
value.id = newId;
}
fireEvent(this, "value-changed", {
value,
});
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
value: migrateAutomationTrigger(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
.card-content {
padding: 16px;
}
.card-content.yaml {
padding: 0 1px;
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-editor": HaAutomationTriggerEditor;
}
}

View File

@@ -18,28 +18,24 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } 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 { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefault } from "../../../../common/dom/prevent_default"; import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import { debounce } from "../../../../common/util/debounce"; import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-md-button-menu"; import "../../../../components/ha-automation-row";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-textfield"; import "../../../../components/ha-md-button-menu";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type { AutomationClipboard, Trigger } from "../../../../data/automation"; import type { AutomationClipboard, Trigger } from "../../../../data/automation";
import { import { subscribeTrigger } from "../../../../data/automation";
migrateAutomationTrigger,
subscribeTrigger,
} from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n"; import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
@@ -50,8 +46,11 @@ import {
showConfirmationDialog, showConfirmationDialog,
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
import "./types/ha-automation-trigger-calendar"; import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-conversation"; import "./types/ha-automation-trigger-conversation";
import "./types/ha-automation-trigger-device"; import "./types/ha-automation-trigger-device";
@@ -109,17 +108,25 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public last?: boolean; @property({ type: Boolean }) public last?: boolean;
@state() private _warnings?: string[]; @property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@state() private _yamlMode = false; @state() private _yamlMode = false;
@state() private _requestShowId = false;
@state() private _triggered?: Record<string, unknown>; @state() private _triggered?: Record<string, unknown>;
@state() private _triggerColor = false; @state() private _triggerColor = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @state() private _selected = false;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@property({ type: Boolean }) public narrow = false;
@query("ha-automation-trigger-editor")
public triggerEditor?: HaAutomationTriggerEditor;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
@@ -135,19 +142,186 @@ export default class HaAutomationTriggerRow extends LitElement {
private _triggerUnsub?: Promise<UnsubscribeFunc>; private _triggerUnsub?: Promise<UnsubscribeFunc>;
private _renderRow() {
const type = this._getType(this.trigger);
const supported = this._uiSupported(type);
const yamlMode = this._yamlMode || !supported;
return html`
<ha-svg-icon
slot="leading-icon"
class="trigger-icon"
.path=${TRIGGER_ICONS[type]}
></ha-svg-icon>
<h3 slot="header">
${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${!this.optionsInSidebar
? html` <ha-md-menu-item
.clickAction=${this._renameTrigger}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${"enabled" in this.trigger && this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<ha-automation-trigger-editor
.hass=${this.hass}
.trigger=${this.trigger}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.showId=${this._requestShowId}
.uiSupported=${supported}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>`
: nothing}
`;
}
protected render() { protected render() {
if (!this.trigger) return nothing; if (!this.trigger) return nothing;
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
const supported =
customElements.get(`ha-automation-trigger-${type}`) !== undefined;
const yamlMode = this._yamlMode || !supported;
const showId = "id" in this.trigger || this._requestShowId;
return html` return html`
<ha-card outlined> <ha-card outlined class=${this._selected ? "selected" : ""}>
${"enabled" in this.trigger && this.trigger.enabled === false ${"enabled" in this.trigger && this.trigger.enabled === false
? html` ? html`
<div class="disabled-bar"> <div class="disabled-bar">
@@ -157,223 +331,21 @@ export default class HaAutomationTriggerRow extends LitElement {
</div> </div>
` `
: nothing} : nothing}
${this.optionsInSidebar
<ha-expansion-panel left-chevron> ? html`<ha-automation-row
<ha-svg-icon .disabled=${"enabled" in this.trigger &&
slot="leading-icon" this.trigger.enabled === false}
class="trigger-icon" @click=${this._toggleSidebar}
.path=${TRIGGER_ICONS[type]} .selected=${this._selected}
></ha-svg-icon> >${this._selected
<h3 slot="header"> ? "selected"
${describeTrigger(this.trigger, this.hass, this._entityReg)} : nothing}${this._renderRow()}</ha-automation-row
</h3> >`
: html`
<slot name="icons" slot="icons"></slot> <ha-expansion-panel left-chevron>
${this._renderRow()}
<ha-md-button-menu </ha-expansion-panel>
slot="icons" `}
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
.clickAction=${this._renameTrigger}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
"card-content": true,
disabled:
"enabled" in this.trigger && this.trigger.enabled === false,
})}
>
${this._warnings
? html`<ha-alert
alert-type="warning"
.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`
${!supported
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
{ platform: type }
)}
`
: ""}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${showId && !isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id || ""}
.disabled=${this.disabled}
@change=${this._idChanged}
>
</ha-textfield>
`
: ""}
<div
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._onUiChanged}
>
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
})}
</div>
`}
</div>
</ha-expansion-panel>
<div <div
class="triggered ${classMap({ class="triggered ${classMap({
active: this._triggered !== undefined, active: this._triggered !== undefined,
@@ -389,6 +361,13 @@ export default class HaAutomationTriggerRow extends LitElement {
`; `;
} }
protected willUpdate(changedProperties) {
// on yaml toggle --> clear warnings
if (changedProperties.has("yamlMode")) {
this._warnings = undefined;
}
}
protected override updated(changedProps: PropertyValues<this>): void { protected override updated(changedProps: PropertyValues<this>): void {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("trigger")) { if (changedProps.has("trigger")) {
@@ -474,6 +453,46 @@ export default class HaAutomationTriggerRow extends LitElement {
} }
} }
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(trigger?: Trigger): void {
if (this.narrow) {
this.scrollIntoView();
}
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameTrigger();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
},
disable: this._onDisable,
delete: this._onDelete,
config: trigger || this.trigger,
type: "trigger",
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
yamlMode: this._yamlMode,
});
this._selected = true;
}
private _setClipboard() { private _setClipboard() {
this._clipboard = { this._clipboard = {
...this._clipboard, ...this._clipboard,
@@ -494,6 +513,10 @@ export default class HaAutomationTriggerRow extends LitElement {
destructive: true, destructive: true,
confirm: () => { confirm: () => {
fireEvent(this, "value-changed", { value: null }); fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
}, },
}); });
}; };
@@ -503,58 +526,18 @@ export default class HaAutomationTriggerRow extends LitElement {
const enabled = !(this.trigger.enabled ?? true); const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled }; const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
if (this._yamlMode) { this.openSidebar(value); // refresh sidebar
this._yamlEditor?.setValue(value);
if (this._yamlMode && !this.optionsInSidebar) {
this.triggerEditor?.yamlEditor?.setValue(value);
} }
}; };
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
this._requestShowId = true;
const value = { ...this.trigger };
if (!newId) {
delete value.id;
} else {
value.id = newId;
}
fireEvent(this, "value-changed", {
value,
});
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
this._warnings = undefined;
fireEvent(this, "value-changed", {
value: migrateAutomationTrigger(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
private _switchUiMode() { private _switchUiMode() {
this._warnings = undefined;
this._yamlMode = false; this._yamlMode = false;
} }
private _switchYamlMode() { private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = true; this._yamlMode = true;
} }
@@ -601,15 +584,21 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,
}); });
if (this._yamlMode) {
this._yamlEditor?.setValue(value); if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this.triggerEditor?.yamlEditor?.setValue(value);
} }
} }
}; };
private _showTriggerId = () => { private _showTriggerId = () => {
this._requestShowId = true; this._requestShowId = true;
this.expand();
if (!this.optionsInSidebar) {
this.expand();
}
}; };
private _duplicateTrigger = () => { private _duplicateTrigger = () => {
@@ -623,6 +612,9 @@ export default class HaAutomationTriggerRow extends LitElement {
private _cutTrigger = () => { private _cutTrigger = () => {
this._setClipboard(); this._setClipboard();
fireEvent(this, "value-changed", { value: null }); fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
}; };
private _moveUp = () => { private _moveUp = () => {
@@ -639,7 +631,10 @@ export default class HaAutomationTriggerRow extends LitElement {
} else { } else {
this._switchYamlMode(); this._switchYamlMode();
} }
this.expand();
if (!this.optionsInSidebar) {
this.expand();
}
}; };
public expand() { public expand() {
@@ -648,52 +643,19 @@ export default class HaAutomationTriggerRow extends LitElement {
}); });
} }
private _getType = memoizeOne((trigger: Trigger) =>
isTriggerList(trigger) ? "list" : trigger.trigger
);
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-trigger-${type}`) !== undefined
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, rowStyles,
css` css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.trigger-icon {
display: none;
}
@media (min-width: 870px) {
.trigger-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
}
}
.card-content {
padding: 16px;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.triggered { .triggered {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
@@ -709,17 +671,13 @@ export default class HaAutomationTriggerRow extends LitElement {
overflow: hidden; overflow: hidden;
transition: max-height 0.3s; transition: max-height 0.3s;
text-align: center; text-align: center;
border-top-right-radius: calc( border-top-right-radius: var(
var(--ha-card-border-radius, 12px) - var( --ha-card-border-radius,
--ha-card-border-width, var(--ha-border-radius-lg)
1px
)
); );
border-top-left-radius: calc( border-top-left-radius: var(
var(--ha-card-border-radius, 12px) - var( --ha-card-border-radius,
--ha-card-border-width, var(--ha-border-radius-lg)
1px
)
); );
} }
.triggered.active { .triggered.active {
@@ -732,19 +690,6 @@ export default class HaAutomationTriggerRow extends LitElement {
background-color: var(--accent-color); background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color)); color: var(--text-accent-color, var(--text-primary-color));
} }
ha-textfield {
display: block;
margin-bottom: 24px;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`, `,
]; ];
} }

View File

@@ -36,6 +36,13 @@ export default class HaAutomationTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@property({ type: Boolean }) public root = false;
@state() private _showReorder = false; @state() private _showReorder = false;
@state() @state()
@@ -95,7 +102,9 @@ export default class HaAutomationTrigger extends LitElement {
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
.narrow=${this.narrow}
?highlight=${this.highlightedTriggers?.includes(trg)} ?highlight=${this.highlightedTriggers?.includes(trg)}
.optionsInSidebar=${this.optionsInSidebar}
> >
${this._showReorder && !this.disabled ${this._showReorder && !this.disabled
? html` ? html`
@@ -111,6 +120,8 @@ export default class HaAutomationTrigger extends LitElement {
<ha-button <ha-button
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._addTriggerDialog} @click=${this._addTriggerDialog}
.appearance=${this.root ? "accent" : "filled"}
.size=${this.root ? "medium" : "small"}
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add" "ui.panel.config.automation.editor.triggers.add"
@@ -164,7 +175,11 @@ export default class HaAutomationTrigger extends LitElement {
"ha-automation-trigger-row:last-of-type" "ha-automation-trigger-row:last-of-type"
)!; )!;
row.updateComplete.then(() => { row.updateComplete.then(() => {
row.expand(); if (this.optionsInSidebar) {
row.openSidebar();
} else {
row.expand();
}
row.scrollIntoView(); row.scrollIntoView();
row.focus(); row.focus();
}); });
@@ -279,15 +294,18 @@ export default class HaAutomationTrigger extends LitElement {
static styles = css` static styles = css`
.triggers { .triggers {
padding: 16px; padding: 16px 0 16px 16px;
margin: -16px; margin: -16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
:host([root]) .triggers {
padding-right: 8px;
}
.sortable-ghost { .sortable-ghost {
background: none; background: none;
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
} }
.sortable-drag { .sortable-drag {
background: none; background: none;

View File

@@ -3847,6 +3847,7 @@
"type_select": "Trigger type", "type_select": "Trigger type",
"unknown_trigger": "[%key:ui::panel::config::devices::automation::triggers::unknown_trigger%]", "unknown_trigger": "[%key:ui::panel::config::devices::automation::triggers::unknown_trigger%]",
"triggering_event_detail": "Triggering event detail", "triggering_event_detail": "Triggering event detail",
"trigger": "Trigger",
"groups": { "groups": {
"entity": { "entity": {
"label": "Entity", "label": "Entity",
@@ -4107,6 +4108,7 @@
"unsupported_condition": "No visual editor support for condition: {condition}", "unsupported_condition": "No visual editor support for condition: {condition}",
"type_select": "Condition type", "type_select": "Condition type",
"unknown_condition": "[%key:ui::panel::config::devices::automation::conditions::unknown_condition%]", "unknown_condition": "[%key:ui::panel::config::devices::automation::conditions::unknown_condition%]",
"condition": "Condition",
"groups": { "groups": {
"entity": { "entity": {
"label": "Entity", "label": "Entity",
@@ -4275,6 +4277,7 @@
"unsupported_action": "No visual editor support for this action", "unsupported_action": "No visual editor support for this action",
"type_select": "Action type", "type_select": "Action type",
"continue_on_error": "Continue on error", "continue_on_error": "Continue on error",
"action": "Action",
"groups": { "groups": {
"helpers": { "helpers": {
"label": "Helpers" "label": "Helpers"
@@ -4441,6 +4444,8 @@
"conditions": "Conditions", "conditions": "Conditions",
"no_conditions": "[%key:ui::panel::config::devices::automation::conditions::no_conditions%]", "no_conditions": "[%key:ui::panel::config::devices::automation::conditions::no_conditions%]",
"sequence": "Actions", "sequence": "Actions",
"option_label": "Option",
"option_description": "Choose actions based on conditions",
"description": { "description": {
"picker": "Choose what to do based on conditions (Similar to If-then, but more powerful).", "picker": "Choose what to do based on conditions (Similar to If-then, but more powerful).",
"full": "Choose {number, plural,\n one {an option}\n other{between {number} options}\n}", "full": "Choose {number, plural,\n one {an option}\n other{between {number} options}\n}",