mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-28 03:57:20 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c958da6f8b |
@@ -1,31 +1,24 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import {
|
||||
DEFAULT_SERVICE_ICON,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
serviceIcon,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-service-icon")
|
||||
export class HaServiceIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public service?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
protected _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
protected _config?: ContextType<typeof configContext>;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -35,13 +28,13 @@ export class HaServiceIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._connection || !this._config) {
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceIcon(
|
||||
this._connection.connection,
|
||||
this._config?.config,
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this.service
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
|
||||
@@ -62,7 +62,11 @@ class HaServicePicker extends LitElement {
|
||||
index
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
|
||||
<ha-service-icon slot="start" .service=${item.id}></ha-service-icon>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${item.id}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
${item.service_id && this.showServiceId
|
||||
@@ -108,7 +112,11 @@ class HaServicePicker extends LitElement {
|
||||
service;
|
||||
|
||||
return html`
|
||||
<ha-service-icon slot="start" .service=${serviceId}></ha-service-icon>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${serviceId}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${serviceName}</span>
|
||||
${this.showServiceId
|
||||
? html`<span slot="supporting-text" class="code"
|
||||
|
||||
@@ -451,6 +451,7 @@ export class HatScriptGraph extends LitElement {
|
||||
${node.action
|
||||
? html`<ha-service-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.service=${node.action}
|
||||
></ha-service-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -653,7 +653,6 @@ export interface ActionSidebarConfig extends BaseSidebarConfig {
|
||||
disable: () => void;
|
||||
continueOnError: () => void;
|
||||
duplicate: () => void;
|
||||
convert: () => void;
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
insertAfter: (value: Action | Action[]) => boolean;
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../../common/entity/compute_object_id";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
ACTION_COMBINED_BLOCKS,
|
||||
} from "../../../../data/action";
|
||||
import type { Action, ServiceAction } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getAutomationActionType } from "./ha-automation-action-row";
|
||||
|
||||
type FieldSelector = Record<string, unknown> | undefined;
|
||||
|
||||
const getSelectorType = (selector: FieldSelector): string | undefined => {
|
||||
if (!selector) {
|
||||
return undefined;
|
||||
}
|
||||
const keys = Object.keys(selector);
|
||||
return keys.length === 1 ? keys[0] : undefined;
|
||||
};
|
||||
|
||||
const getSelectOptionValues = (
|
||||
selector: FieldSelector
|
||||
): string[] | undefined => {
|
||||
const config = selector?.select as
|
||||
| { options?: readonly (string | { value: string })[] }
|
||||
| null
|
||||
| undefined;
|
||||
if (!config?.options) {
|
||||
return undefined;
|
||||
}
|
||||
return config.options.map((opt) =>
|
||||
typeof opt === "string" ? opt : opt.value
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A value is compatible with the new field if either:
|
||||
* - the new field has no selector (unknown shape — accept),
|
||||
* - the old field had no selector but the new one does (best effort — accept),
|
||||
* - the selector types match. For `select` selectors, additionally require
|
||||
* that every supplied value is present in the new option list.
|
||||
*/
|
||||
const isFieldValueCompatible = (
|
||||
value: unknown,
|
||||
oldSelector: FieldSelector,
|
||||
newSelector: FieldSelector
|
||||
): boolean => {
|
||||
const newType = getSelectorType(newSelector);
|
||||
if (newType === undefined) {
|
||||
return true;
|
||||
}
|
||||
const oldType = getSelectorType(oldSelector);
|
||||
if (oldType !== undefined && oldType !== newType) {
|
||||
return false;
|
||||
}
|
||||
if (newType === "select") {
|
||||
const allowed = getSelectOptionValues(newSelector);
|
||||
if (!allowed) {
|
||||
return true;
|
||||
}
|
||||
const allowedSet = new Set(allowed);
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
return values.every((v) => typeof v === "string" && allowedSet.has(v));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const filterTargetEntitiesByDomain = (
|
||||
target: HassServiceTarget,
|
||||
domain: string | undefined
|
||||
): HassServiceTarget => {
|
||||
if (!domain || target.entity_id === undefined) {
|
||||
return target;
|
||||
}
|
||||
const entityIds = Array.isArray(target.entity_id)
|
||||
? target.entity_id
|
||||
: [target.entity_id];
|
||||
const filtered = entityIds.filter((id) => computeDomain(id) === domain);
|
||||
const { entity_id: _entityId, ...rest } = target;
|
||||
if (filtered.length === 0) {
|
||||
return rest;
|
||||
}
|
||||
return { ...rest, entity_id: filtered };
|
||||
};
|
||||
|
||||
export const BASE_ACTION_FIELDS = [
|
||||
"alias",
|
||||
"note",
|
||||
"enabled",
|
||||
"continue_on_error",
|
||||
] as const;
|
||||
|
||||
const BUILDING_BLOCK_TYPES = new Set<string>([
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
...ACTION_COMBINED_BLOCKS,
|
||||
]);
|
||||
|
||||
export const isBuildingBlockAction = (action: Action): boolean => {
|
||||
const type = getAutomationActionType(action);
|
||||
return type !== undefined && BUILDING_BLOCK_TYPES.has(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Encode an action as a stable picker key.
|
||||
* - Service actions → `domain.service` (always contains a dot).
|
||||
* - Everything else → the action type identifier (no dot).
|
||||
*/
|
||||
export const getActionKey = (action: Action): string | undefined => {
|
||||
const type = getAutomationActionType(action);
|
||||
if (type === "service") {
|
||||
return (action as ServiceAction).action || undefined;
|
||||
}
|
||||
return type;
|
||||
};
|
||||
|
||||
const isServiceKey = (key: string) => key.includes(".");
|
||||
|
||||
/** Build a fresh action with default content for the given picker key. */
|
||||
export const buildActionFromKey = (key: string): Action => {
|
||||
if (isServiceKey(key)) {
|
||||
return {
|
||||
action: key,
|
||||
metadata: {},
|
||||
} as ServiceAction;
|
||||
}
|
||||
const elClass = customElements.get(`ha-automation-action-${key}`) as
|
||||
| (CustomElementConstructor & { defaultConfig?: Action })
|
||||
| undefined;
|
||||
if (elClass?.defaultConfig) {
|
||||
return { ...elClass.defaultConfig };
|
||||
}
|
||||
return { [key]: {} } as Action;
|
||||
};
|
||||
|
||||
const isServiceAction = (action: Action): action is ServiceAction =>
|
||||
"action" in action && !!(action as ServiceAction).action;
|
||||
|
||||
/**
|
||||
* Merge fields from `oldAction` into `newAction` that are still compatible.
|
||||
* Behavior: copy base fields if old has them, and for service → service copy
|
||||
* `target` and any `data` keys that the new service still supports.
|
||||
*/
|
||||
export const convertAction = (
|
||||
oldAction: Action,
|
||||
newAction: Action,
|
||||
services: HomeAssistant["services"]
|
||||
): Action => {
|
||||
const merged: Action = { ...newAction };
|
||||
|
||||
for (const field of BASE_ACTION_FIELDS) {
|
||||
const oldValue = (oldAction as Record<string, unknown>)[field];
|
||||
if (oldValue !== undefined) {
|
||||
(merged as Record<string, unknown>)[field] = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isServiceAction(oldAction) && isServiceAction(merged)) {
|
||||
if (oldAction.target) {
|
||||
const newDomain = merged.action
|
||||
? computeDomain(merged.action)
|
||||
: undefined;
|
||||
merged.target = filterTargetEntitiesByDomain(oldAction.target, newDomain);
|
||||
}
|
||||
|
||||
if (oldAction.data && merged.action && oldAction.action) {
|
||||
const newDomain = computeDomain(merged.action);
|
||||
const newService = computeObjectId(merged.action);
|
||||
const newFields = services[newDomain]?.[newService]?.fields;
|
||||
const oldDomain = computeDomain(oldAction.action);
|
||||
const oldService = computeObjectId(oldAction.action);
|
||||
const oldFields = services[oldDomain]?.[oldService]?.fields;
|
||||
if (newFields) {
|
||||
const carried: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(oldAction.data)) {
|
||||
const newField = newFields[key];
|
||||
if (
|
||||
newField &&
|
||||
isFieldValueCompatible(
|
||||
value,
|
||||
oldFields?.[key]?.selector,
|
||||
newField.selector
|
||||
)
|
||||
) {
|
||||
carried[key] = value;
|
||||
}
|
||||
}
|
||||
if (Object.keys(carried).length > 0) {
|
||||
merged.data = carried;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
@@ -1,400 +0,0 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiArrowRight, mdiRoomService } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../../../../components/ha-picker-field";
|
||||
import "../../../../components/ha-service-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
ACTION_COMBINED_BLOCKS,
|
||||
ACTION_ICONS,
|
||||
YAML_ONLY_ACTION_TYPES,
|
||||
} from "../../../../data/action";
|
||||
import {
|
||||
internationalizationContext,
|
||||
servicesContext,
|
||||
} from "../../../../data/context";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
buildActionFromKey,
|
||||
convertAction,
|
||||
getActionKey,
|
||||
} from "./convert-action";
|
||||
import type { ConvertActionDialogParams } from "./show-dialog-convert-action";
|
||||
|
||||
interface ConvertItem extends PickerComboBoxItem {
|
||||
isService: boolean;
|
||||
}
|
||||
|
||||
const EXCLUDED_TYPES = new Set<string>([
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
...ACTION_COMBINED_BLOCKS,
|
||||
...YAML_ONLY_ACTION_TYPES,
|
||||
// Generic catch-all action types covered by the services list below
|
||||
"service",
|
||||
// Umbrella repeat — not a leaf type
|
||||
"repeat",
|
||||
]);
|
||||
|
||||
@customElement("dialog-convert-action")
|
||||
class DialogConvertAction extends DialogMixin<ConvertActionDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state() private _pickedKey?: string;
|
||||
|
||||
@state() private _mode: "current" | "new" = "current";
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: servicesContext, subscribe: true })
|
||||
private _services!: ContextType<typeof servicesContext>;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.params) {
|
||||
this._pickedKey = getActionKey(this.params.currentAction);
|
||||
this._mode = "current";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentKey = getActionKey(this.params.currentAction);
|
||||
const canCommit =
|
||||
this._pickedKey !== undefined && this._pickedKey !== currentKey;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.title"
|
||||
)}
|
||||
>
|
||||
<div class="content">
|
||||
${currentKey
|
||||
? html`
|
||||
<div class="preview">
|
||||
${this._renderActionChip(currentKey)}
|
||||
<ha-svg-icon
|
||||
class="arrow"
|
||||
.path=${mdiArrowRight}
|
||||
></ha-svg-icon>
|
||||
${this._pickedKey && this._pickedKey !== currentKey
|
||||
? this._renderActionChip(this._pickedKey)
|
||||
: "?"}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-generic-picker
|
||||
required
|
||||
.value=${this._pickedKey}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer(
|
||||
this._i18n.localize,
|
||||
this._services
|
||||
)}
|
||||
.notFoundLabel=${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.no_matches"
|
||||
)}
|
||||
@value-changed=${this._pickedKeyChanged}
|
||||
></ha-generic-picker>
|
||||
<ha-alert alert-type="warning">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.warning"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-radio-group
|
||||
name="mode"
|
||||
.value=${this._mode}
|
||||
@change=${this._modeChanged}
|
||||
>
|
||||
<ha-radio-option value="current">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.mode_current"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="new">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.mode_new"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this._i18n.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!canCommit}
|
||||
@click=${this._convert}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.convert"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _items = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"]
|
||||
): ConvertItem[] => {
|
||||
const items: ConvertItem[] = [];
|
||||
|
||||
for (const type of Object.keys(ACTION_ICONS)) {
|
||||
if (EXCLUDED_TYPES.has(type)) {
|
||||
continue;
|
||||
}
|
||||
const label =
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${type}.label` as any
|
||||
) || type;
|
||||
items.push({
|
||||
id: type,
|
||||
primary: label,
|
||||
icon_path: ACTION_ICONS[type as keyof typeof ACTION_ICONS],
|
||||
isService: false,
|
||||
sorting_label: `0_${label}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (services) {
|
||||
for (const domain of Object.keys(services)) {
|
||||
const domainName = domainToName(localize, domain);
|
||||
for (const service of Object.keys(services[domain])) {
|
||||
const serviceId = `${domain}.${service}`;
|
||||
const def = services[domain][service];
|
||||
const serviceName =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.name` as any,
|
||||
def.description_placeholders
|
||||
) ||
|
||||
def.name ||
|
||||
service;
|
||||
const description =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.description` as any,
|
||||
def.description_placeholders
|
||||
) ||
|
||||
def.description ||
|
||||
"";
|
||||
items.push({
|
||||
id: serviceId,
|
||||
primary: `${domainName}: ${serviceName}`,
|
||||
secondary: description,
|
||||
isService: true,
|
||||
search_labels: {
|
||||
serviceId,
|
||||
domainName,
|
||||
serviceName,
|
||||
description,
|
||||
},
|
||||
sorting_label: `1_${domainName}_${serviceName}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () => this._items(this._i18n.localize, this._services);
|
||||
|
||||
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item,
|
||||
index
|
||||
) => {
|
||||
const convertItem = item as ConvertItem;
|
||||
return html`
|
||||
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
|
||||
${convertItem.isService
|
||||
? html`<ha-service-icon
|
||||
slot="start"
|
||||
.service=${item.id}
|
||||
></ha-service-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
private _valueRenderer = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"]
|
||||
): PickerValueRenderer =>
|
||||
(value) => {
|
||||
if (value.includes(".")) {
|
||||
const [domain, service] = value.split(".");
|
||||
const def = services?.[domain]?.[service];
|
||||
const domainName = domainToName(localize, domain);
|
||||
const serviceName =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.name` as any,
|
||||
def?.description_placeholders
|
||||
) ||
|
||||
def?.name ||
|
||||
service;
|
||||
return html`
|
||||
<ha-service-icon slot="start" .service=${value}></ha-service-icon>
|
||||
<span slot="headline">${domainName}: ${serviceName}</span>
|
||||
`;
|
||||
}
|
||||
const iconPath = ACTION_ICONS[value as keyof typeof ACTION_ICONS];
|
||||
const label =
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${value}.label` as any
|
||||
) || value;
|
||||
return html`
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiRoomService}
|
||||
></ha-svg-icon>`}
|
||||
<span slot="headline">${label}</span>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
private _renderActionChip(key: string) {
|
||||
const localize = this._i18n.localize;
|
||||
if (key.includes(".")) {
|
||||
const [domain, service] = key.split(".");
|
||||
const def = this._services?.[domain]?.[service];
|
||||
const domainName = domainToName(localize, domain);
|
||||
const serviceName =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.name` as any,
|
||||
def?.description_placeholders
|
||||
) ||
|
||||
def?.name ||
|
||||
service;
|
||||
return html`
|
||||
<div class="chip">
|
||||
<ha-service-icon .service=${key}></ha-service-icon>
|
||||
<span>${domainName}: ${serviceName}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const iconPath = ACTION_ICONS[key as keyof typeof ACTION_ICONS];
|
||||
const label =
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${key}.label` as any
|
||||
) || key;
|
||||
return html`
|
||||
<div class="chip">
|
||||
<ha-svg-icon .path=${iconPath || mdiRoomService}></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _pickedKeyChanged = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
this._pickedKey = ev.detail?.value || undefined;
|
||||
};
|
||||
|
||||
private _modeChanged = (ev: Event) => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
if (value === "current" || value === "new") {
|
||||
this._mode = value;
|
||||
}
|
||||
};
|
||||
|
||||
private _convert = () => {
|
||||
if (!this.params || !this._pickedKey) {
|
||||
return;
|
||||
}
|
||||
const newAction = buildActionFromKey(this._pickedKey);
|
||||
const merged = convertAction(
|
||||
this.params.currentAction,
|
||||
newAction,
|
||||
this._services
|
||||
);
|
||||
if (this._mode === "new") {
|
||||
this.params.duplicateConvert(merged);
|
||||
} else {
|
||||
this.params.convert(merged);
|
||||
}
|
||||
this.closeDialog();
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: min(560px, 100vw);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
.preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview .chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-1) var(--ha-space-3);
|
||||
border-radius: 999px;
|
||||
background-color: var(--secondary-background-color);
|
||||
min-width: 0;
|
||||
}
|
||||
.preview .chip span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.preview .arrow {
|
||||
color: var(--secondary-text-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-convert-action": DialogConvertAction;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
mdiPlusCircleMultipleOutline,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
mdiSwapHorizontal,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
@@ -91,10 +90,8 @@ import { showEditorToast } from "../editor-toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import { isBuildingBlockAction } from "./convert-action";
|
||||
import "./ha-automation-action-editor";
|
||||
import type HaAutomationActionEditor from "./ha-automation-action-editor";
|
||||
import { showConvertActionDialog } from "./show-dialog-convert-action";
|
||||
import "./types/ha-automation-action-choose";
|
||||
import "./types/ha-automation-action-condition";
|
||||
import "./types/ha-automation-action-delay";
|
||||
@@ -311,6 +308,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<ha-service-icon
|
||||
slot="leading-icon"
|
||||
class="action-icon"
|
||||
.hass=${this.hass}
|
||||
.service=${this.action.action}
|
||||
></ha-service-icon>
|
||||
`
|
||||
@@ -522,16 +520,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
></ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${!isBuildingBlockAction(this.action)
|
||||
? html`<ha-dropdown-item value="convert" .disabled=${this.disabled}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert"
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
value="toggle_yaml_mode"
|
||||
@@ -989,24 +977,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _convertAction = () => {
|
||||
showConvertActionDialog(this, {
|
||||
currentAction: this.action,
|
||||
convert: (newAction) => {
|
||||
fireEvent(this, "value-changed", { value: newAction });
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(newAction);
|
||||
} else if (this._yamlMode) {
|
||||
this._actionEditor?.yamlEditor?.setValue(newAction);
|
||||
}
|
||||
},
|
||||
duplicateConvert: (newAction) => {
|
||||
this._insertAfter(newAction);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _insertAfter = (value: Action | Action[]) => {
|
||||
if (ensureArray(value).some((val) => !isAction(val))) {
|
||||
return false;
|
||||
@@ -1132,7 +1102,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
paste: this._pasteAction,
|
||||
pasteAvailable: this._pasteAvailable,
|
||||
duplicate: this._duplicateAction,
|
||||
convert: this._convertAction,
|
||||
insertAfter: this._insertAfter,
|
||||
run: this._runAction,
|
||||
config: {
|
||||
@@ -1237,9 +1206,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
case "move_down":
|
||||
this._moveDown();
|
||||
break;
|
||||
case "convert":
|
||||
this._convertAction();
|
||||
break;
|
||||
case "toggle_yaml_mode":
|
||||
this._toggleYamlMode(ev.target as HTMLElement);
|
||||
break;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { Action } from "../../../../data/script";
|
||||
|
||||
export interface ConvertActionDialogParams {
|
||||
currentAction: Action;
|
||||
convert: (newAction: Action) => void;
|
||||
duplicateConvert: (newAction: Action) => void;
|
||||
}
|
||||
|
||||
const loadDialog = () => import("./dialog-convert-action");
|
||||
|
||||
export const showConvertActionDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: ConvertActionDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-convert-action",
|
||||
dialogImport: loadDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1638,6 +1638,7 @@ class DialogAddAutomationElement
|
||||
result.push({
|
||||
icon: html`
|
||||
<ha-service-icon
|
||||
.hass=${this.hass}
|
||||
.service=${`${dmn}.${service}`}
|
||||
></ha-service-icon>
|
||||
`,
|
||||
@@ -1956,6 +1957,7 @@ class DialogAddAutomationElement
|
||||
items[domain].items.push({
|
||||
icon: html`
|
||||
<ha-service-icon
|
||||
.hass=${this.hass}
|
||||
.service=${`${domain}.${serviceName}`}
|
||||
></ha-service-icon>
|
||||
`,
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
mdiPlusCircleMultipleOutline,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
mdiSwapHorizontal,
|
||||
} from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
@@ -28,7 +27,6 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
|
||||
import type { ActionSidebarConfig } from "../../../../data/automation";
|
||||
import { isBuildingBlockAction } from "../action/convert-action";
|
||||
import type { DomainManifestLookup } from "../../../../data/integration";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import type {
|
||||
@@ -287,22 +285,6 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${!isBuildingBlockAction(this.config.config.action)
|
||||
? html`<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="convert"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert"
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="toggle_yaml_mode"
|
||||
@@ -469,9 +451,6 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
case "duplicate":
|
||||
this.config.duplicate();
|
||||
break;
|
||||
case "convert":
|
||||
this.config.convert();
|
||||
break;
|
||||
case "copy":
|
||||
this.config.copy();
|
||||
break;
|
||||
|
||||
@@ -89,7 +89,12 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
private _countCache = new Map<string, Promise<number | undefined>>();
|
||||
private _countCache = new Map<
|
||||
string,
|
||||
Promise<number | undefined> | number | undefined
|
||||
>();
|
||||
|
||||
private _rerenderCount = true;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
@@ -98,10 +103,15 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
changedProps.has("selector") ||
|
||||
changedProps.has("_registries")
|
||||
) {
|
||||
this._countCache.clear();
|
||||
this._rerenderCount = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
this._rerenderCount = false;
|
||||
}
|
||||
|
||||
private _countMatchingEntities(referencedEntities: string[]): number {
|
||||
const targetSelector = this.selector;
|
||||
const hasEntityFilter = !!targetSelector?.target?.entity;
|
||||
@@ -148,7 +158,11 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetId: string
|
||||
) {
|
||||
const key = `${targetType}:${targetId}`;
|
||||
if (!this._countCache.has(key)) {
|
||||
let fallback = " (-)";
|
||||
if (!this._countCache.has(key) || this._rerenderCount) {
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
fallback = ` (${this._countCache.get(key)})`;
|
||||
}
|
||||
this._countCache.set(
|
||||
key,
|
||||
extractFromTarget(
|
||||
@@ -162,15 +176,30 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
.then((result) =>
|
||||
this._countMatchingEntities(result.referenced_entities)
|
||||
)
|
||||
.catch(() => undefined)
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error counting target entities", err);
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
return until(
|
||||
this._countCache
|
||||
.get(key)!
|
||||
.then((count) => (count === undefined ? nothing : html` (${count})`)),
|
||||
"(-)"
|
||||
);
|
||||
|
||||
if (this._countCache.get(key) instanceof Promise) {
|
||||
return until(
|
||||
(this._countCache.get(key) as Promise<number | undefined>)!.then(
|
||||
(count) => {
|
||||
this._countCache.set(key, count);
|
||||
return count === undefined ? nothing : html` (${count})`;
|
||||
}
|
||||
),
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
return ` (${this._countCache.get(key)})`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -5648,15 +5648,6 @@
|
||||
"run_action_error": "Error running action",
|
||||
"run_action_success": "Action ran successfully",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"convert": "Convert to…",
|
||||
"convert_dialog": {
|
||||
"title": "Convert action",
|
||||
"warning": "Compatible settings are carried over; anything else is removed. You can undo if you change your mind.",
|
||||
"convert": "Convert",
|
||||
"mode_current": "Convert current action",
|
||||
"mode_new": "Convert into a new action",
|
||||
"no_matches": "No matching actions"
|
||||
},
|
||||
"re_order": "[%key:ui::panel::config::automation::editor::triggers::re_order%]",
|
||||
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
|
||||
"cut": "[%key:ui::panel::config::automation::editor::triggers::cut%]",
|
||||
|
||||
Reference in New Issue
Block a user