Group add automation elements in dialog (#19086)

* Group add automation elements in dialog

* Add search

* clear filter on close

* Split out services

* group services by integration type

* Update add-automation-element-dialog.ts

* fix typing

* clear filter on back

* Update add-automation-element-dialog.ts

* Fix search

* scroll to top

* Add service descriptions

* fix clipboard

* Move play media, sort services

* use helpers

* move to data

* Move building blocks to a group

* fix search

* Update add-automation-element-dialog.ts

* Update en.json

* fix alignment of single line and multi line items

* use repeat instead of map
This commit is contained in:
Bram Kragten 2023-12-21 21:01:27 +01:00 committed by GitHub
parent 7b6b5724e1
commit 8f07e6f141
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 926 additions and 352 deletions

View File

@ -47,6 +47,13 @@ export class HaListItem extends ListItemBase {
display: var(--mdc-list-item-meta-display); display: var(--mdc-list-item-meta-display);
align-items: center; align-items: center;
} }
:host([graphic="icon"]:not([twoline]))
.mdc-deprecated-list-item__graphic {
margin-inline-end: var(
--mdc-list-item-graphic-margin,
20px
) !important;
}
:host([multiline-secondary]) { :host([multiline-secondary]) {
height: auto; height: auto;
} }

View File

@ -5,6 +5,8 @@ import {
mdiCallSplit, mdiCallSplit,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiHandBackRight, mdiHandBackRight,
mdiPalette, mdiPalette,
@ -13,10 +15,12 @@ import {
mdiRoomService, mdiRoomService,
mdiShuffleDisabled, mdiShuffleDisabled,
mdiTimerOutline, mdiTimerOutline,
mdiTools,
mdiTrafficLight, mdiTrafficLight,
} from "@mdi/js"; } from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const ACTION_TYPES = { export const ACTION_ICONS = {
condition: mdiAbTesting, condition: mdiAbTesting,
delay: mdiTimerOutline, delay: mdiTimerOutline,
event: mdiGestureDoubleTap, event: mdiGestureDoubleTap,
@ -34,6 +38,43 @@ export const ACTION_TYPES = {
variables: mdiApplicationVariableOutline, variables: mdiApplicationVariableOutline,
} as const; } as const;
export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_TYPES>([ export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([
"variables", "variables",
]); ]);
export const ACTION_GROUPS: AutomationElementGroup = {
device_id: {},
helpers: {
icon: mdiTools,
members: {},
},
building_blocks: {
icon: mdiExcavator,
members: {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat: {},
choose: {},
if: {},
stop: {},
parallel: {},
variables: {},
},
},
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
},
},
} as const;
export const SERVICE_PREFIX = "__SERVICE__";
export const isService = (key: string | undefined): boolean | undefined =>
key?.startsWith(SERVICE_PREFIX);
export const getService = (key: string): string =>
key.substring(SERVICE_PREFIX.length);

View File

@ -275,6 +275,10 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
not: Condition[]; not: Condition[];
} }
export interface AutomationElementGroup {
[key: string]: { icon?: string; members?: AutomationElementGroup };
}
export type Condition = export type Condition =
| StateCondition | StateCondition
| NumericStateCondition | NumericStateCondition

View File

@ -3,16 +3,21 @@ import {
mdiClockOutline, mdiClockOutline,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGateOr, mdiGateOr,
mdiIdentifier, mdiIdentifier,
mdiMapClock,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiNotEqualVariant, mdiNotEqualVariant,
mdiNumeric, mdiNumeric,
mdiShape,
mdiStateMachine, mdiStateMachine,
mdiWeatherSunny, mdiWeatherSunny,
} from "@mdi/js"; } from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const CONDITION_TYPES = { export const CONDITION_ICONS = {
device: mdiDevices, device: mdiDevices,
and: mdiAmpersand, and: mdiAmpersand,
or: mdiGateOr, or: mdiGateOr,
@ -25,3 +30,23 @@ export const CONDITION_TYPES = {
trigger: mdiIdentifier, trigger: mdiIdentifier,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,
}; };
export const CONDITION_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { sun: {}, time: {}, zone: {} },
},
building_blocks: {
icon: mdiExcavator,
members: { and: {}, or: {}, not: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
template: {},
trigger: {},
},
},
} as const;

View File

