mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-02 22:41:47 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6d5290afb | |||
| 680cc8b564 | |||
| 87395f83b4 | |||
| db3732aa31 | |||
| 10751ea4e9 | |||
| e8a2ddbb45 | |||
| e072a66bf0 | |||
| a1305ba8fe | |||
| b73732acdb | |||
| d950514104 | |||
| f37cf1e848 | |||
| a188ef1b7a | |||
| 087ef159df | |||
| e39e1b3f5b | |||
| ff583d2274 | |||
| d4de29e073 | |||
| 97dfed0cc4 | |||
| 8b3df752da | |||
| 8c0d547962 | |||
| 5e3d84f0ad | |||
| b4e30bdf63 | |||
| 4fcae4231c | |||
| 2aecf33955 | |||
| 5f26a2b3da | |||
| b08f5bcb34 | |||
| c329e5b827 | |||
| 97f591337d | |||
| e6e6e75f73 | |||
| ff334de0ca | |||
| 8dbe97b480 | |||
| 7bea54851d | |||
| 9298e00f20 | |||
| 70085d4bad | |||
| d83a553b62 | |||
| cab5c6af30 | |||
| d44d8a6dbd | |||
| 3cf1d94b92 | |||
| 9f5f849e32 | |||
| 27e9926363 | |||
| efe734892a | |||
| b3d79e312d | |||
| ecfef9e112 | |||
| ca960446f0 | |||
| a6eb722025 | |||
| f3ff01ace4 | |||
| d5e1a373ec | |||
| e1b9a1a185 | |||
| efe8eaa941 | |||
| 5856196ef3 | |||
| 2671a8c64b | |||
| 8620653a54 | |||
| c4f4cbd323 | |||
| 2e0df00f0f | |||
| ce02f8072d | |||
| c973aa7516 | |||
| 1e2328707c | |||
| 56368b88cd | |||
| fcd4f177c1 | |||
| 7423ae7316 | |||
| 4427c581f1 | |||
| cf86bb9821 | |||
| 897802dc16 | |||
| 95edd6c2c2 | |||
| dd65173c5a | |||
| cf26753f7d | |||
| d6ab8ffb16 | |||
| 2dc4b16eac | |||
| 1eba765bc2 | |||
| 398479ddd7 | |||
| c4fd7bb3e1 | |||
| 4cfc67a95e | |||
| e38d1964ca | |||
| ec8b5c77bd | |||
| 425f2775e2 | |||
| 3a3d8191a3 | |||
| 04fca68549 | |||
| 3046f3e47d | |||
| 35601a0900 | |||
| e7016c15af | |||
| 624521e30b | |||
| 4876bfa639 | |||
| 5dea0764b2 | |||
| 121ed7ac1f |
@@ -62,10 +62,11 @@ host reflects `aria-multiselectable`.
|
||||
|
||||
**Events**
|
||||
|
||||
- `ha-list-selected` — selection changed. Detail
|
||||
`{ index: number | Set<number>, diff: { added: Set<number>, removed: Set<number> } }`.
|
||||
`index` is a `number` in single mode (`-1` when nothing selected) and a
|
||||
`Set<number>` in multi mode.
|
||||
- `ha-list-item-selected` — an option was selected. Detail is the option's
|
||||
index (`number`). In single mode this is the only selection event; in multi
|
||||
mode it fires for each option added to the selection.
|
||||
- `ha-list-item-deselected` — an option was deselected (multi mode only). Detail
|
||||
is the option's index (`number`).
|
||||
|
||||
**Methods / getters**
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import "../../../../src/components/item/ha-list-item-option";
|
||||
import "../../../../src/components/list/ha-list-base";
|
||||
import "../../../../src/components/list/ha-list-nav";
|
||||
import "../../../../src/components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../src/components/list/types";
|
||||
|
||||
type Appearance = "line" | "checkbox";
|
||||
type Position = "start" | "end";
|
||||
@@ -185,7 +184,7 @@ export class DemoHaList extends LitElement {
|
||||
<ha-card header="Single select, appearance=line">
|
||||
<ha-list-selectable
|
||||
aria-label="Single select"
|
||||
@ha-list-selected=${this._onSingle}
|
||||
@ha-list-item-selected=${this._onSingle}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -205,7 +204,8 @@ export class DemoHaList extends LitElement {
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi select line"
|
||||
@ha-list-selected=${this._onMultiLine}
|
||||
@ha-list-item-selected=${this._onMultiLineSelected}
|
||||
@ha-list-item-deselected=${this._onMultiLineDeselected}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -227,7 +227,8 @@ export class DemoHaList extends LitElement {
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox start"
|
||||
@ha-list-selected=${this._onMultiCheckStart}
|
||||
@ha-list-item-selected=${this._onMultiCheckStartSelected}
|
||||
@ha-list-item-deselected=${this._onMultiCheckStartDeselected}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -253,7 +254,8 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox end"
|
||||
@ha-list-selected=${this._onMultiCheckEnd}
|
||||
@ha-list-item-selected=${this._onMultiCheckEndSelected}
|
||||
@ha-list-item-deselected=${this._onMultiCheckEndDeselected}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
@@ -347,20 +349,58 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre
|
||||
this._buttonClicks++;
|
||||
};
|
||||
|
||||
private _onSingle = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._single = ev.detail.index;
|
||||
private _withIndex(
|
||||
value: number | Set<number>,
|
||||
index: number,
|
||||
selected: boolean
|
||||
): Set<number> {
|
||||
const next = new Set(value instanceof Set ? value : []);
|
||||
if (selected) {
|
||||
next.add(index);
|
||||
} else {
|
||||
next.delete(index);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private _onSingle = (ev: CustomEvent<number>) => {
|
||||
this._single = ev.detail;
|
||||
};
|
||||
|
||||
private _onMultiLine = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiLine = ev.detail.index;
|
||||
private _onMultiLineSelected = (ev: CustomEvent<number>) => {
|
||||
this._multiLine = this._withIndex(this._multiLine, ev.detail, true);
|
||||
};
|
||||
|
||||
private _onMultiCheckStart = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckStart = ev.detail.index;
|
||||
private _onMultiLineDeselected = (ev: CustomEvent<number>) => {
|
||||
this._multiLine = this._withIndex(this._multiLine, ev.detail, false);
|
||||
};
|
||||
|
||||
private _onMultiCheckEnd = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckEnd = ev.detail.index;
|
||||
private _onMultiCheckStartSelected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckStart = this._withIndex(
|
||||
this._multiCheckStart,
|
||||
ev.detail,
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
private _onMultiCheckStartDeselected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckStart = this._withIndex(
|
||||
this._multiCheckStart,
|
||||
ev.detail,
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
private _onMultiCheckEndSelected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckEnd = this._withIndex(this._multiCheckEnd, ev.detail, true);
|
||||
};
|
||||
|
||||
private _onMultiCheckEndDeselected = (ev: CustomEvent<number>) => {
|
||||
this._multiCheckEnd = this._withIndex(
|
||||
this._multiCheckEnd,
|
||||
ev.detail,
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260429.0"
|
||||
version = "20260527.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -12,6 +13,7 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@@ -19,6 +21,8 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -27,6 +31,9 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -956,29 +956,11 @@ export class HaChartBase extends LitElement {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
| undefined;
|
||||
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
||||
| YAXisOption
|
||||
| undefined;
|
||||
const series = ensureArray(this.data).map((s) => {
|
||||
const data = this._hiddenDatasets.has(String(s.id ?? s.name))
|
||||
? undefined
|
||||
: s.data;
|
||||
if (data && s.type === "line") {
|
||||
if (yAxis?.type === "log") {
|
||||
// set <=0 values to null so they render as gaps on a log graph
|
||||
return {
|
||||
...s,
|
||||
data: (data as LineSeriesOption["data"])!.map((v) =>
|
||||
Array.isArray(v)
|
||||
? [
|
||||
v[0],
|
||||
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
|
||||
...v.slice(2),
|
||||
]
|
||||
: v
|
||||
),
|
||||
};
|
||||
}
|
||||
if (s.sampling === "minmax") {
|
||||
const minX = xAxis?.min
|
||||
? xAxis.min instanceof Date
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
@@ -39,7 +40,9 @@ class HaAlert extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize?: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@@ -68,7 +71,7 @@ class HaAlert extends LitElement {
|
||||
${this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismissClicked}
|
||||
.label=${this.localize!("ui.common.dismiss_alert")}
|
||||
.label=${this._localize?.("ui.common.dismiss_alert")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
|
||||
@@ -20,6 +20,7 @@ export class HaCheckListItem extends CheckListItemBase {
|
||||
separateCheckboxClick = false;
|
||||
|
||||
async onChange(event) {
|
||||
event.stopPropagation();
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ export class HaExpansionPanel extends LitElement {
|
||||
}
|
||||
const newExpanded = !this.expanded;
|
||||
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
||||
|
||||
this._container.style.overflow = "hidden";
|
||||
|
||||
if (newExpanded) {
|
||||
|
||||
+136
-132
@@ -1,5 +1,5 @@
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -9,14 +9,18 @@ import { stringCompare } from "../common/string/compare";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
import "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListSelectableVirtualized } from "./list/ha-list-selectable-virtualized";
|
||||
import type { HaListVirtualizedItem } from "./list/ha-list-virtualized";
|
||||
|
||||
interface HaFilterDevicesItem extends HaListVirtualizedItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@@ -34,15 +38,12 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
@query("ha-list-selectable-virtualized")
|
||||
private _listElement?: HaListSelectableVirtualized;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("value") &&
|
||||
!deepEqual(this.value, properties.get("value"))
|
||||
@@ -51,6 +52,20 @@ export class HaFilterDevices extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded || !this._listElement) {
|
||||
return;
|
||||
}
|
||||
this._listElement.style.height = `${this.clientHeight - 49 - 4 - 38}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 38px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -66,6 +81,7 @@ export class HaFilterDevices extends LitElement {
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
@keydown=${this._handleClearFilterKeydown}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -74,75 +90,45 @@ export class HaFilterDevices extends LitElement {
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleSearchKeydown}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(
|
||||
this.hass.devices,
|
||||
this._filter || "",
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderItem}
|
||||
@click=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</ha-list>`
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${this._devices(this.hass.devices, this._filter || "")}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
></ha-list-selectable-virtualized>`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _keyFunction = (device) => device?.id;
|
||||
|
||||
private _renderItem = (device) =>
|
||||
!device
|
||||
private _renderItem = (item?: HaFilterDevicesItem) =>
|
||||
!item
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
tabindex="0"
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
: html`<ha-list-item-option
|
||||
style="width: 100%;"
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${item.id}
|
||||
.selected=${this.value?.includes(item.id) ?? false}
|
||||
>
|
||||
${computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)}
|
||||
</ha-check-list-item>`;
|
||||
<span slot="headline">${item.name}</span>
|
||||
</ha-list-item-option>`;
|
||||
|
||||
private _handleItemKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleItemClick(ev);
|
||||
}
|
||||
private _handleAdded(ev: CustomEvent<number>) {
|
||||
this.value = [
|
||||
...(this.value ?? []),
|
||||
this._devices(this.hass.devices, this._filter || "")[ev.detail].id,
|
||||
];
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
listItem.selected = this.value?.includes(value);
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
private _handleRemoved(ev: CustomEvent<number>) {
|
||||
const id = this._devices(this.hass.devices, this._filter || "")[ev.detail]
|
||||
.id;
|
||||
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
@@ -155,30 +141,38 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
this._filter = target.value ?? "";
|
||||
}
|
||||
|
||||
private _handleSearchKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "ArrowDown" && this._listElement) {
|
||||
ev.preventDefault();
|
||||
this._listElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(devices: HomeAssistant["devices"], filter: string, _value) => {
|
||||
(
|
||||
devices: HomeAssistant["devices"],
|
||||
filter: string
|
||||
): HaFilterDevicesItem[] => {
|
||||
const values = Object.values(devices);
|
||||
return values
|
||||
.map((device) => ({
|
||||
id: device.id,
|
||||
interactive: true,
|
||||
name: computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
),
|
||||
}))
|
||||
.filter(
|
||||
(device) =>
|
||||
!filter ||
|
||||
computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(filter)
|
||||
({ name }) =>
|
||||
!filter || name.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
|
||||
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
|
||||
this.hass.locale.language
|
||||
)
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -217,6 +211,13 @@ export class HaFilterDevices extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClearFilterKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._clearFilter(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
@@ -224,58 +225,61 @@ export class HaFilterDevices extends LitElement {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
this._listElement?.clearSelection();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
static styles = css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-check-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
:host([expanded]) ha-expansion-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable-virtualized {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -23,7 +23,6 @@ import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "./list/types";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@@ -42,7 +41,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@query("ha-list-selectable") private _list?: HTMLElement;
|
||||
@query("ha-list-selectable") private _list?: HaListSelectable;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
@@ -75,6 +74,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
@keydown=${this._handleClearFilterKeydown}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -83,7 +83,8 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
<ha-list-selectable
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
@ha-list-selected=${this._handleListChanged}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.areas.caption"
|
||||
)}
|
||||
@@ -163,46 +164,47 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleListChanged(ev: CustomEvent<HaListSelectedDetail>) {
|
||||
if (!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size) {
|
||||
private _handleAdded(ev: CustomEvent<number>) {
|
||||
if (!this.value) {
|
||||
this.value = {};
|
||||
}
|
||||
|
||||
const addedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
ev.detail
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
if (!addedItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.detail.diff?.added.size) {
|
||||
const addedIndex = ev.detail.diff.added.values().next().value;
|
||||
if (addedIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
const addedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
addedIndex
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
this.value = {
|
||||
...this.value,
|
||||
[addedItem.type]: [
|
||||
...(this.value[addedItem.type] || []),
|
||||
addedItem.value,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.value) {
|
||||
this.value = {};
|
||||
}
|
||||
this.value = {
|
||||
...this.value,
|
||||
[addedItem.type]: [
|
||||
...(this.value[addedItem.type] || []),
|
||||
addedItem.value,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const removedIndex = ev.detail.diff?.removed.values().next().value;
|
||||
if (removedIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
const removedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
removedIndex
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
this.value = {
|
||||
...this.value,
|
||||
[removedItem.type]: this.value![removedItem.type].filter(
|
||||
(val) => val !== removedItem.value
|
||||
),
|
||||
};
|
||||
private _handleRemoved(ev: CustomEvent<number>) {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
ev.detail
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
if (!removedItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = {
|
||||
...this.value,
|
||||
[removedItem.type]: this.value![removedItem.type].filter(
|
||||
(val) => val !== removedItem.value
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
@@ -286,6 +288,13 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClearFilterKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.stopPropagation();
|
||||
this._clearFilter(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
@@ -293,6 +302,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
this._list?.clearSelection();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { internationalizationContext, uiContext } from "../data/context";
|
||||
import "./radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "./radio/ha-radio-group";
|
||||
import "./radio/ha-radio-option";
|
||||
@@ -26,8 +27,6 @@ export interface SelectBoxOption {
|
||||
|
||||
@customElement("ha-select-box")
|
||||
export class HaSelectBox extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public options: SelectBoxOption[] = [];
|
||||
|
||||
@property({ attribute: false }) public value?: string;
|
||||
@@ -40,6 +39,14 @@ export class HaSelectBox extends LitElement {
|
||||
@property({ type: Boolean, attribute: "stacked_image" })
|
||||
public stackedImage = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
protected _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
protected _ui?: ContextType<typeof uiContext>;
|
||||
|
||||
render() {
|
||||
const maxColumns = this.maxColumns ?? 3;
|
||||
const columns = Math.min(maxColumns, this.options.length);
|
||||
@@ -62,11 +69,11 @@ export class HaSelectBox extends LitElement {
|
||||
const disabled = option.disabled || this.disabled || false;
|
||||
const selected = option.value === this.value;
|
||||
|
||||
const isDark = this.hass?.themes.darkMode || false;
|
||||
const isRTL = this.hass
|
||||
const isDark = this._ui?.themes.darkMode || false;
|
||||
const isRTL = this._i18n
|
||||
? computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
)
|
||||
: false;
|
||||
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import type {
|
||||
AutomationBehavior,
|
||||
AutomationBehaviorConditionMode,
|
||||
AutomationBehaviorSelector,
|
||||
AutomationBehaviorTriggerMode,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import type { SelectBoxOption } from "../ha-select-box";
|
||||
import "../ha-select-box";
|
||||
import type { SelectBoxOption } from "../ha-select-box";
|
||||
|
||||
const TRIGGER_BEHAVIORS: AutomationBehaviorTriggerMode[] = [
|
||||
"any",
|
||||
"each",
|
||||
"first",
|
||||
"last",
|
||||
"all",
|
||||
];
|
||||
|
||||
const CONDITION_BEHAVIORS: AutomationBehaviorConditionMode[] = ["any", "all"];
|
||||
|
||||
@customElement("ha-selector-automation_behavior")
|
||||
export class HaSelectorAutomationBehavior extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public selector!: AutomationBehaviorSelector;
|
||||
|
||||
@@ -39,6 +40,9 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
protected render() {
|
||||
const { mode } = this.selector.automation_behavior ?? {};
|
||||
const modeKey = mode ?? "trigger";
|
||||
@@ -60,7 +64,6 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select-box
|
||||
.hass=${this.hass}
|
||||
.options=${options}
|
||||
.value=${this.value ?? ""}
|
||||
max_columns="1"
|
||||
@@ -95,8 +98,10 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
return translated;
|
||||
}
|
||||
}
|
||||
return this.hass.localize(
|
||||
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
|
||||
return (
|
||||
this._localize?.(
|
||||
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
|
||||
) || behavior
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { mdiPlayBox, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import type { MediaPickedEvent } from "../../data/media-player";
|
||||
import {
|
||||
MediaClassBrowserSettings,
|
||||
@@ -13,14 +12,10 @@ import {
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import "../media-player/ha-media-browser-thumbnail";
|
||||
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import "../ha-picture-upload";
|
||||
@@ -54,8 +49,6 @@ export class HaMediaSelector extends LitElement {
|
||||
filter_entity?: string | string[];
|
||||
};
|
||||
|
||||
@state() private _thumbnailUrl?: string | null;
|
||||
|
||||
private _contextEntities: string[] | undefined;
|
||||
|
||||
private get _hasAccept(): boolean {
|
||||
@@ -68,35 +61,6 @@ export class HaMediaSelector extends LitElement {
|
||||
this._contextEntities = ensureArray(this.context?.filter_entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("value")) {
|
||||
const thumbnail = this.value?.metadata?.thumbnail;
|
||||
const oldThumbnail = (changedProps.get("value") as this["value"])
|
||||
?.metadata?.thumbnail;
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && isBrandUrl(thumbnail)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
this._thumbnailUrl = brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnail),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
} else if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -186,10 +150,12 @@ export class HaMediaSelector extends LitElement {
|
||||
),
|
||||
})}
|
||||
image"
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
>
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${this.value.metadata.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
@@ -410,13 +376,11 @@ export class HaMediaSelector extends LitElement {
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
}
|
||||
.centered-image {
|
||||
margin: 4px;
|
||||
background-size: contain;
|
||||
--ha-media-browser-thumbnail-fit: contain;
|
||||
}
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
|
||||
@@ -96,7 +96,6 @@ export class HaSelectSelector extends LitElement {
|
||||
.value=${this.value as string | undefined}
|
||||
@value-changed=${this._selectChanged}
|
||||
.maxColumns=${this.selector.select?.box_max_columns}
|
||||
.hass=${this.hass}
|
||||
></ha-select-box>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
|
||||
@@ -38,6 +38,9 @@ export class HaListItemBase extends HaRowItem {
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("ha-list-item")) {
|
||||
this.setAttribute("ha-list-item", "");
|
||||
}
|
||||
if (!this.hasAttribute("role")) {
|
||||
this.setAttribute("role", this.defaultRole);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, type nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { compareNodeOrder } from "../../common/dom/compare-node-order";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import "./types";
|
||||
import type { HaListItemRegistrationDetail } from "./types";
|
||||
@@ -45,13 +45,13 @@ export class HaListBase extends LitElement {
|
||||
/** Host `role` attribute. Empty string means no role is set. */
|
||||
protected readonly hostRole: string = "list";
|
||||
|
||||
private _activeItemIndex = -1;
|
||||
protected activeItemIndex = -1;
|
||||
|
||||
private _firstFocusableIndex = -1;
|
||||
protected firstFocusableIndex = -1;
|
||||
|
||||
private _lastFocusableIndex = -1;
|
||||
protected lastFocusableIndex = -1;
|
||||
|
||||
private _hasFocusableItem = false;
|
||||
protected hasFocusableItem = false;
|
||||
|
||||
private _unbindKeys?: () => void;
|
||||
|
||||
@@ -63,22 +63,28 @@ export class HaListBase extends LitElement {
|
||||
if (!this.hasAttribute("role") && this.hostRole) {
|
||||
this.setAttribute("role", this.hostRole);
|
||||
}
|
||||
this._unbindKeys = tinykeys(this, {
|
||||
ArrowDown: this._onForward,
|
||||
ArrowUp: this._onBack,
|
||||
Home: this._onHome,
|
||||
End: this._onEnd,
|
||||
Enter: this._onActivate,
|
||||
Space: this._onActivate,
|
||||
});
|
||||
this.addEventListener("focusin", this._onFocusIn);
|
||||
this._unbindKeys = tinykeys(
|
||||
this,
|
||||
{
|
||||
ArrowDown: this._onForward,
|
||||
ArrowUp: this._onBack,
|
||||
Home: this._onHome,
|
||||
End: this._onEnd,
|
||||
PageDown: this._onPageDown,
|
||||
PageUp: this._onPageUp,
|
||||
Enter: this.onActivate,
|
||||
Space: this.onActivate,
|
||||
},
|
||||
{ ignore: this._ignoreKeyEvent }
|
||||
);
|
||||
this.addEventListener("focusin", this.onFocusIn);
|
||||
this.addEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
this.onItemRegister as EventListener
|
||||
);
|
||||
this.addEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
this.onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,25 +92,23 @@ export class HaListBase extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
this._unbindKeys?.();
|
||||
this._unbindKeys = undefined;
|
||||
this.removeEventListener("focusin", this._onFocusIn);
|
||||
this.removeEventListener("focusin", this.onFocusIn);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
this.onItemRegister as EventListener
|
||||
);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
this.onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public focus(options?: FocusOptions) {
|
||||
if (!this.items.length) {
|
||||
if (!this.itemCount) {
|
||||
super.focus(options);
|
||||
return;
|
||||
}
|
||||
this.focusItemAtIndex(
|
||||
this._activeItemIndex >= 0 ? this._activeItemIndex : 0
|
||||
);
|
||||
this.focusItemAtIndex(this.activeItemIndex >= 0 ? this.activeItemIndex : 0);
|
||||
}
|
||||
|
||||
public focusItemAtIndex(index: number) {
|
||||
@@ -115,19 +119,19 @@ export class HaListBase extends LitElement {
|
||||
}
|
||||
|
||||
public getActiveItemIndex(): number {
|
||||
return this._activeItemIndex;
|
||||
return this.activeItemIndex;
|
||||
}
|
||||
|
||||
public setActiveItemIndex(index: number, focusItem = false) {
|
||||
if (!this._hasFocusableItem) {
|
||||
this._activeItemIndex = -1;
|
||||
if (!this.hasFocusableItem) {
|
||||
this.activeItemIndex = -1;
|
||||
return;
|
||||
}
|
||||
this._activeItemIndex = Math.max(0, Math.min(this.items.length - 1, index));
|
||||
if (!this._isFocusable(this._activeItemIndex)) {
|
||||
this._activeItemIndex = this._firstFocusableIndex;
|
||||
this.activeItemIndex = Math.max(0, Math.min(this.itemCount - 1, index));
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
}
|
||||
this._applyActive(focusItem);
|
||||
this.applyActive(focusItem);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,18 +139,18 @@ export class HaListBase extends LitElement {
|
||||
* to layer in extra bookkeeping (e.g. selection state sync).
|
||||
*/
|
||||
public updateListItems() {
|
||||
this._recomputeFocusableIndexes();
|
||||
this.recomputeFocusableIndexes();
|
||||
if (
|
||||
this._activeItemIndex >= this.items.length ||
|
||||
!this._hasFocusableItem ||
|
||||
this._activeItemIndex < 0
|
||||
this.activeItemIndex >= this.itemCount ||
|
||||
!this.hasFocusableItem ||
|
||||
this.activeItemIndex < 0
|
||||
) {
|
||||
this._activeItemIndex = this._firstFocusableIndex;
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
}
|
||||
this._applyActive(false);
|
||||
this.applyActive(false);
|
||||
}
|
||||
|
||||
private _onItemRegister = (
|
||||
protected onItemRegister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
@@ -160,7 +164,7 @@ export class HaListBase extends LitElement {
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _onItemUnregister = (
|
||||
protected onItemUnregister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
@@ -172,136 +176,190 @@ export class HaListBase extends LitElement {
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _recomputeFocusableIndexes() {
|
||||
protected recomputeFocusableIndexes() {
|
||||
let first = -1;
|
||||
let last = -1;
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (this._isFocusable(i)) {
|
||||
for (let i = 0; i < this.itemCount; i++) {
|
||||
if (this.isFocusable(i)) {
|
||||
if (first === -1) {
|
||||
first = i;
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
this._firstFocusableIndex = first;
|
||||
this._lastFocusableIndex = last;
|
||||
this._hasFocusableItem = first !== -1;
|
||||
this.firstFocusableIndex = first;
|
||||
this.lastFocusableIndex = last;
|
||||
this.hasFocusableItem = first !== -1;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<div part="base" class="base">
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
return html`<div part="base" class="base ha-scrollbar">
|
||||
<slot></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _isFocusable(index: number): boolean {
|
||||
protected isFocusable(index: number): boolean {
|
||||
const item = this.items[index];
|
||||
return !!item && item.interactive && !item.disabled;
|
||||
}
|
||||
|
||||
private _applyActive(focusItem: boolean) {
|
||||
protected applyActive(focusItem: boolean) {
|
||||
this.items.forEach((item, i) => {
|
||||
if (!item.interactive || item.disabled) {
|
||||
item.removeAttribute("tabindex");
|
||||
return;
|
||||
}
|
||||
item.tabIndex = i === this._activeItemIndex ? 0 : -1;
|
||||
item.tabIndex = i === this.activeItemIndex ? 0 : -1;
|
||||
});
|
||||
if (focusItem && this._activeItemIndex >= 0) {
|
||||
this.items[this._activeItemIndex]?.focus();
|
||||
if (focusItem && this.activeItemIndex >= 0) {
|
||||
this.items[this.activeItemIndex]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _onFocusIn = (ev: FocusEvent) => {
|
||||
protected onFocusIn = (ev: FocusEvent) => {
|
||||
const path = ev.composedPath();
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (path.includes(this.items[i])) {
|
||||
if (i !== this._activeItemIndex) {
|
||||
this._activeItemIndex = i;
|
||||
this._applyActive(false);
|
||||
if (i !== this.activeItemIndex) {
|
||||
this.activeItemIndex = i;
|
||||
this.applyActive(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _ignoreKeyEvent = (ev: KeyboardEvent): boolean => {
|
||||
if (ev.repeat && (ev.key === "Enter" || ev.key === " ")) {
|
||||
return true;
|
||||
}
|
||||
if (ev.isComposing) {
|
||||
return true;
|
||||
}
|
||||
const target = ev.target as HTMLElement | null;
|
||||
// Allow held arrow/Home/End to repeat for continuous navigation
|
||||
return (
|
||||
!!target &&
|
||||
target !== ev.currentTarget &&
|
||||
target.matches("[contenteditable],input,select,textarea")
|
||||
);
|
||||
};
|
||||
|
||||
private _onForward = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, 1));
|
||||
this.moveFocus(ev, this._stepIndex(this.activeItemIndex, 1));
|
||||
};
|
||||
|
||||
private _onBack = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, -1));
|
||||
this.moveFocus(ev, this._stepIndex(this.activeItemIndex, -1));
|
||||
};
|
||||
|
||||
private _onHome = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._firstFocusableIndex);
|
||||
this.moveFocus(ev, this.firstFocusableIndex);
|
||||
};
|
||||
|
||||
private _onEnd = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._lastFocusableIndex);
|
||||
this.moveFocus(ev, this.lastFocusableIndex);
|
||||
};
|
||||
|
||||
private _onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this._isFocusable(this._activeItemIndex)) {
|
||||
private _onPageDown = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(
|
||||
ev,
|
||||
this._stepIndex(this.activeItemIndex, 1, this.getPageSize())
|
||||
);
|
||||
};
|
||||
|
||||
private _onPageUp = (ev: KeyboardEvent) => {
|
||||
this.moveFocus(
|
||||
ev,
|
||||
this._stepIndex(this.activeItemIndex, -1, this.getPageSize())
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Number of items to jump for PageUp/PageDown. Defaults to 10 (per WAI-ARIA
|
||||
* Authoring Practices: "moves focus a manageable number of nodes,
|
||||
* typically 10"). Subclasses with a known viewport (e.g. virtualized lists)
|
||||
* can override to use the visible page size.
|
||||
*/
|
||||
protected getPageSize(): number {
|
||||
return 10;
|
||||
}
|
||||
|
||||
protected onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
const active = this.items[this._activeItemIndex];
|
||||
const active = this.items[this.activeItemIndex];
|
||||
active.activate();
|
||||
fireEvent(this, "ha-list-activated", {
|
||||
index: this._activeItemIndex,
|
||||
index: this.activeItemIndex,
|
||||
item: active,
|
||||
});
|
||||
};
|
||||
|
||||
private _moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this._hasFocusableItem || next < 0 || next === this._activeItemIndex) {
|
||||
protected moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this.hasFocusableItem) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._activeItemIndex = next;
|
||||
this._applyActive(true);
|
||||
if (next < 0 || next === this.activeItemIndex) {
|
||||
return;
|
||||
}
|
||||
this.activeItemIndex = next;
|
||||
this.applyActive(true);
|
||||
}
|
||||
|
||||
protected get itemCount(): number {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step from `from` by `delta`, skipping non-interactive and disabled items.
|
||||
* Returns `from` when no other focusable item can be reached (honouring
|
||||
* `wrapFocus`).
|
||||
* Pass `count` > 1 to advance by multiple focusable items (PageUp/Down).
|
||||
* Returns the last focusable index reached, or `from` when none is.
|
||||
*/
|
||||
private _stepIndex(from: number, delta: 1 | -1): number {
|
||||
const n = this.items.length;
|
||||
if (!n || !this._hasFocusableItem) {
|
||||
private _stepIndex(from: number, delta: 1 | -1, count = 1): number {
|
||||
const n = this.itemCount;
|
||||
if (!n || !this.hasFocusableItem) {
|
||||
return from;
|
||||
}
|
||||
let last = from;
|
||||
let i = from;
|
||||
for (let step = 0; step < n; step++) {
|
||||
let landed = 0;
|
||||
for (let step = 0; step < n && landed < count; step++) {
|
||||
i += delta;
|
||||
if (i < 0 || i >= n) {
|
||||
if (!this.wrapFocus) {
|
||||
return from;
|
||||
return last;
|
||||
}
|
||||
i = (i + n) % n;
|
||||
}
|
||||
if (this._isFocusable(i)) {
|
||||
return i;
|
||||
if (this.isFocusable(i)) {
|
||||
last = i;
|
||||
landed++;
|
||||
}
|
||||
}
|
||||
return from;
|
||||
return last;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-list-gap, 0);
|
||||
padding: var(--ha-list-padding, 0);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
`;
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-list-gap, 0);
|
||||
padding: var(--ha-list-padding, 0);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class HaListNav extends HaListBase {
|
||||
part="nav"
|
||||
aria-label=${ifDefined(this.ariaLabel ?? undefined)}
|
||||
>
|
||||
<div part="base" class="base" role="list">
|
||||
<div part="base" class="base ha-scrollbar" role="list">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</nav>`;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { Constructor } from "../../types";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import type { HaListBase } from "./ha-list-base";
|
||||
|
||||
export const SelectableMixin = <T extends Constructor<HaListBase>>(
|
||||
superClass: T
|
||||
) => {
|
||||
class SelectableClass extends superClass {
|
||||
@property({ type: Boolean, reflect: true }) public multi = false;
|
||||
|
||||
protected override readonly hostRole = "listbox";
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._onOptionClick);
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("click", this._onOptionClick);
|
||||
}
|
||||
|
||||
public updated(changed: Map<string, unknown>) {
|
||||
super.updated(changed);
|
||||
if (changed.has("multi")) {
|
||||
this.setAttribute(
|
||||
"aria-multiselectable",
|
||||
this.multi ? "true" : "false"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Hook: index of a clicked option element, or `-1` if it's not ours. */
|
||||
protected optionIndexOf(opt: HaListItemOption): number {
|
||||
return this.items.indexOf(opt);
|
||||
}
|
||||
|
||||
public clearSelection() {
|
||||
(this.items as HaListItemOption[]).forEach((opt) => {
|
||||
if (opt.selected) {
|
||||
opt.toggleAttribute("selected", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _onOptionClick = (ev: Event) => {
|
||||
const path = ev.composedPath();
|
||||
for (const el of path) {
|
||||
if (el === this) {
|
||||
return;
|
||||
}
|
||||
if (el instanceof HaListItemOption) {
|
||||
if (el.disabled) {
|
||||
return;
|
||||
}
|
||||
const index = this.optionIndexOf(el);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.multi) {
|
||||
fireEvent(
|
||||
this,
|
||||
`ha-list-item-${el.selected ? "deselected" : "selected"}`,
|
||||
index
|
||||
);
|
||||
el.toggleAttribute("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!el.selected) {
|
||||
fireEvent(this, "ha-list-item-selected", index);
|
||||
// deselect the other optional selected item
|
||||
this.clearSelection();
|
||||
el.toggleAttribute("selected", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return SelectableClass;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import { SelectableMixin } from "./ha-list-selectable-mixin";
|
||||
import { HaListVirtualized } from "./ha-list-virtualized";
|
||||
|
||||
/**
|
||||
* @element ha-list-selectable-virtualized
|
||||
* @extends {HaListVirtualized}
|
||||
*
|
||||
* @summary
|
||||
* Virtualized selection list (role `listbox`). Rows must render
|
||||
* `<ha-list-item-option>` as their top-level element. Selection is index-based:
|
||||
* clicking a row fires `ha-list-item-selected` / `ha-list-item-deselected` with
|
||||
* the row's index, and the row's `selected` attribute is toggled. Consumers own
|
||||
* the source of truth — set each row's `selected` from their own state (for
|
||||
* example, keyed by the option's `value`) and update it in the event handlers.
|
||||
*
|
||||
* Because selection is tracked per-row by the consumer, filtering the visible
|
||||
* `rows` doesn't affect selections for items outside the current view.
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once. In
|
||||
* single-select mode, selecting a row clears any previous selection.
|
||||
*
|
||||
* @fires ha-list-item-selected - Fires when the user selects a row.
|
||||
* `detail` is the row's index (number).
|
||||
* @fires ha-list-item-deselected - Fires when the user deselects a row (multi-select only).
|
||||
* `detail` is the row's index (number).
|
||||
*/
|
||||
@customElement("ha-list-selectable-virtualized")
|
||||
export class HaListSelectableVirtualized extends SelectableMixin(
|
||||
HaListVirtualized
|
||||
) {
|
||||
/**
|
||||
* Hook: maps a clicked option to its absolute index by offsetting its
|
||||
* position among the rendered (virtualized) children by `rangeStart`.
|
||||
* Returns `-1` if it's not one of our rows or nothing is rendered yet.
|
||||
*/
|
||||
protected optionIndexOf(opt: HaListItemOption): number {
|
||||
if (!this.virtualizerElement || this.rangeStart === -1) {
|
||||
return -1;
|
||||
}
|
||||
const index = Array.from(this.virtualizerElement.children).indexOf(opt);
|
||||
if (index === -1) {
|
||||
return -1;
|
||||
}
|
||||
return this.rangeStart + index;
|
||||
}
|
||||
|
||||
/** Deselects every currently rendered (visible) option. */
|
||||
public clearSelection() {
|
||||
if (!this.virtualizerElement || this.rangeStart === -1) {
|
||||
return;
|
||||
}
|
||||
Array.from(this.virtualizerElement.children).forEach((opt) => {
|
||||
if (opt instanceof HaListItemOption && opt.selected) {
|
||||
opt.toggleAttribute("selected", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-selectable-virtualized": HaListSelectableVirtualized;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
import type { HaListSelectedDetail } from "./types";
|
||||
import { SelectableMixin } from "./ha-list-selectable-mixin";
|
||||
|
||||
/**
|
||||
* @element ha-list-selectable
|
||||
@@ -14,196 +12,11 @@ import type { HaListSelectedDetail } from "./types";
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once.
|
||||
*
|
||||
* @fires ha-list-selected - Fired when the selection changes. `detail: HaListSelectedDetail`.
|
||||
* @fires ha-list-item-selected - An option was selected. `detail: number` (option index).
|
||||
* @fires ha-list-item-deselected - An option was deselected (multi mode only). `detail: number` (option index).
|
||||
*/
|
||||
@customElement("ha-list-selectable")
|
||||
export class HaListSelectable extends HaListBase {
|
||||
@property({ type: Boolean, reflect: true }) public multi = false;
|
||||
|
||||
protected override readonly hostRole = "listbox";
|
||||
|
||||
private _selectedIndices?: Set<number>;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._onOptionClick);
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("click", this._onOptionClick);
|
||||
}
|
||||
|
||||
public updated(changed: Map<string, unknown>) {
|
||||
super.updated(changed);
|
||||
if (changed.has("multi")) {
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
if (!this.multi && (this._selectedIndices?.size ?? 0) > 1) {
|
||||
const first = Math.min(...this._selectedIndices!);
|
||||
this._setSelection(new Set([first]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current selection. `number` (or `-1` if nothing) when single,
|
||||
* `Set<number>` when multi.
|
||||
*/
|
||||
public get selected(): number | Set<number> {
|
||||
if (this.multi) {
|
||||
return new Set(this._selectedIndices);
|
||||
}
|
||||
return (this._selectedIndices?.size ?? 0) === 0
|
||||
? -1
|
||||
: this._selectedIndices!.values().next().value!;
|
||||
}
|
||||
|
||||
public get selectedItems(): HaListItemOption[] {
|
||||
return this._sortedSelectedIndices()
|
||||
.map((i) => this.items[i] as HaListItemOption | undefined)
|
||||
.filter((it): it is HaListItemOption => !!it);
|
||||
}
|
||||
|
||||
/** Replace the entire selection. */
|
||||
public setSelected(indices: number | number[] | Set<number>): void {
|
||||
const next =
|
||||
typeof indices === "number"
|
||||
? indices < 0
|
||||
? new Set<number>()
|
||||
: new Set([indices])
|
||||
: new Set(indices);
|
||||
if (!this.multi && next.size > 1) {
|
||||
const first = Math.min(...next);
|
||||
this._setSelection(new Set([first]));
|
||||
return;
|
||||
}
|
||||
this._setSelection(next);
|
||||
}
|
||||
|
||||
public select(index: number): void {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
const next = new Set(this._selectedIndices);
|
||||
next.add(index);
|
||||
this._setSelection(next);
|
||||
} else {
|
||||
this._setSelection(new Set([index]));
|
||||
}
|
||||
}
|
||||
|
||||
public toggle(index: number, force?: boolean): void {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
const next = new Set(this._selectedIndices);
|
||||
const isSelected = next.has(index);
|
||||
const shouldSelect = force !== undefined ? force : !isSelected;
|
||||
if (shouldSelect) {
|
||||
next.add(index);
|
||||
} else {
|
||||
next.delete(index);
|
||||
}
|
||||
this._setSelection(next);
|
||||
} else {
|
||||
const isSelected = this._selectedIndices!.has(index);
|
||||
const shouldSelect = force !== undefined ? force : !isSelected;
|
||||
this._setSelection(shouldSelect ? new Set([index]) : new Set());
|
||||
}
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this._setSelection(new Set());
|
||||
}
|
||||
|
||||
public updateListItems() {
|
||||
super.updateListItems();
|
||||
this._syncItemSelectedState(true);
|
||||
}
|
||||
|
||||
private _sortedSelectedIndices(): number[] {
|
||||
return [...this._selectedIndices!].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
private _syncItemSelectedState(reset = false): void {
|
||||
if (!this._selectedIndices || reset) {
|
||||
this._selectedIndices = new Set<number>();
|
||||
this.items.forEach((item, i) => {
|
||||
const opt = item as HaListItemOption;
|
||||
if (opt.selected) {
|
||||
this._selectedIndices!.add(i);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.items.forEach((item, i) => {
|
||||
const opt = item as HaListItemOption;
|
||||
const shouldBe = this._selectedIndices!.has(i);
|
||||
if (opt.selected !== shouldBe) {
|
||||
opt.selected = shouldBe;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _setSelection(next: Set<number>): void {
|
||||
const prev = this._selectedIndices!;
|
||||
const added = new Set<number>();
|
||||
const removed = new Set<number>();
|
||||
next.forEach((i) => {
|
||||
if (!prev.has(i)) {
|
||||
added.add(i);
|
||||
}
|
||||
});
|
||||
prev.forEach((i) => {
|
||||
if (!next.has(i)) {
|
||||
removed.add(i);
|
||||
}
|
||||
});
|
||||
if (!added.size && !removed.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectedIndices = next;
|
||||
this._syncItemSelectedState();
|
||||
|
||||
const detail: HaListSelectedDetail = this.multi
|
||||
? { index: new Set(next), diff: { added, removed } }
|
||||
: {
|
||||
index: next.size === 0 ? -1 : next.values().next().value!,
|
||||
diff: { added, removed },
|
||||
};
|
||||
fireEvent(this, "ha-list-selected", detail);
|
||||
}
|
||||
|
||||
private _onOptionClick = (ev: Event) => {
|
||||
const path = ev.composedPath();
|
||||
for (const el of path) {
|
||||
if (el === this) {
|
||||
return;
|
||||
}
|
||||
if (el instanceof HaListItemOption) {
|
||||
const index = this.items.indexOf(el);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
const item = this.items[index];
|
||||
if (item.disabled) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
this.toggle(index);
|
||||
} else {
|
||||
this.select(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
export class HaListSelectable extends SelectableMixin(HaListBase) {}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize.js";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
import type { HaListItemRegistrationDetail } from "./types";
|
||||
|
||||
/**
|
||||
* A single row in a {@link HaListVirtualized}. Identified by a stable `id`
|
||||
* used as the virtualizer key. Extra fields are passed through to the
|
||||
* `rowRenderer`.
|
||||
*/
|
||||
export interface HaListVirtualizedItem {
|
||||
/** Stable key used by the virtualizer to track the row across re-renders. */
|
||||
id: string;
|
||||
/** Whether the row can be focused and activated. Defaults to `false`. */
|
||||
interactive?: boolean;
|
||||
disabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @element ha-list-virtualized
|
||||
* @extends {HaListBase}
|
||||
*
|
||||
* @summary
|
||||
* Virtualized list. Renders only the rows currently in view to keep large
|
||||
* lists performant, while preserving the roving-tabindex keyboard navigation
|
||||
* of {@link HaListBase}.
|
||||
*
|
||||
* @csspart base - The scrollable outer container (`<div>`).
|
||||
*
|
||||
* @attr {number} pin-index - Row index to scroll to when the list first
|
||||
* renders. Cleared once the user scrolls.
|
||||
* @attr {string} pin-block - Block alignment for `pin-index`: `start`,
|
||||
* `center` (default), `end`, or `nearest`.
|
||||
*
|
||||
* @fires ha-list-activated - Fired when a row is activated via Enter/Space. `detail: { index, item }`.
|
||||
*/
|
||||
@customElement("ha-list-virtualized")
|
||||
export class HaListVirtualized extends HaListBase {
|
||||
@state() private _virtualizerReady = false;
|
||||
|
||||
/**
|
||||
* The list data. Each item is rendered by `rowRenderer`; its `interactive`
|
||||
* and `disabled` flags determine whether the row is focusable.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public rows!: HaListVirtualizedItem[];
|
||||
|
||||
/** Renders a single row from its data and index. */
|
||||
@property({ attribute: false })
|
||||
public rowRenderer?: RenderItemFunction<HaListVirtualizedItem>;
|
||||
|
||||
/** Row index to scroll to on first render (the "pinned" row). */
|
||||
@property({ attribute: "pin-index", type: Number }) public pinIndex?: number;
|
||||
|
||||
/** Block alignment used when scrolling to `pinIndex`. */
|
||||
@property({ attribute: "pin-block" }) public pinBlock:
|
||||
| "start"
|
||||
| "center"
|
||||
| "end"
|
||||
| "nearest" = "center";
|
||||
|
||||
@state() private _unpinned = false;
|
||||
|
||||
@query("lit-virtualizer")
|
||||
protected virtualizerElement?: LitVirtualizer<HaListVirtualizedItem>;
|
||||
|
||||
protected rangeStart = -1;
|
||||
protected rangeEnd = -1;
|
||||
private _activeItemFocus = false;
|
||||
private _scrollToActiveItem = false;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
this._loadVirtualizer();
|
||||
}
|
||||
|
||||
if (changedProps.has("rows")) {
|
||||
this.recomputeFocusableIndexes();
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadVirtualizer() {
|
||||
await loadVirtualizer();
|
||||
this._virtualizerReady = true;
|
||||
}
|
||||
|
||||
protected override render(): TemplateResult | typeof nothing {
|
||||
if (!this._virtualizerReady) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<div part="base" class="base ha-scrollbar">
|
||||
<lit-virtualizer
|
||||
.keyFunction=${this._keyFunction}
|
||||
tabindex="-1"
|
||||
scroller
|
||||
.items=${this.rows}
|
||||
.renderItem=${this.rowRenderer}
|
||||
style="min-height: 36px; height: 100%;"
|
||||
.layout=${!this._unpinned && this.pinIndex !== undefined
|
||||
? {
|
||||
pin: {
|
||||
index: this.pinIndex,
|
||||
block: this.pinBlock,
|
||||
},
|
||||
}
|
||||
: undefined}
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@rangeChanged=${this._handleRangeChanged}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active (roving-tabindex) row. If the row is outside the rendered
|
||||
* range it is scrolled into view first, then activated/focused once the
|
||||
* virtualizer has laid it out.
|
||||
* @param index - Row index to make active; clamped to the valid range.
|
||||
* @param focusItem - Whether to move DOM focus to the row.
|
||||
*/
|
||||
public setActiveItemIndex(index: number, focusItem = false) {
|
||||
if (!this.hasFocusableItem) {
|
||||
this.activeItemIndex = -1;
|
||||
return;
|
||||
}
|
||||
this.activeItemIndex = Math.max(0, Math.min(this.rows.length - 1, index));
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
this.activeItemIndex = this.firstFocusableIndex;
|
||||
}
|
||||
if (
|
||||
this.activeItemIndex >= this.rangeStart &&
|
||||
this.activeItemIndex <= this.rangeEnd
|
||||
) {
|
||||
this.applyActive(focusItem);
|
||||
} else {
|
||||
this._activeItemFocus = focusItem;
|
||||
this._scrollToActiveItem = true;
|
||||
this.virtualizerElement
|
||||
?.element(index)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the row at `index`, scrolling it into view if needed. No-op until
|
||||
* the virtualizer is ready or when `index` is negative.
|
||||
*/
|
||||
public override focusItemAtIndex(index: number) {
|
||||
if (!this._virtualizerReady || index < 0) {
|
||||
return;
|
||||
}
|
||||
this.setActiveItemIndex(index, true);
|
||||
}
|
||||
|
||||
protected override applyActive(focusItem: boolean) {
|
||||
if (this.virtualizerElement && this.rangeStart > -1) {
|
||||
Array.from(this.virtualizerElement.children).forEach((child, index) => {
|
||||
const el = child as HTMLElement;
|
||||
if (index + this.rangeStart === this.activeItemIndex) {
|
||||
el.tabIndex = 0;
|
||||
if (focusItem) {
|
||||
el.focus();
|
||||
}
|
||||
} else {
|
||||
el.removeAttribute("tabindex");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private async _handleRangeChanged(ev: { first: number; last: number }) {
|
||||
this.rangeStart = ev.first;
|
||||
this.rangeEnd = ev.last;
|
||||
|
||||
await this.virtualizerElement?.layoutComplete;
|
||||
this._applySetSize();
|
||||
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
const inRange =
|
||||
this.activeItemIndex >= this.rangeStart &&
|
||||
this.activeItemIndex <= this.rangeEnd;
|
||||
const focus = this._scrollToActiveItem && inRange && this._activeItemFocus;
|
||||
this.applyActive(focus);
|
||||
if (this._scrollToActiveItem && inRange) {
|
||||
this._activeItemFocus = false;
|
||||
this._scrollToActiveItem = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose total count + position to assistive tech, since only a slice of
|
||||
// items is in the DOM at any time.
|
||||
private _applySetSize() {
|
||||
if (!this.virtualizerElement || this.rangeStart < 0) {
|
||||
return;
|
||||
}
|
||||
const total = this.rows?.length ?? 0;
|
||||
Array.from(this.virtualizerElement.children).forEach((child, index) => {
|
||||
const el = child as HTMLElement;
|
||||
el.setAttribute("aria-setsize", String(total));
|
||||
el.setAttribute("aria-posinset", String(this.rangeStart + index + 1));
|
||||
});
|
||||
}
|
||||
|
||||
protected onFocusIn = (ev: FocusEvent) => {
|
||||
if (
|
||||
!this.virtualizerElement ||
|
||||
this.rangeStart === -1 ||
|
||||
this.rangeEnd === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const path = ev.composedPath();
|
||||
const children = Array.from(this.virtualizerElement.children);
|
||||
for (let i = this.rangeStart; i <= this.rangeEnd; i++) {
|
||||
if (path.includes(children[i - this.rangeStart])) {
|
||||
if (i !== this.activeItemIndex) {
|
||||
this.activeItemIndex = i;
|
||||
if (i < this.rangeStart || i > this.rangeEnd) {
|
||||
this._activeItemFocus = true;
|
||||
this._scrollToActiveItem = true;
|
||||
this.virtualizerElement
|
||||
?.element(this.activeItemIndex)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
} else {
|
||||
this.applyActive(false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected override onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this.isFocusable(this.activeItemIndex)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.virtualizerElement &&
|
||||
this.activeItemIndex >= this.rangeStart &&
|
||||
this.activeItemIndex <= this.rangeEnd
|
||||
) {
|
||||
const active = this.virtualizerElement?.children[
|
||||
this.activeItemIndex - this.rangeStart
|
||||
] as HaListItemBase | undefined;
|
||||
if (active && active instanceof HaListItemBase) {
|
||||
ev.preventDefault();
|
||||
active.activate();
|
||||
fireEvent(this, "ha-list-activated", {
|
||||
index: this.activeItemIndex,
|
||||
item: active,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected isFocusable(index: number): boolean {
|
||||
const item = this.rows[index];
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
const { disabled = false, interactive = false } = this.rows[index];
|
||||
return interactive && !disabled;
|
||||
}
|
||||
|
||||
protected override get itemCount(): number {
|
||||
return this.rows?.length ?? 0;
|
||||
}
|
||||
|
||||
protected override moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this.hasFocusableItem) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
if (next < 0 || next === this.activeItemIndex) {
|
||||
return;
|
||||
}
|
||||
this.activeItemIndex = next;
|
||||
if (next < this.rangeStart || next > this.rangeEnd) {
|
||||
this._activeItemFocus = true;
|
||||
this._scrollToActiveItem = true;
|
||||
this.virtualizerElement?.element(this.activeItemIndex)?.scrollIntoView({
|
||||
block: "nearest",
|
||||
});
|
||||
} else {
|
||||
this.applyActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override getPageSize(): number {
|
||||
if (this.rangeStart < 0 || this.rangeEnd < 0) {
|
||||
return super.getPageSize();
|
||||
}
|
||||
return Math.max(1, this.rangeEnd - this.rangeStart + 1);
|
||||
}
|
||||
|
||||
private _keyFunction = (item: HaListVirtualizedItem) => item.id;
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleUnpinned() {
|
||||
this._unpinned = true;
|
||||
}
|
||||
|
||||
protected override onItemRegister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
protected override onItemUnregister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
// ignore
|
||||
};
|
||||
|
||||
static styles = [
|
||||
...HaListBase.styles,
|
||||
css`
|
||||
.base {
|
||||
height: 100%;
|
||||
}
|
||||
[ha-list-item] {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-virtualized": HaListVirtualized;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
|
||||
export interface HaListSelectedDetail {
|
||||
index: number | Set<number>;
|
||||
diff?: { added: Set<number>; removed: Set<number> };
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
export interface HaListActivatedDetail {
|
||||
index: number;
|
||||
item: HaListItemBase;
|
||||
@@ -17,7 +11,8 @@ export interface HaListItemRegistrationDetail {
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"ha-list-selected": HaListSelectedDetail;
|
||||
"ha-list-item-selected": number;
|
||||
"ha-list-item-deselected": number;
|
||||
"ha-list-activated": HaListActivatedDetail;
|
||||
"ha-list-item-register": HaListItemRegistrationDetail;
|
||||
"ha-list-item-unregister": HaListItemRegistrationDetail;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
|
||||
const SMALL_THUMBNAIL_THRESHOLD = 16;
|
||||
|
||||
const isSvgUrl = (url: string): boolean =>
|
||||
/\.svg(\?|#|$)/i.test(url) || url.startsWith("data:image/svg+xml");
|
||||
|
||||
const resolveThumbnailURL = (
|
||||
hass: HomeAssistant,
|
||||
thumbnailUrl: string
|
||||
): Promise<string> => {
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
return Promise.resolve(
|
||||
brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: hass.themes?.darkMode,
|
||||
},
|
||||
hass.auth.data.hassUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Local thumbnails require authentication; fetch and inline as base64.
|
||||
return hass
|
||||
.fetchWithAuth(thumbnailUrl)
|
||||
.then((response) => response.blob())
|
||||
.then(
|
||||
(blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
resolve(typeof reader.result === "string" ? reader.result : "");
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve(thumbnailUrl);
|
||||
};
|
||||
|
||||
@customElement("ha-media-browser-thumbnail")
|
||||
export class HaMediaBrowserThumbnail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public url?: string;
|
||||
|
||||
@state() private _resolvedUrl?: string;
|
||||
|
||||
@state() private _small = false;
|
||||
|
||||
@state() private _brand = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("url")) {
|
||||
this._resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private async _resolve(): Promise<void> {
|
||||
this._small = false;
|
||||
this._brand = !!this.url && isBrandUrl(this.url);
|
||||
if (!this.url) {
|
||||
this._resolvedUrl = undefined;
|
||||
return;
|
||||
}
|
||||
const requested = this.url;
|
||||
try {
|
||||
const resolved = await resolveThumbnailURL(this.hass, requested);
|
||||
if (requested !== this.url) return;
|
||||
this._resolvedUrl = resolved;
|
||||
this._probeSize(resolved);
|
||||
} catch (_err) {
|
||||
if (requested === this.url) this._resolvedUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _probeSize(url: string): void {
|
||||
// SVGs (including brand icons) scale natively; pixelated rendering would
|
||||
// break vector output.
|
||||
if (this.url && isBrandUrl(this.url)) return;
|
||||
if (isSvgUrl(url)) return;
|
||||
const img = new Image();
|
||||
img.addEventListener("load", () => {
|
||||
if (this._resolvedUrl !== url) return;
|
||||
if (
|
||||
img.naturalWidth > 0 &&
|
||||
img.naturalWidth <= SMALL_THUMBNAIL_THRESHOLD
|
||||
) {
|
||||
this._small = true;
|
||||
}
|
||||
});
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._resolvedUrl) return nothing;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
image: true,
|
||||
small: this._small,
|
||||
brand: this._brand,
|
||||
})}
|
||||
style="background-image: url(${this._resolvedUrl})"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: var(--ha-media-browser-thumbnail-fit, contain);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.image.brand {
|
||||
background-size: 40%;
|
||||
}
|
||||
.image.small {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-browser-thumbnail": HaMediaBrowserThumbnail;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { slugify } from "../../common/string/slugify";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@@ -39,11 +38,6 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-alert";
|
||||
@@ -52,6 +46,7 @@ import "../ha-card";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "./ha-media-browser-thumbnail";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
@@ -411,12 +406,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
|
||||
const backgroundImage = currentItem.thumbnail
|
||||
? this._getThumbnailURLorBase64(currentItem.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
${
|
||||
currentItem.can_play
|
||||
@@ -431,13 +420,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: ${until(
|
||||
backgroundImage,
|
||||
""
|
||||
)}"
|
||||
>
|
||||
<div class="img">
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${currentItem.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
${this.narrow &&
|
||||
currentItem?.can_play &&
|
||||
(!this.accept ||
|
||||
@@ -638,12 +625,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const backgroundImage = child.thumbnail
|
||||
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<div class="child" .item=${child} @click=${this._childClicked}>
|
||||
<ha-card outlined>
|
||||
@@ -655,10 +636,13 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
"centered-image": ["app", "directory"].includes(
|
||||
child.media_class
|
||||
),
|
||||
"brand-image": isBrandUrl(child.thumbnail),
|
||||
})} image"
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
></div>
|
||||
>
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${child.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
@@ -703,13 +687,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const currentItem = this._currentItem;
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
|
||||
|
||||
const backgroundImage =
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
const showImage = mediaClass.show_list_images && !!child.thumbnail;
|
||||
|
||||
return html`
|
||||
<ha-list-item
|
||||
@@ -717,7 +695,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||||
>
|
||||
${backgroundImage === "none" && !child.can_play
|
||||
${!showImage && !child.can_play
|
||||
? html`<ha-svg-icon
|
||||
.path=${MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
@@ -731,9 +709,14 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
graphic: true,
|
||||
thumbnail: mediaClass.show_list_images === true,
|
||||
})}
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
slot="graphic"
|
||||
>
|
||||
${showImage
|
||||
? html`<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${child.thumbnail}
|
||||
></ha-media-browser-thumbnail>`
|
||||
: nothing}
|
||||
${child.can_play
|
||||
? html`<ha-icon-button
|
||||
class="play ${classMap({
|
||||
@@ -753,51 +736,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private async _getThumbnailURLorBase64(
|
||||
thumbnailUrl: string | undefined
|
||||
): Promise<string> {
|
||||
if (!thumbnailUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
return brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return new Promise((resolve, reject) => {
|
||||
this.hass
|
||||
.fetchWithAuth(thumbnailUrl!)
|
||||
// Since we are fetching with an authorization header, we cannot just put the
|
||||
// URL directly into the document; we need to embed the image. We could do this
|
||||
// using blob URLs, but then we would need to keep track of them in order to
|
||||
// release them properly. Instead, we embed the thumbnail using base64.
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
resolve(typeof result === "string" ? result : "");
|
||||
};
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
private _actionClicked = (ev: MouseEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
@@ -1048,14 +986,20 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.header-content .img {
|
||||
position: relative;
|
||||
height: 175px;
|
||||
width: 175px;
|
||||
margin-right: 16px;
|
||||
background-size: cover;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
width 0.4s,
|
||||
height 0.4s;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
}
|
||||
.header-content .img ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.header-info {
|
||||
display: flex;
|
||||
@@ -1191,18 +1135,12 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
}
|
||||
|
||||
.centered-image {
|
||||
margin: 0 8px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.brand-image {
|
||||
background-size: 40%;
|
||||
--ha-media-browser-thumbnail-fit: contain;
|
||||
}
|
||||
|
||||
.children ha-card .icon-holder {
|
||||
@@ -1278,17 +1216,21 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
ha-list-item .graphic {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
ha-list-item .graphic ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
ha-list-item .graphic .play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
|
||||
@@ -99,6 +99,8 @@ export class HaRadioOption extends Radio {
|
||||
--ha-radio-option-checked-background-color,
|
||||
var(--ha-color-fill-primary-normal-resting)
|
||||
);
|
||||
color: var(--checked-icon-color);
|
||||
border-color: var(--checked-icon-color);
|
||||
}
|
||||
|
||||
[part~="label"] {
|
||||
|
||||
+6
-11
@@ -1,4 +1,3 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type {
|
||||
Connection,
|
||||
HassEntityAttributeBase,
|
||||
@@ -96,7 +95,7 @@ export interface TriggerList {
|
||||
|
||||
export interface BaseTrigger {
|
||||
alias?: string;
|
||||
comment?: string;
|
||||
note?: string;
|
||||
/** @deprecated Use `trigger` instead */
|
||||
platform?: string;
|
||||
trigger: string;
|
||||
@@ -242,7 +241,7 @@ export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
alias?: string;
|
||||
comment?: string;
|
||||
note?: string;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
@@ -491,12 +490,12 @@ export const migrateAutomationTrigger = (
|
||||
|
||||
export const flattenTriggers = (
|
||||
triggers: undefined | Trigger | Trigger[]
|
||||
): Exclude<Trigger, TriggerList>[] => {
|
||||
): Trigger[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const flatTriggers: Exclude<Trigger, TriggerList>[] = [];
|
||||
const flatTriggers: Trigger[] = [];
|
||||
|
||||
ensureArray(triggers).forEach((t) => {
|
||||
if ("triggers" in t) {
|
||||
@@ -610,7 +609,7 @@ export interface AutomationClipboard {
|
||||
export interface BaseSidebarConfig {
|
||||
delete: () => void;
|
||||
close: (focus?: boolean) => void;
|
||||
editComment: () => void;
|
||||
editNote: () => void;
|
||||
}
|
||||
|
||||
export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
@@ -672,7 +671,7 @@ export interface OptionSidebarConfig extends BaseSidebarConfig {
|
||||
rename: () => void;
|
||||
duplicate: () => void;
|
||||
defaultOption?: boolean;
|
||||
comment?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
|
||||
@@ -698,7 +697,3 @@ export interface ShowAutomationEditorParams {
|
||||
data?: Partial<AutomationConfig>;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export const automationConfigContext = createContext<
|
||||
AutomationConfig | undefined
|
||||
>("automationConfig");
|
||||
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
LegacyTrigger,
|
||||
Trigger,
|
||||
} from "./automation";
|
||||
import { flattenTriggers } from "./automation";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
import type {
|
||||
DeviceCondition,
|
||||
@@ -108,41 +107,6 @@ const formatNumericLimitValue = (
|
||||
: value;
|
||||
};
|
||||
|
||||
export interface TriggerInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
triggerType: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const getTriggerInfos = (
|
||||
triggers: Trigger[] | undefined,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[]
|
||||
): TriggerInfo[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
}
|
||||
const map = new Map<string, TriggerInfo>();
|
||||
for (const t of flattenTriggers(triggers)) {
|
||||
if (isTriggerList(t) || !t.id) {
|
||||
continue;
|
||||
}
|
||||
const existing = map.get(t.id);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
map.set(t.id, {
|
||||
id: t.id,
|
||||
label: describeTrigger(t, hass, entityRegistry),
|
||||
triggerType: t.trigger,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
export const describeTrigger = (
|
||||
trigger: Trigger,
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -40,7 +40,6 @@ export const createConfigFlow = (
|
||||
"config/config_entries/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
entry_id,
|
||||
},
|
||||
HEADERS
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
export interface DeviceAutomation {
|
||||
alias?: string;
|
||||
comment?: string;
|
||||
note?: string;
|
||||
device_id: string;
|
||||
domain: string;
|
||||
entity_id?: string;
|
||||
|
||||
@@ -161,6 +161,10 @@ export interface VacuumEntityOptions {
|
||||
last_seen_segments?: Segment[];
|
||||
}
|
||||
|
||||
export interface DeviceTrackerEntityOptions {
|
||||
associated_zone?: string | null;
|
||||
}
|
||||
|
||||
export interface EntityRegistryOptions {
|
||||
number?: NumberEntityOptions;
|
||||
sensor?: SensorEntityOptions;
|
||||
@@ -172,6 +176,7 @@ export interface EntityRegistryOptions {
|
||||
cover?: CoverEntityOptions;
|
||||
valve?: ValveEntityOptions;
|
||||
vacuum?: VacuumEntityOptions;
|
||||
device_tracker?: DeviceTrackerEntityOptions;
|
||||
switch_as_x?: SwitchAsXEntityOptions;
|
||||
conversation?: Record<string, unknown>;
|
||||
"cloud.alexa"?: Record<string, unknown>;
|
||||
@@ -197,7 +202,8 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
| LightEntityOptions
|
||||
| CoverEntityOptions
|
||||
| ValveEntityOptions
|
||||
| VacuumEntityOptions;
|
||||
| VacuumEntityOptions
|
||||
| DeviceTrackerEntityOptions;
|
||||
aliases?: (string | null)[];
|
||||
labels?: string[];
|
||||
categories?: Record<string, string | null>;
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { ShortcutItem } from "./home_shortcuts";
|
||||
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
default_panel?: string;
|
||||
apps_info_dismissed?: boolean;
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
|
||||
import type { LovelaceCardConfig } from "./lovelace/config/card";
|
||||
|
||||
export interface CustomCardSuggestion<
|
||||
T extends LovelaceCardConfig = LovelaceCardConfig,
|
||||
> {
|
||||
label?: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CustomCardEntry {
|
||||
type: string;
|
||||
@@ -8,6 +16,10 @@ export interface CustomCardEntry {
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
documentationURL?: string;
|
||||
getEntitySuggestion?: (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) => CustomCardSuggestion | CustomCardSuggestion[] | null;
|
||||
}
|
||||
|
||||
export interface CustomBadgeEntry {
|
||||
|
||||
+16
-5
@@ -2,7 +2,10 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { subscribeDeviceRegistry } from "./device/device_registry";
|
||||
import {
|
||||
subscribeDeviceRegistry,
|
||||
type DeviceRegistryEntry,
|
||||
} from "./device/device_registry";
|
||||
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
|
||||
|
||||
export enum NetworkType {
|
||||
@@ -77,9 +80,9 @@ export const startExternalCommissioning = async (hass: HomeAssistant) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const redirectOnNewMatterDevice = (
|
||||
export const watchForNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
callback?: () => void
|
||||
callback: (device: DeviceRegistryEntry) => void
|
||||
): UnsubscribeFunc => {
|
||||
let curMatterDevices: Set<string> | undefined;
|
||||
const unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => {
|
||||
@@ -101,8 +104,7 @@ export const redirectOnNewMatterDevice = (
|
||||
if (newMatterDevices.length) {
|
||||
unsubDeviceReg();
|
||||
curMatterDevices = undefined;
|
||||
callback?.();
|
||||
navigate(`/config/devices/device/${newMatterDevices[0].id}`);
|
||||
callback(newMatterDevices[0]);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
@@ -111,6 +113,15 @@ export const redirectOnNewMatterDevice = (
|
||||
};
|
||||
};
|
||||
|
||||
export const redirectOnNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
callback?: () => void
|
||||
): UnsubscribeFunc =>
|
||||
watchForNewMatterDevice(hass, (device) => {
|
||||
callback?.();
|
||||
navigate(`/config/devices/device/${device.id}`);
|
||||
});
|
||||
|
||||
export const addMatterDevice = (hass: HomeAssistant) => {
|
||||
startExternalCommissioning(hass);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ export const createOptionsFlow = (hass: HomeAssistant, handler: string) =>
|
||||
"config/config_entries/options/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ export const isMaxMode = arrayLiteralIncludes(MODES_MAX);
|
||||
|
||||
export const baseActionStruct = object({
|
||||
alias: optional(string()),
|
||||
comment: optional(string()),
|
||||
note: optional(string()),
|
||||
continue_on_error: optional(boolean()),
|
||||
enabled: optional(boolean()),
|
||||
});
|
||||
@@ -106,7 +106,7 @@ export interface Field {
|
||||
|
||||
interface BaseAction {
|
||||
alias?: string;
|
||||
comment?: string;
|
||||
note?: string;
|
||||
continue_on_error?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export interface ForEachRepeat extends BaseRepeat {
|
||||
|
||||
export interface Option {
|
||||
alias?: string;
|
||||
comment?: string;
|
||||
note?: string;
|
||||
conditions: string | Condition[];
|
||||
sequence: Action | Action[];
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export interface BooleanSelector {
|
||||
boolean: {} | null;
|
||||
}
|
||||
|
||||
export type AutomationBehaviorTriggerMode = "first" | "last" | "any";
|
||||
export type AutomationBehaviorTriggerMode = "first" | "all" | "each";
|
||||
|
||||
export type AutomationBehaviorConditionMode = "all" | "any";
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export const createSubConfigFlow = (
|
||||
"config/config_entries/subentries/flow",
|
||||
{
|
||||
handler: [configEntryId, subFlowType],
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
subentry_id,
|
||||
},
|
||||
HEADERS
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
Trigger,
|
||||
TriggerList,
|
||||
} from "./automation";
|
||||
import { flattenTriggers } from "./automation";
|
||||
import type { Selector, TargetSelector } from "./selector";
|
||||
|
||||
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
@@ -57,49 +56,6 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||
"triggers" in trigger;
|
||||
|
||||
export const getTriggerIds = (triggers: Trigger[]): string[] =>
|
||||
flattenTriggers(triggers)
|
||||
.map((trigger) => trigger.id)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
export const getNextNumericTriggerId = (triggers: Trigger[]): string => {
|
||||
let max = 0;
|
||||
for (const id of getTriggerIds(triggers)) {
|
||||
const num = Number(id);
|
||||
if (Number.isInteger(num) && num > max) {
|
||||
max = num;
|
||||
}
|
||||
}
|
||||
return String(max + 1);
|
||||
};
|
||||
|
||||
const computeUniqueId = (id: string, existing: Set<string>): string => {
|
||||
if (!existing.has(id)) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Split into a base and a trailing integer suffix so we can bump the
|
||||
// suffix on collision (e.g. "foo2" -> "foo3"); if there's no trailing
|
||||
// digit we start at 2 ("foo" -> "foo2").
|
||||
const match = id.match(/^(.*?)(\d+)$/);
|
||||
let base: string;
|
||||
let num: number;
|
||||
if (match) {
|
||||
base = match[1];
|
||||
num = Number(match[2]) + 1;
|
||||
} else {
|
||||
base = id;
|
||||
num = 2;
|
||||
}
|
||||
while (existing.has(`${base}${num}`)) {
|
||||
num++;
|
||||
}
|
||||
return `${base}${num}`;
|
||||
};
|
||||
|
||||
export const getUniqueTriggerId = (id: string, triggers: Trigger[]): string =>
|
||||
computeUniqueId(id, new Set(getTriggerIds(triggers)));
|
||||
|
||||
export interface TriggerDescription {
|
||||
target?: TargetSelector["target"];
|
||||
fields: Record<
|
||||
|
||||
@@ -167,7 +167,6 @@ export interface DataEntryFlowDialogParams {
|
||||
entryId?: string;
|
||||
}) => void;
|
||||
flowConfig: FlowConfig;
|
||||
showAdvanced?: boolean;
|
||||
dialogParentElement?: HTMLElement;
|
||||
navigateToResult?: boolean;
|
||||
carryOverDevices?: string[];
|
||||
|
||||
@@ -48,7 +48,6 @@ class StepFlowAbort extends LitElement {
|
||||
showConfigFlowDialog(this.params.dialogParentElement!, {
|
||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||
startFlowHandler: this.handler,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: this.params.navigateToResult,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import type { Constructor } from "../types";
|
||||
import { isMobileClient } from "../util/is_mobile";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
|
||||
export const MobileAwareMixin = <T extends Constructor<LitElement>>(
|
||||
superClass: T
|
||||
@@ -12,16 +12,16 @@ export const MobileAwareMixin = <T extends Constructor<LitElement>>(
|
||||
|
||||
protected _isMobileClient = isMobileClient;
|
||||
|
||||
protected mobileSizeQuery =
|
||||
"all and (max-width: 450px), all and (max-height: 500px)";
|
||||
|
||||
private _unsubMql?: () => void;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._unsubMql = listenMediaQuery(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)",
|
||||
(matches) => {
|
||||
this._isMobileSize = matches;
|
||||
}
|
||||
);
|
||||
this._unsubMql = listenMediaQuery(this.mobileSizeQuery, (matches) => {
|
||||
this._isMobileSize = matches;
|
||||
});
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { IFRAME_SANDBOX } from "../../util/iframe";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeRouteTail } from "../../common/url/route";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
@@ -137,8 +136,6 @@ class HaPanelApp extends LitElement {
|
||||
})}
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
.sandbox=${IFRAME_SANDBOX}
|
||||
allow="microphone; camera; clipboard-write"
|
||||
@load=${this._checkLoaded}
|
||||
${ref(this._iframeRef)}
|
||||
>
|
||||
|
||||
@@ -342,7 +342,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
private _addCalendar = async (): Promise<void> => {
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: "local_calendar",
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest: await fetchIntegrationManifest(this.hass, "local_calendar"),
|
||||
dialogClosedCallback: ({ flowFinished }) => {
|
||||
if (flowFinished) {
|
||||
|
||||
@@ -31,7 +31,6 @@ class SupervisorAppInfoDashboard extends LitElement {
|
||||
<supervisor-app-info
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
.controlEnabled=${this.controlEnabled}
|
||||
></supervisor-app-info>
|
||||
|
||||
@@ -92,14 +92,15 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { MobileAwareMixin } from "../../../../../mixins/mobile-aware-mixin";
|
||||
import { mdiHomeAssistant } from "../../../../../resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { Route } from "../../../../../types";
|
||||
import { bytesToString } from "../../../../../util/bytes-to-string";
|
||||
import { getAppDisplayName } from "../../common/app";
|
||||
import "../components/supervisor-app-metric";
|
||||
import "../../components/supervisor-apps-tag";
|
||||
import "../../components/supervisor-apps-state";
|
||||
import "../../components/supervisor-apps-tag";
|
||||
import "../components/supervisor-app-metric";
|
||||
import { extractChangelog } from "../util/supervisor-app";
|
||||
import "./supervisor-app-system-managed";
|
||||
|
||||
@@ -123,7 +124,7 @@ const RATING_ICON = {
|
||||
const POLL_INTERVAL_SECONDS = 5;
|
||||
|
||||
@customElement("supervisor-app-info")
|
||||
class SupervisorAppInfo extends LitElement {
|
||||
class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
@@ -163,6 +164,9 @@ class SupervisorAppInfo extends LitElement {
|
||||
|
||||
private _pollInterval?: number;
|
||||
|
||||
protected mobileSizeQuery =
|
||||
"all and (max-width: 1120px), all and (max-height: 500px)";
|
||||
|
||||
private get _currentAddon(): HassioAddonDetails | StoreAddonDetails {
|
||||
return this._addon || this.addon;
|
||||
}
|
||||
@@ -863,11 +867,11 @@ class SupervisorAppInfo extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="app ${this.narrow || !this._currentAddon.version
|
||||
class="app ${this._isMobileSize || !this._currentAddon.version
|
||||
? "column"
|
||||
: ""}"
|
||||
>
|
||||
${this.narrow || !this._currentAddon.version
|
||||
${this._isMobileSize || !this._currentAddon.version
|
||||
? html`
|
||||
${this._renderInfoCard()}
|
||||
${this._currentAddon.version
|
||||
|
||||
@@ -145,8 +145,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@@ -13,8 +13,6 @@ class HaConfigAreas extends HassRouterPage {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
@@ -37,7 +35,6 @@ class HaConfigAreas extends HassRouterPage {
|
||||
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class HaAutomationActionEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.action.alias ? { alias: this.action.alias } : {}),
|
||||
...(this.action.comment ? { comment: this.action.comment } : {}),
|
||||
...(this.action.note ? { note: this.action.note } : {}),
|
||||
...ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value });
|
||||
|
||||
@@ -297,8 +297,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
?.target
|
||||
: undefined;
|
||||
|
||||
const commentTooltipText = truncateWithEllipsis(
|
||||
this.action.comment?.trim() || "",
|
||||
const noteTooltipText = truncateWithEllipsis(
|
||||
this.action.note?.trim() || "",
|
||||
250
|
||||
);
|
||||
|
||||
@@ -337,18 +337,19 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
serviceTargetSpec
|
||||
)
|
||||
: nothing}
|
||||
${commentTooltipText
|
||||
${noteTooltipText
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="comment-icon"
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
)}
|
||||
class="comment-indicator"
|
||||
class="note-indicator"
|
||||
></ha-svg-icon
|
||||
><ha-tooltip for="comment-icon"
|
||||
><p>${commentTooltipText}</p></ha-tooltip
|
||||
><ha-tooltip for="note-icon"
|
||||
><p>${noteTooltipText}</p></ha-tooltip
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
@@ -407,11 +408,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit_comment">
|
||||
<ha-dropdown-item value="edit_note">
|
||||
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.action.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.action.note ? "edit" : "add"}`
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
@@ -941,25 +942,25 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _editCommentAction = async (): Promise<void> => {
|
||||
const comment = await showPromptDialog(this, {
|
||||
private _editNoteAction = async (): Promise<void> => {
|
||||
const note = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.action.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.action.note ? "edit" : "add"}`
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
),
|
||||
inputType: "string",
|
||||
defaultValue: this.action.comment,
|
||||
defaultValue: this.action.note,
|
||||
confirmText: this.hass.localize("ui.common.submit"),
|
||||
multiline: true,
|
||||
});
|
||||
if (comment !== null) {
|
||||
if (note !== null) {
|
||||
const value = { ...this.action };
|
||||
if (comment === "") {
|
||||
delete value.comment;
|
||||
if (note === "") {
|
||||
delete value.note;
|
||||
} else {
|
||||
value.comment = comment;
|
||||
value.note = note;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
@@ -1089,7 +1090,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
rename: () => {
|
||||
this._renameAction();
|
||||
},
|
||||
editComment: this._editCommentAction,
|
||||
editNote: this._editNoteAction,
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
this.openSidebar();
|
||||
@@ -1185,8 +1186,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
case "rename":
|
||||
this._renameAction();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this._editCommentAction();
|
||||
case "edit_note":
|
||||
this._editNoteAction();
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicateAction();
|
||||
|
||||
@@ -95,7 +95,6 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
.value=${this._newMode}
|
||||
@value-changed=${this._modeChanged}
|
||||
.maxColumns=${1}
|
||||
.hass=${this.hass}
|
||||
></ha-select-box>
|
||||
|
||||
${isMaxMode(this._newMode)
|
||||
|
||||
@@ -123,7 +123,7 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.condition.alias ? { alias: this.condition.alias } : {}),
|
||||
...(this.condition.comment ? { comment: this.condition.comment } : {}),
|
||||
...(this.condition.note ? { note: this.condition.note } : {}),
|
||||
...ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -26,7 +25,7 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
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";
|
||||
@@ -53,26 +52,18 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-trigger-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
AutomationConfig,
|
||||
Condition,
|
||||
ConditionSidebarConfig,
|
||||
PlatformCondition,
|
||||
TriggerCondition,
|
||||
} from "../../../../data/automation";
|
||||
import {
|
||||
automationConfigContext,
|
||||
isCondition,
|
||||
subscribeCondition,
|
||||
testCondition,
|
||||
} from "../../../../data/automation";
|
||||
import {
|
||||
describeCondition,
|
||||
getTriggerInfos,
|
||||
} from "../../../../data/automation_i18n";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import {
|
||||
@@ -92,7 +83,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { showEditorToast } from "../editor-toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import "../ha-trigger-id-chip";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import "./ha-automation-condition-editor";
|
||||
@@ -164,11 +154,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@state() private _selected = false;
|
||||
|
||||
@state() private _liveTestResult: LiveTestState = "unknown";
|
||||
|
||||
@state()
|
||||
@consume({ context: automationConfigContext, subscribe: true })
|
||||
private _automationConfig?: AutomationConfig;
|
||||
@state() private _liveTestResult: {
|
||||
state: LiveTestState;
|
||||
message?: string;
|
||||
} = { state: "unknown" };
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@@ -216,8 +205,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
const conditionTargetSpec =
|
||||
this.conditionDescriptions[this.condition.condition]?.target;
|
||||
|
||||
const commentTooltipText = truncateWithEllipsis(
|
||||
this.condition.comment?.trim() || "",
|
||||
const noteTooltipText = truncateWithEllipsis(
|
||||
this.condition.note?.trim() || "",
|
||||
250
|
||||
);
|
||||
|
||||
@@ -228,13 +217,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
<h3 slot="header">
|
||||
${this.condition.condition === "trigger"
|
||||
? this._renderTriggerConditionDescription(
|
||||
this.condition as TriggerCondition
|
||||
)
|
||||
: capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${target !== undefined || (descriptionHasTarget && !this._isNew)
|
||||
? this._renderTargets(
|
||||
target,
|
||||
@@ -242,19 +227,18 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
conditionTargetSpec
|
||||
)
|
||||
: nothing}
|
||||
${this.condition.comment?.trim()
|
||||
${this.condition.note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="comment-icon"
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
)}
|
||||
class="comment-indicator"
|
||||
class="note-indicator"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="comment-icon"
|
||||
><p>${commentTooltipText}</p></ha-tooltip
|
||||
>
|
||||
<ha-tooltip for="note-icon"><p>${noteTooltipText}</p></ha-tooltip>
|
||||
`
|
||||
: nothing}
|
||||
</h3>
|
||||
@@ -304,11 +288,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit_comment">
|
||||
<ha-dropdown-item value="edit_note">
|
||||
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.condition.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.condition.note ? "edit" : "add"}`
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
@@ -549,11 +533,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this.condition.condition !== "trigger"
|
||||
? this._liveTestResult
|
||||
? this._liveTestResult.state
|
||||
: "unknown"}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult : "unknown"}`
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult.state : "unknown"}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test
|
||||
></ha-automation-row>`
|
||||
: html`
|
||||
@@ -585,102 +570,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getTriggerInfos = memoizeOne(getTriggerInfos);
|
||||
|
||||
private _renderTriggerConditionDescription(condition: TriggerCondition) {
|
||||
const ids = ensureArray(condition.id ?? [])
|
||||
.map((id) => (typeof id === "string" ? id : String(id)))
|
||||
.filter((id) => id !== "");
|
||||
const prefix = capitalizeFirstLetter(
|
||||
this.hass
|
||||
.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.description.full",
|
||||
{ id: "" }
|
||||
)
|
||||
.trim()
|
||||
);
|
||||
if (!ids.length) {
|
||||
return html`${prefix}
|
||||
<div class="trigger warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.description.no_trigger"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const triggerInfos = this._getTriggerInfos(
|
||||
ensureArray(this._automationConfig?.triggers || []),
|
||||
this.hass,
|
||||
this._entityReg
|
||||
);
|
||||
const infoById = new Map(triggerInfos.map((info) => [info.id, info]));
|
||||
return html`${prefix}
|
||||
${ids.map((id) => {
|
||||
const info = infoById.get(id);
|
||||
if (!info) {
|
||||
return html`<div class="trigger">
|
||||
<ha-trigger-id-chip id=${`trigger-${id}`} warning .triggerId=${id}>
|
||||
<ha-svg-icon slot="start" .path=${mdiAlert}></ha-svg-icon>
|
||||
</ha-trigger-id-chip>
|
||||
${ids.length < 4
|
||||
? html`<span
|
||||
>${this.hass.localize("state.default.unavailable")}</span
|
||||
>`
|
||||
: nothing}
|
||||
|
||||
<ha-tooltip .for=${`trigger-${id}`}>
|
||||
${ids.length >= 4
|
||||
? html`<div>
|
||||
${this.hass.localize("state.default.unavailable")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.unavailable_info",
|
||||
{ id: html`<b>${id}</b>` }
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>`;
|
||||
}
|
||||
const triggerIcon = html`<ha-trigger-icon
|
||||
.slot=${ids.length < 4 ? "start" : ""}
|
||||
.hass=${this.hass}
|
||||
.trigger=${info.triggerType}
|
||||
></ha-trigger-icon>`;
|
||||
|
||||
const isDuplicateId = info.count > 1;
|
||||
|
||||
return html`
|
||||
<div class="trigger">
|
||||
${ids.length < 4 ? triggerIcon : nothing}
|
||||
<ha-trigger-id-chip
|
||||
id=${`trigger-${id}`}
|
||||
.triggerId=${id}
|
||||
.warning=${isDuplicateId}
|
||||
>
|
||||
${isDuplicateId
|
||||
? html`<ha-svg-icon slot="start" .path=${mdiAlert}></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-trigger-id-chip>
|
||||
${ids.length < 4
|
||||
? html`<span>${info.label}</span>`
|
||||
: html`<ha-tooltip .for=${`trigger-${id}`}></ha-tooltip>`}
|
||||
${isDuplicateId || ids.length >= 4
|
||||
? html`<ha-tooltip .for=${`trigger-${id}`}>
|
||||
${ids.length >= 4
|
||||
? html`<div>${triggerIcon}${info.label}</div>`
|
||||
: nothing}
|
||||
${isDuplicateId
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate_id_warning"
|
||||
)
|
||||
: nothing}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}`;
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
@@ -736,7 +625,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _resetSubscription() {
|
||||
this._liveTestResult = "unknown";
|
||||
this._liveTestResult = {
|
||||
state: "unknown",
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.live_test_state.unknown"
|
||||
),
|
||||
};
|
||||
if (this._conditionUnsub) {
|
||||
this._conditionUnsub.then((unsub) => unsub());
|
||||
this._conditionUnsub = undefined;
|
||||
@@ -761,7 +655,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
if (result.error) {
|
||||
this._handleLiveTestError(result.error);
|
||||
} else {
|
||||
this._liveTestResult = result.result ? "pass" : "fail";
|
||||
this._liveTestResult = {
|
||||
state: result.result ? "pass" : "fail",
|
||||
message: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.testing_${result.result ? "pass" : "error"}`
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
this.condition
|
||||
@@ -778,7 +677,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
private _handleLiveTestError(error: any) {
|
||||
const invalid =
|
||||
typeof error !== "string" && error.code === "invalid_format";
|
||||
this._liveTestResult = invalid ? "invalid" : "unknown";
|
||||
this._liveTestResult = {
|
||||
state: invalid ? "invalid" : "unknown",
|
||||
message: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.${invalid ? "invalid_condition" : "live_test_state.unknown"}`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private _onValueChange(event: CustomEvent) {
|
||||
@@ -945,25 +849,25 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _editCommentCondition = async (): Promise<void> => {
|
||||
const comment = await showPromptDialog(this, {
|
||||
private _editNoteCondition = async (): Promise<void> => {
|
||||
const note = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.condition.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.condition.note ? "edit" : "add"}`
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
),
|
||||
inputType: "string",
|
||||
defaultValue: this.condition.comment,
|
||||
defaultValue: this.condition.note,
|
||||
confirmText: this.hass.localize("ui.common.submit"),
|
||||
multiline: true,
|
||||
});
|
||||
if (comment !== null) {
|
||||
if (note !== null) {
|
||||
const value = { ...this.condition };
|
||||
if (comment === "") {
|
||||
delete value.comment;
|
||||
if (note === "") {
|
||||
delete value.note;
|
||||
} else {
|
||||
value.comment = comment;
|
||||
value.note = note;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
@@ -1118,7 +1022,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
rename: () => {
|
||||
this._renameCondition();
|
||||
},
|
||||
editComment: this._editCommentCondition,
|
||||
editNote: this._editNoteCondition,
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
this.openSidebar();
|
||||
@@ -1190,8 +1094,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
case "rename":
|
||||
this._renameCondition();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this._editCommentCondition();
|
||||
case "edit_note":
|
||||
this._editNoteCondition();
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicateCondition();
|
||||
@@ -1224,26 +1128,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
rowStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
.trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
background-color: var(--ha-color-fill-neutral-normal-resting);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
padding-inline: var(--ha-space-2);
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
height: 32px;
|
||||
}
|
||||
.trigger.warning {
|
||||
background-color: var(--ha-color-fill-warning-normal-resting);
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [rowStyles, overflowStyles];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
const numericStateConditionStruct = object({
|
||||
alias: optional(string()),
|
||||
comment: optional(string()),
|
||||
note: optional(string()),
|
||||
condition: literal("numeric_state"),
|
||||
entity_id: optional(string()),
|
||||
attribute: optional(string()),
|
||||
|
||||
@@ -25,7 +25,7 @@ import type { ConditionElement } from "../ha-automation-condition-row";
|
||||
|
||||
const stateConditionStruct = object({
|
||||
alias: optional(string()),
|
||||
comment: optional(string()),
|
||||
note: optional(string()),
|
||||
condition: literal("state"),
|
||||
entity_id: optional(string()),
|
||||
attribute: optional(string()),
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } 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-alert";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../../components/list/types";
|
||||
import {
|
||||
automationConfigContext,
|
||||
flattenTriggers,
|
||||
type AutomationConfig,
|
||||
type Trigger,
|
||||
type TriggerCondition,
|
||||
} from "../../../../../data/automation";
|
||||
import {
|
||||
getTriggerInfos,
|
||||
type TriggerInfo,
|
||||
} from "../../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../ha-trigger-id-chip";
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
@customElement("ha-automation-condition-trigger")
|
||||
export class HaTriggerCondition extends LitElement {
|
||||
@@ -35,25 +30,9 @@ export class HaTriggerCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: automationConfigContext, subscribe: true })
|
||||
private _automationConfig?: AutomationConfig;
|
||||
@state() private _triggerIds: string[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
private _entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
private _triggerInfos = memoizeOne(
|
||||
(
|
||||
triggers: AutomationConfig["triggers"] | undefined,
|
||||
entityReg: EntityRegistryEntry[]
|
||||
): TriggerInfo[] =>
|
||||
getTriggerInfos(
|
||||
triggers ? ensureArray(triggers) : undefined,
|
||||
this.hass,
|
||||
entityReg
|
||||
)
|
||||
);
|
||||
private _unsub?: UnsubscribeFunc;
|
||||
|
||||
public static get defaultConfig(): TriggerCondition {
|
||||
return {
|
||||
@@ -62,146 +41,89 @@ 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() {
|
||||
const selectedIds = ensureArray(this.condition.id || []).filter(
|
||||
(id): id is string => typeof id === "string" && id !== ""
|
||||
);
|
||||
|
||||
const triggerInfos = this._triggerInfos(
|
||||
this._automationConfig?.triggers,
|
||||
this._entityReg
|
||||
);
|
||||
|
||||
if (!triggerInfos.length && !selectedIds.length) {
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
|
||||
)}
|
||||
</ha-alert>
|
||||
`;
|
||||
if (!this._triggerIds.length) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
|
||||
);
|
||||
}
|
||||
|
||||
const schema = this._schema(this._triggerIds);
|
||||
|
||||
return html`
|
||||
<ha-list-selectable @ha-list-selected=${this._valueChanged} multi>
|
||||
${this._renderOptions(selectedIds, triggerInfos)}
|
||||
</ha-list-selectable>
|
||||
<ha-form
|
||||
.schema=${schema}
|
||||
.data=${this.condition}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOptions(selectedIds: string[], triggerInfos: TriggerInfo[]) {
|
||||
const unknownTriggerIds = selectedIds.filter(
|
||||
(id) => !triggerInfos.some((info) => info.id === id)
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.trigger.${schema.name}`
|
||||
);
|
||||
|
||||
const alertIcon = html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAlert}
|
||||
></ha-svg-icon>`;
|
||||
|
||||
return html`
|
||||
${unknownTriggerIds.map(
|
||||
(id) => html`
|
||||
<ha-list-item-option
|
||||
.value=${id}
|
||||
.selected=${true}
|
||||
appearance="checkbox"
|
||||
>
|
||||
<div class="option" slot="headline">
|
||||
<ha-trigger-id-chip
|
||||
id=${`trigger-${id}`}
|
||||
warning
|
||||
.triggerId=${id}
|
||||
>
|
||||
${alertIcon}
|
||||
</ha-trigger-id-chip>
|
||||
${this.hass.localize("state.default.unavailable")}
|
||||
<ha-tooltip .for=${`trigger-${id}`}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.unavailable_info",
|
||||
{ id: html`<b>${id}</b>` }
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
${triggerInfos.map(
|
||||
(info) => html`
|
||||
<ha-list-item-option
|
||||
.value=${info.id}
|
||||
.selected=${selectedIds.includes(info.id)}
|
||||
appearance="checkbox"
|
||||
>
|
||||
<div class="option" slot="headline">
|
||||
<ha-trigger-id-chip
|
||||
id=${`trigger-${info.id}`}
|
||||
.warning=${info.count > 1}
|
||||
.triggerId=${info.id}
|
||||
>
|
||||
${info.count > 1 ? alertIcon : nothing}
|
||||
</ha-trigger-id-chip>
|
||||
${info.label}${info.count > 1
|
||||
? html`<ha-tooltip .for=${`trigger-${info.id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.duplicated_info"
|
||||
)}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
private _automationUpdated(config?: AutomationConfig) {
|
||||
this._triggerIds = config?.triggers
|
||||
? getTriggersIds(ensureArray(config.triggers))
|
||||
: [];
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent<HaListSelectedDetail>): void {
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (
|
||||
!ev.detail.diff ||
|
||||
(!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
const ids = ensureArray(this.condition.id || []);
|
||||
|
||||
const valueSet = ev.detail.diff.added.size
|
||||
? ev.detail.diff.added
|
||||
: ev.detail.diff.removed;
|
||||
|
||||
const index = valueSet.values().next().value;
|
||||
|
||||
if (index === undefined) {
|
||||
return;
|
||||
}
|
||||
const triggerId = (
|
||||
(ev.currentTarget as HaListSelectable).items[index] as HaListItemOption
|
||||
).value;
|
||||
if (triggerId === undefined || triggerId === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.detail.diff.added.size) {
|
||||
ids.push(triggerId);
|
||||
} else {
|
||||
const removeIndex = ids.indexOf(triggerId);
|
||||
if (removeIndex > -1) {
|
||||
ids.splice(removeIndex, 1);
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: { ...this.condition, id: ids } });
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-1);
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { provide } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiCog,
|
||||
@@ -21,9 +20,10 @@ import {
|
||||
mdiTransitConnection,
|
||||
mdiUndo,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
@@ -31,7 +31,6 @@ import { goBack, navigate } from "../../../common/navigate";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -46,7 +45,6 @@ import type {
|
||||
Trigger,
|
||||
} from "../../../data/automation";
|
||||
import {
|
||||
automationConfigContext,
|
||||
deleteAutomation,
|
||||
fetchAutomationFileConfig,
|
||||
getAutomationEditorInitData,
|
||||
@@ -74,12 +72,13 @@ import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { Entries, ValueChangedEvent } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showEditorToast } from "./editor-toast";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
|
||||
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
|
||||
import { ADD_AUTOMATION_ELEMENT_QUERY_PARAM } from "./show-add-automation-element-dialog";
|
||||
import "./blueprint-automation-editor";
|
||||
import { showEditorToast } from "./editor-toast";
|
||||
import type { EditorDomainHooks } from "./ha-automation-script-editor-mixin";
|
||||
import {
|
||||
AutomationScriptEditorMixin,
|
||||
@@ -87,7 +86,7 @@ import {
|
||||
} from "./ha-automation-script-editor-mixin";
|
||||
import "./manual-automation-editor";
|
||||
import type { HaManualAutomationEditor } from "./manual-automation-editor";
|
||||
import { ADD_AUTOMATION_ELEMENT_QUERY_PARAM } from "./show-add-automation-element-dialog";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -95,6 +94,10 @@ declare global {
|
||||
}
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"subscribe-automation-config": {
|
||||
callback: (config: AutomationConfig) => void;
|
||||
unsub?: UnsubscribeFunc;
|
||||
};
|
||||
"ui-mode-not-available": Error;
|
||||
"move-down": undefined;
|
||||
"move-up": undefined;
|
||||
@@ -122,9 +125,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
@provide({ context: automationConfigContext })
|
||||
@state()
|
||||
protected config?: AutomationConfig;
|
||||
private _configSubscriptions: Record<
|
||||
string,
|
||||
(config?: AutomationConfig) => void
|
||||
> = {};
|
||||
|
||||
private _configSubscriptionsId = 1;
|
||||
|
||||
private _newAutomationId?: string;
|
||||
|
||||
@@ -398,7 +404,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
</ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
|
||||
<div
|
||||
class=${this.mode === "yaml" ? "yaml-mode" : ""}
|
||||
@subscribe-automation-config=${this._subscribeAutomationConfig}
|
||||
>
|
||||
${this.mode === "gui"
|
||||
? html`
|
||||
<div>
|
||||
@@ -629,6 +638,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
) {
|
||||
this._setEntityId();
|
||||
}
|
||||
|
||||
if (changedProps.has("config")) {
|
||||
Object.values(this._configSubscriptions).forEach((sub) =>
|
||||
sub(this.config)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _setEntityId() {
|
||||
@@ -1006,6 +1021,15 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
}
|
||||
}
|
||||
|
||||
private _subscribeAutomationConfig(ev) {
|
||||
const id = this._configSubscriptionsId++;
|
||||
this._configSubscriptions[id] = ev.detail.callback;
|
||||
ev.detail.unsub = () => {
|
||||
delete this._configSubscriptions[id];
|
||||
};
|
||||
ev.detail.callback(this.config);
|
||||
}
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
return {
|
||||
s: () => this._handleSaveAutomation(),
|
||||
|
||||
+9
-9
@@ -6,9 +6,9 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-settings-row";
|
||||
import { internationalizationContext } from "../../../data/context";
|
||||
|
||||
@customElement("ha-automation-comment")
|
||||
export class HaAutomationComment extends LitElement {
|
||||
@property() public comment!: string;
|
||||
@customElement("ha-automation-note")
|
||||
export class HaAutomationNote extends LitElement {
|
||||
@property() public note!: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@@ -18,9 +18,9 @@ export class HaAutomationComment extends LitElement {
|
||||
return html`
|
||||
<ha-settings-row narrow>
|
||||
<div class="heading" slot="heading">
|
||||
<span class="title" id="comment-label">
|
||||
<span class="title" id="note-label">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
@@ -31,13 +31,13 @@ export class HaAutomationComment extends LitElement {
|
||||
${this._i18n.localize("ui.common.edit")}
|
||||
</ha-button>
|
||||
</div>
|
||||
<p aria-labelledby="comment-label">${this.comment}</p>
|
||||
<p aria-labelledby="note-label">${this.note}</p>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
fireEvent(this, "edit-comment");
|
||||
fireEvent(this, "edit-note");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -70,10 +70,10 @@ export class HaAutomationComment extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-comment": HaAutomationComment;
|
||||
"ha-automation-note": HaAutomationNote;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"edit-comment": undefined;
|
||||
"edit-note": undefined;
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,6 @@ class HaConfigAutomation extends HassRouterPage {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ attribute: false }) public automations: AutomationEntity[] = [];
|
||||
@@ -79,7 +77,6 @@ class HaConfigAutomation extends HassRouterPage {
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.route = this.routeTail;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
pageEl.cloudStatus = this.cloudStatus;
|
||||
|
||||
if (this.hass) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { mdiPound } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-svg-icon";
|
||||
|
||||
/**
|
||||
* Home Assistant trigger ID chip component
|
||||
*
|
||||
* @element ha-trigger-id-chip
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A small chip that displays an automation trigger ID prefixed with a hash icon.
|
||||
*
|
||||
* @slot start - Optional content rendered before the hash icon (usually an icon).
|
||||
*
|
||||
* @attr {string} trigger-id - The trigger ID to display.
|
||||
* @attr {boolean} warning - Renders the chip with warning colors.
|
||||
*/
|
||||
@customElement("ha-trigger-id-chip")
|
||||
export class HaTriggerIdChip extends LitElement {
|
||||
@property({ attribute: "trigger-id" }) public triggerId!: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public warning = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<slot name="start"></slot>
|
||||
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
|
||||
<span>${this.triggerId}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border: var(--ha-border-width-sm) solid
|
||||
var(--ha-color-border-neutral-normal);
|
||||
--mdc-icon-size: 16px;
|
||||
display: inline-flex;
|
||||
gap: var(--ha-space-1);
|
||||
align-items: center;
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
padding: 0 var(--ha-space-1);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
:host([warning]) {
|
||||
border-color: var(--ha-color-border-warning-normal);
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-trigger-id-chip": HaTriggerIdChip;
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,11 @@ import {
|
||||
normalizeAutomationConfig,
|
||||
} from "../../../data/automation";
|
||||
import { getActionType, type Action } from "../../../data/script";
|
||||
import { showEditorToast } from "./editor-toast";
|
||||
import "./action/ha-automation-action";
|
||||
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 { ManualEditorMixin } from "./ha-manual-editor-mixin";
|
||||
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { manualEditorStyles, saveFabStyles } from "./styles";
|
||||
|
||||
@@ -142,8 +142,8 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
private _renderRow() {
|
||||
const commentTooltipText = truncateWithEllipsis(
|
||||
this.option?.comment?.trim() || "",
|
||||
const noteTooltipText = truncateWithEllipsis(
|
||||
this.option?.note?.trim() || "",
|
||||
250
|
||||
);
|
||||
|
||||
@@ -157,19 +157,18 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.default"
|
||||
)}
|
||||
${this.option?.comment?.trim()
|
||||
${this.option?.note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="comment-icon"
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
)}
|
||||
class="comment-indicator"
|
||||
class="note-indicator"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="comment-icon"
|
||||
><p>${commentTooltipText}</p></ha-tooltip
|
||||
>
|
||||
<ha-tooltip for="note-icon"><p>${noteTooltipText}</p></ha-tooltip>
|
||||
`
|
||||
: nothing}
|
||||
</h3>
|
||||
@@ -199,14 +198,14 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit_comment">
|
||||
<ha-dropdown-item value="edit_note">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiCommentEditOutline}
|
||||
></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.option?.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.option?.note ? "edit" : "add"}`
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
@@ -394,8 +393,8 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
case "rename":
|
||||
this._renameOption();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this._editCommentOption();
|
||||
case "edit_note":
|
||||
this._editNoteOption();
|
||||
break;
|
||||
case "delete":
|
||||
this._removeOption();
|
||||
@@ -460,28 +459,28 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _editCommentOption = async (): Promise<void> => {
|
||||
private _editNoteOption = async (): Promise<void> => {
|
||||
if (!this.option) {
|
||||
return;
|
||||
}
|
||||
const comment = await showPromptDialog(this, {
|
||||
const note = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.option.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.option.note ? "edit" : "add"}`
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
),
|
||||
inputType: "string",
|
||||
defaultValue: this.option.comment,
|
||||
defaultValue: this.option.note,
|
||||
confirmText: this.hass.localize("ui.common.submit"),
|
||||
multiline: true,
|
||||
});
|
||||
if (comment !== null) {
|
||||
if (note !== null) {
|
||||
const value: Option = { ...this.option };
|
||||
if (comment === "") {
|
||||
delete value.comment;
|
||||
if (note === "") {
|
||||
delete value.note;
|
||||
} else {
|
||||
value.comment = comment;
|
||||
value.note = note;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
@@ -537,11 +536,11 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
rename: () => {
|
||||
this._renameOption();
|
||||
},
|
||||
editComment: this._editCommentOption,
|
||||
editNote: this._editNoteOption,
|
||||
delete: this._removeOption,
|
||||
duplicate: this._duplicateOption,
|
||||
defaultOption: !!this.defaultActions,
|
||||
comment: sidebarOption?.comment,
|
||||
note: sidebarOption?.note,
|
||||
} satisfies OptionSidebarConfig);
|
||||
this._selected = true;
|
||||
this._collapsed = false;
|
||||
|
||||
@@ -39,7 +39,7 @@ import { isMac } from "../../../../util/is_mac";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import { getAutomationActionType } from "../action/ha-automation-action-row";
|
||||
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
|
||||
import "../ha-automation-comment";
|
||||
import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@@ -177,11 +177,11 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item slot="menu-items" value="edit_comment">
|
||||
<ha-dropdown-item slot="menu-items" value="edit_note">
|
||||
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.config.config.action.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.config.config.action.note ? "edit" : "add"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -388,11 +388,11 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-action-editor>`
|
||||
)}
|
||||
${this.config.config.action.comment?.trim() && !this.yamlMode
|
||||
? html`<ha-automation-comment
|
||||
@edit-comment=${this.config.editComment}
|
||||
.comment=${this.config.config.action.comment}
|
||||
></ha-automation-comment>`
|
||||
${this.config.config.action.note?.trim() && !this.yamlMode
|
||||
? html`<ha-automation-note
|
||||
@edit-note=${this.config.editNote}
|
||||
.note=${this.config.config.action.note}
|
||||
></ha-automation-note>`
|
||||
: nothing}
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
@@ -442,8 +442,8 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
case "rename":
|
||||
this.config.rename();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this.config.editComment();
|
||||
case "edit_note":
|
||||
this.config.editNote();
|
||||
break;
|
||||
case "run":
|
||||
this.config.run();
|
||||
|
||||
@@ -35,7 +35,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import "../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
|
||||
import "../ha-automation-comment";
|
||||
import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@@ -153,13 +153,13 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="edit_comment"
|
||||
value="edit_note"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.config.config.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.config.config.note ? "edit" : "add"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -347,11 +347,11 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
sidebar
|
||||
></ha-automation-condition-editor>`
|
||||
)}
|
||||
${this.config.config.comment?.trim() && !this.yamlMode
|
||||
? html`<ha-automation-comment
|
||||
@edit-comment=${this.config.editComment}
|
||||
.comment=${this.config.config.comment}
|
||||
></ha-automation-comment>`
|
||||
${this.config.config.note?.trim() && !this.yamlMode
|
||||
? html`<ha-automation-note
|
||||
@edit-note=${this.config.editNote}
|
||||
.note=${this.config.config.note}
|
||||
></ha-automation-note>`
|
||||
: nothing}
|
||||
<div class="testing-wrapper">
|
||||
<div
|
||||
@@ -417,8 +417,8 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
case "rename":
|
||||
this.config.rename();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this.config.editComment();
|
||||
case "edit_note":
|
||||
this.config.editNote();
|
||||
break;
|
||||
case "test":
|
||||
this.config.test();
|
||||
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { OptionSidebarConfig } from "../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import "../ha-automation-comment";
|
||||
import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "./ha-automation-sidebar-card";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
|
||||
@customElement("ha-automation-sidebar-option")
|
||||
export default class HaAutomationSidebarOption extends LitElement {
|
||||
@@ -76,7 +76,7 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="edit_comment"
|
||||
value="edit_note"
|
||||
.disabled=${!!disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -85,7 +85,7 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.config.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.config.note ? "edit" : "add"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -144,11 +144,11 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
`}
|
||||
|
||||
<div class="description">${description}</div>
|
||||
${!this.config.defaultOption && this.config.comment?.trim()
|
||||
? html`<ha-automation-comment
|
||||
@edit-comment=${this.config.editComment}
|
||||
.comment=${this.config.comment}
|
||||
></ha-automation-comment>`
|
||||
${!this.config.defaultOption && this.config.note?.trim()
|
||||
? html`<ha-automation-note
|
||||
@edit-note=${this.config.editNote}
|
||||
.note=${this.config.note}
|
||||
></ha-automation-note>`
|
||||
: nothing}
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
@@ -164,8 +164,8 @@ export default class HaAutomationSidebarOption extends LitElement {
|
||||
case "rename":
|
||||
this.config.rename();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this.config.editComment();
|
||||
case "edit_note":
|
||||
this.config.editNote();
|
||||
break;
|
||||
case "duplicate":
|
||||
this.config.duplicate();
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import "../../script/ha-script-field-editor";
|
||||
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
|
||||
import "../ha-automation-comment";
|
||||
import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "./ha-automation-sidebar-card";
|
||||
|
||||
@@ -68,11 +68,11 @@ export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<ha-dropdown-item slot="menu-items" value="edit_comment">
|
||||
<ha-dropdown-item slot="menu-items" value="edit_note">
|
||||
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${this.config.config.field.description ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${this.config.config.field.description ? "edit" : "add"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -137,10 +137,10 @@ export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
></ha-script-field-editor>`
|
||||
)}
|
||||
${this.config.config.field.description?.trim() && !this.yamlMode
|
||||
? html`<ha-automation-comment
|
||||
@edit-comment=${this.config.editComment}
|
||||
.comment=${this.config.config.field.description}
|
||||
></ha-automation-comment>`
|
||||
? html`<ha-automation-note
|
||||
@edit-note=${this.config.editNote}
|
||||
.note=${this.config.config.field.description}
|
||||
></ha-automation-note>`
|
||||
: nothing}
|
||||
</ha-automation-sidebar-card>`;
|
||||
}
|
||||
@@ -189,8 +189,8 @@ export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
case "toggle_yaml_mode":
|
||||
this._toggleYamlMode();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this.config.editComment();
|
||||
case "edit_note":
|
||||
this.config.editNote();
|
||||
break;
|
||||
case "delete":
|
||||
this.config.delete();
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from "../../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import "../ha-automation-comment";
|
||||
import "../ha-automation-note";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
|
||||
@@ -132,7 +132,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
${type !== "list"
|
||||
? html`<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="edit_comment"
|
||||
value="edit_note"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -141,7 +141,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${(this.config.config as Exclude<Trigger, TriggerList>).comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${(this.config.config as Exclude<Trigger, TriggerList>).note ? "edit" : "add"}`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -343,12 +343,12 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
></ha-automation-trigger-editor>`
|
||||
)}
|
||||
${!isTriggerList(this.config.config) &&
|
||||
this.config.config.comment?.trim() &&
|
||||
this.config.config.note?.trim() &&
|
||||
!this.yamlMode
|
||||
? html`<ha-automation-comment
|
||||
@edit-comment=${this.config.editComment}
|
||||
.comment=${this.config.config.comment}
|
||||
></ha-automation-comment>`
|
||||
? html`<ha-automation-note
|
||||
@edit-note=${this.config.editNote}
|
||||
.note=${this.config.config.note}
|
||||
></ha-automation-note>`
|
||||
: nothing}
|
||||
</ha-automation-sidebar-card>
|
||||
`;
|
||||
@@ -401,8 +401,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
case "rename":
|
||||
this.config.rename();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this.config.editComment();
|
||||
case "edit_note":
|
||||
this.config.editNote();
|
||||
break;
|
||||
case "show_id":
|
||||
this._showTriggerId();
|
||||
|
||||
@@ -4,7 +4,7 @@ export const baseTriggerStruct = object({
|
||||
trigger: string(),
|
||||
id: optional(string()),
|
||||
enabled: optional(boolean()),
|
||||
comment: optional(string()),
|
||||
note: optional(string()),
|
||||
});
|
||||
|
||||
export const forDictStruct = object({
|
||||
|
||||
@@ -53,14 +53,14 @@ export const rowStyles = css`
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.comment-indicator {
|
||||
.note-indicator {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
.comment-indicator + ha-tooltip::part(body) {
|
||||
.note-indicator + ha-tooltip::part(body) {
|
||||
cursor: default;
|
||||
max-width: 300px;
|
||||
}
|
||||
.comment-indicator + ha-tooltip p {
|
||||
.note-indicator + ha-tooltip p {
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
|
||||
...(this.trigger.comment ? { comment: this.trigger.comment } : {}),
|
||||
...(this.trigger.note ? { note: this.trigger.note } : {}),
|
||||
...ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -29,7 +28,6 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -50,21 +48,15 @@ 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,
|
||||
AutomationConfig,
|
||||
PlatformTrigger,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
TriggerSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import {
|
||||
automationConfigContext,
|
||||
isTrigger,
|
||||
subscribeTrigger,
|
||||
} from "../../../../data/automation";
|
||||
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@@ -81,7 +73,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { showEditorToast } from "../editor-toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import "../ha-trigger-id-chip";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import "./ha-automation-trigger-editor";
|
||||
@@ -187,30 +178,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: automationConfigContext, subscribe: true })
|
||||
@transform<AutomationConfig, boolean>({
|
||||
transformer: function (this: HaAutomationTriggerRow, value) {
|
||||
if (
|
||||
!this.trigger ||
|
||||
isTriggerList(this.trigger) ||
|
||||
!(this.trigger as Exclude<Trigger, TriggerList>).id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const triggerId = (this.trigger as Exclude<Trigger, TriggerList>).id;
|
||||
// count how often this trigger id is used in the automation, if more than once, show warning
|
||||
return (
|
||||
ensureArray(value?.triggers || []).filter(
|
||||
(trigger) =>
|
||||
(trigger as Exclude<Trigger, TriggerList>).id === triggerId
|
||||
).length > 1
|
||||
);
|
||||
},
|
||||
watch: ["trigger"],
|
||||
})
|
||||
private _duplicateTriggerId = false;
|
||||
|
||||
get selected() {
|
||||
return this._selected;
|
||||
}
|
||||
@@ -257,9 +224,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
?.target
|
||||
: undefined;
|
||||
|
||||
const commentTooltipText = truncateWithEllipsis(
|
||||
const noteTooltipText = truncateWithEllipsis(
|
||||
(type !== "list" &&
|
||||
(this.trigger as Exclude<Trigger, TriggerList>).comment?.trim()) ||
|
||||
(this.trigger as Exclude<Trigger, TriggerList>).note?.trim()) ||
|
||||
"",
|
||||
250
|
||||
);
|
||||
@@ -277,28 +244,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
|
||||
></ha-trigger-icon>`}
|
||||
<h3 slot="header">
|
||||
${type !== "list" && (this.trigger as Exclude<Trigger, TriggerList>).id
|
||||
? html`<ha-trigger-id-chip
|
||||
id="trigger-id-chip"
|
||||
.warning=${this._duplicateTriggerId}
|
||||
slot="leading-icon"
|
||||
.triggerId=${(this.trigger as Exclude<Trigger, TriggerList>).id}
|
||||
>
|
||||
${this._duplicateTriggerId
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAlert}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-trigger-id-chip>
|
||||
${this._duplicateTriggerId
|
||||
? html`<ha-tooltip for="trigger-id-chip">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate_id_warning"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: nothing} `
|
||||
: nothing}
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${target !== undefined || (descriptionHasTarget && !this._isNew)
|
||||
? this._renderTargets(
|
||||
@@ -308,19 +253,18 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)
|
||||
: nothing}
|
||||
${type !== "list" &&
|
||||
(this.trigger as Exclude<Trigger, TriggerList>).comment?.trim()
|
||||
(this.trigger as Exclude<Trigger, TriggerList>).note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="comment-icon"
|
||||
tabindex="0"
|
||||
id="note-icon"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
)}
|
||||
class="comment-indicator"
|
||||
class="note-indicator"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="comment-icon"
|
||||
><p>${commentTooltipText}</p></ha-tooltip
|
||||
>
|
||||
<ha-tooltip for="note-icon"><p>${noteTooltipText}</p></ha-tooltip>
|
||||
`
|
||||
: nothing}
|
||||
</h3>
|
||||
@@ -364,14 +308,14 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
${type !== "list"
|
||||
? html`<ha-dropdown-item value="edit_comment">
|
||||
? html`<ha-dropdown-item value="edit_note">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiCommentEditOutline}
|
||||
></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${(this.trigger as Exclude<Trigger, TriggerList>).comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${(this.trigger as Exclude<Trigger, TriggerList>).note ? "edit" : "add"}`
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
@@ -752,7 +696,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
rename: () => {
|
||||
this._renameTrigger();
|
||||
},
|
||||
editComment: this._editCommentTrigger,
|
||||
editNote: this._editNoteTrigger,
|
||||
toggleYamlMode: () => {
|
||||
this._toggleYamlMode();
|
||||
this.openSidebar();
|
||||
@@ -896,27 +840,27 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _editCommentTrigger = async (): Promise<void> => {
|
||||
private _editNoteTrigger = async (): Promise<void> => {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const trigger = this.trigger;
|
||||
const comment = await showPromptDialog(this, {
|
||||
const note = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.comment.${trigger.comment ? "edit" : "add"}`
|
||||
`ui.panel.config.automation.editor.note.${trigger.note ? "edit" : "add"}`
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.comment.label"
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
),
|
||||
inputType: "string",
|
||||
defaultValue: trigger.comment,
|
||||
defaultValue: trigger.note,
|
||||
confirmText: this.hass.localize("ui.common.submit"),
|
||||
multiline: true,
|
||||
});
|
||||
if (comment !== null) {
|
||||
if (note !== null) {
|
||||
const value = { ...trigger };
|
||||
if (comment === "") {
|
||||
delete value.comment;
|
||||
if (note === "") {
|
||||
delete value.note;
|
||||
} else {
|
||||
value.comment = comment;
|
||||
value.note = note;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
@@ -1041,8 +985,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
case "rename":
|
||||
this._renameTrigger();
|
||||
break;
|
||||
case "edit_comment":
|
||||
this._editCommentTrigger();
|
||||
case "edit_note":
|
||||
this._editNoteTrigger();
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicateTrigger();
|
||||
|
||||
@@ -8,7 +8,6 @@ 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";
|
||||
@@ -22,20 +21,15 @@ import {
|
||||
} from "../../../../data/automation";
|
||||
import { subscribeLabFeature } from "../../../../data/labs";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import {
|
||||
getNextNumericTriggerId,
|
||||
getUniqueTriggerId,
|
||||
isTriggerList,
|
||||
subscribeTriggers,
|
||||
} from "../../../../data/trigger";
|
||||
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
import {
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
@@ -73,53 +67,6 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
this.highlightedTriggers = items;
|
||||
}
|
||||
|
||||
protected override pasteItem(ev: CustomEvent) {
|
||||
if (this.root && ev.detail.item) {
|
||||
const pasted = deepClone(ev.detail.item) as Trigger;
|
||||
if (!isTriggerList(pasted)) {
|
||||
pasted.id = pasted.id
|
||||
? getUniqueTriggerId(pasted.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
ev.detail.item = pasted;
|
||||
}
|
||||
super.pasteItem(ev);
|
||||
}
|
||||
|
||||
protected override insertAfter(ev: CustomEvent) {
|
||||
// Only dedupe when a single trigger is being inserted.
|
||||
const incoming = ensureArray(ev.detail.value) as Trigger[];
|
||||
if (this.root && incoming.length === 1) {
|
||||
const trigger = deepClone(incoming[0]);
|
||||
if (!isTriggerList(trigger)) {
|
||||
trigger.id = trigger.id
|
||||
? getUniqueTriggerId(trigger.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
ev.detail.value = trigger;
|
||||
}
|
||||
super.insertAfter(ev);
|
||||
}
|
||||
|
||||
protected override duplicateItem(ev: CustomEvent) {
|
||||
if (this.root) {
|
||||
const index = (ev.target as any).index;
|
||||
const duplicated = deepClone(this.triggers[index]);
|
||||
if (!isTriggerList(duplicated)) {
|
||||
duplicated.id = duplicated.id
|
||||
? getUniqueTriggerId(duplicated.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.triggers.toSpliced(index + 1, 0, duplicated),
|
||||
});
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
super.duplicateItem(ev);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
@@ -266,36 +213,23 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
private _addTrigger = (value: string, target?: HassServiceTarget) => {
|
||||
let triggers: Trigger[];
|
||||
if (value === PASTE_VALUE) {
|
||||
const pasted = deepClone(this._clipboard!.trigger!);
|
||||
if (this.root && !isTriggerList(pasted)) {
|
||||
pasted.id = pasted.id
|
||||
? getUniqueTriggerId(pasted.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
triggers = this.triggers.concat(pasted);
|
||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger!));
|
||||
} else if (isDynamic(value)) {
|
||||
triggers = this.triggers.concat({
|
||||
trigger: getValueFromDynamic(value),
|
||||
target,
|
||||
});
|
||||
} else {
|
||||
let newTrigger: Trigger;
|
||||
if (isDynamic(value)) {
|
||||
newTrigger = {
|
||||
trigger: getValueFromDynamic(value),
|
||||
target,
|
||||
};
|
||||
} else {
|
||||
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-trigger-${trigger}`
|
||||
) as CustomElementConstructor & {
|
||||
defaultConfig: Trigger;
|
||||
};
|
||||
newTrigger = {
|
||||
...elClass.defaultConfig,
|
||||
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
|
||||
};
|
||||
}
|
||||
if (this.root && !isTriggerList(newTrigger)) {
|
||||
newTrigger.id = getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
triggers = this.triggers.concat(newTrigger);
|
||||
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-trigger-${trigger}`
|
||||
) as CustomElementConstructor & {
|
||||
defaultConfig: Trigger;
|
||||
};
|
||||
triggers = this.triggers.concat({
|
||||
...elClass.defaultConfig,
|
||||
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
|
||||
});
|
||||
}
|
||||
this.focusLastItemOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: triggers });
|
||||
|
||||
@@ -29,7 +29,7 @@ const DEFAULT_KEYS: (keyof PlatformTrigger)[] = [
|
||||
"trigger",
|
||||
"target",
|
||||
"alias",
|
||||
"comment",
|
||||
"note",
|
||||
"id",
|
||||
"variables",
|
||||
"enabled",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiCog, mdiContentCopy } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -13,10 +13,9 @@ import "../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import {
|
||||
automationConfigContext,
|
||||
type AutomationConfig,
|
||||
type WebhookTrigger,
|
||||
import type {
|
||||
AutomationConfig,
|
||||
WebhookTrigger,
|
||||
} from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { showEditorToast } from "../../editor-toast";
|
||||
@@ -34,9 +33,9 @@ export class HaWebhookTrigger extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@consume({ context: automationConfigContext, subscribe: true })
|
||||
@state()
|
||||
private _config?: AutomationConfig;
|
||||
@state() private _config?: AutomationConfig;
|
||||
|
||||
private _unsub?: UnsubscribeFunc;
|
||||
|
||||
public static get defaultConfig(): WebhookTrigger {
|
||||
return {
|
||||
@@ -47,6 +46,24 @@ export class HaWebhookTrigger extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const details = {
|
||||
callback: (config) => {
|
||||
this._config = config;
|
||||
},
|
||||
};
|
||||
fireEvent(this, "subscribe-automation-config", details);
|
||||
this._unsub = (details as any).unsub;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub();
|
||||
}
|
||||
}
|
||||
|
||||
private _generateWebhookId(): string {
|
||||
// The webhook_id should be treated like a password. Generate a default
|
||||
// value that would be hard for someone to guess. This generates a
|
||||
|
||||
@@ -22,8 +22,6 @@ class HaConfigBlueprint extends HassRouterPage {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public blueprints: Record<string, Blueprints> = {};
|
||||
|
||||
@@ -61,7 +59,6 @@ class HaConfigBlueprint extends HassRouterPage {
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.route = this.routeTail;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
pageEl.blueprints = this.blueprints;
|
||||
|
||||
if (
|
||||
|
||||
@@ -167,14 +167,15 @@ export class DialogTryTts extends LitElement {
|
||||
}
|
||||
this._message = message;
|
||||
|
||||
if (this._target === "browser") {
|
||||
const target = this._target || "browser";
|
||||
if (target === "browser") {
|
||||
// We create the audio element here + do a play, because iOS requires it to be done by user action
|
||||
const audio = new Audio();
|
||||
audio.play();
|
||||
this._playBrowser(message, audio);
|
||||
} else {
|
||||
this.hass.callService("tts", "cloud_say", {
|
||||
entity_id: this._target,
|
||||
entity_id: target,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
try {
|
||||
await cloudRegister(this.hass, email, password);
|
||||
this._verificationEmailSent(email);
|
||||
this._verificationEmailSent(email, "account_created");
|
||||
} catch (err: any) {
|
||||
this._password = "";
|
||||
this._requestInProgress = false;
|
||||
@@ -238,15 +238,18 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
const email = emailField.value || "";
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
const doResend = async (username: string) => {
|
||||
try {
|
||||
await cloudResendVerification(this.hass, username);
|
||||
this._verificationEmailSent(username);
|
||||
this._verificationEmailSent(username, "verification_email_sent");
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doResend(username.toLowerCase());
|
||||
} else {
|
||||
this._requestInProgress = false;
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
@@ -258,13 +261,16 @@ export class CloudRegister extends LitElement {
|
||||
await doResend(email);
|
||||
}
|
||||
|
||||
private _verificationEmailSent(email: string) {
|
||||
private _verificationEmailSent(
|
||||
email: string,
|
||||
messageKey: "account_created" | "verification_email_sent"
|
||||
) {
|
||||
this._requestInProgress = false;
|
||||
this._password = "";
|
||||
fireEvent(this, "cloud-email-changed", { value: email });
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
"ui.panel.config.cloud.register.account_created"
|
||||
`ui.panel.config.cloud.register.${messageKey}`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,7 +211,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
<div class="unit-system-options">
|
||||
<ha-select-box
|
||||
name="unit_system"
|
||||
.hass=${this.hass}
|
||||
.value=${this._unitSystem}
|
||||
.disabled=${disabled}
|
||||
@value-changed=${this._unitSystemChanged}
|
||||
|
||||
@@ -42,8 +42,6 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@state() private _latestBackupDate?: Date;
|
||||
|
||||
@state() private _boardName?: string;
|
||||
|
||||
@@ -35,8 +35,6 @@ export class DeveloperYamlConfig extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@state() private _validating = false;
|
||||
|
||||
@state() private _reloadableDomains: TranslatedReloadableDomain[] = [];
|
||||
|
||||
@@ -188,8 +188,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@state() private _related?: RelatedResult;
|
||||
|
||||
@state() private _diagnosticDownloadLinks: DeviceAction[] = [];
|
||||
|
||||
@@ -18,8 +18,6 @@ class HaConfigDevices extends HassRouterPage {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
@@ -53,7 +51,6 @@ class HaConfigDevices extends HassRouterPage {
|
||||
pageEl.manifests = this._manifests;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,6 @@ class HaConfigEnergy extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -23,6 +23,7 @@ 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";
|
||||
@@ -56,6 +57,7 @@ import { updateDeviceRegistryEntry } from "../../../data/device/device_registry"
|
||||
import type {
|
||||
AlarmControlPanelEntityOptions,
|
||||
CalendarEntityOptions,
|
||||
DeviceTrackerEntityOptions,
|
||||
EntityRegistryEntry,
|
||||
EntityRegistryEntryUpdateParams,
|
||||
ExtEntityRegistryEntry,
|
||||
@@ -97,7 +99,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
export const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: [
|
||||
[
|
||||
"awning",
|
||||
@@ -138,6 +140,10 @@ const SWITCH_AS_DOMAINS_INVERT = ["cover", "lock", "valve"];
|
||||
|
||||
const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
const SCANNER_SOURCE_TYPES = ["router", "bluetooth", "bluetooth_le"];
|
||||
|
||||
const ZONE_DOMAINS = ["zone"];
|
||||
|
||||
@customElement("entity-registry-settings-editor")
|
||||
export class EntityRegistrySettingsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -202,6 +208,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@state() private _calendarColor?: string | null;
|
||||
|
||||
@state() private _associatedZone?: string;
|
||||
|
||||
@state() private _noDeviceArea?: boolean;
|
||||
|
||||
private _origEntityId!: string;
|
||||
@@ -292,6 +300,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._calendarColor = this.entry.options?.calendar?.color;
|
||||
}
|
||||
|
||||
if (domain === "device_tracker") {
|
||||
this._associatedZone =
|
||||
this.entry.options?.device_tracker?.associated_zone ?? "zone.home";
|
||||
}
|
||||
|
||||
if (domain === "weather") {
|
||||
const stateObj: HassEntity | undefined =
|
||||
this.hass.states[this.entry.entity_id];
|
||||
@@ -713,6 +726,21 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
></ha-color-picker>
|
||||
`
|
||||
: nothing}
|
||||
${domain === "device_tracker" &&
|
||||
SCANNER_SOURCE_TYPES.includes(stateObj?.attributes?.source_type)
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._associatedZone}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.associated_zone"
|
||||
)}
|
||||
.includeDomains=${ZONE_DOMAINS}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._associatedZoneChanged}
|
||||
></ha-entity-picker>
|
||||
`
|
||||
: nothing}
|
||||
${domain === "sensor" &&
|
||||
this._deviceClass &&
|
||||
stateObj?.attributes.unit_of_measurement &&
|
||||
@@ -1208,6 +1236,17 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
(params.options as CalendarEntityOptions).color = this._calendarColor;
|
||||
}
|
||||
}
|
||||
if (
|
||||
domain === "device_tracker" &&
|
||||
this._associatedZone !== undefined &&
|
||||
(this.entry.options?.device_tracker?.associated_zone ?? "zone.home") !==
|
||||
this._associatedZone
|
||||
) {
|
||||
params.options_domain = "device_tracker";
|
||||
params.options = {
|
||||
associated_zone: this._associatedZone,
|
||||
} as DeviceTrackerEntityOptions;
|
||||
}
|
||||
if (
|
||||
domain === "weather" &&
|
||||
(stateObj?.attributes?.precipitation_unit !== this._precipitation_unit ||
|
||||
@@ -1447,6 +1486,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._calendarColor = ev.detail.value || null;
|
||||
}
|
||||
|
||||
private _associatedZoneChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._associatedZone = ev.detail.value || "zone.home";
|
||||
}
|
||||
|
||||
private _precipitationUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._precipitation_unit = ev.detail.value;
|
||||
|
||||
@@ -819,7 +819,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
|
||||
el.route = this.routeTail;
|
||||
el.hass = this.hass;
|
||||
el.showAdvanced = Boolean(this.hass.userData?.showAdvanced);
|
||||
el.isWide = isWide;
|
||||
el.narrow = this.narrow;
|
||||
el.cloudStatus = this._cloudStatus;
|
||||
|
||||
@@ -1213,7 +1213,6 @@ ${rejected
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: domain,
|
||||
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -92,8 +92,6 @@ class HaConfigInfo extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _osInfo?: HassioHassOSInfo;
|
||||
|
||||
@@ -3,7 +3,7 @@ import Fuse from "fuse.js";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
@@ -16,11 +16,18 @@ 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-list";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-virtualized";
|
||||
import type {
|
||||
HaListVirtualized,
|
||||
HaListVirtualizedItem,
|
||||
} from "../../../components/list/ha-list-virtualized";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
DISCOVERY_SOURCES,
|
||||
@@ -46,16 +53,17 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyleDialog, haStyleScrollbar } from "../../../resources/styles";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./ha-domain-integrations";
|
||||
import "./ha-integration-list-item";
|
||||
import type { HaIntegrationListItem } from "./ha-integration-list-item";
|
||||
import type { AddIntegrationDialogParams } from "./show-add-integration-dialog";
|
||||
import { showYamlIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { showSingleConfigEntryWarning } from "./show-single-config-entry-warning";
|
||||
|
||||
export interface IntegrationListItem {
|
||||
export interface IntegrationListItem extends HaListVirtualizedItem {
|
||||
name: string;
|
||||
domain: string;
|
||||
config_flow?: boolean;
|
||||
@@ -100,6 +108,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
@query("ha-list-virtualized") private _listElement?: HaListVirtualized;
|
||||
|
||||
private _width?: number;
|
||||
|
||||
private _height?: number;
|
||||
@@ -185,8 +195,9 @@ class AddIntegrationDialog extends LitElement {
|
||||
(!this._width || !this._height)
|
||||
) {
|
||||
// Store the width and height so that when we search, box doesn't jump
|
||||
const boundingRect =
|
||||
this.shadowRoot!.querySelector("ha-list")?.getBoundingClientRect();
|
||||
const boundingRect = this.shadowRoot!.querySelector(
|
||||
"ha-list-virtualized"
|
||||
)?.getBoundingClientRect();
|
||||
this._width = boundingRect?.width;
|
||||
this._height = boundingRect?.height;
|
||||
}
|
||||
@@ -206,6 +217,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
discoveredFlowsCount > 0
|
||||
? [
|
||||
{
|
||||
id: "_discovered",
|
||||
interactive: true,
|
||||
name: localize(
|
||||
"ui.panel.config.integrations.discovered_devices",
|
||||
{ count: discoveredFlowsCount }
|
||||
@@ -222,6 +235,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
(domain) => components.includes(domain)
|
||||
)
|
||||
.map((domain) => ({
|
||||
id: `device_${domain}`,
|
||||
interactive: true,
|
||||
name: localize(`ui.panel.config.integrations.add_${domain}_device`),
|
||||
domain,
|
||||
config_flow: true,
|
||||
@@ -262,6 +277,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
integrations.push({
|
||||
id: domain,
|
||||
interactive: true,
|
||||
domain,
|
||||
name: integration.name || domainToName(localize, domain),
|
||||
config_flow: supportedIntegration.config_flow,
|
||||
@@ -278,6 +295,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
) {
|
||||
// Brand
|
||||
integrations.push({
|
||||
id: domain,
|
||||
interactive: true,
|
||||
domain,
|
||||
name: integration.name || domainToName(localize, domain),
|
||||
iot_standards: integration.iot_standards,
|
||||
@@ -295,6 +314,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
} else if (filter && "integration_type" in integration) {
|
||||
// Integration without a config flow
|
||||
yamlIntegrations.push({
|
||||
id: domain,
|
||||
interactive: true,
|
||||
domain,
|
||||
name: integration.name || domainToName(localize, domain),
|
||||
config_flow: integration.config_flow,
|
||||
@@ -320,6 +341,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
const helpers = Object.entries(h).map(([domain, integration]) => ({
|
||||
id: domain,
|
||||
interactive: true,
|
||||
domain,
|
||||
name: integration.name || domainToName(localize, domain),
|
||||
config_flow: integration.config_flow,
|
||||
@@ -517,6 +540,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
}
|
||||
if (supportIntegration) {
|
||||
this._handleIntegrationPicked({
|
||||
id: integration.supported_by,
|
||||
domain: integration.supported_by,
|
||||
name:
|
||||
supportIntegration.name ||
|
||||
@@ -543,45 +567,33 @@ class AddIntegrationDialog extends LitElement {
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.panel.config.integrations.search_brand"
|
||||
)}
|
||||
@keypress=${this._maybeSubmit}
|
||||
@keydown=${this._maybeSubmit}
|
||||
></ha-input-search>
|
||||
${integrations
|
||||
? html`<ha-list ?autofocus=${this._narrow}>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
tabindex="-1"
|
||||
class="ha-scrollbar"
|
||||
style=${styleMap({
|
||||
width: `${this._width}px`,
|
||||
height: this._narrow
|
||||
? "calc(100vh - 184px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))"
|
||||
: "500px",
|
||||
})}
|
||||
@click=${this._integrationPicked}
|
||||
@keypress=${this._handleKeyPress}
|
||||
.items=${integrations}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderRow}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</ha-list>`
|
||||
? html`<ha-list-virtualized
|
||||
.rows=${integrations}
|
||||
.rowRenderer=${this._renderRow}
|
||||
style=${styleMap({
|
||||
width: `${this._width}px`,
|
||||
height: this._narrow
|
||||
? "calc(100vh - 184px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))"
|
||||
: "500px",
|
||||
})}
|
||||
>
|
||||
</ha-list-virtualized>`
|
||||
: html`<div class="flex center">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>`} `;
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
private _keyFunction = (integration: IntegrationListItem) =>
|
||||
integration.domain;
|
||||
|
||||
private _renderRow = (integration: IntegrationListItem) => {
|
||||
if (!integration) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-integration-list-item
|
||||
.hass=${this.hass}
|
||||
@click=${this._integrationPicked}
|
||||
.integration=${integration}
|
||||
tabindex="0"
|
||||
>
|
||||
</ha-integration-list-item>
|
||||
`;
|
||||
@@ -647,19 +659,13 @@ class AddIntegrationDialog extends LitElement {
|
||||
this._filter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
private _integrationPicked(ev) {
|
||||
const listItem = ev.target.closest("ha-integration-list-item");
|
||||
if (!listItem) {
|
||||
private _integrationPicked = (ev: Event) => {
|
||||
const listItem = ev.currentTarget as HaIntegrationListItem;
|
||||
if (!listItem?.integration) {
|
||||
return;
|
||||
}
|
||||
this._handleIntegrationPicked(listItem.integration);
|
||||
}
|
||||
|
||||
private _handleKeyPress(ev) {
|
||||
if (ev.key === "Enter") {
|
||||
this._integrationPicked(ev);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async _handleIntegrationPicked(integration: IntegrationListItem) {
|
||||
if (integration.supported_by) {
|
||||
@@ -761,7 +767,6 @@ class AddIntegrationDialog extends LitElement {
|
||||
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest,
|
||||
navigateToResult: this._navigateToResult,
|
||||
});
|
||||
@@ -782,6 +787,11 @@ class AddIntegrationDialog extends LitElement {
|
||||
}
|
||||
|
||||
private _maybeSubmit(ev: KeyboardEvent) {
|
||||
if (ev.key === "ArrowDown" && this._listElement) {
|
||||
ev.preventDefault();
|
||||
this._listElement.focus();
|
||||
return;
|
||||
}
|
||||
if (ev.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
@@ -803,7 +813,6 @@ class AddIntegrationDialog extends LitElement {
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
@@ -831,15 +840,9 @@ class AddIntegrationDialog extends LitElement {
|
||||
ha-spinner {
|
||||
margin: 24px 0;
|
||||
}
|
||||
ha-list {
|
||||
ha-list-virtualized {
|
||||
position: relative;
|
||||
}
|
||||
lit-virtualizer {
|
||||
contain: size layout !important;
|
||||
}
|
||||
ha-integration-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -626,7 +626,6 @@ export class HaConfigEntryRow extends LitElement {
|
||||
private _handleReconfigure = async () => {
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: this.data.entry.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest: await fetchIntegrationManifest(
|
||||
this.hass,
|
||||
this.data.entry.domain
|
||||
|
||||
@@ -138,8 +138,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@property({ attribute: false }) public configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ attribute: false })
|
||||
|
||||
@@ -129,8 +129,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public configEntries?: ConfigEntryExtended[];
|
||||
@@ -989,7 +987,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
this.hass,
|
||||
integration.supported_by!
|
||||
),
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,8 +51,6 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
@@ -207,7 +205,6 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
|
||||
pageEl.configEntriesInProgress = this._configEntriesInProgress;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -7,11 +8,10 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import { localizeConfigFlowTitle } from "../../../data/config_flow";
|
||||
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
@@ -46,23 +46,20 @@ class HaDomainIntegrations extends LitElement {
|
||||
public showManageLink = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-list>
|
||||
return html`<ha-list-base>
|
||||
${this.flowsInProgress?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.panel.config.integrations.discovered")}
|
||||
</h3>
|
||||
${this.flowsInProgress.map(
|
||||
(flow) =>
|
||||
html`<ha-list-item
|
||||
graphic="medium"
|
||||
twoLine
|
||||
html`<ha-list-item-button
|
||||
.flow=${flow}
|
||||
@request-selected=${this._flowInProgressPicked}
|
||||
hasMeta
|
||||
@click=${this._flowInProgressPicked}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
loading="lazy"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
@@ -75,16 +72,16 @@ class HaDomainIntegrations extends LitElement {
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<span
|
||||
<span slot="headline"
|
||||
>${localizeConfigFlowTitle(this.hass.localize, flow)}</span
|
||||
>
|
||||
<span slot="secondary"
|
||||
<span slot="supporting-text"
|
||||
>${domainToName(this.hass.localize, flow.handler)}</span
|
||||
>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>`
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
)}
|
||||
<li divider role="separator"></li>
|
||||
<wa-divider></wa-divider>
|
||||
${this.integration &&
|
||||
"integrations" in this.integration &&
|
||||
this.integration.integrations
|
||||
@@ -105,14 +102,12 @@ class HaDomainIntegrations extends LitElement {
|
||||
.map((standard) => {
|
||||
const domain: (typeof PROTOCOL_INTEGRATIONS)[number] =
|
||||
standardToDomain[standard] || standard;
|
||||
return html`<ha-list-item
|
||||
graphic="medium"
|
||||
return html`<ha-list-item-button
|
||||
.domain=${domain}
|
||||
@request-selected=${this._standardPicked}
|
||||
hasMeta
|
||||
@click=${this._standardPicked}
|
||||
>
|
||||
<img
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src=${brandsUrl(
|
||||
@@ -126,13 +121,13 @@ class HaDomainIntegrations extends LitElement {
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<span
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.integrations.add_${domain}_device`
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>`;
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`;
|
||||
})
|
||||
: ""}
|
||||
${this.integration &&
|
||||
@@ -156,8 +151,6 @@ class HaDomainIntegrations extends LitElement {
|
||||
.map(
|
||||
([dom, val]) =>
|
||||
html`<ha-integration-list-item
|
||||
.hass=${this.hass}
|
||||
.domain=${dom}
|
||||
.integration=${{
|
||||
...val,
|
||||
domain: dom,
|
||||
@@ -165,20 +158,18 @@ class HaDomainIntegrations extends LitElement {
|
||||
is_built_in: val.is_built_in !== false,
|
||||
cloud: val.iot_class?.startsWith("cloud_"),
|
||||
}}
|
||||
@request-selected=${this._integrationPicked}
|
||||
@click=${this._integrationPicked}
|
||||
>
|
||||
</ha-integration-list-item>`
|
||||
)
|
||||
: ""}
|
||||
${(PROTOCOL_INTEGRATIONS as readonly string[]).includes(this.domain)
|
||||
? html`<ha-list-item
|
||||
graphic="medium"
|
||||
? html`<ha-list-item-button
|
||||
.domain=${this.domain}
|
||||
@request-selected=${this._standardPicked}
|
||||
hasMeta
|
||||
@click=${this._standardPicked}
|
||||
>
|
||||
<img
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src=${brandsUrl(
|
||||
@@ -192,23 +183,23 @@ class HaDomainIntegrations extends LitElement {
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<span
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.integrations.add_${
|
||||
this.domain as (typeof PROTOCOL_INTEGRATIONS)[number]
|
||||
}_device`
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>`
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: ""}
|
||||
${this.integration &&
|
||||
"config_flow" in this.integration &&
|
||||
this.integration.config_flow
|
||||
? html`${this.flowsInProgress?.length
|
||||
? html`<ha-list-item
|
||||
? html`<ha-list-item-button
|
||||
.domain=${this.domain}
|
||||
@request-selected=${this._integrationPicked}
|
||||
@click=${this._integrationPicked}
|
||||
.integration=${{
|
||||
...this.integration,
|
||||
domain: this.domain,
|
||||
@@ -218,17 +209,20 @@ class HaDomainIntegrations extends LitElement {
|
||||
is_built_in: this.integration.is_built_in !== false,
|
||||
cloud: this.integration.iot_class?.startsWith("cloud_"),
|
||||
}}
|
||||
hasMeta
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.integrations.new_flow", {
|
||||
integration:
|
||||
this.integration.name ||
|
||||
domainToName(this.hass.localize, this.domain),
|
||||
})}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>`
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.new_flow",
|
||||
{
|
||||
integration:
|
||||
this.integration.name ||
|
||||
domainToName(this.hass.localize, this.domain),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: html`<ha-integration-list-item
|
||||
.hass=${this.hass}
|
||||
.domain=${this.domain}
|
||||
.integration=${{
|
||||
...this.integration,
|
||||
@@ -239,38 +233,31 @@ class HaDomainIntegrations extends LitElement {
|
||||
is_built_in: this.integration.is_built_in !== false,
|
||||
cloud: this.integration.iot_class?.startsWith("cloud_"),
|
||||
}}
|
||||
@request-selected=${this._integrationPicked}
|
||||
@click=${this._integrationPicked}
|
||||
>
|
||||
</ha-integration-list-item>`}`
|
||||
: ""}
|
||||
${this.showManageLink &&
|
||||
// Only show manage link if not already on the integrations dashboard
|
||||
!location.pathname.startsWith("/config/integrations")
|
||||
? html`<ha-list-item
|
||||
twoLine
|
||||
@request-selected=${this._manageDiscovered}
|
||||
hasMeta
|
||||
>
|
||||
<span
|
||||
? html`<ha-list-item-button @click=${this._manageDiscovered}>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.manage_discovered"
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary"
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.manage_discovered_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>`
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: nothing}
|
||||
</ha-list> `;
|
||||
</ha-list-base> `;
|
||||
}
|
||||
|
||||
private async _integrationPicked(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
const domain = (ev.currentTarget as any).domain;
|
||||
|
||||
if (
|
||||
@@ -318,7 +305,6 @@ class HaDomainIntegrations extends LitElement {
|
||||
root instanceof ShadowRoot ? (root.host as HTMLElement) : this,
|
||||
{
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: this.navigateToResult,
|
||||
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||
}
|
||||
@@ -326,10 +312,7 @@ class HaDomainIntegrations extends LitElement {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
private async _flowInProgressPicked(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private async _flowInProgressPicked(ev: Event) {
|
||||
const flow: DataEntryFlowProgress = (ev.currentTarget as any).flow;
|
||||
const root = this.getRootNode();
|
||||
showConfigFlowDialog(
|
||||
@@ -337,25 +320,18 @@ class HaDomainIntegrations extends LitElement {
|
||||
{
|
||||
continueFlowId: flow.flow_id,
|
||||
navigateToResult: this.navigateToResult,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest: await fetchIntegrationManifest(this.hass, flow.handler),
|
||||
}
|
||||
);
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
private _manageDiscovered(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _manageDiscovered() {
|
||||
fireEvent(this, "close-dialog");
|
||||
navigate("/config/integrations/dashboard?historyBack=1");
|
||||
}
|
||||
|
||||
private _standardPicked(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
const domain = (ev.currentTarget as any).domain;
|
||||
const root = this.getRootNode();
|
||||
fireEvent(this, "close-dialog");
|
||||
@@ -372,11 +348,10 @@ class HaDomainIntegrations extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
--mdc-list-item-graphic-size: 40px;
|
||||
--mdc-list-side-padding: 24px;
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
}
|
||||
h3 {
|
||||
margin: 8px 24px 0;
|
||||
margin: var(--ha-space-2) var(--ha-space-6) 0;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
@@ -385,11 +360,14 @@ class HaDomainIntegrations extends LitElement {
|
||||
margin-top: 0;
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
li[divider] {
|
||||
margin-top: 8px;
|
||||
wa-divider {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,196 +1,127 @@
|
||||
import type { GraphicType } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import {
|
||||
mdiDevices,
|
||||
mdiFileCodeOutline,
|
||||
mdiPackageVariant,
|
||||
mdiWeb,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import type { IntegrationListItem } from "./dialog-add-integration";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { HaListItemButton } from "../../../components/item/ha-list-item-button";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { IntegrationListItem } from "./dialog-add-integration";
|
||||
|
||||
@customElement("ha-integration-list-item")
|
||||
export class HaIntegrationListItem extends ListItemBase {
|
||||
public hass!: HomeAssistant;
|
||||
export class HaIntegrationListItem extends HaListItemButton {
|
||||
@property({ attribute: false }) public integration!: IntegrationListItem;
|
||||
|
||||
@property({ attribute: false }) public integration?: IntegrationListItem;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ type: String, reflect: true }) graphic: GraphicType = "medium";
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) hasMeta = true;
|
||||
|
||||
// @ts-expect-error
|
||||
protected override renderSingleLine() {
|
||||
if (!this.integration) {
|
||||
return nothing;
|
||||
}
|
||||
return html`${this.integration.name ||
|
||||
domainToName(this.hass.localize, this.integration.domain)}
|
||||
${this.integration.is_helper ? " (helper)" : ""}`;
|
||||
protected override _renderInner(): TemplateResult {
|
||||
const integration = this.integration;
|
||||
const yamlOnly =
|
||||
!integration.config_flow &&
|
||||
!integration.integrations &&
|
||||
!integration.iot_standards;
|
||||
return html`
|
||||
<div part="start" class="start">
|
||||
${integration.is_discovered
|
||||
? html`<ha-svg-icon
|
||||
class="discovered-icon"
|
||||
.path=${mdiDevices}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-domain-icon
|
||||
brand-fallback
|
||||
.domain=${integration.domain}
|
||||
></ha-domain-icon>`}
|
||||
</div>
|
||||
<div part="content" class="content">
|
||||
<div part="headline" class="headline">
|
||||
${integration.name ||
|
||||
domainToName(this._localize, integration.domain)}
|
||||
${integration.is_helper
|
||||
? // @ts-expect-error translation key not yet defined
|
||||
` (${this._localize("ui.panel.config.integrations.config_entry.helper")})`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div part="end" class="end">
|
||||
${integration.cloud
|
||||
? html`<ha-svg-icon id="icon-cloud" .path=${mdiWeb}></ha-svg-icon>
|
||||
<ha-tooltip for="icon-cloud" placement="left">
|
||||
${this._localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
${!integration.is_built_in
|
||||
? html`<ha-svg-icon
|
||||
id="icon-custom"
|
||||
class=${integration.overwrites_built_in
|
||||
? "overwrites"
|
||||
: "custom"}
|
||||
.path=${mdiPackageVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-custom" placement="left">
|
||||
${this._localize(
|
||||
integration.overwrites_built_in
|
||||
? "ui.panel.config.integrations.config_entry.custom_overwrites_core"
|
||||
: "ui.panel.config.integrations.config_entry.custom_integration"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
${yamlOnly
|
||||
? html`<ha-svg-icon
|
||||
id="icon-yaml"
|
||||
.path=${mdiFileCodeOutline}
|
||||
class="open-in-new"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-yaml" placement="left">
|
||||
${this._localize(
|
||||
"ui.panel.config.integrations.config_entry.yaml_only"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: html`<ha-icon-next></ha-icon-next>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
protected override renderGraphic() {
|
||||
if (!this.integration) {
|
||||
return nothing;
|
||||
}
|
||||
const graphicClasses = {
|
||||
multi: this.multipleGraphics,
|
||||
};
|
||||
|
||||
return html` <span
|
||||
class="mdc-deprecated-list-item__graphic material-icons ${classMap(
|
||||
graphicClasses
|
||||
)}"
|
||||
>
|
||||
${this.integration.is_discovered
|
||||
? html`<ha-svg-icon
|
||||
class="discovered-icon"
|
||||
.path=${mdiDevices}
|
||||
></ha-svg-icon>`
|
||||
: html`<img
|
||||
alt=""
|
||||
loading="lazy"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: this.integration.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
protected override renderMeta() {
|
||||
if (!this.integration) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<span class="mdc-deprecated-list-item__meta material-icons">
|
||||
${this.integration.cloud
|
||||
? html` <ha-svg-icon id="icon-cloud" .path=${mdiWeb}></ha-svg-icon>
|
||||
<ha-tooltip for="icon-cloud" placement="left"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
${!this.integration.is_built_in
|
||||
? html`<span
|
||||
class=${this.integration.overwrites_built_in
|
||||
? "overwrites"
|
||||
: "custom"}
|
||||
>
|
||||
<ha-svg-icon
|
||||
id="icon-custom"
|
||||
.path=${mdiPackageVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-custom" placement="left"
|
||||
>${this.hass.localize(
|
||||
this.integration.overwrites_built_in
|
||||
? "ui.panel.config.integrations.config_entry.custom_overwrites_core"
|
||||
: "ui.panel.config.integrations.config_entry.custom_integration"
|
||||
)}</ha-tooltip
|
||||
></span
|
||||
>`
|
||||
: nothing}
|
||||
${!this.integration.config_flow &&
|
||||
!this.integration.integrations &&
|
||||
!this.integration.iot_standards
|
||||
? html` <ha-svg-icon
|
||||
id="icon-yaml"
|
||||
.path=${mdiFileCodeOutline}
|
||||
class="open-in-new"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="icon-yaml" placement="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.yaml_only"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: html`<ha-icon-next></ha-icon-next>`}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-list-side-padding: 24px;
|
||||
--mdc-list-item-graphic-size: 40px;
|
||||
}
|
||||
:host([graphic="avatar"]:not([twoLine])),
|
||||
:host([graphic="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
}
|
||||
span.material-icons:first-of-type {
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: var(
|
||||
--mdc-list-item-graphic-margin,
|
||||
16px
|
||||
) !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
span.material-icons:last-of-type {
|
||||
margin-inline-start: auto !important;
|
||||
margin-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.discovered-icon {
|
||||
--mdc-icon-size: 40px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mdc-deprecated-list-item__meta > * {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.mdc-deprecated-list-item__meta > *:last-child {
|
||||
margin-right: 0px;
|
||||
margin-inline-end: 0px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-icon-next {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.open-in-new {
|
||||
--mdc-icon-size: 22px;
|
||||
padding: 1px;
|
||||
}
|
||||
.custom {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.overwrites {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static styles: CSSResultGroup = [
|
||||
HaListItemButton.styles,
|
||||
css`
|
||||
.start {
|
||||
--mdc-icon-size: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.end {
|
||||
color: var(--ha-color-text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.discovered-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.open-in-new {
|
||||
--mdc-icon-size: 22px;
|
||||
padding: 1px;
|
||||
}
|
||||
.end .custom {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.end .overwrites {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+193
-6
@@ -3,16 +3,28 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import {
|
||||
commissionMatterDevice,
|
||||
redirectOnNewMatterDevice,
|
||||
watchForNewMatterDevice,
|
||||
} from "../../../../../data/matter";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import { updateDeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import {
|
||||
getAutomaticEntityIds,
|
||||
getExtendedEntityRegistryEntries,
|
||||
updateEntityRegistryEntry,
|
||||
type ExtEntityRegistryEntry,
|
||||
} from "../../../../../data/entity/entity_registry";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import "./matter-add-device/matter-add-device-apple-home";
|
||||
import "./matter-add-device/matter-add-device-existing";
|
||||
import "./matter-add-device/matter-add-device-generic";
|
||||
@@ -21,6 +33,7 @@ import "./matter-add-device/matter-add-device-google-home-fallback";
|
||||
import "./matter-add-device/matter-add-device-main";
|
||||
import "./matter-add-device/matter-add-device-new";
|
||||
import "./matter-add-device/matter-add-device-commissioning";
|
||||
import "./matter-add-device/matter-add-device-device-added";
|
||||
import { showToast } from "../../../../../util/toast";
|
||||
|
||||
export type MatterAddDeviceStep =
|
||||
@@ -31,7 +44,8 @@ export type MatterAddDeviceStep =
|
||||
| "google_home_fallback"
|
||||
| "apple_home"
|
||||
| "generic"
|
||||
| "commissioning";
|
||||
| "commissioning"
|
||||
| "device_added";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -50,6 +64,7 @@ const BACK_STEP: Record<MatterAddDeviceStep, MatterAddDeviceStep | undefined> =
|
||||
apple_home: "existing",
|
||||
generic: "existing",
|
||||
commissioning: undefined,
|
||||
device_added: undefined,
|
||||
};
|
||||
|
||||
@customElement("dialog-matter-add-device")
|
||||
@@ -62,23 +77,90 @@ class DialogMatterAddDevice extends LitElement {
|
||||
|
||||
@state() _step: MatterAddDeviceStep = "main";
|
||||
|
||||
@state() private _newDevice?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _mainEntity?: ExtEntityRegistryEntry;
|
||||
|
||||
@state() private _deviceAddedState: {
|
||||
name: string;
|
||||
area: string | undefined;
|
||||
deviceClass: string | undefined;
|
||||
hasPendingUpdates: boolean;
|
||||
} = {
|
||||
name: "",
|
||||
area: undefined,
|
||||
deviceClass: undefined,
|
||||
hasPendingUpdates: false,
|
||||
};
|
||||
|
||||
private _mainEntityFetched = false;
|
||||
|
||||
private _unsub?: UnsubscribeFunc;
|
||||
|
||||
public showDialog(): void {
|
||||
this._open = true;
|
||||
this._unsub = redirectOnNewMatterDevice(this.hass, () =>
|
||||
this.closeDialog()
|
||||
);
|
||||
this._unsub = watchForNewMatterDevice(this.hass, (device) => {
|
||||
this._newDevice = device;
|
||||
this._step = "device_added";
|
||||
this._fetchMainEntity();
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
protected updated(changedProps: Map<string, unknown>): void {
|
||||
// Retry fetching main entity when hass updates (entities may not be available immediately)
|
||||
if (
|
||||
changedProps.has("hass") &&
|
||||
this._newDevice &&
|
||||
!this._mainEntityFetched
|
||||
) {
|
||||
this._fetchMainEntity();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchMainEntity(): Promise<void> {
|
||||
if (!this._newDevice || this._mainEntityFetched) return;
|
||||
|
||||
const entityIds = Object.values(this.hass.entities)
|
||||
.filter((e) => e.device_id === this._newDevice!.id)
|
||||
.map((e) => e.entity_id);
|
||||
|
||||
if (!entityIds.length) return;
|
||||
|
||||
this._mainEntityFetched = true;
|
||||
|
||||
const entries = await getExtendedEntityRegistryEntries(
|
||||
this.hass,
|
||||
entityIds
|
||||
);
|
||||
|
||||
const mainEntry = Object.values(entries).find(
|
||||
(e) => e.original_name === null
|
||||
);
|
||||
if (!mainEntry) return;
|
||||
|
||||
const domain = computeDomain(mainEntry.entity_id);
|
||||
if (domain === "cover" || domain === "binary_sensor") {
|
||||
this._mainEntity = mainEntry;
|
||||
}
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._step = "main";
|
||||
this._pairingCode = "";
|
||||
this._newDevice = undefined;
|
||||
this._mainEntity = undefined;
|
||||
this._mainEntityFetched = false;
|
||||
this._deviceAddedState = {
|
||||
name: "",
|
||||
area: undefined,
|
||||
deviceClass: undefined,
|
||||
hasPendingUpdates: false,
|
||||
};
|
||||
this._unsub?.();
|
||||
this._unsub = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -93,6 +175,17 @@ class DialogMatterAddDevice extends LitElement {
|
||||
this._pairingCode = ev.detail.code;
|
||||
}
|
||||
|
||||
private _handleDeviceAddedChanged(
|
||||
ev: CustomEvent<{
|
||||
name: string;
|
||||
area: string | undefined;
|
||||
deviceClass: string | undefined;
|
||||
hasPendingUpdates: boolean;
|
||||
}>
|
||||
) {
|
||||
this._deviceAddedState = ev.detail;
|
||||
}
|
||||
|
||||
private _back() {
|
||||
const backStep = BACK_STEP[this._step];
|
||||
if (!backStep) return;
|
||||
@@ -104,12 +197,15 @@ class DialogMatterAddDevice extends LitElement {
|
||||
<div
|
||||
@pairing-code-changed=${this._handlePairingCodeChanged}
|
||||
@step-selected=${this._handleStepSelected}
|
||||
@device-added-changed=${this._handleDeviceAddedChanged}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
${dynamicElement(
|
||||
`matter-add-device-${this._step.replaceAll("_", "-")}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
device: this._newDevice,
|
||||
mainEntity: this._mainEntity,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
@@ -129,8 +225,86 @@ class DialogMatterAddDevice extends LitElement {
|
||||
),
|
||||
duration: 2000,
|
||||
});
|
||||
this._step = savedStep;
|
||||
}
|
||||
this._step = savedStep;
|
||||
// On success, keep showing commissioning spinner until watchForNewMatterDevice fires
|
||||
}
|
||||
|
||||
private async _finishDeviceAdded(): Promise<void> {
|
||||
const device = this._newDevice!;
|
||||
const { name, area, deviceClass, hasPendingUpdates } =
|
||||
this._deviceAddedState;
|
||||
|
||||
if (hasPendingUpdates) {
|
||||
const origName = computeDeviceName(device) ?? "";
|
||||
const nameChanged = name !== origName;
|
||||
const origArea = device.area_id ?? undefined;
|
||||
const areaChanged = area !== origArea;
|
||||
|
||||
if (nameChanged || areaChanged) {
|
||||
await updateDeviceRegistryEntry(this.hass, device.id, {
|
||||
...(nameChanged && { name_by_user: name || null }),
|
||||
...(areaChanged && { area_id: area || null }),
|
||||
}).catch((err: Error) =>
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error_saving_device",
|
||||
{ error: err.message }
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (nameChanged && name) {
|
||||
const entityIds = Object.values(this.hass.entities)
|
||||
.filter((e) => e.device_id === device.id)
|
||||
.map((e) => e.entity_id);
|
||||
|
||||
if (entityIds.length) {
|
||||
const mapping = await getAutomaticEntityIds(this.hass, entityIds);
|
||||
await Promise.allSettled(
|
||||
Object.entries(mapping)
|
||||
.filter((entry): entry is [string, string] => !!entry[1])
|
||||
.map(([oldId, newId]) =>
|
||||
updateEntityRegistryEntry(this.hass, oldId, {
|
||||
new_entity_id: newId,
|
||||
}).catch((err: Error) =>
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error_saving_entity",
|
||||
{ error: err.message }
|
||||
),
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._mainEntity) {
|
||||
const origClass =
|
||||
this._mainEntity.device_class ??
|
||||
this._mainEntity.original_device_class ??
|
||||
undefined;
|
||||
if (deviceClass !== origClass) {
|
||||
await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._mainEntity.entity_id,
|
||||
{ device_class: deviceClass || null }
|
||||
).catch((err: Error) =>
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error_saving_entity",
|
||||
{ error: err.message }
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
navigate(`/config/devices/device/${device.id}`);
|
||||
}
|
||||
|
||||
private _renderActions() {
|
||||
@@ -156,6 +330,19 @@ class DialogMatterAddDevice extends LitElement {
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
if (this._step === "device_added") {
|
||||
return html`
|
||||
<ha-button slot="primaryAction" @click=${this._finishDeviceAdded}>
|
||||
${this._deviceAddedState.hasPendingUpdates
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.matter-add-device.device_added.finish"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.dialogs.matter-add-device.device_added.skip"
|
||||
)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../../../common/entity/compute_domain";
|
||||
import { computeDeviceName } from "../../../../../../common/entity/compute_device_name";
|
||||
import { caseInsensitiveStringCompare } from "../../../../../../common/string/compare";
|
||||
import { fireEvent } from "../../../../../../common/dom/fire_event";
|
||||
import "../../../../../../components/ha-area-picker";
|
||||
import "../../../../../../components/input/ha-input";
|
||||
import "../../../../../../components/ha-select";
|
||||
import "../../../../../../components/ha-dropdown-item";
|
||||
import type { HaSelectSelectEvent } from "../../../../../../components/ha-select";
|
||||
import type { ExtEntityRegistryEntry } from "../../../../../../data/entity/entity_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../../../data/device/device_registry";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { brandsUrl } from "../../../../../../util/brands-url";
|
||||
import { sharedStyles } from "./matter-add-device-shared-styles";
|
||||
import { OVERRIDE_DEVICE_CLASSES } from "../../../../entities/entity-registry-settings-editor";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"device-added-changed": {
|
||||
name: string;
|
||||
area: string | undefined;
|
||||
deviceClass: string | undefined;
|
||||
hasPendingUpdates: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("matter-add-device-device-added")
|
||||
class MatterAddDeviceDeviceAdded extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@property({ attribute: false }) public mainEntity?: ExtEntityRegistryEntry;
|
||||
|
||||
@state() private _deviceName = "";
|
||||
|
||||
@state() private _area: string | undefined;
|
||||
|
||||
@state() private _deviceClass: string | undefined;
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
private _deviceClassInitialized = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (!this._initialized && this.device) {
|
||||
this._initialized = true;
|
||||
this._deviceName = computeDeviceName(this.device) ?? "";
|
||||
this._area = this.device.area_id ?? undefined;
|
||||
}
|
||||
if (
|
||||
!this._deviceClassInitialized &&
|
||||
(changedProps.has("mainEntity") || this._initialized) &&
|
||||
this.mainEntity
|
||||
) {
|
||||
this._deviceClassInitialized = true;
|
||||
this._deviceClass =
|
||||
this.mainEntity.device_class ??
|
||||
this.mainEntity.original_device_class ??
|
||||
undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private get _deviceClassOptions(): string[][] | undefined {
|
||||
if (!this.mainEntity) return undefined;
|
||||
const domain = computeDomain(this.mainEntity.entity_id);
|
||||
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
if (!deviceClasses) return undefined;
|
||||
|
||||
const options: string[][] = [[], []];
|
||||
for (const deviceClass of deviceClasses) {
|
||||
if (
|
||||
this.mainEntity.original_device_class &&
|
||||
deviceClass.includes(this.mainEntity.original_device_class)
|
||||
) {
|
||||
options[0] = deviceClass;
|
||||
} else {
|
||||
options[1].push(...deviceClass);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private get _hasPendingUpdates(): boolean {
|
||||
const origName = computeDeviceName(this.device) ?? "";
|
||||
const origArea = this.device.area_id ?? undefined;
|
||||
const origDeviceClass =
|
||||
this.mainEntity?.device_class ??
|
||||
this.mainEntity?.original_device_class ??
|
||||
undefined;
|
||||
return (
|
||||
this._deviceName !== origName ||
|
||||
this._area !== origArea ||
|
||||
(this.mainEntity !== undefined && this._deviceClass !== origDeviceClass)
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps: Map<string, unknown>) {
|
||||
if (
|
||||
changedProps.has("_deviceName") ||
|
||||
changedProps.has("_area") ||
|
||||
changedProps.has("_deviceClass")
|
||||
) {
|
||||
fireEvent(this, "device-added-changed", {
|
||||
name: this._deviceName,
|
||||
area: this._area,
|
||||
deviceClass: this._deviceClass,
|
||||
hasPendingUpdates: this._hasPendingUpdates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _deviceClassesSorted = memoizeOne(
|
||||
(domain: string, deviceClasses: string[]) =>
|
||||
deviceClasses
|
||||
.map((deviceClass) => ({
|
||||
deviceClass,
|
||||
label: this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
),
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.device) return nothing;
|
||||
|
||||
const domain = this.mainEntity
|
||||
? computeDomain(this.mainEntity.entity_id)
|
||||
: undefined;
|
||||
const deviceClassOptions = this._deviceClassOptions;
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<div class="device">
|
||||
<div class="device-info">
|
||||
<img
|
||||
alt="Matter"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: "matter",
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div class="device-name">
|
||||
<span>${computeDeviceName(this.device)}</span>
|
||||
<span class="secondary">Matter</span>
|
||||
</div>
|
||||
</div>
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.device_name"
|
||||
)}
|
||||
.value=${this._deviceName}
|
||||
@change=${this._deviceNameChanged}
|
||||
></ha-input>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._area}
|
||||
@value-changed=${this._areaPicked}
|
||||
></ha-area-picker>
|
||||
${deviceClassOptions && domain
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_class"
|
||||
)}
|
||||
.value=${this._deviceClass
|
||||
? this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${this._deviceClass}`
|
||||
)
|
||||
: undefined}
|
||||
clearable
|
||||
@selected=${this._deviceClassChanged}
|
||||
>
|
||||
${this._deviceClassesSorted(
|
||||
domain,
|
||||
deviceClassOptions[0]
|
||||
).map(
|
||||
(entry) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${entry.deviceClass}
|
||||
.selected=${entry.deviceClass === this._deviceClass}
|
||||
>
|
||||
${entry.label}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
${deviceClassOptions[0].length && deviceClassOptions[1].length
|
||||
? html`<wa-divider></wa-divider>`
|
||||
: nothing}
|
||||
${this._deviceClassesSorted(
|
||||
domain,
|
||||
deviceClassOptions[1]
|
||||
).map(
|
||||
(entry) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${entry.deviceClass}
|
||||
.selected=${entry.deviceClass === this._deviceClass}
|
||||
>
|
||||
${entry.label}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _deviceNameChanged(ev: InputEvent) {
|
||||
this._deviceName = (ev.currentTarget as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _areaPicked(ev: CustomEvent<{ value: string }>) {
|
||||
this._area = ev.detail.value || undefined;
|
||||
}
|
||||
|
||||
private _deviceClassChanged(ev: HaSelectSelectEvent<string, true>) {
|
||||
this._deviceClass = ev.detail.value;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
sharedStyles,
|
||||
css`
|
||||
.device {
|
||||
border: 1px solid var(--divider-color);
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.device-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
.device-info img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.device-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
ha-input {
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
ha-area-picker,
|
||||
ha-select {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"matter-add-device-device-added": MatterAddDeviceDeviceAdded;
|
||||
}
|
||||
}
|
||||
+6
@@ -85,6 +85,12 @@ class MatterAddDeviceNew extends LitElement {
|
||||
static styles = [
|
||||
sharedStyles,
|
||||
css`
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
.app-qr {
|
||||
margin: 24px auto 0 auto;
|
||||
display: flex;
|
||||
|
||||
@@ -506,7 +506,6 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
this._refresh();
|
||||
},
|
||||
startFlowHandler: "otbr",
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+61
-71
@@ -3,18 +3,16 @@ import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../../components/list/types";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/list/ha-list-selectable-virtualized";
|
||||
import type { ZHADeviceEndpoint, ZHAGroup } from "../../../../../data/zha";
|
||||
import {
|
||||
addMembersToGroup,
|
||||
@@ -23,7 +21,6 @@ import {
|
||||
} from "../../../../../data/zha";
|
||||
import type { HassDialog } from "../../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleScrollbar } from "../../../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ZHAAddGroupMembersDialogParams } from "./show-dialog-zha-add-group-members";
|
||||
|
||||
@@ -50,8 +47,6 @@ class DialogZHAAddGroupMembers
|
||||
|
||||
@state() private _selectedDevicesToAdd: string[] = [];
|
||||
|
||||
@state() private _virtualizerReady = false;
|
||||
|
||||
private _fetchDataToken = 0;
|
||||
|
||||
public showDialog(params: ZHAAddGroupMembersDialogParams): void {
|
||||
@@ -80,7 +75,6 @@ class DialogZHAAddGroupMembers
|
||||
this._loading = false;
|
||||
this._processingAdd = false;
|
||||
this._selectedDevicesToAdd = [];
|
||||
this._virtualizerReady = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -89,7 +83,10 @@ class DialogZHAAddGroupMembers
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const deviceEndpoints = this._filteredDeviceEndpoints;
|
||||
const deviceEndpoints = this._filteredDeviceEndpoints(
|
||||
this._filter,
|
||||
this._availableDeviceEndpoints
|
||||
);
|
||||
const showSearch =
|
||||
this._availableDeviceEndpoints.length > 5 || this._filter;
|
||||
|
||||
@@ -100,7 +97,6 @@ class DialogZHAAddGroupMembers
|
||||
"ui.panel.config.zha.groups.add_members"
|
||||
)}
|
||||
?prevent-scrim-close=${this._selectedDevicesToAdd.length > 0}
|
||||
@after-show=${this._loadVirtualizer}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-icon-button
|
||||
@@ -126,22 +122,14 @@ class DialogZHAAddGroupMembers
|
||||
<div class="list-container">
|
||||
${deviceEndpoints.length
|
||||
? html`
|
||||
${this._virtualizerReady
|
||||
? html`
|
||||
<ha-list-selectable
|
||||
multi
|
||||
@ha-list-selected=${this._handleSelected}
|
||||
>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
.items=${deviceEndpoints}
|
||||
.renderItem=${this._renderDeviceEndpoint}
|
||||
.keyFunction=${this._keyFunction}
|
||||
></lit-virtualizer>
|
||||
</ha-list-selectable>
|
||||
`
|
||||
: this._renderLoadingSpinner()}
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${deviceEndpoints}
|
||||
.rowRenderer=${this._renderDeviceEndpoint}
|
||||
@ha-list-item-selected=${this._handleSelected}
|
||||
@ha-list-item-deselected=${this._handleDeselected}
|
||||
>
|
||||
</ha-list-selectable-virtualized>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-list">
|
||||
@@ -205,34 +193,32 @@ class DialogZHAAddGroupMembers
|
||||
);
|
||||
}
|
||||
|
||||
private get _filteredDeviceEndpoints(): ZHADeviceEndpoint[] {
|
||||
const normalizedFilter = this._filter.trim().toLowerCase();
|
||||
const deviceEndpoints = this._availableDeviceEndpoints;
|
||||
private _filteredDeviceEndpoints = memoizeOne(
|
||||
(filter: string, availableDeviceEndpoints: ZHADeviceEndpoint[]) => {
|
||||
const normalizedFilter = filter.trim().toLowerCase();
|
||||
let deviceEndpoints = availableDeviceEndpoints;
|
||||
|
||||
if (!normalizedFilter) {
|
||||
return deviceEndpoints;
|
||||
if (normalizedFilter) {
|
||||
deviceEndpoints = deviceEndpoints.filter((deviceEndpoint) =>
|
||||
[
|
||||
this._deviceEndpointName(deviceEndpoint),
|
||||
this._deviceEndpointDetails(deviceEndpoint),
|
||||
deviceEndpoint.device.ieee,
|
||||
deviceEndpoint.device.manufacturer,
|
||||
deviceEndpoint.device.model,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => value!.toLowerCase().includes(normalizedFilter))
|
||||
);
|
||||
}
|
||||
|
||||
return deviceEndpoints.map((deviceEndpoint) => ({
|
||||
id: this._deviceEndpointId(deviceEndpoint),
|
||||
interactive: true,
|
||||
...deviceEndpoint,
|
||||
}));
|
||||
}
|
||||
|
||||
return deviceEndpoints.filter((deviceEndpoint) =>
|
||||
[
|
||||
this._deviceEndpointName(deviceEndpoint),
|
||||
this._deviceEndpointDetails(deviceEndpoint),
|
||||
deviceEndpoint.device.ieee,
|
||||
deviceEndpoint.device.manufacturer,
|
||||
deviceEndpoint.device.model,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => value!.toLowerCase().includes(normalizedFilter))
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadVirtualizer(): Promise<void> {
|
||||
await loadVirtualizer();
|
||||
this._virtualizerReady = true;
|
||||
}
|
||||
|
||||
private _keyFunction = (deviceEndpoint: unknown): string =>
|
||||
this._deviceEndpointId(deviceEndpoint as ZHADeviceEndpoint);
|
||||
);
|
||||
|
||||
private _renderDeviceEndpoint: RenderItemFunction<ZHADeviceEndpoint> = (
|
||||
deviceEndpoint
|
||||
@@ -305,26 +291,30 @@ class DialogZHAAddGroupMembers
|
||||
this._filter = (ev.currentTarget as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _handleSelected(ev: CustomEvent<HaListSelectedDetail>): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
private _handleSelected(ev: CustomEvent<number>): void {
|
||||
let selectedDevicesToAdd = this._selectedDevicesToAdd;
|
||||
const item = this._filteredDeviceEndpoints(
|
||||
this._filter,
|
||||
this._availableDeviceEndpoints
|
||||
)[ev.detail];
|
||||
if (item && !selectedDevicesToAdd.includes(item.id)) {
|
||||
selectedDevicesToAdd = [...selectedDevicesToAdd, item.id];
|
||||
}
|
||||
|
||||
ev.detail.diff?.added.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value && !selectedDevicesToAdd.includes(item.value)) {
|
||||
selectedDevicesToAdd = [...selectedDevicesToAdd, item.value];
|
||||
}
|
||||
});
|
||||
|
||||
ev.detail.diff?.removed.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDevicesToAdd = selectedDevicesToAdd.filter(
|
||||
(selectedDeviceId) => selectedDeviceId !== item.value
|
||||
);
|
||||
}
|
||||
});
|
||||
this._selectedDevicesToAdd = selectedDevicesToAdd;
|
||||
}
|
||||
|
||||
private _handleDeselected(ev: CustomEvent<number>): void {
|
||||
let selectedDevicesToAdd = this._selectedDevicesToAdd;
|
||||
const item = this._filteredDeviceEndpoints(
|
||||
this._filter,
|
||||
this._availableDeviceEndpoints
|
||||
)[ev.detail];
|
||||
if (item && selectedDevicesToAdd.includes(item.id)) {
|
||||
selectedDevicesToAdd = selectedDevicesToAdd.filter(
|
||||
(value) => value !== item.id
|
||||
);
|
||||
}
|
||||
this._selectedDevicesToAdd = selectedDevicesToAdd;
|
||||
}
|
||||
|
||||
@@ -385,7 +375,7 @@ class DialogZHAAddGroupMembers
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
ha-list-selectable-virtualized {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../../components/list/types";
|
||||
import {
|
||||
areasContext,
|
||||
internationalizationContext,
|
||||
@@ -103,7 +102,8 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
? html`
|
||||
<ha-list-selectable
|
||||
multi
|
||||
@ha-list-selected=${this._handleListSelectionChanged}
|
||||
@ha-list-item-selected=${this._handleItemSelected}
|
||||
@ha-list-item-deselected=${this._handleItemDeselected}
|
||||
>
|
||||
${repeat(
|
||||
deviceEndpoints,
|
||||
@@ -261,36 +261,38 @@ export class ZHADeviceEndpointList extends LitElement {
|
||||
this._filter = (ev.currentTarget as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _handleListSelectionChanged(
|
||||
ev: CustomEvent<HaListSelectedDetail>
|
||||
): void {
|
||||
private _handleItemSelected(ev: CustomEvent<number>): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDeviceIds = this._selectedDeviceIds;
|
||||
|
||||
ev.detail.diff?.added.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDeviceIds = this._setSelectedDeviceId(
|
||||
selectedDeviceIds,
|
||||
item.value,
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
const item = list.items[ev.detail] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDeviceIds = this._setSelectedDeviceId(
|
||||
selectedDeviceIds,
|
||||
item.value,
|
||||
true
|
||||
);
|
||||
|
||||
ev.detail.diff?.removed.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDeviceIds = this._setSelectedDeviceId(
|
||||
selectedDeviceIds,
|
||||
item.value,
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
this._selectedDeviceIds = selectedDeviceIds;
|
||||
this._fireSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this._selectedDeviceIds = selectedDeviceIds;
|
||||
this._fireSelectionChanged();
|
||||
private _handleItemDeselected(ev: CustomEvent<number>): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDeviceIds = this._selectedDeviceIds;
|
||||
|
||||
const item = list.items[ev.detail] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDeviceIds = this._setSelectedDeviceId(
|
||||
selectedDeviceIds,
|
||||
item.value,
|
||||
false
|
||||
);
|
||||
|
||||
this._selectedDeviceIds = selectedDeviceIds;
|
||||
this._fireSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _setSelectedDeviceId(
|
||||
|
||||
@@ -27,8 +27,6 @@ class HaConfigScript extends HassRouterPage {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ attribute: false }) public scripts: ScriptEntity[] = [];
|
||||
@@ -78,7 +76,6 @@ class HaConfigScript extends HassRouterPage {
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.route = this.routeTail;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
pageEl.cloudStatus = this.cloudStatus;
|
||||
|
||||
if (this.hass) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user