mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-16 23:20:38 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
717ae1dbc3 | ||
|
|
afe6e84e3c | ||
|
|
3ddbc8f4e3 | ||
|
|
216468cf50 | ||
|
|
0ef3e9c71a | ||
|
|
1266b0c87a | ||
|
|
a695710beb | ||
|
|
f991efbc0a | ||
|
|
47306d1afd | ||
|
|
1692067392 | ||
|
|
546315ca5b | ||
|
|
7c98912b73 | ||
|
|
8c72e9c22d | ||
|
|
892514ed21 | ||
|
|
06be6f433d | ||
|
|
e9a6e2936d | ||
|
|
d3b7d787b4 | ||
|
|
0662f8238f |
@@ -3,7 +3,6 @@ import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
import "../../../src/state-summary/state-card-content";
|
||||
import "../ha-demo-options";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/managers/notification-manager";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import "./components/page-description";
|
||||
|
||||
@customElement("ha-demo-options")
|
||||
class HaDemoOptions extends LitElement {
|
||||
|
||||
+12
-11
@@ -52,15 +52,15 @@
|
||||
"@codemirror/view": "6.43.11",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.6.1",
|
||||
"@formatjs/intl-displaynames": "7.3.13",
|
||||
"@formatjs/intl-durationformat": "0.10.18",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.11",
|
||||
"@formatjs/intl-listformat": "8.3.13",
|
||||
"@formatjs/intl-locale": "5.3.10",
|
||||
"@formatjs/intl-numberformat": "9.4.0",
|
||||
"@formatjs/intl-pluralrules": "6.3.13",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.13",
|
||||
"@formatjs/intl-datetimeformat": "7.6.2",
|
||||
"@formatjs/intl-displaynames": "7.3.14",
|
||||
"@formatjs/intl-durationformat": "0.11.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.12",
|
||||
"@formatjs/intl-listformat": "8.3.14",
|
||||
"@formatjs/intl-locale": "5.3.11",
|
||||
"@formatjs/intl-numberformat": "9.4.1",
|
||||
"@formatjs/intl-pluralrules": "6.3.14",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.14",
|
||||
"@fullcalendar/core": "6.1.21",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"@fullcalendar/interaction": "6.1.21",
|
||||
@@ -103,10 +103,10 @@
|
||||
"echarts-extension-chart2music": "0.1.1",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.2",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.14",
|
||||
"intl-messageformat": "11.2.15",
|
||||
"js-yaml": "5.4.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -116,6 +116,7 @@
|
||||
"maplibre-gl": "5.24.0",
|
||||
"marked": "18.0.12",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.1",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
export const stopPropagation = (ev) => ev.stopPropagation();
|
||||
|
||||
export const stopKeydownEnterSpacePropagation = (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Enter" || ev.key === " " || ev.key === "Spacebar") {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,44 +1,34 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { computeRTL } from "../util/compute_rtl";
|
||||
import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
import {
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
entityUseDeviceName,
|
||||
} from "./compute_entity_name";
|
||||
import { computeFloorName } from "./compute_floor_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
import {
|
||||
getEntityContext,
|
||||
getEntityEntryContext,
|
||||
} from "./context/get_entity_context";
|
||||
import type { EntityNameItem, EntityNameOptions } from "./entity_name_config";
|
||||
|
||||
const DEFAULT_SEPARATOR = " ";
|
||||
|
||||
export const DEFAULT_ENTITY_NAME = [
|
||||
{ type: "parent_device" },
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
] satisfies EntityNameItem[];
|
||||
|
||||
export const ENTITY_NAME_TYPES = [
|
||||
"floor",
|
||||
"area",
|
||||
"parent_device",
|
||||
"device",
|
||||
"entity",
|
||||
] as const;
|
||||
|
||||
export type EntityNameType = (typeof ENTITY_NAME_TYPES)[number];
|
||||
|
||||
export type EntityNameItem =
|
||||
| {
|
||||
type: EntityNameType;
|
||||
}
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export interface EntityNameOptions {
|
||||
separator?: string;
|
||||
}
|
||||
export { DEFAULT_ENTITY_NAME, ENTITY_NAME_TYPES } from "./entity_name_config";
|
||||
export type {
|
||||
EntityNameItem,
|
||||
EntityNameOptions,
|
||||
EntityNameType,
|
||||
} from "./entity_name_config";
|
||||
|
||||
// Joins items that need no entity context. Returns undefined as soon as one
|
||||
// item references the registries (device, area, ...).
|
||||
@@ -139,18 +129,49 @@ export const computeEntityNameList = (
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
): (string | undefined)[] => {
|
||||
const { device, parentDevice, area, floor } = getEntityContext(
|
||||
stateObj,
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
EntityRegistryDisplayEntry | undefined;
|
||||
|
||||
if (!entry) {
|
||||
return name.map((item) =>
|
||||
item.type === "entity"
|
||||
? computeStateName(stateObj)
|
||||
: item.type === "text"
|
||||
? item.text
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
return computeEntityEntryNameList(
|
||||
entry,
|
||||
name,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
};
|
||||
|
||||
export const computeEntityEntryNameList = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
name: EntityNameItem[],
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
): (string | undefined)[] => {
|
||||
const { device, parentDevice, area, floor } = getEntityEntryContext(
|
||||
entry,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
const names = name.map((item) => {
|
||||
return name.map((item) => {
|
||||
switch (item.type) {
|
||||
case "entity":
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
return computeEntityEntryName(entry, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "parent_device":
|
||||
@@ -165,8 +186,29 @@ export const computeEntityNameList = (
|
||||
return "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return names;
|
||||
export const computeEntitySearchLabels = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
) => {
|
||||
const { device, parentDevice, area } = getEntityContext(
|
||||
stateObj,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
return {
|
||||
entityName: computeEntityName(stateObj, entities, devices) || null,
|
||||
friendlyName: computeStateName(stateObj) || null,
|
||||
deviceName: (device && computeDeviceName(device)) || null,
|
||||
parentDeviceName: (parentDevice && computeDeviceName(parentDevice)) || null,
|
||||
areaName: (area && computeAreaName(area)) || null,
|
||||
};
|
||||
};
|
||||
|
||||
export interface EntityPickerDisplay {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Shape of a composed entity name, kept free of runtime dependencies so that
|
||||
* modules in the core entry chunk can name an entity without pulling in the
|
||||
* registry-aware formatters from `compute_entity_name_display`.
|
||||
*/
|
||||
|
||||
export const ENTITY_NAME_TYPES = [
|
||||
"floor",
|
||||
"area",
|
||||
"parent_device",
|
||||
"device",
|
||||
"entity",
|
||||
] as const;
|
||||
|
||||
export type EntityNameType = (typeof ENTITY_NAME_TYPES)[number];
|
||||
|
||||
export type EntityNameItem =
|
||||
| {
|
||||
type: EntityNameType;
|
||||
}
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export interface EntityNameOptions {
|
||||
separator?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_ENTITY_NAME = [
|
||||
{ type: "parent_device" },
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
] satisfies EntityNameItem[];
|
||||
@@ -90,6 +90,7 @@ export class HaAutomationConditionSummary extends LitElement {
|
||||
></ha-automation-row-options>`
|
||||
: nothing
|
||||
}
|
||||
<slot name="references"></slot>
|
||||
${
|
||||
note
|
||||
? html`
|
||||
|
||||
@@ -11,9 +11,10 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import {
|
||||
computeEntityPickerDisplay,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
getStatisticLabel,
|
||||
@@ -233,11 +234,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const output: StatisticComboBoxItem[] = [];
|
||||
|
||||
statisticIds.forEach((meta) => {
|
||||
@@ -291,31 +287,17 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
const id = meta.statistic_id;
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || id;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
hass,
|
||||
stateObj
|
||||
);
|
||||
const searchLabels = computeEntitySearchLabels(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
output.push({
|
||||
@@ -325,14 +307,12 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary,
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
},
|
||||
sorting_label: [
|
||||
sortingPrefix,
|
||||
searchLabels.deviceName,
|
||||
searchLabels.entityName,
|
||||
].join("_"),
|
||||
search_labels: searchLabels,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -407,35 +387,17 @@ export class HaStatisticPicker extends LitElement {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
stateObj
|
||||
);
|
||||
const searchLabels = computeEntitySearchLabels(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || statisticId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
|
||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
||||
return {
|
||||
@@ -445,15 +407,12 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary,
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
statisticId,
|
||||
},
|
||||
sorting_label: [
|
||||
sortingPrefix,
|
||||
searchLabels.deviceName,
|
||||
searchLabels.entityName,
|
||||
].join("_"),
|
||||
search_labels: { ...searchLabels, statisticId },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeEntityNameList } from "../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { computeEntityPickerDisplay } from "../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
@@ -183,11 +182,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
const allEntityIds = Object.values(controlEntities).flat();
|
||||
const uniqueEntityIds = Array.from(new Set(allEntityIds));
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
uniqueEntityIds.forEach((entityId) => {
|
||||
if (isSelected(entityId)) {
|
||||
return;
|
||||
@@ -197,29 +191,10 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass!,
|
||||
stateObj
|
||||
);
|
||||
|
||||
entityItems.push({
|
||||
type: "entity",
|
||||
|
||||
@@ -22,7 +22,6 @@ import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import "./voice-assistant-brand-icon";
|
||||
import { voiceAssistants } from "../data/expose";
|
||||
import "../panels/config/voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
@customElement("ha-filter-voice-assistants")
|
||||
export class HaFilterVoiceAssistants extends LitElement {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-check-list-item";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-dropdown";
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
|
||||
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.cssresult.js";
|
||||
import { styles } from "@material/web/button/internal/outlined-styles.cssresult.js";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-outlined-button")
|
||||
export class HaOutlinedButton extends OutlinedButton {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-outline: var(--outline-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-button": HaOutlinedButton;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { OutlinedField } from "@material/web/field/internal/outlined-field";
|
||||
import { styles } from "@material/web/field/internal/outlined-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/field/internal/shared-styles.cssresult.js";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { literal } from "lit/static-html";
|
||||
|
||||
@customElement("ha-outlined-field")
|
||||
export class HaOutlinedField extends OutlinedField {
|
||||
protected readonly fieldTag = literal`ha-outlined-field`;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
.container::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--ha-outlined-field-container-color, transparent);
|
||||
opacity: var(--ha-outlined-field-container-opacity, 1);
|
||||
border-start-start-radius: var(--_container-shape-start-start);
|
||||
border-start-end-radius: var(--_container-shape-start-end);
|
||||
border-end-start-radius: var(--_container-shape-end-start);
|
||||
border-end-end-radius: var(--_container-shape-end-end);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-field": HaOutlinedField;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { SubMenu } from "@material/web/menu/internal/submenu/sub-menu";
|
||||
import { styles } from "@material/web/menu/internal/submenu/sub-menu-styles.cssresult.js";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-sub-menu")
|
||||
export class HaSubMenu extends SubMenu {
|
||||
async show() {
|
||||
super.show();
|
||||
this.menu.hasOverflow = false;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sub-menu": HaSubMenu;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import "../ha-list";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tip";
|
||||
import "./ha-media-player-browse";
|
||||
import "./ha-media-upload-button";
|
||||
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import {
|
||||
computeEntityPickerDisplay,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import {
|
||||
areasContext,
|
||||
@@ -107,9 +109,6 @@ export class HaMediaPlayerPicker extends LitElement {
|
||||
const webBrowserLabel = this._i18n.localize(
|
||||
"ui.components.media-browser.web-browser"
|
||||
);
|
||||
const lang = this._i18n.language || "en";
|
||||
const isRTL =
|
||||
this._i18n.translationMetadata.translations[lang]?.isRTL || false;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -128,34 +127,22 @@ export class HaMediaPlayerPicker extends LitElement {
|
||||
...Object.values(this._states)
|
||||
.filter(this._filterPlayerEntities)
|
||||
.map<MediaPlayerComboBoxItem>((stateObj) => {
|
||||
const friendlyName = computeStateName(stateObj);
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors
|
||||
);
|
||||
const entityId = stateObj.entity_id;
|
||||
const domainName = domainToName(
|
||||
this._i18n.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
{
|
||||
entities: this._entities,
|
||||
devices: this._devices,
|
||||
areas: this._areas,
|
||||
floors: this._floors,
|
||||
language: this._i18n.language,
|
||||
translationMetadata: this._i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
);
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
@@ -165,12 +152,14 @@ export class HaMediaPlayerPicker extends LitElement {
|
||||
domain_name: domainName,
|
||||
sorting_label: [primary, secondary].filter(Boolean).join("_"),
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
...computeEntitySearchLabels(
|
||||
stateObj,
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors
|
||||
),
|
||||
domainName: domainName || null,
|
||||
friendlyName: friendlyName || null,
|
||||
entityId,
|
||||
},
|
||||
stateObj,
|
||||
|
||||
@@ -7,8 +7,7 @@ import memoizeOne from "memoize-one";
|
||||
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import {
|
||||
areasContext,
|
||||
devicesContext,
|
||||
@@ -54,40 +53,24 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
|
||||
private _computeDisplayData = memoizeOne(
|
||||
(
|
||||
entityId: string,
|
||||
entities: ContextType<typeof entitiesContext>,
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
isRTL: boolean,
|
||||
i18n: ContextType<typeof internationalizationContext>,
|
||||
stateObj: HassEntity
|
||||
) => {
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
) =>
|
||||
computeEntityPickerDisplay(
|
||||
{
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return { primary, secondary };
|
||||
}
|
||||
floors,
|
||||
language: i18n.language,
|
||||
translationMetadata: i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -104,18 +87,12 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
icon = mdiSpeakerPause;
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors,
|
||||
isRTL,
|
||||
this._i18n,
|
||||
stateObj
|
||||
);
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||
import "../../ha-adaptive-dialog";
|
||||
import "../../ha-dialog-header";
|
||||
import "../../ha-icon-button";
|
||||
import "../../ha-icon-next";
|
||||
import "../../ha-svg-icon";
|
||||
|
||||
@@ -26,12 +26,11 @@ import {
|
||||
computeDeviceNameDisplay,
|
||||
} from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import {
|
||||
getDeviceArea,
|
||||
getDeviceAreaId,
|
||||
} from "../../common/entity/context/get_device_context";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
@@ -672,40 +671,12 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
if (type === "entity") {
|
||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
||||
const entityName = stateObject
|
||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
||||
: item;
|
||||
const { area, device, parentDevice } = stateObject
|
||||
? getEntityContext(
|
||||
stateObject,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: { area: undefined, device: undefined, parentDevice: undefined };
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const parentDeviceName = parentDevice
|
||||
? computeDeviceName(parentDevice)
|
||||
: undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const context = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
);
|
||||
const { primary, secondary } = stateObject
|
||||
? computeEntityPickerDisplay(this.hass, stateObject)
|
||||
: { primary: item, secondary: undefined };
|
||||
return {
|
||||
name: entityName || deviceName || item,
|
||||
context,
|
||||
name: primary,
|
||||
context: secondary,
|
||||
stateObject,
|
||||
notFound: !stateObject && item !== "all" && item !== "none",
|
||||
};
|
||||
|
||||
@@ -326,7 +326,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
this._entityReg,
|
||||
currentDetail,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
this._manifests
|
||||
)
|
||||
: selectedType === "chooseOption"
|
||||
|
||||
@@ -301,7 +301,7 @@ export interface TemplateCondition extends BaseCondition {
|
||||
|
||||
export interface TriggerCondition extends BaseCondition {
|
||||
condition: "trigger";
|
||||
id: string;
|
||||
id: string | string[];
|
||||
}
|
||||
|
||||
type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
|
||||
|
||||
@@ -149,6 +149,8 @@ const formatNumericLimitValue = (
|
||||
export interface DescribeOptions {
|
||||
// Skip the user defined alias and describe the underlying config.
|
||||
ignoreAlias?: boolean;
|
||||
// Rows with trigger-reference chips render the IDs separately.
|
||||
hideTriggerIds?: boolean;
|
||||
}
|
||||
|
||||
export const describeTrigger = (
|
||||
@@ -923,6 +925,12 @@ const tryDescribeCondition = (
|
||||
return condition.alias;
|
||||
}
|
||||
|
||||
if (condition.condition === "trigger" && options?.hideTriggerIds) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.trigger.description.summary`
|
||||
);
|
||||
}
|
||||
|
||||
if (!condition.condition) {
|
||||
const shorthands: ("and" | "or" | "not")[] = ["and", "or", "not"];
|
||||
for (const key of shorthands) {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from "../common/datetime/calc_date";
|
||||
import type { DateRange } from "../common/datetime/calc_date_range";
|
||||
import { calcDateRange } from "../common/datetime/calc_date_range";
|
||||
import { DEFAULT_ENTITY_NAME } from "../common/entity/compute_entity_name_display";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
|
||||
import { groupBy } from "../common/util/group-by";
|
||||
@@ -328,12 +327,6 @@ export const computeEnergyLabel = (
|
||||
return customName;
|
||||
}
|
||||
|
||||
const stateObj = hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
return hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
|
||||
}
|
||||
|
||||
return getStatisticLabel(hass, statisticId, statisticsMetaData);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { getEntityAreaId } from "../../common/entity/context/get_entity_context";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import {
|
||||
computeEntityPickerDisplay,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -123,31 +124,11 @@ export const getEntities = (
|
||||
|
||||
// These values are the same for every entity, so compute them once instead
|
||||
// of inside the map over (potentially thousands of) entities.
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
const domainNames = new Map<string, string>();
|
||||
|
||||
items = entityIds.map<EntityComboBoxItem>((entityId) => {
|
||||
const stateObj = hass.states[entityId];
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
let domainName = domainNames.get(domain);
|
||||
if (domainName === undefined) {
|
||||
@@ -155,14 +136,7 @@ export const getEntities = (
|
||||
domainNames.set(domain, domainName);
|
||||
}
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const { primary, secondary } = computeEntityPickerDisplay(hass, stateObj);
|
||||
|
||||
return {
|
||||
id: `${idPrefix}${entityId}`,
|
||||
@@ -171,12 +145,14 @@ export const getEntities = (
|
||||
domain_name: domainName,
|
||||
sorting_label: [primary, secondary].filter(Boolean).join("_"),
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
...computeEntitySearchLabels(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
),
|
||||
domainName: domainName || null,
|
||||
friendlyName: friendlyName || null,
|
||||
entityId: entityId,
|
||||
},
|
||||
stateObj: stateObj,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { DEFAULT_ENTITY_NAME } from "../common/entity/entity_name_config";
|
||||
import type { HaDurationData } from "../components/ha-duration-input";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { firstWeekday } from "../common/datetime/first_weekday";
|
||||
@@ -348,8 +348,9 @@ export const getStatisticLabel = (
|
||||
): string => {
|
||||
const entity = hass.states[statisticsId];
|
||||
if (entity) {
|
||||
return computeStateName(entity);
|
||||
return hass.formatEntityName(entity, DEFAULT_ENTITY_NAME);
|
||||
}
|
||||
// External statistics have no entity to resolve a name against.
|
||||
return statisticsMetaData?.name || statisticsId;
|
||||
};
|
||||
|
||||
|
||||
+11
-6
@@ -6,7 +6,7 @@ import { formatListWithAnds } from "../common/string/format-list";
|
||||
import { isTemplate } from "../common/string/has-template";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Condition } from "./automation";
|
||||
import { describeCondition } from "./automation_i18n";
|
||||
import { describeCondition, type DescribeOptions } from "./automation_i18n";
|
||||
import { localizeDeviceAutomationAction } from "./device/device_automation";
|
||||
import type { EntityRegistryEntry } from "./entity/entity_registry";
|
||||
import { domainToName } from "./integration";
|
||||
@@ -47,7 +47,7 @@ export const describeAction = <T extends ActionType>(
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false,
|
||||
options?: DescribeOptions,
|
||||
manifests?: DomainManifestLookup
|
||||
): string => {
|
||||
try {
|
||||
@@ -56,7 +56,7 @@ export const describeAction = <T extends ActionType>(
|
||||
entityRegistry,
|
||||
action,
|
||||
actionType,
|
||||
ignoreAlias,
|
||||
options,
|
||||
manifests
|
||||
);
|
||||
if (typeof description !== "string") {
|
||||
@@ -79,10 +79,10 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false,
|
||||
options?: DescribeOptions,
|
||||
manifests?: DomainManifestLookup
|
||||
): string => {
|
||||
if (action.alias && !ignoreAlias) {
|
||||
if (action.alias && !options?.ignoreAlias) {
|
||||
return action.alias;
|
||||
}
|
||||
if (!actionType) {
|
||||
@@ -322,7 +322,12 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.check_condition.description.full`,
|
||||
{
|
||||
condition: describeCondition(action as Condition, hass, entityRegistry),
|
||||
condition: describeCondition(
|
||||
action as Condition,
|
||||
hass,
|
||||
entityRegistry,
|
||||
options
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+19
-1
@@ -1,4 +1,6 @@
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import type { Context, HomeAssistant, ServiceCallRequest } from "../types";
|
||||
import type { Action } from "./script";
|
||||
|
||||
export const callExecuteScript = (
|
||||
@@ -22,3 +24,19 @@ export const serviceCallWillDisconnect = (
|
||||
"update.home_assistant_core_update",
|
||||
"update.home_assistant_operating_system_update",
|
||||
].includes(serviceData?.entity_id));
|
||||
|
||||
// Core merges the target into the service data, so a target entity_id
|
||||
// replaces the legacy service data one rather than adding to it. Its schema
|
||||
// also accepts comma separated ids and lowercases them.
|
||||
export const getServiceCallEntityIds = (
|
||||
serviceData?: ServiceCallRequest["serviceData"],
|
||||
target?: ServiceCallRequest["target"]
|
||||
): string[] => [
|
||||
...new Set(
|
||||
(ensureArray(target?.entity_id ?? serviceData?.entity_id) ?? [])
|
||||
.filter((id): id is string => typeof id === "string")
|
||||
.flatMap((id) => id.split(","))
|
||||
.map((id) => id.trim().toLowerCase())
|
||||
.filter(isValidEntityId)
|
||||
),
|
||||
];
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { throttle } from "../../../../common/util/throttle";
|
||||
import "../../../../components/ha-hs-color-picker";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-labeled-slider";
|
||||
import { apiContext } from "../../../../data/context";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { apiContext, internationalizationContext } from "../../../data/context";
|
||||
import { setDateValue } from "../../../data/date";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
|
||||
@@ -131,6 +131,10 @@ class MoreInfoFan extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsOnOff =
|
||||
supportsFeature(this.stateObj, FanEntityFeature.TURN_ON) ||
|
||||
supportsFeature(this.stateObj, FanEntityFeature.TURN_OFF);
|
||||
|
||||
const supportsSpeed = supportsFeature(
|
||||
this.stateObj,
|
||||
FanEntityFeature.SET_SPEED
|
||||
@@ -174,21 +178,15 @@ class MoreInfoFan extends LitElement {
|
||||
`
|
||||
}
|
||||
${
|
||||
supportSpeedPercentage
|
||||
supportSpeedPercentage && supportsOnOff
|
||||
? html`
|
||||
<div class="buttons">
|
||||
${
|
||||
supportSpeedPercentage
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@click=${this._toggle}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-outlined-icon-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@click=${this._toggle}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
|
||||
@@ -38,7 +38,6 @@ import "../../../state-control/light/ha-state-control-light-brightness";
|
||||
import { apiContext, formattersContext } from "../../../data/context";
|
||||
import "../components/ha-more-info-control-select-container";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import "../components/lights/ha-favorite-color-button";
|
||||
import "../components/lights/ha-more-info-light-favorite-colors";
|
||||
import "../components/lights/light-color-rgb-picker";
|
||||
import "../components/lights/light-color-temp-picker";
|
||||
|
||||
@@ -10,7 +10,6 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-outlined-icon-button";
|
||||
import "../../../components/ha-state-icon";
|
||||
import { apiContext } from "../../../data/context";
|
||||
import type { LockEntity } from "../../../data/lock";
|
||||
|
||||
@@ -31,7 +31,6 @@ import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-marquee-text";
|
||||
import "../../../components/ha-select";
|
||||
import type { HaSlider } from "../../../components/ha-slider";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
|
||||
@@ -5,11 +5,9 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import "../../../components/entity/state-info";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-service-control";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { apiContext, internationalizationContext } from "../../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
|
||||
@@ -6,10 +6,8 @@ import { consumeLocalize } from "../../../common/decorators/consume-context-entr
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { sanitizeHttpUrl } from "../../../common/url/sanitize-http-url";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-faded";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/progress/ha-progress-bar";
|
||||
|
||||
@@ -29,17 +29,12 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { mainWindow } from "../../common/dom/get_main_window";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
} from "../../common/entity/compute_entity_name";
|
||||
import {
|
||||
getEntityContext,
|
||||
getEntityEntryContext,
|
||||
} from "../../common/entity/context/get_entity_context";
|
||||
computeEntityEntryNameList,
|
||||
computeEntityNameList,
|
||||
type EntityNameItem,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import {
|
||||
getHistoryState,
|
||||
@@ -130,6 +125,13 @@ declare global {
|
||||
|
||||
const DEFAULT_VIEW: MoreInfoView = "info";
|
||||
|
||||
const BREADCRUMB_NAME: EntityNameItem[] = [
|
||||
{ type: "area" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
];
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
EntitySettingsState | Helper | Record<string, string[]> | null,
|
||||
@@ -564,44 +566,27 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
const showCloseIcon =
|
||||
isDefaultView && this._parentEntityIds.length === 0 && !this._childView;
|
||||
|
||||
const context = stateObj
|
||||
? getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: this._entry
|
||||
? getEntityEntryContext(
|
||||
this._entry,
|
||||
const breadcrumb = (
|
||||
stateObj
|
||||
? computeEntityNameList(
|
||||
stateObj,
|
||||
BREADCRUMB_NAME,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const parentDeviceName = context?.parentDevice
|
||||
? computeDeviceName(context.parentDevice)
|
||||
: undefined;
|
||||
const areaName = context?.area ? computeAreaName(context.area) : undefined;
|
||||
|
||||
const breadcrumb = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
deviceName,
|
||||
entityName,
|
||||
].filter((v): v is string => Boolean(v));
|
||||
: this._entry
|
||||
? computeEntityEntryNameList(
|
||||
this._entry,
|
||||
BREADCRUMB_NAME,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: [entityId]
|
||||
).filter((v): v is string => Boolean(v));
|
||||
const addToMenuItem = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
);
|
||||
@@ -1031,6 +1016,32 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityId")) {
|
||||
this._reportShownEntityToExternalApp(
|
||||
changedProps.get("_entityId") as string | null | undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _reportShownEntityToExternalApp(
|
||||
previousEntityId: string | null | undefined
|
||||
) {
|
||||
const external = this.hass.auth.external;
|
||||
if (!external) {
|
||||
return;
|
||||
}
|
||||
if (this._entityId) {
|
||||
external.fireMessage({
|
||||
type: "more_info/opened",
|
||||
payload: { entity_id: this._entityId },
|
||||
});
|
||||
} else if (previousEntityId) {
|
||||
external.fireMessage({
|
||||
type: "more_info/closed",
|
||||
payload: { entity_id: previousEntityId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _entryUpdated(ev: CustomEvent<ExtEntityRegistryEntry>) {
|
||||
|
||||
@@ -27,7 +27,6 @@ import { updateDeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import type { InputSelectEntity } from "../../data/input_select";
|
||||
import { setSelectOption } from "../../data/select";
|
||||
import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail";
|
||||
import "../../panels/lovelace/entity-rows/hui-select-entity-row";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getTranslation } from "../../util/common-translation";
|
||||
import { AssistantSetupStyles } from "./styles";
|
||||
|
||||
@@ -6,7 +6,6 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-spinner";
|
||||
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
|
||||
import { interceptWakeWord } from "../../data/assist_satellite";
|
||||
|
||||
@@ -184,6 +184,29 @@ interface EMOutgoingMessageAddEntityTo extends EMMessage {
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageEntityControlled extends EMMessage {
|
||||
type: "entity/controlled";
|
||||
payload: {
|
||||
entity_ids: string[];
|
||||
domain: string;
|
||||
service: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageMoreInfoOpened extends EMMessage {
|
||||
type: "more_info/opened";
|
||||
payload: {
|
||||
entity_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageMoreInfoClosed extends EMMessage {
|
||||
type: "more_info/closed";
|
||||
payload: {
|
||||
entity_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageFocusElement extends EMMessage {
|
||||
type: "focus_element";
|
||||
payload: {
|
||||
@@ -216,6 +239,8 @@ type EMOutgoingMessageWithoutAnswer =
|
||||
| EMOutgoingMessageHaptic
|
||||
| EMOutgoingMessageImportThreadCredentials
|
||||
| EMOutgoingMessageMatterCommission
|
||||
| EMOutgoingMessageMoreInfoOpened
|
||||
| EMOutgoingMessageMoreInfoClosed
|
||||
| EMOutgoingMessageSidebarShow
|
||||
| EMOutgoingMessageTagWrite
|
||||
| EMOutgoingMessageThemeUpdate
|
||||
@@ -223,6 +248,7 @@ type EMOutgoingMessageWithoutAnswer =
|
||||
| EMOutgoingMessageImprovScan
|
||||
| EMOutgoingMessageImprovConfigureDevice
|
||||
| EMOutgoingMessageAddEntityTo
|
||||
| EMOutgoingMessageEntityControlled
|
||||
| EMOutgoingMessageFocusElement
|
||||
| EMOutgoingMessageReloadAndClearCache
|
||||
| EMOutgoingMessageAssistSettings;
|
||||
|
||||
@@ -12,15 +12,12 @@ import { isDate } from "../../common/string/is_date";
|
||||
import "../../components/entity/state-info";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-time-input";
|
||||
import "../../components/ha-dialog";
|
||||
import type { CalendarEventMutableParams } from "../../data/calendar";
|
||||
import { deleteCalendarEvent } from "../../data/calendar";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../lovelace/components/hui-generic-entity-row";
|
||||
import { renderRRuleAsText } from "./recurrence";
|
||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||
import type { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../lovelace/components/hui-generic-entity-row";
|
||||
import "./ha-recurrence-rule-editor";
|
||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||
import type { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
|
||||
|
||||
@@ -476,6 +476,13 @@ export class HAFullCalendar extends LitElement {
|
||||
const wasShowingToday = this._isShowingToday();
|
||||
const nextMidnight = new TZDate(new Date(), this._calendarTimeZone());
|
||||
nextMidnight.setHours(24, 0, 0, 0);
|
||||
const delay = nextMidnight.getTime() - Date.now();
|
||||
|
||||
// Guard against a NaN/negative delay (e.g. Intl longOffset unsupported on
|
||||
// Chromium < 95) so the midnight refresh can't fire in a tight loop (#54182).
|
||||
if (!Number.isFinite(delay) || delay <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._midnightRefreshTimeout = window.setTimeout(() => {
|
||||
if (wasShowingToday) {
|
||||
@@ -485,7 +492,7 @@ export class HAFullCalendar extends LitElement {
|
||||
}
|
||||
|
||||
this._scheduleMidnightRefresh();
|
||||
}, nextMidnight.getTime() - Date.now());
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private _clearMidnightRefreshTimeout(): void {
|
||||
|
||||
@@ -28,6 +28,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../trigger/ha-automation-trigger-references";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -61,6 +62,7 @@ import type {
|
||||
ActionSidebarConfig,
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
TriggerCondition,
|
||||
} from "../../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
@@ -230,6 +232,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
private _runResultTimeout?: number;
|
||||
|
||||
private _sidebarAction?: Action;
|
||||
|
||||
get selected() {
|
||||
return this._selected;
|
||||
}
|
||||
@@ -257,6 +261,21 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
// Controller updates (trigger selection, ID migration, or cleanup) bypass the
|
||||
// sidebar's save callback. Refresh its snapshot unless it already has this
|
||||
// action, so ordinary sidebar edits do not reopen it and disrupt focus.
|
||||
if (
|
||||
changedProperties.has("action") &&
|
||||
this._selected &&
|
||||
this.optionsInSidebar &&
|
||||
this.action !== this._sidebarAction
|
||||
) {
|
||||
this.openSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
@@ -360,7 +379,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._entityReg,
|
||||
this.action,
|
||||
undefined,
|
||||
false,
|
||||
{ hideTriggerIds: true },
|
||||
this._manifests
|
||||
)
|
||||
)}
|
||||
@@ -381,6 +400,14 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
></ha-automation-row-options>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._isTriggerConditionAction(type)
|
||||
? html` <ha-automation-trigger-references
|
||||
.condition=${this.action as TriggerCondition}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-trigger-references>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
noteTooltipText
|
||||
? html`
|
||||
@@ -701,6 +728,14 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _isTriggerConditionAction(
|
||||
type: ReturnType<typeof getAutomationActionType>
|
||||
) {
|
||||
return (
|
||||
type === "condition" && (this.action as Condition).condition === "trigger"
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.action) return nothing;
|
||||
|
||||
@@ -1020,7 +1055,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._entityReg,
|
||||
this.action,
|
||||
undefined,
|
||||
true,
|
||||
{ ignoreAlias: true },
|
||||
this._manifests
|
||||
)
|
||||
),
|
||||
@@ -1177,10 +1212,12 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
public openSidebar(action?: Action): void {
|
||||
const sidebarAction = action ?? this.action;
|
||||
this._sidebarAction = sidebarAction;
|
||||
const actionType = getAutomationActionType(sidebarAction);
|
||||
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
this._sidebarAction = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
},
|
||||
close: (focus?: boolean) => {
|
||||
|
||||
@@ -8,9 +8,7 @@ import type {
|
||||
LocalizeKeys,
|
||||
} from "../../../../../common/translations/localize";
|
||||
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import type { PickerComboBoxItem } from "../../../../../components/ha-picker-combo-box";
|
||||
import "../../../../../components/ha-select";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../../components/ha-service-picker";
|
||||
import "../../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
|
||||
import "../../../../../components/input/ha-input";
|
||||
|
||||
@@ -15,7 +15,7 @@ import { mainWindow } from "../../../common/dom/get_main_window";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeEntityPickerDisplay } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeFloorName } from "../../../common/entity/compute_floor_name";
|
||||
import { getDeviceArea } from "../../../common/entity/context/get_device_context";
|
||||
import { isNumericState } from "../../../common/number/format_number";
|
||||
@@ -950,29 +950,10 @@ class DialogAddAutomationElement
|
||||
this._manifests?.[domain]
|
||||
);
|
||||
} else {
|
||||
const stateObj = this.hass.states[targetId];
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
subtitle = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
entityName ? deviceName : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(separator);
|
||||
subtitle = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
this.hass.states[targetId]
|
||||
).secondary;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/chips/ha-filter-chip";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-button-toggle-group";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
|
||||
@@ -24,6 +24,7 @@ import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../trigger/ha-automation-trigger-references";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -50,6 +51,7 @@ import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
ConditionSidebarConfig,
|
||||
TriggerCondition,
|
||||
} from "../../../../data/automation";
|
||||
import { isCondition, testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
@@ -152,6 +154,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
private _testingTimeout?: number;
|
||||
|
||||
private _sidebarCondition?: Condition;
|
||||
|
||||
get selected() {
|
||||
return this._selected;
|
||||
}
|
||||
@@ -201,12 +205,24 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<ha-automation-condition-summary
|
||||
slot="header"
|
||||
.label=${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
describeCondition(this.condition, this.hass, this._entityReg, {
|
||||
hideTriggerIds: true,
|
||||
})
|
||||
)}
|
||||
.condition=${this.condition}
|
||||
.description=${this.conditionDescriptions[this.condition.condition]}
|
||||
.isNew=${this._isNew}
|
||||
></ha-automation-condition-summary>
|
||||
>
|
||||
${
|
||||
this.condition.condition === "trigger"
|
||||
? html`<ha-automation-trigger-references
|
||||
slot="references"
|
||||
.condition=${this.condition as TriggerCondition}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-trigger-references>`
|
||||
: nothing
|
||||
}
|
||||
</ha-automation-condition-summary>
|
||||
<ha-automation-row-event-chip
|
||||
.show=${this._testing}
|
||||
.variant=${this._testingResult ? "success" : "warning"}
|
||||
@@ -567,6 +583,21 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
// Controller updates (trigger selection, ID migration, or cleanup) bypass the
|
||||
// sidebar's save callback. Refresh its snapshot unless it already has this
|
||||
// condition, so ordinary sidebar edits do not reopen it and disrupt focus.
|
||||
if (
|
||||
changedProperties.has("condition") &&
|
||||
this._selected &&
|
||||
this.optionsInSidebar &&
|
||||
this.condition !== this._sidebarCondition
|
||||
) {
|
||||
this.openSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._testingTimeout !== undefined) {
|
||||
@@ -898,8 +929,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
public openSidebar(condition?: Condition): void {
|
||||
const sidebarCondition = condition || this.condition;
|
||||
this._sidebarCondition = sidebarCondition;
|
||||
fireEvent(this, "open-sidebar", {
|
||||
save: (value) => {
|
||||
this._sidebarCondition = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
},
|
||||
close: (focus?: boolean) => {
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlert, mdiLinkVariantOff } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-select";
|
||||
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-trigger-icon";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { TriggerCondition } from "../../../../../data/automation";
|
||||
import {
|
||||
flattenTriggers,
|
||||
type AutomationConfig,
|
||||
type Trigger,
|
||||
type TriggerCondition,
|
||||
} from "../../../../../data/automation";
|
||||
automationTriggerContext,
|
||||
type AutomationTriggerContext,
|
||||
type TriggerIdOption,
|
||||
} from "../../trigger/automation-trigger-id";
|
||||
import { describeTrigger } from "../../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
const getTriggersIds = (triggers: Trigger[]): string[] => {
|
||||
const triggerIds = flattenTriggers(triggers)
|
||||
.map((t) => ("id" in t ? t.id : undefined))
|
||||
.filter(Boolean) as string[];
|
||||
return Array.from(new Set(triggerIds));
|
||||
};
|
||||
import { rowStyles } from "../../styles";
|
||||
|
||||
@customElement("ha-automation-condition-trigger")
|
||||
export class HaTriggerCondition extends LitElement {
|
||||
@@ -30,9 +33,13 @@ export class HaTriggerCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _triggerIds: string[] = [];
|
||||
@state()
|
||||
@consume({ context: automationTriggerContext, subscribe: true })
|
||||
private _triggers?: AutomationTriggerContext;
|
||||
|
||||
private _unsub?: UnsubscribeFunc;
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
private _entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
public static get defaultConfig(): TriggerCondition {
|
||||
return {
|
||||
@@ -41,89 +48,214 @@ export class HaTriggerCondition extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(triggerIds: string[]) =>
|
||||
[
|
||||
{
|
||||
name: "id",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
options: triggerIds,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const details = { callback: (config) => this._automationUpdated(config) };
|
||||
fireEvent(this, "subscribe-automation-config", details);
|
||||
this._unsub = (details as any).unsub;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._triggerIds.length) {
|
||||
const selectedIds = ensureArray(this.condition.id).filter(Boolean);
|
||||
const triggerIdOptions = this._triggers?.options ?? [];
|
||||
const missingIds = this._missingIds(this.condition.id, triggerIdOptions);
|
||||
|
||||
if (!triggerIdOptions.length && !selectedIds.length) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
|
||||
);
|
||||
}
|
||||
|
||||
const schema = this._schema(this._triggerIds);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.schema=${schema}
|
||||
.data=${this.condition}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
${
|
||||
missingIds.length
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.missing_triggers"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
triggerIdOptions.some((option) => option.duplicate)
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.duplicate_ids"
|
||||
)}
|
||||
<ha-button
|
||||
slot="action"
|
||||
size="s"
|
||||
appearance="filled"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._fixDuplicateIds}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.duplicate_ids_fix"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
<ha-list-selectable
|
||||
multi
|
||||
controlled
|
||||
@ha-list-item-selected=${this._handleSelected}
|
||||
@ha-list-item-deselected=${this._handleDeselected}
|
||||
>
|
||||
${triggerIdOptions.map(
|
||||
(option) => html`
|
||||
<ha-list-item-option
|
||||
.value=${option.id}
|
||||
.selected=${selectedIds.includes(option.id)}
|
||||
.disabled=${this.disabled}
|
||||
appearance="checkbox"
|
||||
>
|
||||
<span slot="start" class="trigger-row-leading">
|
||||
<span class="trigger-index-badge">${option.index + 1}</span>
|
||||
<ha-trigger-icon
|
||||
.trigger=${
|
||||
"trigger" in option.trigger ? option.trigger.trigger : ""
|
||||
}
|
||||
></ha-trigger-icon>
|
||||
</span>
|
||||
<span slot="headline">
|
||||
${capitalizeFirstLetter(
|
||||
describeTrigger(option.trigger, this.hass, this._entityReg)
|
||||
)}
|
||||
</span>
|
||||
${
|
||||
option.duplicate
|
||||
? html`<span slot="end" class="duplicate-trigger-badge">
|
||||
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
|
||||
<span class="duplicate-trigger-id">${option.id}</span>
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
${missingIds.map(
|
||||
(id) => html`
|
||||
<ha-list-item-option
|
||||
.value=${id}
|
||||
.selected=${true}
|
||||
.disabled=${this.disabled}
|
||||
appearance="checkbox"
|
||||
>
|
||||
<span slot="start" class="missing-trigger-badge">
|
||||
<ha-svg-icon .path=${mdiLinkVariantOff}></ha-svg-icon>
|
||||
<span class="missing-trigger-id">${id}</span>
|
||||
</span>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.missing_trigger"
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.trigger.${schema.name}`
|
||||
);
|
||||
|
||||
private _automationUpdated(config?: AutomationConfig) {
|
||||
this._triggerIds = config?.triggers
|
||||
? getTriggersIds(ensureArray(config.triggers))
|
||||
: [];
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
private _handleSelected = (ev: CustomEvent<number>) => {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (typeof newValue.id === "string") {
|
||||
if (!this._triggerIds.some((id) => id === newValue.id)) {
|
||||
newValue.id = "";
|
||||
}
|
||||
} else if (Array.isArray(newValue.id)) {
|
||||
newValue.id = newValue.id.filter((_id) =>
|
||||
this._triggerIds.some((id) => id === _id)
|
||||
);
|
||||
if (!newValue.id.length) {
|
||||
newValue.id = "";
|
||||
}
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
const item = list.items[ev.detail] as HTMLElement & { value?: string };
|
||||
const id = item?.value;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this._selectId(id, true);
|
||||
};
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
private _handleDeselected = (ev: CustomEvent<number>) => {
|
||||
ev.stopPropagation();
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
const item = list.items[ev.detail] as HTMLElement & { value?: string };
|
||||
const id = item?.value;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this._selectId(id, false);
|
||||
};
|
||||
|
||||
private _selectId(id: string, checked: boolean) {
|
||||
const ids = ensureArray(this.condition.id).filter(Boolean);
|
||||
const selectedIds = checked
|
||||
? ids.includes(id)
|
||||
? ids
|
||||
: [...ids, id]
|
||||
: ids.filter((_id) => _id !== id);
|
||||
if (this._triggers) {
|
||||
this._triggers.select(this.condition, selectedIds);
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
id: selectedIds.length ? selectedIds : "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _fixDuplicateIds = () => {
|
||||
if (!this.disabled) {
|
||||
return this._triggers?.fixDuplicateIds();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private _missingIds = memoizeOne(
|
||||
(ids: TriggerCondition["id"], triggerIdOptions: TriggerIdOption[]) => {
|
||||
const availableIds = new Set(triggerIdOptions.map((option) => option.id));
|
||||
return ensureArray(ids).filter((id) => id && !availableIds.has(id));
|
||||
}
|
||||
);
|
||||
|
||||
static styles = [
|
||||
rowStyles,
|
||||
css`
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-list-selectable {
|
||||
--ha-list-gap: var(--ha-space-2);
|
||||
--ha-list-padding: 0;
|
||||
}
|
||||
ha-list-item-option {
|
||||
--ha-list-item-padding: var(--ha-space-1) var(--ha-space-2);
|
||||
}
|
||||
.trigger-row-leading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
ha-trigger-icon {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
.missing-trigger-badge,
|
||||
.duplicate-trigger-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-1);
|
||||
padding: 2px var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background: var(--ha-color-fill-warning-normal-resting);
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
line-height: 18px;
|
||||
}
|
||||
.missing-trigger-badge ha-svg-icon,
|
||||
.duplicate-trigger-badge ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.missing-trigger-id,
|
||||
.duplicate-trigger-id {
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: var(--ha-font-size-s);
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -42,6 +42,7 @@ import type {
|
||||
AutomationEntity,
|
||||
BlueprintAutomationConfig,
|
||||
Condition,
|
||||
SidebarConfig,
|
||||
Trigger,
|
||||
} from "../../../data/automation";
|
||||
import {
|
||||
@@ -88,6 +89,8 @@ import "./manual-automation-editor";
|
||||
import type { HaManualAutomationEditor } from "./manual-automation-editor";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
import { AutomationTriggerController } from "./trigger/automation-trigger-controller";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-editor": HaAutomationEditor;
|
||||
@@ -132,6 +135,15 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
|
||||
private _configSubscriptionsId = 1;
|
||||
|
||||
private _triggerController = new AutomationTriggerController(this, {
|
||||
getConfig: () => this.config,
|
||||
canEdit: () => !this.readOnly && !this.saving,
|
||||
commit: (config) => {
|
||||
this._manualEditor?.resetPastedConfig();
|
||||
this._updateConfig(config);
|
||||
},
|
||||
});
|
||||
|
||||
private _newAutomationId?: string;
|
||||
|
||||
protected domainHooks: EditorDomainHooks<AutomationConfig> = {
|
||||
@@ -503,6 +515,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
@sidebar-config-changed=${this._sidebarConfigChanged}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._renderDeprecatedMigratedAlert()}
|
||||
@@ -780,12 +793,26 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
|
||||
ev.stopPropagation();
|
||||
const config = ev.detail.value;
|
||||
this._updateConfig(
|
||||
"use_blueprint" in config
|
||||
? config
|
||||
: this._triggerController.cleanupRemovedIds(config)
|
||||
);
|
||||
}
|
||||
|
||||
private _sidebarConfigChanged = (
|
||||
ev: CustomEvent<{ value: SidebarConfig | undefined }>
|
||||
) => {
|
||||
this._triggerController.checkShowIndices(ev.detail.value);
|
||||
};
|
||||
|
||||
private _updateConfig(config: AutomationConfig) {
|
||||
if (this.config) {
|
||||
this._undoRedoController.commit(this.config);
|
||||
}
|
||||
|
||||
this.config = ev.detail.value;
|
||||
this.config = config;
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
|
||||
@@ -8,6 +8,60 @@ import { nextRender } from "../../../common/util/render-status";
|
||||
import type { AutomationClipboard } from "../../../data/automation";
|
||||
import type { Constructor, HomeAssistant } from "../../../types";
|
||||
|
||||
const rowKeys = new WeakMap<object, string>();
|
||||
|
||||
const getAutomationRowKey = (item: object): string => {
|
||||
if (!rowKeys.has(item)) {
|
||||
rowKeys.set(item, Math.random().toString());
|
||||
}
|
||||
return rowKeys.get(item)!;
|
||||
};
|
||||
|
||||
// Keep the selected row and its sidebar callbacks alive across immutable edits.
|
||||
// Keys stay out of the automation YAML and the objects stored for undo.
|
||||
export const preserveAutomationRowKey = <T>(original: T, updated: T): T => {
|
||||
if (
|
||||
original &&
|
||||
updated &&
|
||||
typeof original === "object" &&
|
||||
typeof updated === "object"
|
||||
) {
|
||||
const key = rowKeys.get(original);
|
||||
if (key !== undefined) {
|
||||
rowKeys.set(updated, key);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reconcile rows after a transformation that keeps their paths and list order.
|
||||
* Matching object keys and array indices represent the same logical row, so Lit's
|
||||
* repeat directive can reuse its element, selection state, and sidebar callbacks.
|
||||
* Do not use this to reconcile insertions, removals, or reordering between the two
|
||||
* inputs: matching indices would then associate keys with different rows.
|
||||
*/
|
||||
export const preserveAutomationRowKeys = (
|
||||
original: unknown,
|
||||
updated: unknown
|
||||
): void => {
|
||||
if (
|
||||
original === updated ||
|
||||
!original ||
|
||||
!updated ||
|
||||
typeof original !== "object" ||
|
||||
typeof updated !== "object"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
preserveAutomationRowKey(original, updated);
|
||||
const before = original as Record<string, unknown>;
|
||||
const after = updated as Record<string, unknown>;
|
||||
for (const key of Object.keys(before)) {
|
||||
preserveAutomationRowKeys(before[key], after[key]);
|
||||
}
|
||||
};
|
||||
|
||||
export const AutomationSortableListMixin = <T extends object>(
|
||||
superClass: Constructor<LitElement>
|
||||
) => {
|
||||
@@ -36,8 +90,6 @@ export const AutomationSortableListMixin = <T extends object>(
|
||||
|
||||
protected focusItemIndexOnChange?: number;
|
||||
|
||||
private _itemKeys = new WeakMap<T, string>();
|
||||
|
||||
protected get items(): T[] {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
@@ -47,11 +99,7 @@ export const AutomationSortableListMixin = <T extends object>(
|
||||
}
|
||||
|
||||
protected getKey(item: T): string {
|
||||
if (!this._itemKeys.has(item)) {
|
||||
this._itemKeys.set(item, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._itemKeys.get(item)!;
|
||||
return getAutomationRowKey(item);
|
||||
}
|
||||
|
||||
protected moveUp(ev) {
|
||||
@@ -146,10 +194,7 @@ export const AutomationSortableListMixin = <T extends object>(
|
||||
items.splice(index, 1);
|
||||
} else {
|
||||
// Store key on new value.
|
||||
const key = this.getKey(items[index]);
|
||||
this._itemKeys.set(newValue, key);
|
||||
|
||||
items[index] = newValue;
|
||||
items[index] = preserveAutomationRowKey(items[index], newValue);
|
||||
}
|
||||
|
||||
this.items = items;
|
||||
|
||||
@@ -13,9 +13,13 @@ import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -49,9 +53,14 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import "../../../components/ha-trace-picker";
|
||||
import "../../../components/ha-split-panel";
|
||||
import type { HaSplitPanel } from "../../../components/ha-split-panel";
|
||||
|
||||
const TABS = ["details", "timeline", "logbook", "automation_config"] as const;
|
||||
|
||||
const STORAGE_KEY_SPLIT_POSITION = "automation-trace-split-position";
|
||||
const DEFAULT_SPLIT_POSITION = 20;
|
||||
|
||||
@customElement("ha-automation-trace")
|
||||
export class HaAutomationTrace extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -84,6 +93,8 @@ export class HaAutomationTrace extends LitElement {
|
||||
|
||||
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
|
||||
|
||||
@state() private _splitPosition = DEFAULT_SPLIT_POSITION;
|
||||
|
||||
@query("hat-script-graph") private _graph?: HatScriptGraph;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -91,10 +102,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
? this.hass.states[this._entityId]
|
||||
: undefined;
|
||||
|
||||
const graph = this._graph;
|
||||
const trackedNodes = graph?.trackedNodes;
|
||||
const renderedNodes = graph?.renderedNodes;
|
||||
|
||||
const title = stateObj?.attributes.friendly_name || this._entityId;
|
||||
|
||||
let devButtons: TemplateResult | string = "";
|
||||
@@ -240,94 +247,19 @@ export class HaAutomationTrace extends LitElement {
|
||||
? ""
|
||||
: html`
|
||||
<div class="main">
|
||||
<div class="graph">
|
||||
<hat-script-graph
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
></hat-script-graph>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
${
|
||||
this.narrow
|
||||
? this._renderPanes()
|
||||
: html`
|
||||
<ha-split-panel
|
||||
class="split"
|
||||
.position=${this._splitPosition}
|
||||
@wa-reposition=${this._splitRepositioned}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
${this._renderPanes()}
|
||||
</ha-split-panel>
|
||||
`
|
||||
)}
|
||||
${
|
||||
this._trace.blueprint_inputs
|
||||
? html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-tab-group>
|
||||
${
|
||||
this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
? nothing
|
||||
: this._view === "details"
|
||||
? html`
|
||||
<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.trackedNodes=${trackedNodes}
|
||||
.renderedNodes=${renderedNodes!}
|
||||
></ha-trace-path-details>
|
||||
`
|
||||
: this._view === "automation_config"
|
||||
? html`
|
||||
<ha-trace-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-config>
|
||||
`
|
||||
: this._view === "logbook"
|
||||
? html`
|
||||
<ha-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-trace-logbook>
|
||||
`
|
||||
: this._view === "blueprint"
|
||||
? html`
|
||||
<ha-trace-blueprint-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-blueprint-config>
|
||||
`
|
||||
: html`
|
||||
<ha-trace-timeline
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.selected=${this._selected}
|
||||
@value-changed=${this._timelinePathPicked}
|
||||
></ha-trace-timeline>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -335,12 +267,115 @@ export class HaAutomationTrace extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanes(): TemplateResult {
|
||||
const graph = this._graph;
|
||||
const trackedNodes = graph?.trackedNodes;
|
||||
const renderedNodes = graph?.renderedNodes;
|
||||
|
||||
return html`
|
||||
<div class="graph" slot="start">
|
||||
<hat-script-graph
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
></hat-script-graph>
|
||||
</div>
|
||||
|
||||
<div class="info" slot="end">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
${
|
||||
this._trace!.blueprint_inputs
|
||||
? html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-tab-group>
|
||||
${
|
||||
this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
? nothing
|
||||
: this._view === "details"
|
||||
? html`
|
||||
<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.trackedNodes=${trackedNodes}
|
||||
.renderedNodes=${renderedNodes!}
|
||||
></ha-trace-path-details>
|
||||
`
|
||||
: this._view === "automation_config"
|
||||
? html`
|
||||
<ha-trace-config .trace=${this._trace}></ha-trace-config>
|
||||
`
|
||||
: this._view === "logbook"
|
||||
? html`
|
||||
<ha-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-trace-logbook>
|
||||
`
|
||||
: this._view === "blueprint"
|
||||
? html`
|
||||
<ha-trace-blueprint-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-blueprint-config>
|
||||
`
|
||||
: html`
|
||||
<ha-trace-timeline
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.selected=${this._selected}
|
||||
@value-changed=${this._timelinePathPicked}
|
||||
></ha-trace-timeline>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
|
||||
const storedPosition = localStorage?.[STORAGE_KEY_SPLIT_POSITION];
|
||||
if (storedPosition) {
|
||||
const parsed = parseFloat(storedPosition);
|
||||
if (!isNaN(parsed) && parsed > 0 && parsed < 100) {
|
||||
this._splitPosition = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.automationId) {
|
||||
return;
|
||||
}
|
||||
@@ -428,6 +463,19 @@ export class HaAutomationTrace extends LitElement {
|
||||
this._selected = ev.detail;
|
||||
}
|
||||
|
||||
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
|
||||
this._splitPosition = ev.target.position;
|
||||
this._storeSplitPosition();
|
||||
}
|
||||
|
||||
private _storeSplitPosition = debounce(
|
||||
() => {
|
||||
localStorage[STORAGE_KEY_SPLIT_POSITION] = String(this._splitPosition);
|
||||
},
|
||||
500,
|
||||
false
|
||||
);
|
||||
|
||||
private _refreshTraces() {
|
||||
this._loadTraces();
|
||||
}
|
||||
@@ -618,13 +666,22 @@ export class HaAutomationTrace extends LitElement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-split-panel.split {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
--ha-split-panel-min: 10%;
|
||||
--ha-split-panel-max: 80%;
|
||||
--ha-split-panel-divider-hit-area: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.graph {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
max-width: 50%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
hat-script-graph {
|
||||
flex: 1;
|
||||
@@ -642,6 +699,8 @@ export class HaAutomationTrace extends LitElement {
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,13 @@ export const ManualEditorMixin = <TConfig>(
|
||||
// deselect previous selected row
|
||||
this.sidebarConfig?.close?.();
|
||||
this.sidebarConfig = ev.detail;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("sidebar-config-changed", {
|
||||
detail: { value: this.sidebarConfig },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
|
||||
// be sure the sidebar editor is recreated
|
||||
this.sidebarKey++;
|
||||
@@ -199,6 +206,13 @@ export const ManualEditorMixin = <TConfig>(
|
||||
...this.sidebarConfig,
|
||||
...ev.detail.value,
|
||||
};
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("sidebar-config-changed", {
|
||||
detail: { value: this.sidebarConfig },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public triggerCloseSidebar() {
|
||||
@@ -214,6 +228,13 @@ export const ManualEditorMixin = <TConfig>(
|
||||
|
||||
protected handleCloseSidebar() {
|
||||
this.sidebarConfig = undefined;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("sidebar-config-changed", {
|
||||
detail: { value: undefined },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected replaceExistingConfig(config: TConfig) {
|
||||
|
||||
@@ -44,6 +44,12 @@ import {
|
||||
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { manualEditorStyles, saveFabStyles } from "./styles";
|
||||
import "./trigger/ha-automation-trigger";
|
||||
import {
|
||||
cleanupRemovedGeneratedTriggerReferences,
|
||||
getExplicitTriggerIds,
|
||||
isGeneratedTriggerId,
|
||||
stripGeneratedTriggerIds,
|
||||
} from "./trigger/automation-trigger-id";
|
||||
|
||||
const baseConfigStruct = object({
|
||||
alias: optional(string()),
|
||||
@@ -407,19 +413,29 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
}
|
||||
});
|
||||
|
||||
let appendedConfig = config;
|
||||
if ("triggers" in config) {
|
||||
const pastedTriggers = ensureArray(config.triggers);
|
||||
// Strip generated trigger IDs and remove matching references from pasted
|
||||
// conditions/actions so they do not dangle against the stripped triggers.
|
||||
const strippedIds = new Set(
|
||||
getExplicitTriggerIds(pastedTriggers).filter(isGeneratedTriggerId)
|
||||
);
|
||||
appendedConfig = strippedIds.size
|
||||
? cleanupRemovedGeneratedTriggerReferences(config, strippedIds)
|
||||
: config;
|
||||
workingCopy.triggers = ensureArray(workingCopy.triggers || []).concat(
|
||||
ensureArray(config.triggers)
|
||||
pastedTriggers.map((t) => stripGeneratedTriggerIds(t))
|
||||
);
|
||||
}
|
||||
if ("conditions" in config) {
|
||||
if ("conditions" in appendedConfig) {
|
||||
workingCopy.conditions = ensureArray(workingCopy.conditions || []).concat(
|
||||
ensureArray(config.conditions)
|
||||
ensureArray(appendedConfig.conditions)
|
||||
);
|
||||
}
|
||||
if ("actions" in config) {
|
||||
if ("actions" in appendedConfig) {
|
||||
workingCopy.actions = ensureArray(workingCopy.actions || []).concat(
|
||||
ensureArray(config.actions)
|
||||
ensureArray(appendedConfig.actions)
|
||||
) as Action[];
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import "../../../../components/ha-svg-icon";
|
||||
import type {
|
||||
Condition,
|
||||
OptionSidebarConfig,
|
||||
TriggerCondition,
|
||||
} from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@@ -45,6 +46,7 @@ import type HaAutomationAction from "../action/ha-automation-action";
|
||||
import "../condition/ha-automation-condition";
|
||||
import type HaAutomationCondition from "../condition/ha-automation-condition";
|
||||
import { showEditorToast } from "../editor-toast";
|
||||
import "../trigger/ha-automation-trigger-references";
|
||||
import {
|
||||
editorStyles,
|
||||
indentStyle,
|
||||
@@ -106,7 +108,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
this._expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _getDescription() {
|
||||
private _getDescription(withTriggerReferences = false) {
|
||||
const conditions = ensureArray<Condition | string>(this.option!.conditions);
|
||||
if (!conditions || conditions.length === 0) {
|
||||
return this.hass.localize(
|
||||
@@ -117,9 +119,12 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
if (typeof conditions[0] === "string") {
|
||||
str += conditions[0];
|
||||
} else {
|
||||
str += describeCondition(conditions[0], this.hass, this._entityReg);
|
||||
str += describeCondition(conditions[0], this.hass, this._entityReg, {
|
||||
hideTriggerIds: withTriggerReferences,
|
||||
});
|
||||
}
|
||||
if (conditions.length > 1) {
|
||||
// When chips are rendered, the additional-condition count follows them.
|
||||
if (conditions.length > 1 && !withTriggerReferences) {
|
||||
str += this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option_description_additional",
|
||||
{ numberOfAdditionalConditions: conditions.length - 1 }
|
||||
@@ -144,6 +149,17 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
private _renderRow() {
|
||||
const conditions = ensureArray<Condition | string>(
|
||||
this.option?.conditions ?? []
|
||||
);
|
||||
const firstCondition = conditions[0];
|
||||
const triggerCondition =
|
||||
!this.option?.alias &&
|
||||
!this._expanded &&
|
||||
typeof firstCondition === "object" &&
|
||||
firstCondition?.condition === "trigger"
|
||||
? (firstCondition as TriggerCondition)
|
||||
: undefined;
|
||||
const noteTooltipText = truncateWithEllipsis(
|
||||
this.option?.note?.trim() || "",
|
||||
250
|
||||
@@ -156,11 +172,27 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
? `${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: this.index + 1 }
|
||||
)}: ${this.option.alias || (this._expanded ? "" : this._getDescription())}`
|
||||
)}: ${this.option.alias || (this._expanded ? "" : this._getDescription(!!triggerCondition))}`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.default"
|
||||
)
|
||||
}
|
||||
${
|
||||
triggerCondition
|
||||
? html`<ha-automation-trigger-references
|
||||
.condition=${triggerCondition}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-trigger-references>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
triggerCondition && conditions.length > 1
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option_description_additional",
|
||||
{ numberOfAdditionalConditions: conditions.length - 1 }
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.option?.note?.trim()
|
||||
? html`
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
mdiContentCut,
|
||||
mdiContentPaste,
|
||||
mdiDelete,
|
||||
mdiIdentifier,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
@@ -57,8 +56,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
@@ -66,7 +63,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._requestShowId = false;
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
@@ -153,28 +149,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
</ha-dropdown-item>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!this.yamlMode &&
|
||||
!("id" in this.config.config) &&
|
||||
!this._requestShowId
|
||||
? html`<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="show_id"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<span
|
||||
class="shortcut-placeholder ${isMac ? "mac" : ""}"
|
||||
></span>
|
||||
</div>
|
||||
</ha-dropdown-item>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<wa-divider slot="menu-items"></wa-divider>
|
||||
|
||||
<ha-dropdown-item
|
||||
@@ -366,7 +340,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
@yaml-changed=${this._yamlChangedSidebar}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
.showId=${this._requestShowId}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@@ -419,10 +392,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
};
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
@@ -437,9 +406,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
case "edit_note":
|
||||
this.config.editNote();
|
||||
break;
|
||||
case "show_id":
|
||||
this._showTriggerId();
|
||||
break;
|
||||
case "duplicate":
|
||||
this.config.duplicate();
|
||||
break;
|
||||
|
||||
@@ -77,6 +77,54 @@ export const rowStyles = css`
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.trigger-leading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.trigger-leading {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
|
||||
.trigger-leading ha-trigger-icon,
|
||||
.trigger-leading ha-svg-icon {
|
||||
--mdc-icon-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.trigger-index-badge {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px dotted var(--ha-color-border-neutral-normal);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
box-sizing: border-box;
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: 1;
|
||||
text-box-trim: both;
|
||||
text-box-edge: cap alphabetic;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
opacity 180ms ease-out,
|
||||
transform 180ms ease-out,
|
||||
width 180ms ease-out,
|
||||
margin-inline-end 180ms ease-out,
|
||||
border-width 180ms ease-out;
|
||||
}
|
||||
|
||||
.trigger-index-badge.hidden {
|
||||
opacity: 0;
|
||||
transform: translateX(calc(-8px * var(--scale-direction)));
|
||||
width: 0;
|
||||
margin-inline-end: 0;
|
||||
border-width: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.note-indicator {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { ContextConsumer, ContextProvider } from "@lit/context";
|
||||
import type { LitElement, ReactiveController } from "lit";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type {
|
||||
AutomationConfig,
|
||||
SidebarConfig,
|
||||
TriggerCondition,
|
||||
} from "../../../../data/automation";
|
||||
import { internationalizationContext } from "../../../../data/context";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
assignGeneratedTriggerIds,
|
||||
automationTriggerContext,
|
||||
cleanupRemovedGeneratedTriggerReferences,
|
||||
cleanupUnusedGeneratedTriggerIds,
|
||||
getTriggerIdOptions,
|
||||
getExplicitTriggerIds,
|
||||
isGeneratedTriggerId,
|
||||
makeDuplicateTriggerIdsUnique,
|
||||
updateTriggerCondition,
|
||||
} from "./automation-trigger-id";
|
||||
import { preserveAutomationRowKeys } from "../ha-automation-sortable-list-mixin";
|
||||
|
||||
interface AutomationTriggerControllerOptions {
|
||||
/** Read the editor's current configuration, including after an awaited dialog. */
|
||||
getConfig: () => AutomationConfig | undefined;
|
||||
canEdit: () => boolean;
|
||||
/** Apply one complete change through the editor's undo and dirty-state handling. */
|
||||
commit: (config: AutomationConfig) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates trigger-ID editing.
|
||||
*
|
||||
* Descendants consume shared options and callbacks through automationTriggerContext.
|
||||
* A selection assigns any missing trigger IDs and updates the referencing condition
|
||||
* in one commit. Duplicate-ID migration uses the same commit path. Ordinary trigger
|
||||
* edits instead pass through cleanupRemovedIds before the editor commits them.
|
||||
*
|
||||
* The transformation helpers only change configuration. This controller preserves
|
||||
* the corresponding row keys so an open sidebar continues to track the edited row.
|
||||
*/
|
||||
export class AutomationTriggerController implements ReactiveController {
|
||||
private _i18n: ContextConsumer<
|
||||
typeof internationalizationContext,
|
||||
LitElement
|
||||
>;
|
||||
|
||||
private _provider: ContextProvider<typeof automationTriggerContext>;
|
||||
|
||||
// Unassigned options contain candidate IDs. Reuse them until the trigger list
|
||||
// changes, so rendering and selection use the same IDs across host updates.
|
||||
private _getTriggerIdOptions = memoizeOne(getTriggerIdOptions);
|
||||
|
||||
constructor(
|
||||
private _host: LitElement,
|
||||
private _options: AutomationTriggerControllerOptions
|
||||
) {
|
||||
this._i18n = new ContextConsumer(_host, {
|
||||
context: internationalizationContext,
|
||||
subscribe: true,
|
||||
});
|
||||
this._provider = new ContextProvider(_host, {
|
||||
context: automationTriggerContext,
|
||||
initialValue: {
|
||||
options: [],
|
||||
showIndices: false,
|
||||
select: this._selectTriggerIds,
|
||||
fixDuplicateIds: this._fixDuplicateTriggerIds,
|
||||
},
|
||||
});
|
||||
_host.addController(this);
|
||||
}
|
||||
|
||||
/** Publish options before descendants render, including after undo or redo. */
|
||||
public hostUpdate() {
|
||||
const config = this._options.getConfig();
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
const options = this._getTriggerIdOptions(config.triggers);
|
||||
if (options !== this._provider.value.options) {
|
||||
this._provider.setValue({ ...this._provider.value, options });
|
||||
}
|
||||
}
|
||||
|
||||
public checkShowIndices(sidebarConfig: SidebarConfig | undefined) {
|
||||
const showIndices = this._isTriggerConditionSidebarActive(sidebarConfig);
|
||||
if (showIndices !== this._provider.value.showIndices) {
|
||||
this._provider.setValue({ ...this._provider.value, showIndices });
|
||||
}
|
||||
}
|
||||
|
||||
private _isTriggerConditionSidebarActive(
|
||||
sidebarConfig: SidebarConfig | undefined
|
||||
): boolean {
|
||||
if (!sidebarConfig || !("config" in sidebarConfig)) {
|
||||
return false;
|
||||
}
|
||||
const config = sidebarConfig.config as
|
||||
{ condition?: unknown } | { action?: { condition?: unknown } };
|
||||
if ("condition" in config && config.condition === "trigger") {
|
||||
return true;
|
||||
}
|
||||
return !!(
|
||||
"action" in config &&
|
||||
config.action &&
|
||||
"condition" in config.action &&
|
||||
config.action.condition === "trigger"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an ordinary editor update without committing it. Reconcile keys against
|
||||
* the incoming configuration, not the previous one: the user may have reordered
|
||||
* or deleted triggers, while cleanup itself only changes condition references.
|
||||
*/
|
||||
public cleanupRemovedIds(config: AutomationConfig): AutomationConfig {
|
||||
const current = this._options.getConfig();
|
||||
const previousIds = new Set(
|
||||
current ? getExplicitTriggerIds(current.triggers) : []
|
||||
);
|
||||
const nextIds = new Set(getExplicitTriggerIds(config.triggers));
|
||||
const removedIds = new Set(
|
||||
[...previousIds].filter(
|
||||
(id) => !nextIds.has(id) && isGeneratedTriggerId(id)
|
||||
)
|
||||
);
|
||||
let cleaned = cleanupUnusedGeneratedTriggerIds(config);
|
||||
cleaned = cleanupRemovedGeneratedTriggerReferences(cleaned, removedIds);
|
||||
preserveAutomationRowKeys(config, cleaned);
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private _selectTriggerIds = (condition: TriggerCondition, ids: string[]) => {
|
||||
const config = this._options.getConfig();
|
||||
if (!config || !this._options.canEdit()) {
|
||||
return;
|
||||
}
|
||||
const triggers = assignGeneratedTriggerIds(
|
||||
config.triggers,
|
||||
this._getTriggerIdOptions(config.triggers),
|
||||
ids
|
||||
);
|
||||
const updated = updateTriggerCondition(
|
||||
config,
|
||||
condition,
|
||||
{ ...condition, id: ids.length ? ids : "" },
|
||||
triggers
|
||||
);
|
||||
// Generated IDs that no condition or action references are no longer in use.
|
||||
// Strip them so the YAML does not accumulate stale identifiers.
|
||||
this._commit(cleanupUnusedGeneratedTriggerIds(updated));
|
||||
};
|
||||
|
||||
private _fixDuplicateTriggerIds = async () => {
|
||||
const localize = this._i18n.value?.localize;
|
||||
if (!this._options.getConfig() || !this._options.canEdit() || !localize) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await showConfirmationDialog(this._host, {
|
||||
title: localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.assign_unique_ids_title"
|
||||
),
|
||||
text: localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.assign_unique_ids_description"
|
||||
),
|
||||
confirmText: localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.duplicate_ids_fix"
|
||||
),
|
||||
dismissText: localize("ui.common.cancel"),
|
||||
});
|
||||
const config = this._options.getConfig();
|
||||
if (confirmed && config && this._options.canEdit()) {
|
||||
this._commit(makeDuplicateTriggerIdsUnique(config));
|
||||
}
|
||||
};
|
||||
|
||||
private _commit(config: AutomationConfig) {
|
||||
const current = this._options.getConfig();
|
||||
if (config === current) {
|
||||
return;
|
||||
}
|
||||
preserveAutomationRowKeys(current, config);
|
||||
this._options.commit(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import { nanoid } from "nanoid";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import {
|
||||
expandConditionWithShorthand,
|
||||
flattenTriggers,
|
||||
} from "../../../../data/automation";
|
||||
import type {
|
||||
AutomationConfig,
|
||||
Condition,
|
||||
LogicalCondition,
|
||||
Trigger,
|
||||
TriggerCondition,
|
||||
} from "../../../../data/automation";
|
||||
import { getActionType } from "../../../../data/script";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import type {
|
||||
Action,
|
||||
ChooseAction,
|
||||
IfAction,
|
||||
Option,
|
||||
ParallelAction,
|
||||
RepeatAction,
|
||||
SequenceAction,
|
||||
} from "../../../../data/script";
|
||||
|
||||
export const GENERATED_TRIGGER_ID_PREFIX = "generated-";
|
||||
|
||||
export interface TriggerIdOption {
|
||||
/** Existing ID, or a candidate that is stored on the trigger when selected. */
|
||||
id: string;
|
||||
trigger: Trigger;
|
||||
/** Zero-based position among flattened leaf triggers; list wrappers are excluded. */
|
||||
index: number;
|
||||
/** The ID is an unstored candidate, not an existing ID already on the trigger. */
|
||||
draft: boolean;
|
||||
/** Another leaf trigger has the same nonempty stored ID. */
|
||||
duplicate: boolean;
|
||||
}
|
||||
|
||||
/** Controller-provided options and operations for editors, sidebars, and row summaries. */
|
||||
export interface AutomationTriggerContext {
|
||||
options: TriggerIdOption[];
|
||||
/** True while the user is editing a `triggered_by` condition; trigger rows show their index badges only in this state. */
|
||||
showIndices: boolean;
|
||||
/** Pass the original condition object so identical conditions remain distinguishable. */
|
||||
select: (condition: TriggerCondition, ids: string[]) => void;
|
||||
/** Requests confirmation before migrating duplicate IDs throughout the automation. */
|
||||
fixDuplicateIds: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const automationTriggerContext =
|
||||
createContext<AutomationTriggerContext>("automationTriggers");
|
||||
|
||||
/**
|
||||
* Identify IDs reserved for automatic UI cleanup by their generated prefix.
|
||||
* IDs without this prefix remain user-owned, including references to missing triggers.
|
||||
*/
|
||||
export const isGeneratedTriggerId = (id: unknown): id is string =>
|
||||
typeof id === "string" && id.startsWith(GENERATED_TRIGGER_ID_PREFIX);
|
||||
|
||||
/** Read a leaf trigger's stored ID; list wrappers do not have selectable IDs. */
|
||||
const getTriggerId = (trigger: Trigger): string | undefined =>
|
||||
isTriggerList(trigger) ? undefined : trigger.id;
|
||||
|
||||
/** Collect each ID that occurs more than once. */
|
||||
const duplicateIds = (ids: string[]) =>
|
||||
new Set(ids.filter((id, index) => ids.indexOf(id) !== index));
|
||||
|
||||
/** Map array items or a truthy scalar, leaving absent or falsy scalar values unchanged. */
|
||||
const mapScalarOrArray = <T>(
|
||||
value: T | T[] | undefined | null,
|
||||
mapper: (item: T) => T
|
||||
): T | T[] | undefined | null => {
|
||||
return Array.isArray(value)
|
||||
? value.map(mapper)
|
||||
: value
|
||||
? mapper(value)
|
||||
: value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate an ID absent from both sets and reserve it in `generatedIds`.
|
||||
* `reservedIds` contains existing IDs that the new ID must not collide with.
|
||||
*/
|
||||
const getGeneratedTriggerId = (
|
||||
reservedIds: Set<string>,
|
||||
generatedIds: Set<string>
|
||||
): string => {
|
||||
let id = `${GENERATED_TRIGGER_ID_PREFIX}${nanoid(4)}`;
|
||||
while (reservedIds.has(id) || generatedIds.has(id)) {
|
||||
id = `${GENERATED_TRIGGER_ID_PREFIX}${nanoid(4)}`;
|
||||
}
|
||||
generatedIds.add(id);
|
||||
return id;
|
||||
};
|
||||
|
||||
/** Collect nonempty stored leaf IDs, including generated IDs and duplicates. */
|
||||
export const getExplicitTriggerIds = (
|
||||
triggers: Trigger | Trigger[] | undefined
|
||||
) =>
|
||||
flattenTriggers(triggers)
|
||||
.map(getTriggerId)
|
||||
.filter((id): id is string => Boolean(id));
|
||||
|
||||
/**
|
||||
* Build selectable options in flattened leaf order without modifying the triggers.
|
||||
* Each call creates fresh candidates for missing or empty IDs. The controller
|
||||
* memoizes the result until the trigger-list reference changes.
|
||||
*/
|
||||
export const getTriggerIdOptions = (
|
||||
triggers: Trigger | Trigger[] | undefined
|
||||
): TriggerIdOption[] => {
|
||||
const explicitIds = getExplicitTriggerIds(triggers);
|
||||
const duplicates = duplicateIds(explicitIds);
|
||||
const reservedIds = new Set(explicitIds);
|
||||
const generatedIds = new Set<string>();
|
||||
|
||||
return flattenTriggers(triggers).map((trigger, index) => {
|
||||
const explicitId = getTriggerId(trigger);
|
||||
return {
|
||||
id: explicitId || getGeneratedTriggerId(reservedIds, generatedIds),
|
||||
trigger,
|
||||
index,
|
||||
draft: !explicitId,
|
||||
duplicate: explicitId ? duplicates.has(explicitId) : false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform leaf triggers recursively, retaining list wrappers and scalar/array shape.
|
||||
* Copy only changed branches; callbacks must return replacements rather than mutate leaves.
|
||||
*/
|
||||
const walkLeafTriggers = (
|
||||
triggers: Trigger | Trigger[],
|
||||
callback: (trigger: Trigger) => Trigger
|
||||
): Trigger | Trigger[] => {
|
||||
if (Array.isArray(triggers)) {
|
||||
let changed = false;
|
||||
const mapped = triggers.map((trigger) => {
|
||||
const result = walkLeafTriggers(trigger, callback) as Trigger;
|
||||
if (result !== trigger) {
|
||||
changed = true;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
return changed ? mapped : triggers;
|
||||
}
|
||||
if (isTriggerList(triggers)) {
|
||||
const newInner = triggers.triggers
|
||||
? (walkLeafTriggers(triggers.triggers, callback) as Trigger | Trigger[])
|
||||
: triggers.triggers;
|
||||
if (newInner === triggers.triggers) {
|
||||
return triggers;
|
||||
}
|
||||
return { ...triggers, triggers: newInner };
|
||||
}
|
||||
return callback(triggers);
|
||||
};
|
||||
|
||||
/** Store only selected candidate IDs, matching options to their original trigger objects. */
|
||||
export const assignGeneratedTriggerIds = (
|
||||
triggers: Trigger | Trigger[],
|
||||
triggerIdOptions: TriggerIdOption[],
|
||||
selectedIds: string | string[]
|
||||
): Trigger | Trigger[] => {
|
||||
const selected = new Set(ensureArray(selectedIds));
|
||||
const assignments = new Map<Trigger, string>();
|
||||
|
||||
triggerIdOptions.forEach((option) => {
|
||||
if (option.draft && selected.has(option.id)) {
|
||||
assignments.set(option.trigger, option.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (!assignments.size) {
|
||||
return triggers;
|
||||
}
|
||||
|
||||
return walkLeafTriggers(triggers, (trigger) =>
|
||||
assignments.has(trigger)
|
||||
? { ...trigger, id: assignments.get(trigger) }
|
||||
: trigger
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace, expand, or remove each referenced ID; `undefined` removes a reference.
|
||||
* Return the original condition when every mapped ID is unchanged. Otherwise
|
||||
* deduplicate IDs and preserve array form, or use it for multiple replacement IDs.
|
||||
* An emptied scalar reference becomes an empty string.
|
||||
*/
|
||||
const mapReferencedTriggerIds = (
|
||||
condition: TriggerCondition,
|
||||
mapper: (id: string) => string | string[] | undefined
|
||||
): TriggerCondition => {
|
||||
let changed = false;
|
||||
const ids = ensureArray(condition.id).flatMap((id) => {
|
||||
const mappedId = mapper(id);
|
||||
if (mappedId !== id) {
|
||||
changed = true;
|
||||
}
|
||||
return mappedId ?? [];
|
||||
});
|
||||
|
||||
if (!changed) {
|
||||
return condition;
|
||||
}
|
||||
|
||||
const uniqueIds = Array.from(new Set(ids));
|
||||
return {
|
||||
...condition,
|
||||
id:
|
||||
Array.isArray(condition.id) || uniqueIds.length > 1
|
||||
? uniqueIds
|
||||
: uniqueIds[0] || "",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Update trigger conditions throughout an automation without mutating its input.
|
||||
* The traversal follows only schema-defined condition/action fields, preserving
|
||||
* shorthand syntax, templates, and arbitrary action data.
|
||||
*/
|
||||
class AutomationTriggerConditionMapper {
|
||||
/** The callback must return a replacement or the original condition without mutating it. */
|
||||
constructor(
|
||||
private _update: (condition: TriggerCondition) => TriggerCondition
|
||||
) {}
|
||||
|
||||
/** Return a new configuration with mapped conditions and actions; leave triggers untouched. */
|
||||
public map(config: AutomationConfig): AutomationConfig {
|
||||
return {
|
||||
...config,
|
||||
conditions: this._mapConditions(config.conditions),
|
||||
actions: this._mapActions(config.actions) as Action | Action[],
|
||||
};
|
||||
}
|
||||
|
||||
/** Map conditions while preserving scalar/array form and an absent value. */
|
||||
private _mapConditions(conditions: Condition | Condition[] | undefined) {
|
||||
return mapScalarOrArray(conditions, this._mapCondition) as
|
||||
Condition | Condition[] | undefined;
|
||||
}
|
||||
|
||||
/** Apply the update to trigger conditions, descending into logical groups. */
|
||||
private _mapCondition = (condition: Condition): Condition => {
|
||||
if (typeof condition !== "object" || condition === null) {
|
||||
return condition;
|
||||
}
|
||||
const expanded = expandConditionWithShorthand(condition);
|
||||
if (expanded !== condition) {
|
||||
const key = Array.isArray(condition.condition)
|
||||
? "condition"
|
||||
: expanded.condition;
|
||||
return {
|
||||
...condition,
|
||||
[key]: this._mapConditions((expanded as LogicalCondition).conditions),
|
||||
};
|
||||
}
|
||||
if (condition.condition === "trigger" && "id" in condition) {
|
||||
return this._update(condition);
|
||||
}
|
||||
if (
|
||||
"conditions" in condition &&
|
||||
(condition.condition === "and" ||
|
||||
condition.condition === "or" ||
|
||||
condition.condition === "not")
|
||||
) {
|
||||
return {
|
||||
...condition,
|
||||
conditions: this._mapConditions(condition.conditions),
|
||||
} as LogicalCondition;
|
||||
}
|
||||
return condition;
|
||||
};
|
||||
|
||||
/** Map actions while preserving scalar/array form and an absent value. */
|
||||
private _mapActions(actions: Action | Action[] | undefined) {
|
||||
return mapScalarOrArray(actions, this._mapAction) as
|
||||
Action | Action[] | undefined;
|
||||
}
|
||||
|
||||
/** Map a choose branch's conditions and sequence, leaving template strings untouched. */
|
||||
private _mapChooseOption = (option: Option): Option => {
|
||||
return {
|
||||
...option,
|
||||
conditions:
|
||||
typeof option.conditions === "string"
|
||||
? option.conditions
|
||||
: (this._mapConditions(option.conditions) as Condition[]),
|
||||
sequence: this._mapActions(option.sequence) as Action | Action[],
|
||||
};
|
||||
};
|
||||
|
||||
/** Map condition actions and nested blocks; return other action types unchanged. */
|
||||
private _mapAction = (action: Action): Action => {
|
||||
switch (getActionType(action)) {
|
||||
case "check_condition":
|
||||
return this._mapCondition(action as Condition) as Action;
|
||||
case "choose": {
|
||||
const result = { ...(action as ChooseAction) };
|
||||
result.choose = mapScalarOrArray(
|
||||
result.choose,
|
||||
this._mapChooseOption
|
||||
) as Option | Option[] | null;
|
||||
if (result.default) {
|
||||
result.default = this._mapActions(result.default);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case "if": {
|
||||
const result = { ...(action as IfAction) };
|
||||
if (typeof result.if !== "string") {
|
||||
result.if = this._mapConditions(result.if) as Condition[];
|
||||
}
|
||||
result.then = this._mapActions(result.then) as Action | Action[];
|
||||
if (result.else) {
|
||||
result.else = this._mapActions(result.else);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case "repeat": {
|
||||
const repeat = { ...(action as RepeatAction).repeat };
|
||||
if ("while" in repeat) {
|
||||
repeat.while = this._mapConditions(repeat.while) as Condition[];
|
||||
}
|
||||
if ("until" in repeat) {
|
||||
repeat.until = this._mapConditions(repeat.until) as Condition[];
|
||||
}
|
||||
repeat.sequence = this._mapActions(repeat.sequence) as
|
||||
Action | Action[];
|
||||
return { ...action, repeat };
|
||||
}
|
||||
case "sequence": {
|
||||
const result = { ...(action as SequenceAction) };
|
||||
result.sequence = this._mapActions(result.sequence as Action[]) as
|
||||
Action[] | null;
|
||||
return result;
|
||||
}
|
||||
case "parallel": {
|
||||
const result = { ...(action as ParallelAction) };
|
||||
result.parallel = this._mapActions(result.parallel as Action[]) as
|
||||
Action | Action[];
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
return action;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a configuration with the supplied triggers and replace the selected condition
|
||||
* by object identity throughout conditions and actions. The controller commits it.
|
||||
*/
|
||||
export const updateTriggerCondition = (
|
||||
config: AutomationConfig,
|
||||
original: TriggerCondition,
|
||||
updated: TriggerCondition,
|
||||
triggers: Trigger | Trigger[]
|
||||
): AutomationConfig => {
|
||||
/** Replace only the selected object, even when another condition has equal values. */
|
||||
const replace = (condition: TriggerCondition) =>
|
||||
condition === original ? updated : condition;
|
||||
return {
|
||||
...new AutomationTriggerConditionMapper(replace).map(config),
|
||||
triggers,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively strip generated IDs from a trigger or trigger list. Manual IDs
|
||||
* are preserved. Trigger-list wrappers keep their structure; their inner
|
||||
* triggers are stripped the same way.
|
||||
*/
|
||||
export const stripGeneratedTriggerIds = (trigger: Trigger): Trigger => {
|
||||
if (isTriggerList(trigger)) {
|
||||
if (!trigger.triggers) {
|
||||
return trigger;
|
||||
}
|
||||
const newInner = Array.isArray(trigger.triggers)
|
||||
? trigger.triggers.map(stripGeneratedTriggerIds)
|
||||
: stripGeneratedTriggerIds(trigger.triggers);
|
||||
if (newInner === trigger.triggers) {
|
||||
return trigger;
|
||||
}
|
||||
return { ...trigger, triggers: newInner };
|
||||
}
|
||||
if (!trigger.id || !isGeneratedTriggerId(trigger.id)) {
|
||||
return trigger;
|
||||
}
|
||||
const { id: _id, ...rest } = trigger;
|
||||
return rest as Trigger;
|
||||
};
|
||||
|
||||
/** Remove generated IDs from triggers that no trigger condition references. */
|
||||
export const cleanupUnusedGeneratedTriggerIds = (
|
||||
config: AutomationConfig
|
||||
): AutomationConfig => {
|
||||
const referencedIds = new Set<string>();
|
||||
const recordRef = (id: string) => {
|
||||
referencedIds.add(id);
|
||||
return id;
|
||||
};
|
||||
new AutomationTriggerConditionMapper((condition) =>
|
||||
mapReferencedTriggerIds(condition, recordRef)
|
||||
).map(config);
|
||||
|
||||
let changed = false;
|
||||
const newTriggers = walkLeafTriggers(config.triggers, (trigger) => {
|
||||
if (
|
||||
isTriggerList(trigger) ||
|
||||
!trigger.id ||
|
||||
!isGeneratedTriggerId(trigger.id) ||
|
||||
referencedIds.has(trigger.id)
|
||||
) {
|
||||
return trigger;
|
||||
}
|
||||
changed = true;
|
||||
const { id: _id, ...rest } = trigger;
|
||||
return rest as Trigger;
|
||||
});
|
||||
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
triggers: newTriggers,
|
||||
};
|
||||
};
|
||||
|
||||
/** Remove references to generated IDs belonging to triggers removed from the config. */
|
||||
export const cleanupRemovedGeneratedTriggerReferences = (
|
||||
config: AutomationConfig,
|
||||
removedIds: Set<string>
|
||||
): AutomationConfig => {
|
||||
if (!removedIds.size) {
|
||||
return config;
|
||||
}
|
||||
let changed = false;
|
||||
const cleaned = new AutomationTriggerConditionMapper((condition) =>
|
||||
mapReferencedTriggerIds(condition, (id) => {
|
||||
if (isGeneratedTriggerId(id) && removedIds.has(id)) {
|
||||
changed = true;
|
||||
return undefined;
|
||||
}
|
||||
return id;
|
||||
})
|
||||
).map(config);
|
||||
return changed ? cleaned : config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign a fresh generated ID to every referenced leaf sharing a stored ID.
|
||||
* Expand trigger-condition references to all replacements for the old ID, preserving
|
||||
* their original "any of these triggers" meaning. Duplicate IDs that no trigger
|
||||
* condition references are stripped instead, so the fix does not create generated
|
||||
* IDs nobody uses. Return the original config if IDs are unique.
|
||||
* Templates and action data that inspect trigger.id directly are not rewritten;
|
||||
* the controller's confirmation dialog warns about that limitation.
|
||||
*/
|
||||
export const makeDuplicateTriggerIdsUnique = (
|
||||
config: AutomationConfig
|
||||
): AutomationConfig => {
|
||||
const ids = getExplicitTriggerIds(config.triggers);
|
||||
const duplicates = duplicateIds(ids);
|
||||
|
||||
if (!duplicates.size) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const referencedIds = new Set<string>();
|
||||
new AutomationTriggerConditionMapper((condition) =>
|
||||
mapReferencedTriggerIds(condition, (id) => {
|
||||
referencedIds.add(id);
|
||||
return id;
|
||||
})
|
||||
).map(config);
|
||||
|
||||
const referencedDuplicates = new Set(
|
||||
[...duplicates].filter((id) => referencedIds.has(id))
|
||||
);
|
||||
const unreferencedDuplicates = new Set(
|
||||
[...duplicates].filter((id) => !referencedIds.has(id))
|
||||
);
|
||||
|
||||
const reservedIds = new Set(ids.filter((id) => !duplicates.has(id)));
|
||||
const generatedIds = new Set<string>();
|
||||
const assignments = new Map<Trigger, string>();
|
||||
const replacementIds = new Map<string, string[]>();
|
||||
|
||||
// A duplicate ID used to mean "any of these triggers". After assigning each
|
||||
// trigger its own ID, references to the old ID must expand to every new ID.
|
||||
flattenTriggers(config.triggers).forEach((trigger) => {
|
||||
const id = getTriggerId(trigger);
|
||||
if (!id || !referencedDuplicates.has(id)) {
|
||||
return;
|
||||
}
|
||||
const generatedId = getGeneratedTriggerId(reservedIds, generatedIds);
|
||||
assignments.set(trigger, generatedId);
|
||||
replacementIds.set(id, [...(replacementIds.get(id) || []), generatedId]);
|
||||
});
|
||||
|
||||
const triggers = walkLeafTriggers(config.triggers, (trigger) => {
|
||||
if (assignments.has(trigger)) {
|
||||
return { ...trigger, id: assignments.get(trigger) };
|
||||
}
|
||||
if (isTriggerList(trigger)) {
|
||||
return trigger;
|
||||
}
|
||||
const id = getTriggerId(trigger);
|
||||
if (id && unreferencedDuplicates.has(id)) {
|
||||
const { id: _id, ...rest } = trigger;
|
||||
return rest as Trigger;
|
||||
}
|
||||
return trigger;
|
||||
}) as Trigger | Trigger[];
|
||||
|
||||
if (!referencedDuplicates.size) {
|
||||
return {
|
||||
...config,
|
||||
triggers,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...new AutomationTriggerConditionMapper((condition) =>
|
||||
mapReferencedTriggerIds(condition, (id) => replacementIds.get(id) ?? id)
|
||||
).map(config),
|
||||
triggers,
|
||||
};
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import { dynamicElement } from "../../../../common/dom/dynamic-element-directive
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { Trigger } from "../../../../data/automation";
|
||||
import {
|
||||
TRIGGER_ROW_CONFIG_KEYS,
|
||||
@@ -35,8 +34,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
|
||||
|
||||
@property({ attribute: false }) public description?: TriggerDescription;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
@@ -46,8 +43,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
|
||||
const yamlMode = this.yamlMode || !this.uiSupported;
|
||||
|
||||
const showId = "id" in this.trigger || this.showId;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -80,20 +75,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${
|
||||
showId && !isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id"
|
||||
)}
|
||||
.value=${this.trigger.id || ""}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._idChanged}
|
||||
></ha-input>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${
|
||||
this.description
|
||||
@@ -116,24 +97,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const newId = (ev.target as any).value;
|
||||
|
||||
if (newId === (this.trigger.id ?? "")) {
|
||||
return;
|
||||
}
|
||||
const value = { ...this.trigger };
|
||||
if (!newId) {
|
||||
delete value.id;
|
||||
} else {
|
||||
value.id = newId;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
@@ -167,9 +130,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-input {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiLinkVariantOff } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-trigger-icon";
|
||||
import type { TriggerCondition } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
automationTriggerContext,
|
||||
type AutomationTriggerContext,
|
||||
} from "./automation-trigger-id";
|
||||
|
||||
@customElement("ha-automation-trigger-references")
|
||||
export class HaAutomationTriggerReferences extends LitElement {
|
||||
@property({ attribute: false }) public condition!: TriggerCondition;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
private _entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: automationTriggerContext, subscribe: true })
|
||||
private _triggers?: AutomationTriggerContext;
|
||||
|
||||
protected render() {
|
||||
const options = this._triggers?.options ?? [];
|
||||
const selectedIds = ensureArray(this.condition.id).filter(Boolean);
|
||||
const selectedTriggers = options.filter((option) =>
|
||||
selectedIds.includes(option.id)
|
||||
);
|
||||
const missingIds = selectedIds.filter(
|
||||
(id) => !options.some((option) => option.id === id)
|
||||
);
|
||||
|
||||
if (options.length && !selectedIds.length) {
|
||||
return html`<span class="trigger-reference no-selection">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers_selected"
|
||||
)}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
return [
|
||||
selectedTriggers.map(
|
||||
(option) => html`
|
||||
<span class="trigger-reference">
|
||||
<span class="trigger-index-badge">${option.index + 1}</span>
|
||||
<ha-trigger-icon
|
||||
.trigger=${"trigger" in option.trigger ? option.trigger.trigger : ""}
|
||||
></ha-trigger-icon>
|
||||
<span class="trigger-reference-label">
|
||||
${capitalizeFirstLetter(
|
||||
describeTrigger(option.trigger, this.hass, this._entityReg)
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
`
|
||||
),
|
||||
missingIds.map(
|
||||
() => html`
|
||||
<span class="trigger-reference missing">
|
||||
<ha-svg-icon .path=${mdiLinkVariantOff}></ha-svg-icon>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.missing_trigger"
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
`
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.trigger-reference {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
.trigger-reference.missing,
|
||||
.trigger-reference.no-selection {
|
||||
background: var(--ha-color-fill-warning-normal-resting);
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
|
||||
ha-trigger-icon,
|
||||
ha-svg-icon {
|
||||
--mdc-icon-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.trigger-reference-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.trigger-index-badge {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px dotted var(--ha-color-border-neutral-normal);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
box-sizing: border-box;
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: 1;
|
||||
text-box-trim: both;
|
||||
text-box-edge: cap alphabetic;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-trigger-references": HaAutomationTriggerReferences;
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
@@ -78,6 +79,10 @@ import { overflowStyles, rowStyles } from "../styles";
|
||||
import { getDeviceTarget } from "../target/get_device_target";
|
||||
import { getEntityTarget } from "../target/get_entity_target";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import {
|
||||
automationTriggerContext,
|
||||
type AutomationTriggerContext,
|
||||
} from "./automation-trigger-id";
|
||||
import "./ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
@@ -137,6 +142,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@property({ type: Boolean }) public highlight?: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
@@ -181,6 +188,10 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: automationTriggerContext, subscribe: true })
|
||||
private _triggers?: AutomationTriggerContext;
|
||||
|
||||
get selected() {
|
||||
return this._selected;
|
||||
}
|
||||
@@ -206,6 +217,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const triggerIndex = this._triggers?.options.find(
|
||||
(option) => option.trigger === this.trigger
|
||||
)?.index;
|
||||
const type = this._getType(this.trigger, this.triggerDescriptions);
|
||||
|
||||
const supported = this._uiSupported(type);
|
||||
@@ -238,19 +252,51 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
${
|
||||
type === "list"
|
||||
? html`<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_ICONS[type]}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-trigger-icon
|
||||
slot="leading-icon"
|
||||
.hass=${this.hass}
|
||||
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
|
||||
></ha-trigger-icon>`
|
||||
}
|
||||
<div slot="leading-icon" class="trigger-leading">
|
||||
${
|
||||
triggerIndex !== undefined
|
||||
? html`
|
||||
<span
|
||||
id="trigger-index-badge-${triggerIndex}"
|
||||
tabindex=${this._triggers?.showIndices ? "0" : "-1"}
|
||||
class="trigger-index-badge ${
|
||||
this._triggers?.showIndices ? "" : "hidden"
|
||||
}"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.trigger_index_aria_label",
|
||||
{ number: triggerIndex + 1 }
|
||||
)}
|
||||
aria-hidden=${this._triggers?.showIndices ? "false" : "true"}
|
||||
>${triggerIndex + 1}</span
|
||||
>
|
||||
${
|
||||
this._triggers?.showIndices
|
||||
? html`<ha-tooltip for="trigger-index-badge-${triggerIndex}"
|
||||
><p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.trigger_index_tooltip"
|
||||
)}
|
||||
</p></ha-tooltip
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
type === "list"
|
||||
? html`<ha-svg-icon
|
||||
class="trigger-icon"
|
||||
.path=${TRIGGER_ICONS[type]}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-trigger-icon
|
||||
.hass=${this.hass}
|
||||
.trigger=${
|
||||
(this.trigger as Exclude<Trigger, TriggerList>).trigger
|
||||
}
|
||||
></ha-trigger-icon>`
|
||||
}
|
||||
</div>
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeTrigger(this.trigger, this.hass, this._entityReg)
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import { stripGeneratedTriggerIds } from "./automation-trigger-id";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
|
||||
@@ -166,7 +168,9 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
private _addTrigger = (value: string, target?: HassServiceTarget) => {
|
||||
let triggers: Trigger[];
|
||||
if (value === PASTE_VALUE) {
|
||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger!));
|
||||
triggers = this.triggers.concat(
|
||||
stripGeneratedTriggerIds(deepClone(this._clipboard!.trigger!))
|
||||
);
|
||||
} else if (isDynamic(value)) {
|
||||
triggers = this.triggers.concat({
|
||||
trigger: getValueFromDynamic(value),
|
||||
@@ -258,6 +262,47 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
});
|
||||
}
|
||||
|
||||
protected override duplicateItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.items.toSpliced(
|
||||
index + 1,
|
||||
0,
|
||||
stripGeneratedTriggerIds(deepClone(this.items[index]))
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
protected override pasteItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.item) return;
|
||||
|
||||
const index = (ev.target as any).index;
|
||||
const clonedItem = stripGeneratedTriggerIds(deepClone(ev.detail.item));
|
||||
|
||||
this.setHighlightedItems(ensureArray(clonedItem));
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.items.toSpliced(index + 1, 0, clonedItem),
|
||||
});
|
||||
}
|
||||
|
||||
protected override insertAfter(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const index = (ev.target as any).index;
|
||||
const inserted = ensureArray(ev.detail.value).map((item) =>
|
||||
stripGeneratedTriggerIds(deepClone(item))
|
||||
);
|
||||
this.setHighlightedItems(inserted);
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.items.toSpliced(index + 1, 0, ...inserted),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = automationRowsStyles;
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -4,7 +4,6 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-check-list-item";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
|
||||
@@ -8,7 +8,6 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import { downloadEmergencyKit } from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
|
||||
@@ -34,7 +34,6 @@ import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type {
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type {
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
import type { ManagerStateEvent } from "../../../data/backup_manager";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
|
||||
|
||||
@@ -22,7 +22,6 @@ import type { CloudStatus } from "../../../data/cloud";
|
||||
import { subscribeConfigEntries } from "../../../data/config_entries";
|
||||
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
mdiVideo,
|
||||
mdiWebhook,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { formatDate } from "../../../../common/datetime/format_date";
|
||||
import { relativeTime } from "../../../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -20,11 +20,13 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import "../../../../components/list/ha-list-nav";
|
||||
import type { BackupConfig } from "../../../../data/backup";
|
||||
import { cloudBackupHealth, cloudBackupEnabled } from "../../../../data/backup";
|
||||
import { cloudBackupEnabled, cloudBackupHealth } from "../../../../data/backup";
|
||||
import type {
|
||||
CloudStatusLoggedIn,
|
||||
SubscriptionInfo,
|
||||
@@ -260,8 +262,8 @@ export class CloudAccountOverview extends LitElement {
|
||||
/>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.email"
|
||||
@@ -283,16 +285,16 @@ export class CloudAccountOverview extends LitElement {
|
||||
@click=${this._toggleEmail}
|
||||
></ha-icon-button>
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.subscription"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">${this._subscriptionDetail()}</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</div>
|
||||
<div class="card-actions split">
|
||||
<ha-button
|
||||
@@ -348,7 +350,11 @@ export class CloudAccountOverview extends LitElement {
|
||||
"ui.panel.config.cloud.account.overview.features_intro"
|
||||
)}
|
||||
</p>
|
||||
<ha-md-list>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.overview.features_title"
|
||||
)}
|
||||
>
|
||||
${this._featureRow(
|
||||
mdiEarth,
|
||||
this.hass.localize(
|
||||
@@ -411,7 +417,7 @@ export class CloudAccountOverview extends LitElement {
|
||||
webhookStatus,
|
||||
"/config/cloud/webhooks"
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -424,12 +430,12 @@ export class CloudAccountOverview extends LitElement {
|
||||
href: string
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<ha-md-list-item type="link" href=${href}>
|
||||
<ha-list-item-button href=${href}>
|
||||
<ha-svg-icon slot="start" .path=${icon}></ha-svg-icon>
|
||||
<span slot="headline">${title}</span>
|
||||
<span slot="supporting-text">${supporting}</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -661,9 +667,6 @@ export class CloudAccountOverview extends LitElement {
|
||||
width: 36px;
|
||||
height: auto;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
.muted {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@@ -692,10 +695,8 @@ export class CloudAccountOverview extends LitElement {
|
||||
.status-dot.disabled {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
.extras-header {
|
||||
display: flex;
|
||||
@@ -721,14 +722,12 @@ export class CloudAccountOverview extends LitElement {
|
||||
padding-inline: var(--ha-space-4);
|
||||
margin-top: 0;
|
||||
}
|
||||
.extras-content ha-md-list {
|
||||
.extras-content ha-list-nav {
|
||||
padding-top: 0;
|
||||
--md-list-item-leading-space: var(--ha-space-4);
|
||||
--md-list-item-trailing-space: var(--ha-space-4);
|
||||
--ha-row-item-padding-inline: var(--ha-space-4);
|
||||
}
|
||||
.extras-content ha-md-list-item {
|
||||
--md-list-item-top-space: var(--ha-space-2);
|
||||
--md-list-item-bottom-space: var(--ha-space-2);
|
||||
.extras-content ha-list-item-button {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
}
|
||||
.extras-content ha-svg-icon[slot="start"],
|
||||
.extras-content ha-icon-next {
|
||||
|
||||
@@ -11,9 +11,9 @@ import { relativeTime } from "../../../../common/datetime/relative_time";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import type { BackupConfig, BackupInfo } from "../../../../data/backup";
|
||||
import {
|
||||
CLOUD_AGENT,
|
||||
@@ -137,8 +137,8 @@ export class CloudBackupPref extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${
|
||||
@@ -159,11 +159,11 @@ export class CloudBackupPref extends LitElement {
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
${
|
||||
cloudAgent
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiHarddisk}
|
||||
@@ -171,8 +171,8 @@ export class CloudBackupPref extends LitElement {
|
||||
<span slot="headline">
|
||||
${bytesToString(cloudAgent.size)}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiShieldLock}
|
||||
@@ -188,11 +188,11 @@ export class CloudBackupPref extends LitElement {
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${
|
||||
@@ -213,8 +213,8 @@ export class CloudBackupPref extends LitElement {
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="filled" href="/config/backup?historyBack=1">
|
||||
@@ -308,15 +308,12 @@ export class CloudBackupPref extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-top-space: var(--ha-space-2);
|
||||
--md-list-item-bottom-space: var(--ha-space-2);
|
||||
--md-list-item-one-line-container-height: 40px;
|
||||
ha-list-item-base {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
--ha-row-item-min-height: 40px;
|
||||
}
|
||||
ha-svg-icon[slot="start"] {
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -4,10 +4,10 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-tip";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
|
||||
import { formatDate } from "../../../../common/datetime/format_date";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
@@ -179,8 +179,8 @@ export class CloudRemotePref extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.external_activation"
|
||||
@@ -196,8 +196,8 @@ export class CloudRemotePref extends LitElement {
|
||||
.checked=${remote_allow_remote_enable}
|
||||
@change=${this._toggleAllowRemoteEnabledChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.certificate_info"
|
||||
@@ -232,8 +232,8 @@ export class CloudRemotePref extends LitElement {
|
||||
"ui.panel.config.cloud.account.remote.more_info"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-tip .hass=${this.hass}>
|
||||
@@ -318,13 +318,8 @@ export class CloudRemotePref extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-tip {
|
||||
max-width: 600px;
|
||||
|
||||
@@ -5,26 +5,26 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-language-picker";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-textarea";
|
||||
import "../../../../components/ha-tip";
|
||||
import "../../../../components/voice-assistant-brand-icon";
|
||||
import type {
|
||||
HaSelectOption,
|
||||
HaSelectSelectEvent,
|
||||
} from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-textarea";
|
||||
import type { HaTextArea } from "../../../../components/ha-textarea";
|
||||
import "../../../../components/ha-tip";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import "../../../../components/voice-assistant-brand-icon";
|
||||
import { showAutomationEditor } from "../../../../data/automation";
|
||||
import type { CloudStatusLoggedIn } from "../../../../data/cloud";
|
||||
import { updateCloudPref } from "../../../../data/cloud";
|
||||
@@ -153,8 +153,8 @@ export class CloudTTSPref extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.default_language"
|
||||
@@ -176,8 +176,8 @@ export class CloudTTSPref extends LitElement {
|
||||
@value-changed=${this._handleLanguageChange}
|
||||
>
|
||||
</ha-language-picker>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.default_voice"
|
||||
@@ -199,8 +199,8 @@ export class CloudTTSPref extends LitElement {
|
||||
}))}
|
||||
>
|
||||
</ha-select>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.tts.selected_voice_id"
|
||||
@@ -215,8 +215,8 @@ export class CloudTTSPref extends LitElement {
|
||||
)}
|
||||
@click=${this._copyVoiceId}
|
||||
></ha-icon-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
<div class="try-tts">
|
||||
<span class="try-heading">
|
||||
${this.hass.localize(
|
||||
@@ -541,13 +541,8 @@ export class CloudTTSPref extends LitElement {
|
||||
gap: var(--ha-space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: var(--ha-space-4);
|
||||
--md-list-item-trailing-space: var(--ha-space-4);
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: var(--ha-space-4);
|
||||
}
|
||||
ha-language-picker,
|
||||
ha-select {
|
||||
|
||||
@@ -6,7 +6,6 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-markdown-element";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-spinner";
|
||||
import { fetchSupportPackage } from "../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
@@ -43,17 +43,14 @@ import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-integrations";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
|
||||
@@ -4,8 +4,10 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import {
|
||||
computeEntityNameList,
|
||||
computeEntitySearchLabels,
|
||||
} from "../../../../common/entity/compute_entity_name_display";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
@@ -65,22 +67,14 @@ export class HaEnergyUpstreamDevicePicker extends LitElement {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
const [entityName, deviceName, parentDeviceName, areaName] =
|
||||
computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "entity" },
|
||||
{ type: "device" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "area" },
|
||||
],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const [areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
return {
|
||||
id: statisticId,
|
||||
@@ -93,13 +87,13 @@ export class HaEnergyUpstreamDevicePicker extends LitElement {
|
||||
),
|
||||
secondary: areaName,
|
||||
stateObj,
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
parentDeviceName: parentDeviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
},
|
||||
search_labels: computeEntitySearchLabels(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,11 @@ import type { CSSResultGroup } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import {
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
} from "../../../../common/entity/compute_entity_name";
|
||||
import {
|
||||
getEntityContext,
|
||||
getEntityEntryContext,
|
||||
} from "../../../../common/entity/context/get_entity_context";
|
||||
computeEntityEntryNameList,
|
||||
computeEntityNameList,
|
||||
type EntityNameItem,
|
||||
} from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
@@ -32,6 +27,13 @@ import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { VacuumSegmentMappingDialogParams } from "./show-dialog-vacuum-segment-mapping";
|
||||
|
||||
const BREADCRUMB_NAME: EntityNameItem[] = [
|
||||
{ type: "area" },
|
||||
{ type: "parent_device" },
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
];
|
||||
|
||||
interface VacuumSegmentMappingState {
|
||||
areaMapping: Record<string, string[]>;
|
||||
}
|
||||
@@ -133,44 +135,27 @@ export class DialogVacuumSegmentMapping
|
||||
|
||||
const stateObj = this.hass.states[this._params.entityId];
|
||||
|
||||
const context = stateObj
|
||||
? getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: this._entry
|
||||
? getEntityEntryContext(
|
||||
this._entry,
|
||||
const breadcrumb = (
|
||||
stateObj
|
||||
? computeEntityNameList(
|
||||
stateObj,
|
||||
BREADCRUMB_NAME,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: this._params.entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const parentDeviceName = context?.parentDevice
|
||||
? computeDeviceName(context.parentDevice)
|
||||
: undefined;
|
||||
const areaName = context?.area ? computeAreaName(context.area) : undefined;
|
||||
|
||||
const breadcrumb = [
|
||||
areaName,
|
||||
parentDeviceName,
|
||||
deviceName,
|
||||
entityName,
|
||||
].filter((v): v is string => Boolean(v));
|
||||
: this._entry
|
||||
? computeEntityEntryNameList(
|
||||
this._entry,
|
||||
BREADCRUMB_NAME,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: [this._params.entityId]
|
||||
).filter((v): v is string => Boolean(v));
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
|
||||
@@ -27,7 +27,6 @@ import "../../../helpers/forms/ha-input_select-form";
|
||||
import "../../../helpers/forms/ha-input_text-form";
|
||||
import "../../../helpers/forms/ha-schedule-form";
|
||||
import "../../../helpers/forms/ha-timer-form";
|
||||
import "../../../voice-assistants/entity-voice-settings";
|
||||
import "../../entity-registry-settings-editor";
|
||||
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
|
||||
import { getDeleteConfirmationText } from "../../get-delete-confirmation-text";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentCopy, mdiRestore } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -6,7 +7,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "@lit/context";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
@@ -19,23 +19,23 @@ import type {
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-color-picker";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button-next";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-labels-picker";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/input/ha-input";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/item/ha-row-item";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import {
|
||||
CAMERA_ORIENTATIONS,
|
||||
CameraEntityFeature,
|
||||
@@ -45,16 +45,16 @@ import {
|
||||
STREAM_TYPE_HLS,
|
||||
updateCameraPrefs,
|
||||
} from "../../../data/camera";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { deleteConfigEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
createConfigFlow,
|
||||
handleConfigFlowStep,
|
||||
} from "../../../data/config_flow";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import type { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import { updateDeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
@@ -627,7 +627,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
${
|
||||
SWITCH_AS_DOMAINS_INVERT.includes(this._switchAsDomain)
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.invert.label"
|
||||
@@ -643,7 +643,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
.checked=${!!this.entry.options?.switch_as_x?.invert}
|
||||
@change=${this._switchAsInvertChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing
|
||||
} `
|
||||
@@ -960,7 +960,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
${
|
||||
this._cameraPrefs
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.stream.preload_stream"
|
||||
@@ -977,8 +977,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._handleCameraPrefsChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.stream.stream_orientation"
|
||||
@@ -1006,103 +1006,97 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
.value=${this._cameraPrefs.orientation.toString()}
|
||||
>
|
||||
</ha-select>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.helperConfigEntry &&
|
||||
this.helperConfigEntry.supports_options &&
|
||||
this.helperConfigEntry.domain !== "switch_as_x"
|
||||
? html`
|
||||
<ha-list-item
|
||||
class="menu-item"
|
||||
twoline
|
||||
hasMeta
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._showOptionsFlow}
|
||||
>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state",
|
||||
{
|
||||
integration: domainToName(
|
||||
this.hass.localize,
|
||||
this.helperConfigEntry.domain
|
||||
),
|
||||
}
|
||||
)}</span
|
||||
<ha-list-base>
|
||||
${
|
||||
this.helperConfigEntry &&
|
||||
this.helperConfigEntry.supports_options &&
|
||||
this.helperConfigEntry.domain !== "switch_as_x"
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
class="menu-item"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._showOptionsFlow}
|
||||
>
|
||||
<span slot="secondary"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state_secondary",
|
||||
{
|
||||
integration: domainToName(
|
||||
this.hass.localize,
|
||||
this.helperConfigEntry.domain
|
||||
),
|
||||
}
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-list-item
|
||||
class="menu-item"
|
||||
twoline
|
||||
hasMeta
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleVoiceAssistantsClicked}
|
||||
>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.voice_assistants"
|
||||
)}</span
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state",
|
||||
{
|
||||
integration: domainToName(
|
||||
this.hass.localize,
|
||||
this.helperConfigEntry.domain
|
||||
),
|
||||
}
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.configure_state_secondary",
|
||||
{
|
||||
integration: domainToName(
|
||||
this.hass.localize,
|
||||
this.helperConfigEntry.domain
|
||||
),
|
||||
}
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-list-item-button
|
||||
class="menu-item"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleVoiceAssistantsClicked}
|
||||
>
|
||||
<span slot="secondary">
|
||||
${
|
||||
this.entry.aliases.filter((a) => a !== null).length
|
||||
? this.entry.aliases
|
||||
.filter((a): a is string => a !== null)
|
||||
.join(", ")
|
||||
: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||
)
|
||||
}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
|
||||
${
|
||||
domain === "vacuum" &&
|
||||
stateObj &&
|
||||
supportsFeature(stateObj, VacuumEntityFeature.CLEAN_AREA)
|
||||
? html`
|
||||
<ha-list-item
|
||||
class="menu-item"
|
||||
twoline
|
||||
hasMeta
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleVacuumSegmentMappingClicked}
|
||||
>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.vacuum_segment_mapping.title"
|
||||
)}</span
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.voice_assistants"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
${
|
||||
this.entry.aliases.filter((a) => a !== null).length
|
||||
? this.entry.aliases
|
||||
.filter((a): a is string => a !== null)
|
||||
.join(", ")
|
||||
: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||
)
|
||||
}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
${
|
||||
domain === "vacuum" &&
|
||||
stateObj &&
|
||||
supportsFeature(stateObj, VacuumEntityFeature.CLEAN_AREA)
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
class="menu-item"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleVacuumSegmentMappingClicked}
|
||||
>
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.vacuum_segment_mapping.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.vacuum_segment_mapping.title"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.vacuum_segment_mapping.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-base>
|
||||
${
|
||||
this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
@@ -1120,7 +1114,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_label"
|
||||
@@ -1143,7 +1137,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
@change=${this._enabledChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
|
||||
${
|
||||
this._hiddenBy && this._hiddenBy !== "user"
|
||||
@@ -1160,7 +1154,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.visible_label"
|
||||
@@ -1177,12 +1171,12 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
.disabled=${this.disabled || this._disabledBy}
|
||||
@change=${this._hiddenChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
|
||||
${
|
||||
this.entry.device_id
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.use_device_area"
|
||||
@@ -1218,7 +1212,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._useDeviceAreaChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
${
|
||||
this._areaId || this._noDeviceArea
|
||||
? html`<ha-area-picker
|
||||
@@ -1811,7 +1805,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
ha-md-list-item ha-select {
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-row-item ha-select {
|
||||
width: auto;
|
||||
}
|
||||
ha-input,
|
||||
@@ -1823,11 +1820,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
.menu-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
overflow: hidden;
|
||||
--mdc-list-side-padding: 13px;
|
||||
}
|
||||
.entityId {
|
||||
direction: ltr;
|
||||
|
||||
@@ -54,7 +54,6 @@ import type {
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -69,7 +68,6 @@ import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
|
||||
@@ -9,15 +9,16 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { round } from "../../../common/number/round";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import { sanitizeHttpUrl } from "../../../common/url/sanitize-http-url";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/chart/ha-chart-base";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-nav";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { subscribeConfigEntries } from "../../../data/config_entries";
|
||||
import type {
|
||||
@@ -305,24 +306,29 @@ class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
|
||||
${
|
||||
documentationURL
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
.href=${documentationURL}
|
||||
type="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation"
|
||||
)}</span
|
||||
<ha-list-item-button
|
||||
.href=${documentationURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.hardware.documentation_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
@@ -140,8 +140,13 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
this._domain = params.domain;
|
||||
this._item = undefined;
|
||||
if (this._domain && this._domain in HELPERS) {
|
||||
await HELPERS[this._domain].import();
|
||||
this._initDirtyTracking({ type: "deep" }, undefined);
|
||||
this._loading = true;
|
||||
try {
|
||||
await HELPERS[this._domain].import();
|
||||
this._initDirtyTracking({ type: "deep" }, undefined);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
this._open = true;
|
||||
await this.updateComplete;
|
||||
@@ -185,7 +190,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
let content: TemplateResult;
|
||||
let footer: TemplateResult | typeof nothing = nothing;
|
||||
|
||||
if (this._domain) {
|
||||
if (this._domain && !this._loading) {
|
||||
content = html`
|
||||
<div class="form" @value-changed=${this._valueChanged}>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
|
||||
@@ -48,7 +48,6 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-filter-categories";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-entities";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
|
||||
@@ -20,7 +20,6 @@ import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-icon-button-prev";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import { ERROR_STATES, RECOVERABLE_STATES } from "../../../data/config_entries";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { PickConfigEntryDialogParams } from "./show-pick-config-entry-dialog";
|
||||
@@ -43,21 +43,22 @@ export class DialogPickConfigEntry extends LitElement {
|
||||
header-title=${title}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-md-list>
|
||||
<ha-list-base>
|
||||
${this._params.configEntries.map(
|
||||
(entry) =>
|
||||
html`<ha-md-list-item
|
||||
type="button"
|
||||
html`<ha-list-item-button
|
||||
@click=${this._itemPicked}
|
||||
.entry=${entry}
|
||||
.disabled=${
|
||||
!ERROR_STATES.includes(entry.state) &&
|
||||
!RECOVERABLE_STATES.includes(entry.state)
|
||||
}
|
||||
>${entry.title}</ha-md-list-item
|
||||
><span slot="headline"
|
||||
>${entry.title}</span
|
||||
></ha-list-item-button
|
||||
>`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../lovelace/custom-card-helpers";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import "./ha-config-sub-entry-row";
|
||||
|
||||
@customElement("ha-config-entry-device-row")
|
||||
class HaConfigEntryDeviceRow extends LitElement {
|
||||
|
||||
@@ -68,7 +68,6 @@ import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { multiTermSearch } from "../../../resources/fuseMultiTerm";
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from "../../../data/config_flow";
|
||||
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import { ChildPanelReady } from "../../../layouts/panel-ready";
|
||||
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||
|
||||
+20
-26
@@ -4,15 +4,17 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import { stopKeydownEnterSpacePropagation } from "../../../../../common/dom/stop_propagation";
|
||||
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type {
|
||||
BluetoothAllocationsData,
|
||||
BluetoothScannerState,
|
||||
@@ -191,7 +193,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card>
|
||||
<ha-md-list>
|
||||
<ha-list-base>
|
||||
${this._renderAdaptersList(enabledDevices)}
|
||||
${disabledDevices.map(({ device, entry }) => {
|
||||
const areaName =
|
||||
@@ -205,7 +207,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
return html`
|
||||
<ha-md-list-item class="disabled">
|
||||
<ha-list-item-base class="disabled">
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${computeDeviceName(device) || entry.title}
|
||||
@@ -218,12 +220,12 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
})}
|
||||
${disabledEntriesWithoutDevice.map(
|
||||
(entry) => html`
|
||||
<ha-md-list-item class="disabled">
|
||||
<ha-list-item-base class="disabled">
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">${entry.title}</div>
|
||||
<div slot="supporting-text">
|
||||
@@ -238,10 +240,10 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -262,13 +264,13 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
devices: { device: DeviceRegistryEntry; entry: ConfigEntry }[]
|
||||
) {
|
||||
if (devices.length === 0) {
|
||||
return html`<ha-md-list-item>
|
||||
return html`<ha-list-item-base>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_scanner_state_available"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-list-item>`;
|
||||
</ha-list-item-base>`;
|
||||
}
|
||||
|
||||
return devices.map(({ device, entry }) => {
|
||||
@@ -314,8 +316,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
<ha-list-item-button
|
||||
href=${`/config/bluetooth/connection-monitor?source=${btAddress}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
@@ -328,6 +329,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
.path=${mdiCogOutline}
|
||||
.entry=${entry}
|
||||
@click=${this._openOptionFlow}
|
||||
@keydown=${stopKeydownEnterSpacePropagation}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.option_flow"
|
||||
)}
|
||||
@@ -335,7 +337,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${
|
||||
hasMismatch && scannerDetails
|
||||
? this._renderScannerMismatchWarning(
|
||||
@@ -499,22 +501,14 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
|
||||
ha-md-list-item ha-svg-icon[slot="start"] {
|
||||
ha-list-item-button ha-svg-icon[slot="start"],
|
||||
ha-list-item-base ha-svg-icon[slot="start"] {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-md-list-item.disabled ha-svg-icon[slot="start"],
|
||||
ha-md-list-item.disabled [slot="headline"],
|
||||
ha-md-list-item.disabled [slot="supporting-text"] {
|
||||
ha-list-item-base.disabled ha-svg-icon[slot="start"],
|
||||
ha-list-item-base.disabled [slot="headline"],
|
||||
ha-list-item-base.disabled [slot="supporting-text"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
+15
-21
@@ -7,16 +7,16 @@ import {
|
||||
mdiLinkVariant,
|
||||
mdiVectorPolyline,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-nav";
|
||||
import type {
|
||||
BluetoothAllocationsData,
|
||||
BluetoothDeviceData,
|
||||
@@ -213,11 +213,12 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
</ha-button>
|
||||
</div>
|
||||
<div class="card-content network-card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/bluetooth/adapter-info"
|
||||
>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.my_network"
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button href="/config/bluetooth/adapter-info">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAccessPoint}
|
||||
@@ -229,10 +230,9 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
<ha-list-item-button
|
||||
href="/config/bluetooth/connection-monitor"
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -246,10 +246,9 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
<ha-list-item-button
|
||||
href="/config/bluetooth/advertisement-monitor"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBroadcast}></ha-svg-icon>
|
||||
@@ -260,8 +259,8 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
@@ -286,11 +285,6 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.network-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
-2
@@ -9,8 +9,6 @@ import { extractSearchParamsObject } from "../../../../../common/url/search-para
|
||||
import type { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table";
|
||||
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-metric";
|
||||
import "../../../../../components/ha-relative-time";
|
||||
import type {
|
||||
BluetoothAllocationsData,
|
||||
BluetoothConnectionData,
|
||||
|
||||
+10
-11
@@ -9,9 +9,9 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-nav";
|
||||
import type { InfraredDevice } from "../../../../../data/infrared";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
@@ -82,8 +82,12 @@ export class InfraredConfigDashboard extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.infrared.my_devices")}
|
||||
</div>
|
||||
<div class="card-content network-card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item type="link" href="/config/infrared/devices">
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.infrared.my_devices"
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button href="/config/infrared/devices">
|
||||
<ha-svg-icon slot="start" .path=${mdiRemote}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
@@ -92,8 +96,8 @@ export class InfraredConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
@@ -118,11 +122,6 @@ export class InfraredConfigDashboard extends LitElement {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.network-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
+11
-10
@@ -2,25 +2,26 @@ import { mdiDelete, mdiLock, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { stopKeydownEnterSpacePropagation } from "../../../../../common/dom/stop_propagation";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type {
|
||||
MatterLockInfo,
|
||||
MatterLockUser,
|
||||
} from "../../../../../data/matter-lock";
|
||||
import {
|
||||
clearMatterLockUser,
|
||||
getMatterLockInfo,
|
||||
getMatterLockUsers,
|
||||
clearMatterLockUser,
|
||||
} from "../../../../../data/matter-lock";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -162,11 +163,10 @@ class DialogMatterLockManage extends LitElement {
|
||||
)}
|
||||
</p>`
|
||||
: html`
|
||||
<ha-md-list>
|
||||
<ha-list-base>
|
||||
${occupiedUsers.map(
|
||||
(user) => html`
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
<ha-list-item-button
|
||||
.user=${user}
|
||||
@click=${this._handleUserClick}
|
||||
>
|
||||
@@ -191,11 +191,12 @@ class DialogMatterLockManage extends LitElement {
|
||||
.path=${mdiDelete}
|
||||
.user=${user}
|
||||
@click=${this._handleDeleteUserClick}
|
||||
@keydown=${stopKeydownEnterSpacePropagation}
|
||||
></ha-icon-button>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
`
|
||||
}
|
||||
${
|
||||
|
||||
+10
-31
@@ -3,8 +3,8 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../../common/dom/fire_event";
|
||||
import "../../../../../../components/ha-icon-next";
|
||||
import "../../../../../../components/ha-md-list";
|
||||
import "../../../../../../components/ha-md-list-item";
|
||||
import "../../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../../components/list/ha-list-base";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import type { MatterAddDeviceStep } from "../dialog-matter-add-device";
|
||||
import { sharedStyles } from "./matter-add-device-shared-styles";
|
||||
@@ -23,14 +23,8 @@ class MatterAddDeviceExisting extends LitElement {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.step=${"google_home"}
|
||||
@click=${this._onItemClick}
|
||||
@keydown=${this._onItemClick}
|
||||
>
|
||||
<ha-list-base>
|
||||
<ha-list-item-button .step=${"google_home"} @click=${this._onItemClick}>
|
||||
<img
|
||||
src="/static/images/logo_google_home.png"
|
||||
alt=""
|
||||
@@ -43,14 +37,8 @@ class MatterAddDeviceExisting extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.step=${"apple_home"}
|
||||
@click=${this._onItemClick}
|
||||
@keydown=${this._onItemClick}
|
||||
>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button .step=${"apple_home"} @click=${this._onItemClick}>
|
||||
<img
|
||||
src="/static/images/logo_apple_home.png"
|
||||
alt=""
|
||||
@@ -63,14 +51,8 @@ class MatterAddDeviceExisting extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.step=${"generic"}
|
||||
@click=${this._onItemClick}
|
||||
@keydown=${this._onItemClick}
|
||||
>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button .step=${"generic"} @click=${this._onItemClick}>
|
||||
<div class="logo" slot="start">
|
||||
<ha-svg-icon path=${mdiHomeAutomation}></ha-svg-icon>
|
||||
</div>
|
||||
@@ -80,15 +62,12 @@ class MatterAddDeviceExisting extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onItemClick(ev) {
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
const item = ev.currentTarget as any;
|
||||
const step = item.step as MatterAddDeviceStep;
|
||||
fireEvent(this, "step-selected", { step });
|
||||
|
||||
+8
-23
@@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../../common/dom/fire_event";
|
||||
import "../../../../../../components/ha-icon-next";
|
||||
import "../../../../../../components/ha-md-list-item";
|
||||
import "../../../../../../components/ha-md-list";
|
||||
import "../../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../../components/list/ha-list-base";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { sharedStyles } from "./matter-add-device-shared-styles";
|
||||
|
||||
@@ -18,14 +18,8 @@ class MatterAddDeviceMain extends LitElement {
|
||||
${this.hass.localize(`ui.dialogs.matter-add-device.main.question`)}
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.step=${"new"}
|
||||
@click=${this._onItemClick}
|
||||
@keydown=${this._onItemClick}
|
||||
>
|
||||
<ha-list-base>
|
||||
<ha-list-item-button .step=${"new"} @click=${this._onItemClick}>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.matter-add-device.main.answer_new`
|
||||
@@ -37,14 +31,8 @@ class MatterAddDeviceMain extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.step=${"existing"}
|
||||
@click=${this._onItemClick}
|
||||
@keydown=${this._onItemClick}
|
||||
>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button .step=${"existing"} @click=${this._onItemClick}>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.matter-add-device.main.answer_existing`
|
||||
@@ -56,15 +44,12 @@ class MatterAddDeviceMain extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onItemClick(ev) {
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
const item = ev.currentTarget as any;
|
||||
const step = item.step as "new" | "existing";
|
||||
fireEvent(this, "step-selected", { step });
|
||||
|
||||
+2
-3
@@ -23,10 +23,9 @@ export const sharedStyles = css`
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
ha-md-list {
|
||||
ha-list-base {
|
||||
padding: 0;
|
||||
--md-list-item-leading-space: var(--horizontal-padding, 16px);
|
||||
--md-list-item-trailing-space: var(--horizontal-padding, 16px);
|
||||
--ha-row-item-padding-inline: var(--horizontal-padding, 16px);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-input {
|
||||
|
||||
+22
-25
@@ -16,9 +16,9 @@ import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-nav";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import { fetchMatterNetworkTopology } from "../../../../../data/matter";
|
||||
@@ -167,9 +167,12 @@ export class MatterConfigDashboard extends LitElement {
|
||||
</ha-button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.my_network_title"
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
@@ -180,9 +183,8 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
@@ -193,8 +195,8 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -204,8 +206,12 @@ export class MatterConfigDashboard extends LitElement {
|
||||
return html`
|
||||
<ha-card class="nav-card">
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item type="link" href="/config/matter/options">
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.options_title"
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button href="/config/matter/options">
|
||||
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
@@ -218,10 +224,10 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "thread")
|
||||
? html`<ha-md-list-item type="link" href="/config/thread">
|
||||
? html`<ha-list-item-button href="/config/thread">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${THREAD_ICON}
|
||||
@@ -237,10 +243,10 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>`
|
||||
</ha-list-item-button>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-list>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -307,15 +313,6 @@ export class MatterConfigDashboard extends LitElement {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
|
||||
.network-status div.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/item/ha-row-item";
|
||||
import {
|
||||
acceptSharedMatterDevice,
|
||||
canCommissionMatterExternal,
|
||||
@@ -55,122 +54,120 @@ class MatterOptionsPage extends LitElement {
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-list>
|
||||
${
|
||||
canCommissionMatterExternal(this.hass)
|
||||
? html`<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._startMobileCommissioning}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._commission}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._acceptSharedDevice}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._setWifi}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._setThread}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
${
|
||||
canCommissionMatterExternal(this.hass)
|
||||
? html`<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._startMobileCommissioning}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._commission}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._acceptSharedDevice}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._setWifi}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="s"
|
||||
@click=${this._setThread}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
@@ -323,15 +320,6 @@ class MatterOptionsPage extends LitElement {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user