@ -16,7 +16,9 @@ export type IntegrationType =
| "helper" | "helper"
| "hub" | "hub"
| "service" | "service"
| "hardware"; | "hardware"
| "entity"
| "system";
export interface IntegrationManifest { export interface IntegrationManifest {
is_built_in: boolean; is_built_in: boolean;

View File

@ -4,13 +4,16 @@ import {
mdiClockOutline, mdiClockOutline,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker, mdiMapMarker,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiMessageAlert, mdiMessageAlert,
mdiMicrophoneMessage, mdiMicrophoneMessage,
mdiNfcVariant, mdiNfcVariant,
mdiNumeric, mdiNumeric,
mdiShape,
mdiStateMachine, mdiStateMachine,
mdiSwapHorizontal, mdiSwapHorizontal,
mdiWeatherSunny, mdiWeatherSunny,
@ -18,8 +21,9 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg"; import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { AutomationElementGroup } from "./automation";
export const TRIGGER_TYPES = { export const TRIGGER_ICONS = {
calendar: mdiCalendar, calendar: mdiCalendar,
device: mdiDevices, device: mdiDevices,
event: mdiGestureDoubleTap, event: mdiGestureDoubleTap,
@ -38,3 +42,26 @@ export const TRIGGER_TYPES = {
persistent_notification: mdiMessageAlert, persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,
}; };
export const TRIGGER_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { calendar: {}, sun: {}, time: {}, time_pattern: {}, zone: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
geo_location: {},
homeassistant: {},
mqtt: {},
conversation: {},
tag: {},
template: {},
webhook: {},
persistent_notification: {},
},
},
} as const;

View File

@ -37,7 +37,7 @@ 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 type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action"; import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation"; import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
@ -82,9 +82,9 @@ export const getType = (action: Action | undefined) => {
if (["and", "or", "not"].some((key) => key in action)) { if (["and", "or", "not"].some((key) => key in action)) {
return "condition" as const; return "condition" as const;
} }
return Object.keys(ACTION_TYPES).find( return Object.keys(ACTION_ICONS).find(
(option) => option in action (option) => option in action
) as keyof typeof ACTION_TYPES; ) as keyof typeof ACTION_ICONS;
}; };
export interface ActionElement extends LitElement { export interface ActionElement extends LitElement {
@ -190,7 +190,7 @@ export default class HaAutomationActionRow extends LitElement {
<h3 slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="action-icon" class="action-icon"
.path=${ACTION_TYPES[type!]} .path=${ACTION_ICONS[type!]}
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action) describeAction(this.hass, this._entityReg, this.action)

View File

@ -1,57 +1,26 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
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 { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { ACTION_TYPES } from "../../../../data/action"; import { getService, isService } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script"; import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable"; import type { SortableInstance } from "../../../../resources/sortable";
import { Entries, HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
} 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 { getType } from "./ha-automation-action-row";
import "./types/ha-automation-action-activate_scene";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
const PASTE_VALUE = "__paste__";
@customElement("ha-automation-action") @customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement { export default class HaAutomationAction extends LitElement {
@ -150,42 +119,26 @@ export default class HaAutomationAction extends LitElement {
` `
)} )}
</div> </div>
<ha-button-menu <ha-button
@action=${this._addAction} slot="trigger"
outlined
.disabled=${this.disabled} .disabled=${this.disabled}
fixed .label=${this.hass.localize(
> "ui.panel.config.automation.editor.actions.add"
<ha-button
slot="trigger"
outlined
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.add"
)}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._clipboard?.action
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.paste"
)}
(${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${
getType(this._clipboard.action) || "unknown"
}.label`
)})
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)} )}
</ha-button-menu> @click=${this._addActionDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
<ha-button
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.add_building_block"
)}
@click=${this._addActionBuildingBlockDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
`; `;
} }
@ -213,6 +166,43 @@ export default class HaAutomationAction extends LitElement {
} }
} }
private _addActionDialog() {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getType(this._clipboard?.action),
});
}
private _addActionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getType(this._clipboard?.action),
group: "building_blocks",
});
}
private _addAction = (action: string) => {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else if (isService(action)) {
actions = this.actions.concat({
service: getService(action),
});
} else {
const elClass = customElements.get(
`ha-automation-action-${action}`
) as CustomElementConstructor & { defaultConfig: Action };
actions = this.actions.concat(
elClass ? { ...elClass.defaultConfig } : { [action]: {} }
);
}
this._focusLastActionOnChange = true;
fireEvent(this, "value-changed", { value: actions });
};
private async _enterReOrderMode(ev: CustomEvent) { private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return; if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
@ -258,25 +248,6 @@ export default class HaAutomationAction extends LitElement {
return this._actionKeys.get(action)!; return this._actionKeys.get(action)!;
} }
private _addAction(ev: CustomEvent<ActionDetail>) {
const action = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else {
const elClass = customElements.get(
`ha-automation-action-${action}`
) as CustomElementConstructor & { defaultConfig: Action };
actions = this.actions.concat(
elClass ? { ...elClass.defaultConfig } : { [action]: {} }
);
}
this._focusLastActionOnChange = true;
fireEvent(this, "value-changed", { value: actions });
}
private _moveUp(ev) { private _moveUp(ev) {
const index = (ev.target as any).index; const index = (ev.target as any).index;
const newIndex = index - 1; const newIndex = index - 1;
@ -328,22 +299,6 @@ export default class HaAutomationAction extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(ACTION_TYPES) as Entries<typeof ACTION_TYPES>)
.map(
([action, icon]) =>
[
action,
localize(
`ui.panel.config.automation.editor.actions.type.${action}.label`
),
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
sortableStyles, sortableStyles,

View File

@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
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_TYPES } from "../../../../../data/condition"; import { CONDITION_ICONS } from "../../../../../data/condition";
import { Entries, HomeAssistant } from "../../../../../types"; import { 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";
@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] => (localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>) (Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
.map( .map(
([condition, icon]) => ([condition, icon]) =>
[ [

View File

@ -0,0 +1,553 @@
import "@material/mwc-list/mwc-list";
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
import Fuse, { IFuseOptions } from "fuse.js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { domainIcon } from "../../../common/entity/domain_icon";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-dialog";
import type { HaDialog } from "../../../components/ha-dialog";
import "../../../components/ha-header-bar";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import "../../../components/search-input";
import {
ACTION_GROUPS,
ACTION_ICONS,
SERVICE_PREFIX,
getService,
isService,
} from "../../../data/action";
import { AutomationElementGroup } from "../../../data/automation";
import { CONDITION_GROUPS, CONDITION_ICONS } from "../../../data/condition";
import {
IntegrationManifest,
domainToName,
fetchIntegrationManifests,
} from "../../../data/integration";
import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import {
AddAutomationElementDialogParams,
PASTE_VALUE,
} from "./show-add-automation-element-dialog";
const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
condition: {
groups: CONDITION_GROUPS,
icons: CONDITION_ICONS,
},
action: {
groups: ACTION_GROUPS,
icons: ACTION_ICONS,
},
};
interface ListItem {
key: string;
name: string;
description: string;
icon: string;
group: boolean;
}
interface DomainManifestLookup {
[domain: string]: IntegrationManifest;
}
const ENTITY_DOMAINS_OTHER = new Set([
"date",
"datetime",
"device_tracker",
"text",
"time",
"tts",
"update",
"weather",
"image_processing",
]);
@customElement("add-automation-element-dialog")
class DialogAddAutomationElement extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: AddAutomationElementDialogParams;
@state() private _group?: string;
@state() private _prev?: string;
@state() private _filter = "";
@state() private _manifests?: DomainManifestLookup;
@query("ha-dialog") private _dialog?: HaDialog;
public showDialog(params): void {
this._params = params;
this._group = params.group;
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
this._fetchManifests();
}
}
public closeDialog(): void {
if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._params = undefined;
this._group = undefined;
this._prev = undefined;
this._filter = "";
this._manifests = undefined;
}
private _convertToItem = (
key: string,
options,
type: AddAutomationElementDialogParams["type"],
localize: LocalizeFunc
): ListItem => ({
group: Boolean(options.members),
key,
name: localize(
// @ts-ignore
`ui.panel.config.automation.editor.${type}s.${
options.members ? "groups" : "type"
}.${key}.label`
),
description: localize(
// @ts-ignore
`ui.panel.config.automation.editor.${type}s.${
options.members ? "groups" : "type"
}.${key}.description${options.members ? "" : ".picker"}`
),
icon: options.icon || TYPES[type].icons[key],
});
private _getFilteredItems = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
group: string | undefined,
filter: string,
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): ListItem[] => {
const groups: AutomationElementGroup = group
? isService(group)
? {}
: TYPES[type].groups[group].members!
: TYPES[type].groups;
const flattenGroups = (grp: AutomationElementGroup) =>
Object.entries(grp).map(([key, options]) =>
options.members
? flattenGroups(options.members)
: this._convertToItem(key, options, type, localize)
);
const items = flattenGroups(groups).flat();
if (type === "action") {
items.push(...this._services(localize, services, manifests, group));
}
const options: IFuseOptions<ListItem> = {
keys: ["key", "name", "description"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
};
const fuse = new Fuse(items, options);
return fuse.search(filter).map((result) => result.item);
}
);
private _getGroupItems = memoizeOne(
(
type: AddAutomationElementDialogParams["type"],
group: string | undefined,
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests?: DomainManifestLookup
): ListItem[] => {
if (type === "action" && isService(group)) {
const result = this._services(localize, services, manifests, group);
if (group === "service_media_player") {
result.unshift(this._convertToItem("play_media", {}, type, localize));
}
return result;
}
const groups: AutomationElementGroup = group
? TYPES[type].groups[group].members!
: TYPES[type].groups;
const result = Object.entries(groups).map(([key, options]) =>
this._convertToItem(key, options, type, localize)
);
if (type === "action") {
if (!this._group) {
result.unshift(
...this._serviceGroups(localize, services, manifests, undefined)
);
} else if (this._group === "helpers") {
result.unshift(
...this._serviceGroups(localize, services, manifests, "helper")
);
} else if (this._group === "other") {
result.unshift(
...this._serviceGroups(localize, services, manifests, "other")
);
}
}
return result.sort((a, b) => {
if (a.group && b.group) {
return 0;
}
if (a.group && !b.group) {
return 1;
}
if (!a.group && b.group) {
return -1;
}
return stringCompare(a.name, b.name, this.hass.locale.language);
});
}
);
private _serviceGroups = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests: DomainManifestLookup | undefined,
type: "helper" | "other" | undefined
): ListItem[] => {
if (!services || !manifests) {
return [];
}
const result: ListItem[] = [];
Object.keys(services)
.sort()
.forEach((domain) => {
const manifest = manifests[domain];
if (
(type === undefined &&
manifest?.integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain)) ||
(type === "helper" && manifest?.integration_type === "helper") ||
(type === "other" &&
(ENTITY_DOMAINS_OTHER.has(domain) ||
!["helper", "entity"].includes(
manifest?.integration_type || ""
)))
) {
result.push({
group: true,
icon: domainIcon(domain),
key: `${SERVICE_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
});
}
});
return result;
}
);
private _services = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"],
manifests: DomainManifestLookup | undefined,
group?: string
): ListItem[] => {
if (!services) {
return [];
}
const result: ListItem[] = [];
let domain: string | undefined;
if (isService(group)) {
domain = getService(group!);
}
const addDomain = (dmn: string) => {
const services_keys = Object.keys(services[dmn]);
for (const service of services_keys) {
result.push({
group: false,
icon: domainIcon(dmn),
key: `${SERVICE_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
services[dmn][service]?.name ||
service
}`,
description:
this.hass.localize(
`component.${domain}.services.${service}.description`
) || services[dmn][service]?.description,
});
}
};
if (domain) {
addDomain(domain);
return result.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
}
if (group && !["helpers", "other"].includes(group)) {
return [];
}
Object.keys(services)
.sort()
.forEach((dmn) => {
const manifest = manifests?.[dmn];
if (group === "helpers" && manifest?.integration_type !== "helper") {
return;
}
if (
group === "other" &&
(ENTITY_DOMAINS_OTHER.has(dmn) ||
["helper", "entity"].includes(manifest?.integration_type || ""))
) {
return;
}
addDomain(dmn);
});
return result;
}
);
private async _fetchManifests() {
const manifests = {};
const fetched = await fetchIntegrationManifests(this.hass);
for (const manifest of fetched) {
manifests[manifest.domain] = manifest;
}
this._manifests = manifests;
}
protected render() {
if (!this._params) {
return nothing;
}
const items = this._filter
? this._getFilteredItems(
this._params.type,
this._group,
this._filter,
this.hass.localize,
this.hass.services,
this._manifests
)
: this._getGroupItems(
this._params.type,
this._group,
this.hass.localize,
this.hass.services,
this._manifests
);
const groupName = isService(this._group)
? domainToName(
this.hass.localize,
getService(this._group!),
this._manifests?.[getService(this._group!)]
)
: this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.groups.${this._group}.label`
);
return html`
<ha-dialog open hideActions @closed=${this.closeDialog} .heading=${true}>
<div slot="heading">
<ha-header-bar>
<span slot="title"
>${this._group
? groupName
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.add`
)}</span
>
${this._group && this._group !== this._params.group
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
slot="navigationIcon"
dialogAction="cancel"
></ha-icon-button>`}
</ha-header-bar>
<search-input
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._filterChanged}
.label=${groupName
? this.hass.localize(
"ui.panel.config.automation.editor.search_in",
{ group: groupName }
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.search`
)}
></search-input>
</div>
<mwc-list
innerRole="listbox"
itemRoles="option"
rootTabbable
dialogInitialFocus
>
${this._params.clipboardItem &&
!this._filter &&
(!this._group ||
items.find((item) => item.key === this._params!.clipboardItem))
? html`<ha-list-item
twoline
class="paste"
.value=${PASTE_VALUE}
graphic="icon"
hasMeta
@request-selected=${this._selected}
>
${this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.paste`
)}
<span slot="secondary"
>${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
)}</span
>
<ha-svg-icon
slot="graphic"
.path=${mdiContentPaste}
></ha-svg-icon
><ha-svg-icon slot="meta" .path=${mdiPlus}></ha-svg-icon>
</ha-list-item>
<li divider role="separator"></li>`
: ""}
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-list-item
.twoline=${Boolean(item.description)}
.value=${item.key}
.group=${item.group}
graphic="icon"
hasMeta
@request-selected=${this._selected}
>
${item.name}
<span slot="secondary">${item.description}</span>
<ha-svg-icon slot="graphic" .path=${item.icon}></ha-svg-icon>
${item.group
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: html`<ha-svg-icon
slot="meta"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-list-item>
`
)}
</mwc-list>
</ha-dialog>
`;
}
private _back() {
if (this._filter) {
this._filter = "";
return;
}
if (this._prev) {
this._group = this._prev;
this._prev = undefined;
return;
}
this._group = undefined;
}
private _selected(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._dialog!.scrollToPos(0, 0);
const item = ev.currentTarget;
if (item.group) {
this._prev = this._group;
this._group = item.value;
return;
}
this._params!.add(item.value);
this.closeDialog();
}
private _filterChanged(ev) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh;
}
@media all and (min-width: 550px) {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
margin-top: 8px;
display: block;
}
ha-icon-next {
width: 24px;
}
search-input {
display: block;
margin: 0 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"add-automation-element-dialog": DialogAddAutomationElement;
}
}

View File

@ -29,7 +29,7 @@ import "../../../../components/ha-icon-button";
import type { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import { Condition, testCondition } from "../../../../data/automation"; import { Condition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n"; import { describeCondition } from "../../../../data/automation_i18n";
import { CONDITION_TYPES } from "../../../../data/condition"; import { 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 { EntityRegistryEntry } from "../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../data/entity_registry";
@ -123,7 +123,7 @@ export default class HaAutomationConditionRow extends LitElement {
<h3 slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="condition-icon" class="condition-icon"
.path=${CONDITION_TYPES[this.condition.condition]} .path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg) describeCondition(this.condition, this.hass, this._entityReg)

View File

@ -1,25 +1,18 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
@ -28,30 +21,15 @@ import type {
AutomationClipboard, AutomationClipboard,
Condition, Condition,
} from "../../../../data/automation"; } from "../../../../data/automation";
import type { Entries, HomeAssistant } from "../../../../types";
import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row";
// Uncommenting these and this element doesn't load
// import "./types/ha-automation-condition-not";
// import "./types/ha-automation-condition-or";
import { storage } from "../../../../common/decorators/storage";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select";
import { CONDITION_TYPES } from "../../../../data/condition";
import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable"; import type { SortableInstance } from "../../../../resources/sortable";
import "./types/ha-automation-condition-and"; import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-condition-device"; import {
import "./types/ha-automation-condition-numeric_state"; PASTE_VALUE,
import "./types/ha-automation-condition-state"; showAddAutomationElementDialog,
import "./types/ha-automation-condition-sun"; } from "../show-add-automation-element-dialog";
import "./types/ha-automation-condition-template"; import "./ha-automation-condition-row";
import "./types/ha-automation-condition-time"; import type HaAutomationConditionRow from "./ha-automation-condition-row";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
const PASTE_VALUE = "__paste__";
@customElement("ha-automation-condition") @customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement { export default class HaAutomationCondition extends LitElement {
@ -197,43 +175,67 @@ export default class HaAutomationCondition extends LitElement {
` `
)} )}
</div> </div>
<ha-button-menu <ha-button
@action=${this._addCondition} outlined
.disabled=${this.disabled} .disabled=${this.disabled}
fixed .label=${this.hass.localize(
> "ui.panel.config.automation.editor.conditions.add"
<ha-button
slot="trigger"
outlined
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.add"
)}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._clipboard?.condition
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.paste"
)}
(${this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.${this._clipboard.condition.condition}.label`
)})
<ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)} )}
</ha-button-menu> @click=${this._addConditionDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
<ha-button
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.add_building_block"
)}
@click=${this._addConditionBuildingBlockDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
`; `;
} }
private _addConditionDialog() {
showAddAutomationElementDialog(this, {
type: "condition",
add: this._addCondition,
clipboardItem: this._clipboard?.condition?.condition,
});
}
private _addConditionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "condition",
add: this._addCondition,
clipboardItem: this._clipboard?.condition?.condition,
group: "building_blocks",
});
}
private _addCondition = (value) => {
let conditions: Condition[];
if (value === PASTE_VALUE) {
conditions = this.conditions.concat(
deepClone(this._clipboard!.condition)
);
} else {
const condition = value as Condition["condition"];
const elClass = customElements.get(
`ha-automation-condition-${condition}`
) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">;
};
conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig,
});
}
this._focusLastConditionOnChange = true;
fireEvent(this, "value-changed", { value: conditions });
};
private async _enterReOrderMode(ev: CustomEvent) { private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return; if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
@ -282,32 +284,6 @@ export default class HaAutomationCondition extends LitElement {
return this._conditionKeys.get(condition)!; return this._conditionKeys.get(condition)!;
} }
private _addCondition(ev: CustomEvent<ActionDetail>) {
const value = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
let conditions: Condition[];
if (value === PASTE_VALUE) {
conditions = this.conditions.concat(
deepClone(this._clipboard!.condition)
);
} else {
const condition = value as Condition["condition"];
const elClass = customElements.get(
`ha-automation-condition-${condition}`
) as CustomElementConstructor & {
defaultConfig: Omit<Condition, "condition">;
};
conditions = this.conditions.concat({
condition: condition as any,
...elClass.defaultConfig,
});
}
this._focusLastConditionOnChange = true;
fireEvent(this, "value-changed", { value: conditions });
}
private _moveUp(ev) { private _moveUp(ev) {
const index = (ev.target as any).index; const index = (ev.target as any).index;
const newIndex = index - 1; const newIndex = index - 1;
@ -361,22 +337,6 @@ export default class HaAutomationCondition extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
.map(
([condition, icon]) =>
[
condition,
localize(
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
),
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
sortableStyles, sortableStyles,

View File

@ -0,0 +1,22 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const PASTE_VALUE = "__paste__";
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string) => void;
clipboardItem: string | undefined;
group?: string;
}
const loadDialog = () => import("./add-automation-element-dialog");
export const showAddAutomationElementDialog = (
element: HTMLElement,
dialogParams: AddAutomationElementDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "add-automation-element-dialog",
dialogImport: loadDialog,
dialogParams,
});
};

