mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-08 18:39:40 +00:00
1281 lines
37 KiB
TypeScript
1281 lines
37 KiB
TypeScript
import {
|
||
mdiAppleKeyboardCommand,
|
||
mdiClose,
|
||
mdiContentPaste,
|
||
mdiPlus,
|
||
} from "@mdi/js";
|
||
import Fuse from "fuse.js";
|
||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||
import { LitElement, css, html, nothing } from "lit";
|
||
import {
|
||
customElement,
|
||
eventOptions,
|
||
property,
|
||
query,
|
||
state,
|
||
} from "lit/decorators";
|
||
import { classMap } from "lit/directives/class-map";
|
||
import { ifDefined } from "lit/directives/if-defined";
|
||
import { repeat } from "lit/directives/repeat";
|
||
import memoizeOne from "memoize-one";
|
||
import { tinykeys } from "tinykeys";
|
||
import { fireEvent } from "../../../common/dom/fire_event";
|
||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||
import { stringCompare } from "../../../common/string/compare";
|
||
import type {
|
||
LocalizeFunc,
|
||
LocalizeKeys,
|
||
} from "../../../common/translations/localize";
|
||
import { debounce } from "../../../common/util/debounce";
|
||
import { deepEqual } from "../../../common/util/deep-equal";
|
||
import "../../../components/ha-bottom-sheet";
|
||
import "../../../components/ha-button-toggle-group";
|
||
import "../../../components/ha-dialog-header";
|
||
import "../../../components/ha-domain-icon";
|
||
import "../../../components/ha-icon-button";
|
||
import "../../../components/ha-icon-button-prev";
|
||
import "../../../components/ha-icon-next";
|
||
import "../../../components/ha-md-divider";
|
||
import "../../../components/ha-md-list";
|
||
import type { HaMdList } from "../../../components/ha-md-list";
|
||
import "../../../components/ha-md-list-item";
|
||
import "../../../components/ha-service-icon";
|
||
import "../../../components/ha-wa-dialog";
|
||
import "../../../components/search-input";
|
||
import {
|
||
ACTION_BUILDING_BLOCKS_GROUP,
|
||
ACTION_COLLECTIONS,
|
||
ACTION_ICONS,
|
||
SERVICE_PREFIX,
|
||
getService,
|
||
isService,
|
||
} from "../../../data/action";
|
||
import type {
|
||
AutomationElementGroup,
|
||
AutomationElementGroupCollection,
|
||
} from "../../../data/automation";
|
||
import {
|
||
CONDITION_BUILDING_BLOCKS_GROUP,
|
||
CONDITION_COLLECTIONS,
|
||
CONDITION_ICONS,
|
||
} from "../../../data/condition";
|
||
import { getServiceIcons } from "../../../data/icons";
|
||
import type { IntegrationManifest } from "../../../data/integration";
|
||
import {
|
||
domainToName,
|
||
fetchIntegrationManifests,
|
||
} from "../../../data/integration";
|
||
import { TRIGGER_COLLECTIONS, TRIGGER_ICONS } from "../../../data/trigger";
|
||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||
import { HaFuse } from "../../../resources/fuse";
|
||
import type { HomeAssistant } from "../../../types";
|
||
import { isMac } from "../../../util/is_mac";
|
||
import { showToast } from "../../../util/toast";
|
||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||
|
||
const TYPES = {
|
||
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
|
||
condition: {
|
||
collections: CONDITION_COLLECTIONS,
|
||
icons: CONDITION_ICONS,
|
||
},
|
||
action: {
|
||
collections: ACTION_COLLECTIONS,
|
||
icons: ACTION_ICONS,
|
||
},
|
||
};
|
||
|
||
interface ListItem {
|
||
key: string;
|
||
name: string;
|
||
description: string;
|
||
iconPath?: string;
|
||
icon?: TemplateResult;
|
||
}
|
||
|
||
type DomainManifestLookup = Record<string, IntegrationManifest>;
|
||
|
||
const ENTITY_DOMAINS_OTHER = new Set([
|
||
"date",
|
||
"datetime",
|
||
"device_tracker",
|
||
"text",
|
||
"time",
|
||
"tts",
|
||
"update",
|
||
"weather",
|
||
"image_processing",
|
||
]);
|
||
|
||
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||
|
||
const ACTION_SERVICE_KEYWORDS = ["serviceGroups", "helpers", "other"];
|
||
|
||
@customElement("add-automation-element-dialog")
|
||
class DialogAddAutomationElement
|
||
extends KeyboardShortcutMixin(LitElement)
|
||
implements HassDialog
|
||
{
|
||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||
|
||
@state() private _params?: AddAutomationElementDialogParams;
|
||
|
||
@state() private _selectedCollectionIndex?: number;
|
||
|
||
@state() private _selectedGroup?: string;
|
||
|
||
@state() private _tab: "groups" | "blocks" = "groups";
|
||
|
||
@state() private _filter = "";
|
||
|
||
@state() private _manifests?: DomainManifestLookup;
|
||
|
||
@state() private _domains?: Set<string>;
|
||
|
||
@state() private _open = true;
|
||
|
||
@state() private _itemsScrolled = false;
|
||
|
||
@state() private _bottomSheetMode = false;
|
||
|
||
@state() private _narrow = false;
|
||
|
||
@query(".items ha-md-list ha-md-list-item")
|
||
private _itemsListFirstElement?: HaMdList;
|
||
|
||
@query(".items")
|
||
private _itemsListElement?: HTMLDivElement;
|
||
|
||
private _fullScreen = false;
|
||
|
||
private _removeKeyboardShortcuts?: () => void;
|
||
|
||
public showDialog(params): void {
|
||
this._params = params;
|
||
|
||
this.addKeyboardShortcuts();
|
||
|
||
if (this._params?.type === "action") {
|
||
this.hass.loadBackendTranslation("services");
|
||
this._fetchManifests();
|
||
this._calculateUsedDomains();
|
||
getServiceIcons(this.hass);
|
||
}
|
||
this._fullScreen = matchMedia(
|
||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||
).matches;
|
||
|
||
window.addEventListener("resize", this._updateNarrow);
|
||
this._updateNarrow();
|
||
|
||
// prevent view mode switch when resizing window
|
||
this._bottomSheetMode = this._narrow;
|
||
}
|
||
|
||
public closeDialog() {
|
||
this.removeKeyboardShortcuts();
|
||
if (this._params) {
|
||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||
}
|
||
this._open = true;
|
||
this._itemsScrolled = false;
|
||
this._bottomSheetMode = false;
|
||
this._params = undefined;
|
||
this._selectedGroup = undefined;
|
||
this._tab = "groups";
|
||
this._selectedCollectionIndex = undefined;
|
||
this._filter = "";
|
||
this._manifests = undefined;
|
||
this._domains = undefined;
|
||
return true;
|
||
}
|
||
|
||
private _getGroups = (
|
||
type: AddAutomationElementDialogParams["type"],
|
||
group?: string,
|
||
collectionIndex?: number
|
||
): AutomationElementGroup => {
|
||
if (group && collectionIndex !== undefined) {
|
||
return (
|
||
TYPES[type].collections[collectionIndex].groups[group].members || {
|
||
[group]: {},
|
||
}
|
||
);
|
||
}
|
||
|
||
return TYPES[type].collections.reduce(
|
||
(acc, collection) => ({ ...acc, ...collection.groups }),
|
||
{} as AutomationElementGroup
|
||
);
|
||
};
|
||
|
||
private _convertToItem = (
|
||
key: string,
|
||
options,
|
||
type: AddAutomationElementDialogParams["type"],
|
||
localize: LocalizeFunc
|
||
): ListItem => ({
|
||
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"}`
|
||
),
|
||
iconPath: options.icon || TYPES[type].icons[key],
|
||
});
|
||
|
||
private _getFilteredItems = memoizeOne(
|
||
(
|
||
type: AddAutomationElementDialogParams["type"],
|
||
filter: string,
|
||
localize: LocalizeFunc,
|
||
services: HomeAssistant["services"],
|
||
manifests?: DomainManifestLookup
|
||
): ListItem[] => {
|
||
const items = this._items(type, localize, services, manifests);
|
||
|
||
const index = this._fuseIndex(items);
|
||
|
||
const fuse = new HaFuse(
|
||
items,
|
||
{
|
||
ignoreLocation: true,
|
||
includeScore: true,
|
||
minMatchCharLength: Math.min(2, this._filter.length),
|
||
},
|
||
index
|
||
);
|
||
|
||
const results = fuse.multiTermsSearch(filter);
|
||
if (results) {
|
||
return results.map((result) => result.item);
|
||
}
|
||
return items;
|
||
}
|
||
);
|
||
|
||
private _getFilteredBuildingBlocks = memoizeOne(
|
||
(
|
||
type: AddAutomationElementDialogParams["type"],
|
||
filter: string,
|
||
localize: LocalizeFunc
|
||
): ListItem[] => {
|
||
const groups =
|
||
type === "action"
|
||
? ACTION_BUILDING_BLOCKS_GROUP
|
||
: type === "condition"
|
||
? CONDITION_BUILDING_BLOCKS_GROUP
|
||
: {};
|
||
|
||
const items = Object.keys(groups).map((key) =>
|
||
this._convertToItem(key, {}, type, localize)
|
||
);
|
||
|
||
const index = this._fuseIndexBlock(items);
|
||
|
||
const fuse = new HaFuse(
|
||
items,
|
||
{
|
||
ignoreLocation: true,
|
||
includeScore: true,
|
||
minMatchCharLength: Math.min(2, this._filter.length),
|
||
},
|
||
index
|
||
);
|
||
|
||
const results = fuse.multiTermsSearch(filter);
|
||
if (results) {
|
||
return results.map((result) => result.item);
|
||
}
|
||
return items;
|
||
}
|
||
);
|
||
|
||
private _items = memoizeOne(
|
||
(
|
||
type: AddAutomationElementDialogParams["type"],
|
||
localize: LocalizeFunc,
|
||
services: HomeAssistant["services"],
|
||
manifests?: DomainManifestLookup
|
||
): ListItem[] => {
|
||
const groups = this._getGroups(type);
|
||
|
||
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));
|
||
}
|
||
return items;
|
||
}
|
||
);
|
||
|
||
private _fuseIndex = memoizeOne((items: ListItem[]) =>
|
||
Fuse.createIndex(["key", "name", "description"], items)
|
||
);
|
||
|
||
private _fuseIndexBlock = memoizeOne((items: ListItem[]) =>
|
||
Fuse.createIndex(["key", "name", "description"], items)
|
||
);
|
||
|
||
private _getCollections = memoizeOne(
|
||
(
|
||
type: AddAutomationElementDialogParams["type"],
|
||
collections: AutomationElementGroupCollection[],
|
||
domains: Set<string> | undefined,
|
||
localize: LocalizeFunc,
|
||
services: HomeAssistant["services"],
|
||
manifests?: DomainManifestLookup
|
||
): {
|
||
titleKey?: LocalizeKeys;
|
||
groups: ListItem[];
|
||
}[] => {
|
||
const generatedCollections: any = [];
|
||
|
||
collections.forEach((collection) => {
|
||
let collectionGroups = Object.entries(collection.groups);
|
||
const groups: ListItem[] = [];
|
||
|
||
if (
|
||
type === "action" &&
|
||
Object.keys(collection.groups).some((item) =>
|
||
ACTION_SERVICE_KEYWORDS.includes(item)
|
||
)
|
||
) {
|
||
groups.push(
|
||
...this._serviceGroups(
|
||
localize,
|
||
services,
|
||
manifests,
|
||
domains,
|
||
collection.groups.serviceGroups
|
||
? undefined
|
||
: collection.groups.helpers
|
||
? "helper"
|
||
: "other"
|
||
)
|
||
);
|
||
|
||
collectionGroups = collectionGroups.filter(
|
||
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
|
||
);
|
||
}
|
||
|
||
groups.push(
|
||
...collectionGroups.map(([key, options]) =>
|
||
this._convertToItem(key, options, type, localize)
|
||
)
|
||
);
|
||
|
||
generatedCollections.push({
|
||
titleKey: collection.titleKey,
|
||
groups: groups.sort((a, b) =>
|
||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||
),
|
||
});
|
||
});
|
||
return generatedCollections;
|
||
}
|
||
);
|
||
|
||
private _getBlockItems = memoizeOne(
|
||
(
|
||
type: AddAutomationElementDialogParams["type"],
|
||
localize: LocalizeFunc
|
||
): ListItem[] => {
|
||
const groups =
|
||
type === "action"
|
||
? ACTION_BUILDING_BLOCKS_GROUP
|
||
: CONDITION_BUILDING_BLOCKS_GROUP;
|
||
|
||
const result = Object.entries(groups).map(([key, options]) =>
|
||
this._convertToItem(key, options, type, localize)
|
||
);
|
||
|
||
return result.sort((a, b) =>
|
||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||
);
|
||
}
|
||
);
|
||
|
||
private _getGroupItems = memoizeOne(
|
||
(
|
||
type: AddAutomationElementDialogParams["type"],
|
||
group: string,
|
||
collectionIndex: number,
|
||
domains: Set<string> | undefined,
|
||
localize: LocalizeFunc,
|
||
services: HomeAssistant["services"],
|
||
manifests?: DomainManifestLookup
|
||
): ListItem[] => {
|
||
if (type === "action" && isService(group)) {
|
||
return this._services(localize, services, manifests, group);
|
||
}
|
||
|
||
const groups = this._getGroups(type, group, collectionIndex);
|
||
|
||
const result = Object.entries(groups).map(([key, options]) =>
|
||
this._convertToItem(key, options, type, localize)
|
||
);
|
||
|
||
if (type === "action") {
|
||
if (!this._selectedGroup) {
|
||
result.unshift(
|
||
...this._serviceGroups(
|
||
localize,
|
||
services,
|
||
manifests,
|
||
domains,
|
||
undefined
|
||
)
|
||
);
|
||
} else if (this._selectedGroup === "helpers") {
|
||
result.unshift(
|
||
...this._serviceGroups(
|
||
localize,
|
||
services,
|
||
manifests,
|
||
domains,
|
||
"helper"
|
||
)
|
||
);
|
||
} else if (this._selectedGroup === "other") {
|
||
result.unshift(
|
||
...this._serviceGroups(
|
||
localize,
|
||
services,
|
||
manifests,
|
||
domains,
|
||
"other"
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
return result.sort((a, b) =>
|
||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||
);
|
||
}
|
||
);
|
||
|
||
private _serviceGroups = (
|
||
localize: LocalizeFunc,
|
||
services: HomeAssistant["services"],
|
||
manifests: DomainManifestLookup | undefined,
|
||
domains: Set<string> | undefined,
|
||
type: "helper" | "other" | undefined
|
||
): ListItem[] => {
|
||
if (!services || !manifests) {
|
||
return [];
|
||
}
|
||
const result: ListItem[] = [];
|
||
Object.keys(services).forEach((domain) => {
|
||
const manifest = manifests[domain];
|
||
const domainUsed = !domains ? true : domains.has(domain);
|
||
if (
|
||
(type === undefined &&
|
||
(ENTITY_DOMAINS_MAIN.has(domain) ||
|
||
(manifest?.integration_type === "entity" &&
|
||
domainUsed &&
|
||
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
|
||
(type === "helper" && manifest?.integration_type === "helper") ||
|
||
(type === "other" &&
|
||
!ENTITY_DOMAINS_MAIN.has(domain) &&
|
||
(ENTITY_DOMAINS_OTHER.has(domain) ||
|
||
(!domainUsed && manifest?.integration_type === "entity") ||
|
||
!["helper", "entity"].includes(manifest?.integration_type || "")))
|
||
) {
|
||
result.push({
|
||
icon: html`
|
||
<ha-domain-icon
|
||
.hass=${this.hass}
|
||
.domain=${domain}
|
||
brand-fallback
|
||
></ha-domain-icon>
|
||
`,
|
||
key: `${SERVICE_PREFIX}${domain}`,
|
||
name: domainToName(localize, domain, manifest),
|
||
description: "",
|
||
});
|
||
}
|
||
});
|
||
return result.sort((a, b) =>
|
||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||
);
|
||
};
|
||
|
||
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({
|
||
icon: html`
|
||
<ha-service-icon
|
||
.hass=${this.hass}
|
||
.service=${`${dmn}.${service}`}
|
||
></ha-service-icon>
|
||
`,
|
||
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.${dmn}.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;
|
||
}
|
||
|
||
private _calculateUsedDomains() {
|
||
const domains = new Set(Object.keys(this.hass.states).map(computeDomain));
|
||
if (!deepEqual(domains, this._domains)) {
|
||
this._domains = domains;
|
||
}
|
||
}
|
||
|
||
protected willUpdate(changedProperties: PropertyValues): void {
|
||
if (
|
||
this._params?.type === "action" &&
|
||
changedProperties.has("hass") &&
|
||
changedProperties.get("hass")?.states !== this.hass.states
|
||
) {
|
||
this._calculateUsedDomains();
|
||
}
|
||
}
|
||
|
||
private _renderContent() {
|
||
const automationElementType = this._params!.type;
|
||
|
||
const items = this._filter
|
||
? this._getFilteredItems(
|
||
automationElementType,
|
||
this._filter,
|
||
this.hass.localize,
|
||
this.hass.services,
|
||
this._manifests
|
||
)
|
||
: this._tab === "blocks"
|
||
? this._getBlockItems(automationElementType, this.hass.localize)
|
||
: this._selectedGroup
|
||
? this._getGroupItems(
|
||
automationElementType,
|
||
this._selectedGroup,
|
||
this._selectedCollectionIndex ?? 0,
|
||
this._domains,
|
||
this.hass.localize,
|
||
this.hass.services,
|
||
this._manifests
|
||
)
|
||
: undefined;
|
||
|
||
const filteredBlockItems =
|
||
this._filter && automationElementType !== "trigger"
|
||
? this._getFilteredBuildingBlocks(
|
||
automationElementType,
|
||
this._filter,
|
||
this.hass.localize
|
||
)
|
||
: undefined;
|
||
|
||
const collections = this._getCollections(
|
||
automationElementType,
|
||
TYPES[automationElementType].collections,
|
||
this._domains,
|
||
this.hass.localize,
|
||
this.hass.services,
|
||
this._manifests
|
||
);
|
||
|
||
const groupName = isService(this._selectedGroup)
|
||
? domainToName(
|
||
this.hass.localize,
|
||
getService(this._selectedGroup!),
|
||
this._manifests?.[getService(this._selectedGroup!)]
|
||
)
|
||
: this.hass.localize(
|
||
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
|
||
) ||
|
||
this.hass.localize(
|
||
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
|
||
);
|
||
|
||
const typeTitle = this.hass.localize(
|
||
`ui.panel.config.automation.editor.${automationElementType}s.header`
|
||
);
|
||
|
||
const tabButtons = [
|
||
{
|
||
label: this.hass.localize(
|
||
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
||
),
|
||
value: "groups",
|
||
},
|
||
{
|
||
label: this.hass.localize(`ui.panel.config.automation.editor.blocks`),
|
||
value: "blocks",
|
||
},
|
||
];
|
||
|
||
const hideCollections =
|
||
this._filter ||
|
||
this._tab === "blocks" ||
|
||
(this._narrow && this._selectedGroup);
|
||
|
||
return html`
|
||
<div slot="header">
|
||
<ha-dialog-header subtitle-position="above">
|
||
<span slot="title"
|
||
>${this._narrow && this._selectedGroup
|
||
? groupName
|
||
: typeTitle}</span
|
||
>
|
||
|
||
${this._narrow && this._selectedGroup
|
||
? html`<span slot="subtitle">${typeTitle}</span>`
|
||
: nothing}
|
||
${this._narrow && this._selectedGroup
|
||
? html`<ha-icon-button-prev
|
||
slot="navigationIcon"
|
||
@click=${this._back}
|
||
></ha-icon-button-prev>`
|
||
: html`<ha-icon-button
|
||
.path=${mdiClose}
|
||
@click=${this._close}
|
||
slot="navigationIcon"
|
||
></ha-icon-button>`}
|
||
</ha-dialog-header>
|
||
${!this._narrow || !this._selectedGroup
|
||
? html`
|
||
<search-input
|
||
?autofocus=${!this._narrow}
|
||
.hass=${this.hass}
|
||
.filter=${this._filter}
|
||
@value-changed=${this._debounceFilterChanged}
|
||
.label=${this.hass.localize(`ui.common.search`)}
|
||
@focus=${this._onSearchFocus}
|
||
@blur=${this._removeSearchKeybindings}
|
||
></search-input>
|
||
`
|
||
: nothing}
|
||
${this._params?.type !== "trigger" &&
|
||
!this._filter &&
|
||
(!this._narrow || !this._selectedGroup)
|
||
? html`<ha-button-toggle-group
|
||
variant="neutral"
|
||
active-variant="brand"
|
||
.buttons=${tabButtons}
|
||
.active=${this._tab}
|
||
size="small"
|
||
full-width
|
||
@value-changed=${this._switchTab}
|
||
></ha-button-toggle-group>`
|
||
: nothing}
|
||
</div>
|
||
<div class="content">
|
||
<ha-md-list
|
||
class=${classMap({
|
||
groups: true,
|
||
hidden: hideCollections,
|
||
})}
|
||
>
|
||
${this._params!.clipboardItem && !this._filter
|
||
? html`<ha-md-list-item
|
||
interactive
|
||
type="button"
|
||
class="paste"
|
||
.value=${PASTE_VALUE}
|
||
@click=${this._selected}
|
||
>
|
||
<div class="shortcut-label">
|
||
<div class="label">
|
||
<div>
|
||
${this.hass.localize(
|
||
`ui.panel.config.automation.editor.${automationElementType}s.paste`
|
||
)}
|
||
</div>
|
||
<div class="supporting-text">
|
||
${this.hass.localize(
|
||
// @ts-ignore
|
||
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
|
||
)}
|
||
</div>
|
||
</div>
|
||
${!this._narrow
|
||
? html`<span class="shortcut">
|
||
<span
|
||
>${isMac
|
||
? html`<ha-svg-icon
|
||
slot="start"
|
||
.path=${mdiAppleKeyboardCommand}
|
||
></ha-svg-icon>`
|
||
: this.hass.localize(
|
||
"ui.panel.config.automation.editor.ctrl"
|
||
)}</span
|
||
>
|
||
<span>+</span>
|
||
<span>V</span>
|
||
</span>`
|
||
: nothing}
|
||
</div>
|
||
<ha-svg-icon
|
||
slot="start"
|
||
.path=${mdiContentPaste}
|
||
></ha-svg-icon
|
||
><ha-svg-icon
|
||
class="plus"
|
||
slot="end"
|
||
.path=${mdiPlus}
|
||
></ha-svg-icon>
|
||
</ha-md-list-item>
|
||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||
: nothing}
|
||
${collections.map(
|
||
(collection, index) => html`
|
||
${collection.titleKey
|
||
? html`<div class="collection-title">
|
||
${this.hass.localize(collection.titleKey)}
|
||
</div>`
|
||
: nothing}
|
||
${repeat(
|
||
collection.groups,
|
||
(item) => item.key,
|
||
(item) => html`
|
||
<ha-md-list-item
|
||
interactive
|
||
type="button"
|
||
.value=${item.key}
|
||
.index=${index}
|
||
@click=${this._groupSelected}
|
||
class=${item.key === this._selectedGroup ? "selected" : ""}
|
||
>
|
||
<div slot="headline">${item.name}</div>
|
||
${item.icon
|
||
? html`<span slot="start">${item.icon}</span>`
|
||
: item.iconPath
|
||
? html`<ha-svg-icon
|
||
slot="start"
|
||
.path=${item.iconPath}
|
||
></ha-svg-icon>`
|
||
: nothing}
|
||
</ha-md-list-item>
|
||
`
|
||
)}
|
||
`
|
||
)}
|
||
</ha-md-list>
|
||
<div
|
||
class=${classMap({
|
||
items: true,
|
||
blank:
|
||
!this._selectedGroup && !this._filter && this._tab === "groups",
|
||
"empty-search":
|
||
!items?.length && !filteredBlockItems?.length && this._filter,
|
||
hidden:
|
||
this._narrow &&
|
||
!this._selectedGroup &&
|
||
!this._filter &&
|
||
this._tab === "groups",
|
||
})}
|
||
@scroll=${this._onItemsScroll}
|
||
>
|
||
${filteredBlockItems
|
||
? this._renderItemList(
|
||
this.hass.localize(`ui.panel.config.automation.editor.blocks`),
|
||
filteredBlockItems
|
||
)
|
||
: nothing}
|
||
${this._tab === "groups" && !this._selectedGroup && !this._filter
|
||
? this.hass.localize(
|
||
`ui.panel.config.automation.editor.${automationElementType}s.select`
|
||
)
|
||
: !items?.length &&
|
||
this._filter &&
|
||
(!filteredBlockItems || !filteredBlockItems.length)
|
||
? html`<span
|
||
>${this.hass.localize(
|
||
`ui.panel.config.automation.editor.${automationElementType}s.empty_search`,
|
||
{
|
||
term: html`<b>‘${this._filter}’</b>`,
|
||
}
|
||
)}</span
|
||
>`
|
||
: this._renderItemList(
|
||
this.hass.localize(
|
||
`ui.panel.config.automation.editor.${automationElementType}s.name`
|
||
),
|
||
items
|
||
)}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
private _renderItemList(title, items?: ListItem[]) {
|
||
if (!items || !items.length) {
|
||
return nothing;
|
||
}
|
||
|
||
return html`
|
||
<div class="items-title ${this._itemsScrolled ? "scrolled" : ""}">
|
||
${title}
|
||
</div>
|
||
<ha-md-list
|
||
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
|
||
>
|
||
${repeat(
|
||
items,
|
||
(item) => item.key,
|
||
(item) => html`
|
||
<ha-md-list-item
|
||
interactive
|
||
type="button"
|
||
.value=${item.key}
|
||
.group=${item.group}
|
||
@click=${this._selected}
|
||
>
|
||
<div slot="headline">${item.name}</div>
|
||
<div slot="supporting-text">${item.description}</div>
|
||
${item.icon
|
||
? html`<span slot="start">${item.icon}</span>`
|
||
: item.iconPath
|
||
? html`<ha-svg-icon
|
||
slot="start"
|
||
.path=${item.iconPath}
|
||
></ha-svg-icon>`
|
||
: nothing}
|
||
${item.group
|
||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||
: html`<ha-svg-icon
|
||
slot="end"
|
||
class="plus"
|
||
.path=${mdiPlus}
|
||
></ha-svg-icon>`}
|
||
</ha-md-list-item>
|
||
`
|
||
)}
|
||
</ha-md-list>
|
||
`;
|
||
}
|
||
|
||
protected render() {
|
||
if (!this._params) {
|
||
return nothing;
|
||
}
|
||
|
||
if (this._bottomSheetMode) {
|
||
return html`
|
||
<ha-bottom-sheet
|
||
.open=${this._open}
|
||
@closed=${this.closeDialog}
|
||
flexcontent
|
||
>
|
||
${this._renderContent()}
|
||
</ha-bottom-sheet>
|
||
`;
|
||
}
|
||
|
||
return html`
|
||
<ha-wa-dialog .open=${this._open} @closed=${this.closeDialog} flexcontent>
|
||
${this._renderContent()}
|
||
</ha-wa-dialog>
|
||
`;
|
||
}
|
||
|
||
public disconnectedCallback(): void {
|
||
super.disconnectedCallback();
|
||
window.removeEventListener("resize", this._updateNarrow);
|
||
this._removeSearchKeybindings();
|
||
}
|
||
|
||
private _updateNarrow = () => {
|
||
this._narrow =
|
||
window.matchMedia("(max-width: 870px)").matches ||
|
||
window.matchMedia("(max-height: 500px)").matches;
|
||
};
|
||
|
||
private _close() {
|
||
this._open = false;
|
||
}
|
||
|
||
private _back() {
|
||
this._selectedGroup = undefined;
|
||
}
|
||
|
||
private _groupSelected(ev) {
|
||
const group = ev.currentTarget;
|
||
if (this._selectedGroup === group.value) {
|
||
this._selectedGroup = undefined;
|
||
this._selectedCollectionIndex = undefined;
|
||
return;
|
||
}
|
||
this._selectedGroup = group.value;
|
||
this._selectedCollectionIndex = ev.currentTarget.index;
|
||
requestAnimationFrame(() => {
|
||
this._itemsListElement?.scrollTo(0, 0);
|
||
});
|
||
}
|
||
|
||
private _selected(ev) {
|
||
const item = ev.currentTarget;
|
||
this._params!.add(item.value);
|
||
this.closeDialog();
|
||
}
|
||
|
||
private _debounceFilterChanged = debounce(
|
||
(ev) => this._filterChanged(ev),
|
||
200
|
||
);
|
||
|
||
private _filterChanged = (ev) => {
|
||
this._filter = ev.detail.value;
|
||
};
|
||
|
||
private _addClipboard = () => {
|
||
if (this._params?.clipboardItem) {
|
||
this._params!.add(PASTE_VALUE);
|
||
showToast(this, {
|
||
message: this.hass.localize(
|
||
"ui.panel.config.automation.editor.item_pasted",
|
||
{
|
||
item: this.hass.localize(
|
||
// @ts-ignore
|
||
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
|
||
),
|
||
}
|
||
),
|
||
});
|
||
this.closeDialog();
|
||
}
|
||
};
|
||
|
||
protected supportedShortcuts(): SupportedShortcuts {
|
||
return {
|
||
v: () => this._addClipboard(),
|
||
};
|
||
}
|
||
|
||
private _switchTab(ev) {
|
||
this._tab = ev.detail.value;
|
||
}
|
||
|
||
@eventOptions({ passive: true })
|
||
private _onItemsScroll(ev) {
|
||
const top = ev.target.scrollTop ?? 0;
|
||
this._itemsScrolled = top > 0;
|
||
}
|
||
|
||
private _onSearchFocus(ev) {
|
||
this._removeKeyboardShortcuts = tinykeys(ev.target, {
|
||
ArrowDown: this._focusSearchList,
|
||
});
|
||
}
|
||
|
||
private _removeSearchKeybindings() {
|
||
this._removeKeyboardShortcuts?.();
|
||
}
|
||
|
||
private _focusSearchList = (ev) => {
|
||
if (!this._filter || !this._itemsListFirstElement) {
|
||
return;
|
||
}
|
||
|
||
ev.preventDefault();
|
||
this._itemsListFirstElement.focus();
|
||
};
|
||
|
||
static get styles(): CSSResultGroup {
|
||
return [
|
||
css`
|
||
ha-bottom-sheet {
|
||
--ha-bottom-sheet-height: 90vh;
|
||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||
--ha-bottom-sheet-max-width: 888px;
|
||
--ha-bottom-sheet-padding: var(--ha-space-0);
|
||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||
}
|
||
|
||
ha-wa-dialog {
|
||
--dialog-content-padding: var(--ha-space-0);
|
||
--ha-dialog-width-md: 888px;
|
||
--ha-dialog-min-height: min(
|
||
648px,
|
||
calc(
|
||
100vh - max(
|
||
var(--safe-area-inset-bottom),
|
||
var(--ha-space-4)
|
||
) - max(var(--safe-area-inset-top), var(--ha-space-4))
|
||
)
|
||
);
|
||
--ha-dialog-min-height: min(
|
||
648px,
|
||
calc(
|
||
100dvh - max(
|
||
var(--safe-area-inset-bottom),
|
||
var(--ha-space-4)
|
||
) - max(var(--safe-area-inset-top), var(--ha-space-4))
|
||
)
|
||
);
|
||
--ha-dialog-max-height: var(--ha-dialog-min-height);
|
||
}
|
||
|
||
search-input {
|
||
display: block;
|
||
margin: var(--ha-space-0) var(--ha-space-4);
|
||
}
|
||
|
||
ha-button-toggle-group {
|
||
--ha-button-toggle-group-padding: var(--ha-space-3) var(--ha-space-4)
|
||
0;
|
||
}
|
||
|
||
.content {
|
||
flex: 1;
|
||
min-height: 0;
|
||
height: 100%;
|
||
display: flex;
|
||
}
|
||
|
||
ha-md-list {
|
||
padding: 0;
|
||
}
|
||
|
||
.groups {
|
||
overflow: auto;
|
||
flex: 3;
|
||
border-radius: var(--ha-border-radius-xl);
|
||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||
margin: var(--ha-space-3);
|
||
margin-inline-end: var(--ha-space-0);
|
||
--md-list-item-leading-space: var(--ha-space-3);
|
||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||
--md-list-item-bottom-space: var(--ha-space-1);
|
||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||
}
|
||
ha-bottom-sheet .groups {
|
||
margin: var(--ha-space-3);
|
||
}
|
||
.groups .selected {
|
||
background-color: var(--ha-color-fill-primary-normal-active);
|
||
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
|
||
--icon-primary-color: var(--ha-color-on-primary-normal);
|
||
}
|
||
.groups .selected ha-svg-icon {
|
||
color: var(--ha-color-on-primary-normal);
|
||
}
|
||
|
||
.collection-title {
|
||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||
padding: var(--ha-space-1) var(--ha-space-2);
|
||
font-weight: var(--ha-font-weight-bold);
|
||
color: var(--secondary-text-color);
|
||
top: 0;
|
||
position: sticky;
|
||
min-height: var(--ha-space-6);
|
||
display: flex;
|
||
align-items: center;
|
||
z-index: 1;
|
||
}
|
||
|
||
.items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: auto;
|
||
flex: 7;
|
||
}
|
||
|
||
ha-wa-dialog .items {
|
||
margin-top: var(--ha-space-3);
|
||
}
|
||
|
||
ha-bottom-sheet .groups {
|
||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-4));
|
||
}
|
||
|
||
.items.hidden,
|
||
.groups.hidden {
|
||
display: none;
|
||
}
|
||
.items.blank,
|
||
.items.empty-search {
|
||
border-radius: var(--ha-border-radius-xl);
|
||
background-color: var(--ha-color-surface-default);
|
||
align-items: center;
|
||
color: var(--ha-color-text-secondary);
|
||
padding: var(--ha-space-0);
|
||
margin: var(--ha-space-3) var(--ha-space-4)
|
||
max(var(--safe-area-inset-bottom), var(--ha-space-3));
|
||
}
|
||
|
||
.items ha-md-list {
|
||
--md-list-item-two-line-container-height: var(--ha-space-12);
|
||
--md-list-item-leading-space: var(--ha-space-3);
|
||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||
--md-list-item-bottom-space: var(--ha-space-2);
|
||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||
gap: var(--ha-space-2);
|
||
padding: var(--ha-space-0) var(--ha-space-4);
|
||
}
|
||
.items ha-md-list ha-md-list-item {
|
||
border-radius: var(--ha-border-radius-lg);
|
||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||
}
|
||
|
||
.items ha-md-list,
|
||
.groups {
|
||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
|
||
}
|
||
|
||
.items.blank {
|
||
justify-content: center;
|
||
}
|
||
.items.empty-search {
|
||
padding-top: var(--ha-space-6);
|
||
justify-content: start;
|
||
}
|
||
|
||
.items-title {
|
||
position: sticky;
|
||
display: flex;
|
||
align-items: center;
|
||
font-weight: var(--ha-font-weight-medium);
|
||
padding-top: var(--ha-space-2);
|
||
padding-bottom: var(--ha-space-2);
|
||
padding-inline-start: var(--ha-space-8);
|
||
padding-inline-end: var(--ha-space-3);
|
||
top: 0;
|
||
z-index: 1;
|
||
background-color: var(--card-background-color);
|
||
}
|
||
ha-bottom-sheet .items-title {
|
||
padding-top: var(--ha-space-3);
|
||
}
|
||
.items-title.scrolled:first-of-type {
|
||
box-shadow: var(--bar-box-shadow);
|
||
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
|
||
}
|
||
|
||
ha-icon-next {
|
||
width: var(--ha-space-6);
|
||
}
|
||
|
||
ha-md-list-item.paste {
|
||
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
|
||
}
|
||
|
||
ha-svg-icon.plus {
|
||
color: var(--primary-color);
|
||
}
|
||
.shortcut-label {
|
||
display: flex;
|
||
gap: var(--ha-space-3);
|
||
justify-content: space-between;
|
||
}
|
||
.shortcut-label .supporting-text {
|
||
color: var(--secondary-text-color);
|
||
font-size: var(--ha-font-size-s);
|
||
}
|
||
.shortcut-label .shortcut {
|
||
--mdc-icon-size: var(--ha-space-3);
|
||
display: inline-flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
.shortcut-label .shortcut span {
|
||
font-size: var(--ha-font-size-s);
|
||
font-family: var(--ha-font-family-code);
|
||
color: var(--ha-color-text-secondary);
|
||
}
|
||
`,
|
||
];
|
||
}
|
||
}
|
||
|
||
declare global {
|
||
interface HTMLElementTagNameMap {
|
||
"add-automation-element-dialog": DialogAddAutomationElement;
|
||
}
|
||
}
|