Compare commits

..

1 Commits

Author SHA1 Message Date
Wendelin c958da6f8b Fix row target count flickering 2026-05-27 15:38:46 +02:00
12 changed files with 60 additions and 709 deletions
+7 -14
View File
@@ -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) {
+10 -2
View File
@@ -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"
+1
View File
@@ -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}
-1
View File
@@ -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() {
-9
View File
@@ -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%]",