View File

@ -37,7 +37,7 @@ 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";
import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { TRIGGER_TYPES } from "../../../../data/trigger"; import { TRIGGER_ICONS } from "../../../../data/trigger";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@ -150,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
<h3 slot="header"> <h3 slot="header">
<ha-svg-icon <ha-svg-icon
class="trigger-icon" class="trigger-icon"
.path=${TRIGGER_TYPES[this.trigger.platform]} .path=${TRIGGER_ICONS[this.trigger.platform]}
></ha-svg-icon> ></ha-svg-icon>
${describeTrigger(this.trigger, this.hass, this._entityReg)} ${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3> </h3>

View File

@ -1,59 +1,25 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentPaste,
mdiDrag,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
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 { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation"; import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import type { SortableInstance } from "../../../../resources/sortable"; import type { SortableInstance } from "../../../../resources/sortable";
import { Entries, HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row"; import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import "./types/ha-automation-trigger-calendar"; import {
import "./types/ha-automation-trigger-conversation"; PASTE_VALUE,
import "./types/ha-automation-trigger-device"; showAddAutomationElementDialog,
import "./types/ha-automation-trigger-event"; } from "../show-add-automation-element-dialog";
import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag";
import "./types/ha-automation-trigger-template";
import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone";
const PASTE_VALUE = "__paste__";
@customElement("ha-automation-trigger") @customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement { export default class HaAutomationTrigger extends LitElement {
@ -147,47 +113,48 @@ export default class HaAutomationTrigger extends LitElement {
</ha-automation-trigger-row> </ha-automation-trigger-row>
` `
)} )}
<ha-button-menu <ha-button
@action=${this._addTrigger} outlined
.disabled=${this.disabled} .label=${this.hass.localize(
fixed "ui.panel.config.automation.editor.triggers.add"
>
<ha-button
slot="trigger"
outlined
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
)}
.disabled=${this.disabled}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._clipboard?.trigger
? html` <mwc-list-item .value=${PASTE_VALUE} graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.paste"
)}
(${this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${this._clipboard.trigger.platform}.label`
)})
<ha-svg-icon
slot="graphic"
.path=${mdiContentPaste}
></ha-svg-icon
></mwc-list-item>`
: nothing}
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<mwc-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></mwc-list-item>
`
)} )}
</ha-button-menu> .disabled=${this.disabled}
@click=${this._addTriggerDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div> </div>
`; `;
} }
private _addTriggerDialog() {
showAddAutomationElementDialog(this, {
type: "trigger",
add: this._addTrigger,
clipboardItem: this._clipboard?.trigger?.platform,
});
}
private _addTrigger = (value: string) => {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else {
const platform = value as Trigger["platform"];
const elClass = customElements.get(
`ha-automation-trigger-${platform}`
) as CustomElementConstructor & {
defaultConfig: Omit<Trigger, "platform">;
};
triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig,
});
}
this._focusLastTriggerOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
};
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
@ -261,30 +228,6 @@ export default class HaAutomationTrigger extends LitElement {
return this._triggerKeys.get(action)!; return this._triggerKeys.get(action)!;
} }
private _addTrigger(ev: CustomEvent<ActionDetail>) {
const value = (ev.currentTarget as HaSelect).items[ev.detail.index].value;
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else {
const platform = value as Trigger["platform"];
const elClass = customElements.get(
`ha-automation-trigger-${platform}`
) as CustomElementConstructor & {
defaultConfig: Omit<Trigger, "platform">;
};
triggers = this.triggers.concat({
platform: platform as any,
...elClass.defaultConfig,
});
}
this._focusLastTriggerOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
}
private _moveUp(ev) { private _moveUp(ev) {
const index = (ev.target as any).index; const index = (ev.target as any).index;
const newIndex = index - 1; const newIndex = index - 1;
@ -336,22 +279,6 @@ export default class HaAutomationTrigger extends LitElement {
}); });
} }
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string, string][] =>
(Object.entries(TRIGGER_TYPES) as Entries<typeof TRIGGER_TYPES>)
.map(
([action, icon]) =>
[
action,
localize(
`ui.panel.config.automation.editor.triggers.type.${action}.label`
),
icon,
] as [string, string, string]
)
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
sortableStyles, sortableStyles,

