mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +00:00
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:
parent
7b6b5724e1
commit
8f07e6f141
@ -47,6 +47,13 @@ export class HaListItem extends ListItemBase {
|
||||
display: var(--mdc-list-item-meta-display);
|
||||
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]) {
|
||||
height: auto;
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
mdiCallSplit,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiDotsHorizontal,
|
||||
mdiExcavator,
|
||||
mdiGestureDoubleTap,
|
||||
mdiHandBackRight,
|
||||
mdiPalette,
|
||||
@ -13,10 +15,12 @@ import {
|
||||
mdiRoomService,
|
||||
mdiShuffleDisabled,
|
||||
mdiTimerOutline,
|
||||
mdiTools,
|
||||
mdiTrafficLight,
|
||||
} from "@mdi/js";
|
||||
import { AutomationElementGroup } from "./automation";
|
||||
|
||||
export const ACTION_TYPES = {
|
||||
export const ACTION_ICONS = {
|
||||
condition: mdiAbTesting,
|
||||
delay: mdiTimerOutline,
|
||||
event: mdiGestureDoubleTap,
|
||||
@ -34,6 +38,43 @@ export const ACTION_TYPES = {
|
||||
variables: mdiApplicationVariableOutline,
|
||||
} 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",
|
||||
]);
|
||||
|
||||
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);
|
||||
|
@ -275,6 +275,10 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
|
||||
not: Condition[];
|
||||
}
|
||||
|
||||
export interface AutomationElementGroup {
|
||||
[key: string]: { icon?: string; members?: AutomationElementGroup };
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| StateCondition
|
||||
| NumericStateCondition
|
||||
|
@ -3,16 +3,21 @@ import {
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiDotsHorizontal,
|
||||
mdiExcavator,
|
||||
mdiGateOr,
|
||||
mdiIdentifier,
|
||||
mdiMapClock,
|
||||
mdiMapMarkerRadius,
|
||||
mdiNotEqualVariant,
|
||||
mdiNumeric,
|
||||
mdiShape,
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { AutomationElementGroup } from "./automation";
|
||||
|
||||
export const CONDITION_TYPES = {
|
||||
export const CONDITION_ICONS = {
|
||||
device: mdiDevices,
|
||||
and: mdiAmpersand,
|
||||
or: mdiGateOr,
|
||||
@ -25,3 +30,23 @@ export const CONDITION_TYPES = {
|
||||
trigger: mdiIdentifier,
|
||||
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;
|
||||
|
@ -16,7 +16,9 @@ export type IntegrationType =
|
||||
| "helper"
|
||||
| "hub"
|
||||
| "service"
|
||||
| "hardware";
|
||||
| "hardware"
|
||||
| "entity"
|
||||
| "system";
|
||||
|
||||
export interface IntegrationManifest {
|
||||
is_built_in: boolean;
|
||||
|
@ -4,13 +4,16 @@ import {
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiDotsHorizontal,
|
||||
mdiGestureDoubleTap,
|
||||
mdiMapClock,
|
||||
mdiMapMarker,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMessageAlert,
|
||||
mdiMicrophoneMessage,
|
||||
mdiNfcVariant,
|
||||
mdiNumeric,
|
||||
mdiShape,
|
||||
mdiStateMachine,
|
||||
mdiSwapHorizontal,
|
||||
mdiWeatherSunny,
|
||||
@ -18,8 +21,9 @@ import {
|
||||
} from "@mdi/js";
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { AutomationElementGroup } from "./automation";
|
||||
|
||||
export const TRIGGER_TYPES = {
|
||||
export const TRIGGER_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
device: mdiDevices,
|
||||
event: mdiGestureDoubleTap,
|
||||
@ -38,3 +42,26 @@ export const TRIGGER_TYPES = {
|
||||
persistent_notification: mdiMessageAlert,
|
||||
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;
|
||||
|
@ -37,7 +37,7 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
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 { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@ -82,9 +82,9 @@ export const getType = (action: Action | undefined) => {
|
||||
if (["and", "or", "not"].some((key) => key in action)) {
|
||||
return "condition" as const;
|
||||
}
|
||||
return Object.keys(ACTION_TYPES).find(
|
||||
return Object.keys(ACTION_ICONS).find(
|
||||
(option) => option in action
|
||||
) as keyof typeof ACTION_TYPES;
|
||||
) as keyof typeof ACTION_ICONS;
|
||||
};
|
||||
|
||||
export interface ActionElement extends LitElement {
|
||||
@ -190,7 +190,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="action-icon"
|
||||
.path=${ACTION_TYPES[type!]}
|
||||
.path=${ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
|
@ -1,57 +1,26 @@
|
||||
import "@material/mwc-button";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentPaste,
|
||||
mdiDrag,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
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-menu";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { ACTION_TYPES } from "../../../../data/action";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { getService, isService } from "../../../../data/action";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
||||
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 { 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")
|
||||
export default class HaAutomationAction extends LitElement {
|
||||
@ -150,42 +119,26 @@ export default class HaAutomationAction extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ha-button-menu
|
||||
@action=${this._addAction}
|
||||
<ha-button
|
||||
slot="trigger"
|
||||
outlined
|
||||
.disabled=${this.disabled}
|
||||
fixed
|
||||
>
|
||||
<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>
|
||||
`
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.add"
|
||||
)}
|
||||
</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) {
|
||||
if (this.nested) return;
|
||||
ev.stopPropagation();
|
||||
@ -258,25 +248,6 @@ export default class HaAutomationAction extends LitElement {
|
||||
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) {
|
||||
const index = (ev.target as any).index;
|
||||
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 {
|
||||
return [
|
||||
sortableStyles,
|
||||
|
@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import type { Condition } from "../../../../../data/automation";
|
||||
import { CONDITION_TYPES } from "../../../../../data/condition";
|
||||
import { CONDITION_ICONS } from "../../../../../data/condition";
|
||||
import { Entries, HomeAssistant } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
(Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>)
|
||||
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
|
||||
.map(
|
||||
([condition, icon]) =>
|
||||
[
|
||||
|
553
src/panels/config/automation/add-automation-element-dialog.ts
Normal file
553
src/panels/config/automation/add-automation-element-dialog.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ import "../../../../components/ha-icon-button";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Condition, testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import { CONDITION_TYPES } from "../../../../data/condition";
|
||||
import { CONDITION_ICONS } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
@ -123,7 +123,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="condition-icon"
|
||||
.path=${CONDITION_TYPES[this.condition.condition]}
|
||||
.path=${CONDITION_ICONS[this.condition.condition]}
|
||||
></ha-svg-icon>
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
|
@ -1,25 +1,18 @@
|
||||
import "@material/mwc-button";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentPaste,
|
||||
mdiDrag,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
@ -28,30 +21,15 @@ import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} 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 type { SortableInstance } from "../../../../resources/sortable";
|
||||
import "./types/ha-automation-condition-and";
|
||||
import "./types/ha-automation-condition-device";
|
||||
import "./types/ha-automation-condition-numeric_state";
|
||||
import "./types/ha-automation-condition-state";
|
||||
import "./types/ha-automation-condition-sun";
|
||||
import "./types/ha-automation-condition-template";
|
||||
import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
|
||||
const PASTE_VALUE = "__paste__";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends LitElement {
|
||||
@ -197,43 +175,67 @@ export default class HaAutomationCondition extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ha-button-menu
|
||||
@action=${this._addCondition}
|
||||
<ha-button
|
||||
outlined
|
||||
.disabled=${this.disabled}
|
||||
fixed
|
||||
>
|
||||
<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>
|
||||
`
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.add"
|
||||
)}
|
||||
</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) {
|
||||
if (this.nested) return;
|
||||
ev.stopPropagation();
|
||||
@ -282,32 +284,6 @@ export default class HaAutomationCondition extends LitElement {
|
||||
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) {
|
||||
const index = (ev.target as any).index;
|
||||
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 {
|
||||
return [
|
||||
sortableStyles,
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
@ -37,7 +37,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { TRIGGER_TYPES } from "../../../../data/trigger";
|
||||
import { TRIGGER_ICONS } from "../../../../data/trigger";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@ -150,7 +150,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_TYPES[this.trigger.platform]}
|
||||
.path=${TRIGGER_ICONS[this.trigger.platform]}
|
||||
></ha-svg-icon>
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
</h3>
|
||||
|
@ -1,59 +1,25 @@
|
||||
import "@material/mwc-button";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentPaste,
|
||||
mdiDrag,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
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-menu";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { AutomationClipboard, Trigger } from "../../../../data/automation";
|
||||
import { TRIGGER_TYPES } from "../../../../data/trigger";
|
||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
||||
import type { SortableInstance } from "../../../../resources/sortable";
|
||||
import { Entries, HomeAssistant } from "../../../../types";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
import "./types/ha-automation-trigger-conversation";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
import "./types/ha-automation-trigger-event";
|
||||
import "./types/ha-automation-trigger-geo_location";
|
||||
import "./types/ha-automation-trigger-homeassistant";
|
||||
import "./types/ha-automation-trigger-mqtt";
|
||||
import "./types/ha-automation-trigger-numeric_state";
|
||||
import "./types/ha-automation-trigger-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__";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
|
||||
@customElement("ha-automation-trigger")
|
||||
export default class HaAutomationTrigger extends LitElement {
|
||||
@ -147,47 +113,48 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
</ha-automation-trigger-row>
|
||||
`
|
||||
)}
|
||||
<ha-button-menu
|
||||
@action=${this._addTrigger}
|
||||
.disabled=${this.disabled}
|
||||
fixed
|
||||
>
|
||||
<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
|
||||
outlined
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.add"
|
||||
)}
|
||||
</ha-button-menu>
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._addTriggerDialog}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||
</ha-button>
|
||||
</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) {
|
||||
super.updated(changedProps);
|
||||
|
||||
@ -261,30 +228,6 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
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) {
|
||||
const index = (ev.target as any).index;
|
||||
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 {
|
||||
return [
|
||||
sortableStyles,
|
||||
|
@ -487,7 +487,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
<h1 class="card-header">
|
||||
${this._manifest?.integration_type
|
||||
? 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(
|
||||
`ui.panel.config.integrations.integration_page.entries`
|
||||
@ -507,7 +507,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
<ha-button @click=${this._addIntegration}>
|
||||
${this._manifest?.integration_type
|
||||
? 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(
|
||||
`ui.panel.config.integrations.integration_page.add_entry`
|
||||
|
@ -2441,6 +2441,7 @@
|
||||
"edit_yaml": "Edit in YAML",
|
||||
"edit_ui": "Edit in visual editor",
|
||||
"copy_to_clipboard": "Copy to clipboard",
|
||||
"search_in": "Search · {group}",
|
||||
"triggers": {
|
||||
"name": "Triggers",
|
||||
"header": "When",
|
||||
@ -2448,6 +2449,7 @@
|
||||
"learn_more": "Learn more about triggers",
|
||||
"triggered": "Triggered",
|
||||
"add": "Add trigger",
|
||||
"search": "Search trigger",
|
||||
"id": "Trigger ID",
|
||||
"edit_id": "Edit ID",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
@ -2464,6 +2466,17 @@
|
||||
"unsupported_platform": "No visual editor support for platform: {platform}",
|
||||
"type_select": "Trigger type",
|
||||
"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": {
|
||||
"calendar": {
|
||||
"label": "Calendar",
|
||||
@ -2482,6 +2495,9 @@
|
||||
"below": "Below",
|
||||
"for": "Duration (optional)",
|
||||
"zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]"
|
||||
},
|
||||
"description": {
|
||||
"picker": "When something happens to a device. Great way to start."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"learn_more": "Learn more about conditions",
|
||||
"add": "Add condition",
|
||||
"search": "Search condition",
|
||||
"add_building_block": "Add building block",
|
||||
"test": "Test",
|
||||
"testing_error": "Condition did not pass",
|
||||
"testing_pass": "Condition passes",
|
||||
@ -2662,6 +2680,21 @@
|
||||
"unsupported_condition": "No visual editor support for condition: {condition}",
|
||||
"type_select": "Condition type",
|
||||
"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": {
|
||||
"and": {
|
||||
"label": "And",
|
||||
@ -2679,6 +2712,9 @@
|
||||
"for": "Duration",
|
||||
"hvac_mode": "HVAC mode",
|
||||
"preset_mode": "Preset mode"
|
||||
},
|
||||
"description": {
|
||||
"picker": "If something happens to a device. Great way to start."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"learn_more": "Learn more about actions",
|
||||
"add": "Add action",
|
||||
"search": "Search action",
|
||||
"add_building_block": "Add building block",
|
||||
"invalid_action": "Invalid action",
|
||||
"run": "Run",
|
||||
"run_action_error": "Error running action",
|
||||
@ -2802,6 +2840,14 @@
|
||||
"unsupported_action": "No visual editor support for this action",
|
||||
"type_select": "Action type",
|
||||
"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": {
|
||||
"service": {
|
||||
"label": "Call service",
|
||||
@ -2821,6 +2867,7 @@
|
||||
"play_media": {
|
||||
"label": "Play media",
|
||||
"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 }"
|
||||
}
|
||||
},
|
||||
@ -3703,6 +3750,8 @@
|
||||
"entries_service": "Services",
|
||||
"entries_helper": "Helpers",
|
||||
"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",
|
||||
"attention_entries": "Needs attention",
|
||||
"add_entry": "Add entry",
|
||||
@ -3710,7 +3759,9 @@
|
||||
"add_hub": "Add hub",
|
||||
"add_service": "Add service",
|
||||
"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": {
|
||||
"application_credentials": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user