View File

@ -487,7 +487,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
<h1 class="card-header"> <h1 class="card-header">
${this._manifest?.integration_type ${this._manifest?.integration_type
? this.hass.localize( ? this.hass.localize(
`ui.panel.config.integrations.integration_page.entries_${this._manifest?.integration_type}` `ui.panel.config.integrations.integration_page.entries_${this._manifest.integration_type}`
) )
: this.hass.localize( : this.hass.localize(
`ui.panel.config.integrations.integration_page.entries` `ui.panel.config.integrations.integration_page.entries`
@ -507,7 +507,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
<ha-button @click=${this._addIntegration}> <ha-button @click=${this._addIntegration}>
${this._manifest?.integration_type ${this._manifest?.integration_type
? this.hass.localize( ? this.hass.localize(
`ui.panel.config.integrations.integration_page.add_${this._manifest?.integration_type}` `ui.panel.config.integrations.integration_page.add_${this._manifest.integration_type}`
) )
: this.hass.localize( : this.hass.localize(
`ui.panel.config.integrations.integration_page.add_entry` `ui.panel.config.integrations.integration_page.add_entry`

View File

@ -2441,6 +2441,7 @@
"edit_yaml": "Edit in YAML", "edit_yaml": "Edit in YAML",
"edit_ui": "Edit in visual editor", "edit_ui": "Edit in visual editor",
"copy_to_clipboard": "Copy to clipboard", "copy_to_clipboard": "Copy to clipboard",
"search_in": "Search · {group}",
"triggers": { "triggers": {
"name": "Triggers", "name": "Triggers",
"header": "When", "header": "When",
@ -2448,6 +2449,7 @@
"learn_more": "Learn more about triggers", "learn_more": "Learn more about triggers",
"triggered": "Triggered", "triggered": "Triggered",
"add": "Add trigger", "add": "Add trigger",
"search": "Search trigger",
"id": "Trigger ID", "id": "Trigger ID",
"edit_id": "Edit ID", "edit_id": "Edit ID",
"duplicate": "[%key:ui::common::duplicate%]", "duplicate": "[%key:ui::common::duplicate%]",
@ -2464,6 +2466,17 @@
"unsupported_platform": "No visual editor support for platform: {platform}", "unsupported_platform": "No visual editor support for platform: {platform}",
"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%]",
"groups": {
"entity": {
"label": "Entity",
"description": "When something happens to an entity"
},
"time_location": {
"label": "Time and location",
"description": "When someone enters or leaves a zone, or at a specific time."
},
"other": { "label": "Other" }
},
"type": { "type": {
"calendar": { "calendar": {
"label": "Calendar", "label": "Calendar",
@ -2482,6 +2495,9 @@
"below": "Below", "below": "Below",
"for": "Duration (optional)", "for": "Duration (optional)",
"zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]" "zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]"
},
"description": {
"picker": "When something happens to a device. Great way to start."
} }
}, },
"event": { "event": {
@ -2643,6 +2659,8 @@
"description": "This list of conditions needs to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.", "description": "This list of conditions needs to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.",
"learn_more": "Learn more about conditions", "learn_more": "Learn more about conditions",
"add": "Add condition", "add": "Add condition",
"search": "Search condition",
"add_building_block": "Add building block",
"test": "Test", "test": "Test",
"testing_error": "Condition did not pass", "testing_error": "Condition did not pass",
"testing_pass": "Condition passes", "testing_pass": "Condition passes",
@ -2662,6 +2680,21 @@
"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%]",
"groups": {
"entity": {
"label": "Entity",
"description": "If an entity is in a specific state"
},
"time_location": {
"label": "Time and location",
"description": "If someone is in a zone or if the current time is before or after a specified time"
},
"other": { "label": "Other" },
"building_blocks": {
"label": "Building blocks",
"description": "Build more complex conditions"
}
},
"type": { "type": {
"and": { "and": {
"label": "And", "label": "And",
@ -2679,6 +2712,9 @@
"for": "Duration", "for": "Duration",
"hvac_mode": "HVAC mode", "hvac_mode": "HVAC mode",
"preset_mode": "Preset mode" "preset_mode": "Preset mode"
},
"description": {
"picker": "If something happens to a device. Great way to start."
} }
}, },
"not": { "not": {
@ -2781,6 +2817,8 @@
"description": "This list of actions will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.", "description": "This list of actions will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.",
"learn_more": "Learn more about actions", "learn_more": "Learn more about actions",
"add": "Add action", "add": "Add action",
"search": "Search action",
"add_building_block": "Add building block",
"invalid_action": "Invalid action", "invalid_action": "Invalid action",
"run": "Run", "run": "Run",
"run_action_error": "Error running action", "run_action_error": "Error running action",
@ -2802,6 +2840,14 @@
"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",
"groups": {
"helpers": { "label": "Helpers" },
"other": { "label": "Other" },
"building_blocks": {
"label": "Building blocks",
"description": "Build more complex sequences of actions"
}
},
"type": { "type": {
"service": { "service": {
"label": "Call service", "label": "Call service",
@ -2821,6 +2867,7 @@
"play_media": { "play_media": {
"label": "Play media", "label": "Play media",
"description": { "description": {
"picker": "Play media on a media player",
"full": "Play {hasMedia, select, \n true {{media}} \n other {media}\n } on {hasMediaPlayer, select, \n true {{mediaPlayer}} \n other {a media player}\n }" "full": "Play {hasMedia, select, \n true {{media}} \n other {media}\n } on {hasMediaPlayer, select, \n true {{mediaPlayer}} \n other {a media player}\n }"
} }
}, },
@ -3703,6 +3750,8 @@
"entries_service": "Services", "entries_service": "Services",
"entries_helper": "Helpers", "entries_helper": "Helpers",
"entries_hardware": "Hardware", "entries_hardware": "Hardware",
"entries_system": "[%key:ui::panel::config::integrations::integration_page::entries%]",
"entries_entity": "[%key:ui::panel::config::integrations::integration_page::entries%]",
"no_entries": "No entries", "no_entries": "No entries",
"attention_entries": "Needs attention", "attention_entries": "Needs attention",
"add_entry": "Add entry", "add_entry": "Add entry",
@ -3710,7 +3759,9 @@
"add_hub": "Add hub", "add_hub": "Add hub",
"add_service": "Add service", "add_service": "Add service",
"add_helper": "Add helper", "add_helper": "Add helper",
"add_hardware": "Add hardware" "add_hardware": "Add hardware",
"add_entity": "[%key:ui::panel::config::integrations::integration_page::add_entry%]",
"add_system": "[%key:ui::panel::config::integrations::integration_page::add_entry%]"
}, },
"config_entry": { "config_entry": {
"application_credentials": { "application_credentials": {