mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-12 19:17:21 +00:00
Compare commits
32 Commits
floating-p
...
copilot/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76c21c5e78 | ||
|
|
b5afa9d457 | ||
|
|
6f38e4fd75 | ||
|
|
6db7817032 | ||
|
|
05ca8253f0 | ||
|
|
071161e82d | ||
|
|
9cd38c7128 | ||
|
|
44fcb72b79 | ||
|
|
a88fcd41b5 | ||
|
|
6322c19a45 | ||
|
|
74b51b77fe | ||
|
|
0a8c0c1c89 | ||
|
|
6f140d30f5 | ||
|
|
baa84ea36c | ||
|
|
68033fa8b4 | ||
|
|
5fd9bac9e5 | ||
|
|
a1c7f91dd0 | ||
|
|
10538989f1 | ||
|
|
b80481b53e | ||
|
|
2ce1eaf8c6 | ||
|
|
4030ce3f88 | ||
|
|
0b88a78ec0 | ||
|
|
885f31a4b4 | ||
|
|
e825a2b090 | ||
|
|
657f88595b | ||
|
|
fdeca95215 | ||
|
|
13b60ed2ee | ||
|
|
d74127001c | ||
|
|
eb9d1c119a | ||
|
|
cecf0a95c8 | ||
|
|
950a773f7e | ||
|
|
dddcd04ce9 |
@@ -16,8 +16,10 @@ import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -26,8 +28,6 @@ import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
|
||||
export interface RowClickedEvent {
|
||||
id: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expose } from "comlink";
|
||||
import Fuse from "fuse.js";
|
||||
import Fuse, { type FuseOptionKey } from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import { multiTermSearch } from "../../resources/fuseMultiTerm";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -11,9 +11,10 @@ import type {
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], columns: SortableColumnContainer) => {
|
||||
const getSearchKeys = memoizeOne(
|
||||
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
|
||||
const searchKeys = new Set<string>();
|
||||
|
||||
Object.entries(columns).forEach(([key, column]) => {
|
||||
if (column.filterable) {
|
||||
searchKeys.add(
|
||||
@@ -23,10 +24,15 @@ const fuseIndex = memoizeOne(
|
||||
);
|
||||
}
|
||||
});
|
||||
return Fuse.createIndex([...searchKeys], data);
|
||||
return Array.from(searchKeys);
|
||||
}
|
||||
);
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
|
||||
Fuse.createIndex(keys, data)
|
||||
);
|
||||
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
@@ -38,21 +44,13 @@ const filterData = (
|
||||
return data;
|
||||
}
|
||||
|
||||
const index = fuseIndex(data, columns);
|
||||
const keys = getSearchKeys(columns);
|
||||
|
||||
const fuse = new HaFuse(
|
||||
data,
|
||||
{ shouldSort: false, minMatchCharLength: 1 },
|
||||
index
|
||||
);
|
||||
const index = fuseIndex(data, keys);
|
||||
|
||||
const searchResults = fuse.multiTermsSearch(filter);
|
||||
|
||||
if (searchResults) {
|
||||
return searchResults.map((result) => result.item);
|
||||
}
|
||||
|
||||
return data;
|
||||
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
|
||||
threshold: 0.2, // reduce fuzzy matches in data tables
|
||||
});
|
||||
};
|
||||
|
||||
const sortData = (
|
||||
|
||||
@@ -9,6 +9,7 @@ import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
type DevicePickerItem,
|
||||
type DeviceRegistryEntry,
|
||||
@@ -216,6 +217,10 @@ export class HaDevicePicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${deviceComboBoxKeys}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.device-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity_registry";
|
||||
@@ -288,10 +289,14 @@ export class HaEntityPicker extends LitElement {
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.addButtonLabel=${this.addButton
|
||||
? this.hass.localize("ui.components.entity.entity-picker.add")
|
||||
: undefined}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
|
||||
@@ -38,9 +38,21 @@ type StatisticItemType = "entity" | "external" | "no_state";
|
||||
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
||||
statistic_id?: string;
|
||||
stateObj?: HassEntity;
|
||||
domainName?: string;
|
||||
type?: StatisticItemType;
|
||||
}
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "label", weight: 10 },
|
||||
{ name: "search_labels.entityName", weight: 10 },
|
||||
{ name: "search_labels.friendlyName", weight: 9 },
|
||||
{ name: "search_labels.deviceName", weight: 8 },
|
||||
{ name: "search_labels.areaName", weight: 6 },
|
||||
{ name: "search_labels.domainName", weight: 4 },
|
||||
{ name: "statisticId", weight: 3 },
|
||||
{ name: "id", weight: 2 },
|
||||
];
|
||||
|
||||
@customElement("ha-statistic-picker")
|
||||
export class HaStatisticPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -233,7 +245,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
),
|
||||
type,
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, id],
|
||||
icon_path: mdiShape,
|
||||
});
|
||||
} else if (type === "external") {
|
||||
@@ -246,7 +257,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary: domainName,
|
||||
type,
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, domainName, id],
|
||||
search_labels: { label, domainName },
|
||||
icon_path: mdiChartLine,
|
||||
});
|
||||
}
|
||||
@@ -280,13 +291,12 @@ export class HaStatisticPicker extends LitElement {
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: [
|
||||
entityName,
|
||||
deviceName,
|
||||
areaName,
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
id,
|
||||
].filter(Boolean) as string[],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -361,13 +371,13 @@ export class HaStatisticPicker extends LitElement {
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: [
|
||||
entityName,
|
||||
deviceName,
|
||||
areaName,
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
statisticId,
|
||||
].filter(Boolean) as string[],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -394,7 +404,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary: domainName,
|
||||
type: "external",
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, domainName, statisticId],
|
||||
search_labels: { label, domainName, statisticId },
|
||||
icon_path: mdiChartLine,
|
||||
};
|
||||
}
|
||||
@@ -409,7 +419,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
||||
type: "no_state",
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, statisticId],
|
||||
search_labels: { label, statisticId },
|
||||
icon_path: mdiShape,
|
||||
};
|
||||
}
|
||||
@@ -475,6 +485,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.helper=${this.helper}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.statistic-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import {
|
||||
getAreasAndFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
} from "../data/area_floor";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-icon-button";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
|
||||
const SEPARATOR = "________";
|
||||
|
||||
@customElement("ha-area-floor-picker")
|
||||
export class HaAreaFloorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreaFloorValue;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of areas to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-areas
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-areas" })
|
||||
public excludeAreas?: string[];
|
||||
|
||||
/**
|
||||
* List of floors to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-floors
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-floors" })
|
||||
public excludeFloors?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const item = this._parseValue(value);
|
||||
|
||||
const area = item.type === "area" && this.hass.areas[value];
|
||||
|
||||
if (area) {
|
||||
const areaName = computeAreaName(area);
|
||||
return html`
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<slot name="headline">${areaName}</slot>
|
||||
`;
|
||||
}
|
||||
|
||||
const floor = item.type === "floor" && this.hass.floors[value];
|
||||
|
||||
if (floor) {
|
||||
const floorName = computeFloorName(floor);
|
||||
return html`
|
||||
<ha-floor-icon slot="start" .floor=${floor}></ha-floor-icon>
|
||||
<span slot="headline">${floorName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
};
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
|
||||
item,
|
||||
{ index },
|
||||
combobox
|
||||
) => {
|
||||
const nextItem = combobox.filteredItems?.[index + 1];
|
||||
const isLastArea =
|
||||
!nextItem ||
|
||||
nextItem.type === "floor" ||
|
||||
(nextItem.type === "area" && !nextItem.area?.floor_id);
|
||||
|
||||
const rtl = computeRTL(this.hass);
|
||||
|
||||
const hasFloor = item.type === "area" && item.area?.floor_id;
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
type="button"
|
||||
style=${item.type === "area" && hasFloor
|
||||
? "--md-list-item-leading-space: 48px;"
|
||||
: ""}
|
||||
>
|
||||
${item.type === "area" && hasFloor
|
||||
? html`
|
||||
<ha-tree-indicator
|
||||
style=${styleMap({
|
||||
width: "48px",
|
||||
position: "absolute",
|
||||
top: "0px",
|
||||
left: rtl ? undefined : "4px",
|
||||
right: rtl ? "4px" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
.end=${isLastArea}
|
||||
slot="start"
|
||||
></ha-tree-indicator>
|
||||
`
|
||||
: nothing}
|
||||
${item.type === "floor" && item.floor
|
||||
? html`<ha-floor-icon
|
||||
slot="start"
|
||||
.floor=${item.floor}
|
||||
></ha-floor-icon>`
|
||||
: item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path || mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${item.primary}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getAreasAndFloorsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.floors,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._formatValue,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.excludeAreas,
|
||||
this.excludeFloors
|
||||
);
|
||||
|
||||
private _formatValue = memoizeOne((value: AreaFloorValue): string =>
|
||||
[value.type, value.id].join(SEPARATOR)
|
||||
);
|
||||
|
||||
private _parseValue = memoizeOne((value: string): AreaFloorValue => {
|
||||
const [type, id] = value.split(SEPARATOR);
|
||||
|
||||
return { id, type: type as "floor" | "area" };
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
|
||||
|
||||
const value = this.value ? this._formatValue(this.value) : undefined;
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.area-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${value}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = this._parseValue(value);
|
||||
this._setValue(selected);
|
||||
}
|
||||
|
||||
private _setValue(value?: AreaFloorValue) {
|
||||
this.value = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-floor-picker": HaAreaFloorPicker;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,12 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "areaName", weight: 10 },
|
||||
{ name: "aliases", weight: 8 },
|
||||
{ name: "floorName", weight: 6 },
|
||||
{ name: "id", weight: 3 },
|
||||
];
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -291,12 +297,12 @@ export class HaAreaPicker extends LitElement {
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
sorting_label: areaName,
|
||||
search_labels: [
|
||||
areaName,
|
||||
floorName,
|
||||
area.area_id,
|
||||
...area.aliases,
|
||||
].filter((v): v is string => Boolean(v)),
|
||||
search_labels: {
|
||||
areaName: areaName || null,
|
||||
floorName: floorName || null,
|
||||
id: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -379,6 +385,10 @@ export class HaAreaPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.area-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -134,6 +134,7 @@ export class HaAssistChat extends LitElement {
|
||||
})}"
|
||||
breaks
|
||||
cache
|
||||
assist
|
||||
.content=${message.text}
|
||||
>
|
||||
</ha-markdown>
|
||||
|
||||
@@ -35,6 +35,12 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "floorName", weight: 10 },
|
||||
{ name: "aliases", weight: 8 },
|
||||
{ name: "floor_id", weight: 3 },
|
||||
];
|
||||
|
||||
interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
floor?: FloorRegistryEntry;
|
||||
}
|
||||
@@ -286,9 +292,11 @@ export class HaFloorPicker extends LitElement {
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
sorting_label: floor.level?.toString() || "zzzzz",
|
||||
search_labels: [floorName, floor.floor_id, ...floor.aliases].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
),
|
||||
search_labels: {
|
||||
floorName,
|
||||
floor_id: floor.floor_id,
|
||||
aliases: floor.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -393,6 +401,10 @@ export class HaFloorPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.floor-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -4,8 +4,10 @@ import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
@@ -46,8 +48,9 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
/** To prevent lags, getItems needs to be memoized */
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@@ -64,6 +67,9 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchKeys?: FuseWeightedKey[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public notFoundLabel?: string | ((search: string) => string);
|
||||
|
||||
@@ -107,6 +113,8 @@ export class HaGenericPicker extends LitElement {
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
|
||||
|
||||
@query(".container") private _containerElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
@@ -156,6 +164,8 @@ export class HaGenericPicker extends LitElement {
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
.unknown=${this._unknownValue(this.value, this.getItems())}
|
||||
.unknownItemText=${this.unknownItemText}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@click=${this.open}
|
||||
@clear=${this._clear}
|
||||
@@ -229,10 +239,23 @@ export class HaGenericPicker extends LitElement {
|
||||
.sections=${this.sections}
|
||||
.sectionTitleFunction=${this.sectionTitleFunction}
|
||||
.selectedSection=${this.selectedSection}
|
||||
.searchKeys=${this.searchKeys}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _unknownValue = memoizeOne(
|
||||
(value?: string, items?: (PickerComboBoxItem | string)[]) => {
|
||||
if (value === undefined || value === null || value === "" || !items) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.some(
|
||||
(item) => typeof item !== "string" && item.id === value
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { LabelRegistryEntry } from "../data/label_registry";
|
||||
import {
|
||||
createLabelRegistryEntry,
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
subscribeLabelRegistry,
|
||||
} from "../data/label_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
@@ -237,6 +238,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
|
||||
|
||||
@@ -40,14 +40,12 @@ export const getLanguageOptions = (
|
||||
return {
|
||||
id: lang,
|
||||
primary,
|
||||
search_labels: [primary],
|
||||
};
|
||||
});
|
||||
} else if (locale) {
|
||||
options = languages.map((lang) => ({
|
||||
id: lang,
|
||||
primary: formatLanguageCode(lang, locale),
|
||||
search_labels: [formatLanguageCode(lang, locale)],
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -70,13 +70,15 @@ export class HaMarkdown extends LitElement {
|
||||
a {
|
||||
color: var(--markdown-link-color, var(--primary-color));
|
||||
}
|
||||
:host([assist]) img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
transition: height 0.2s ease-in-out;
|
||||
}
|
||||
img {
|
||||
background-color: var(--markdown-image-background-color);
|
||||
border-radius: var(--markdown-image-border-radius);
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
transition: height 0.2s ease-in-out;
|
||||
}
|
||||
p:first-child > img:first-child {
|
||||
vertical-align: top;
|
||||
|
||||
@@ -15,7 +15,10 @@ import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -26,11 +29,26 @@ import "./ha-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
export const DEFAULT_SEARCH_KEYS: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export interface PickerComboBoxItem {
|
||||
id: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
search_labels?: string[];
|
||||
search_labels?: Record<string, string | null>;
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
@@ -77,10 +95,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchKeys?: FuseWeightedKey[];
|
||||
|
||||
@state() private _listScrolled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@@ -133,6 +154,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
|
||||
@state() private _valuePinned = true;
|
||||
|
||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
@@ -194,6 +217,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
.renderItem=${this._renderItem}
|
||||
style="min-height: 36px;"
|
||||
class=${this._listScrolled ? "scrolled" : ""}
|
||||
.layout=${this.value && this._valuePinned
|
||||
? {
|
||||
pin: {
|
||||
index: this._getInitialSelectedIndex(),
|
||||
block: "center",
|
||||
},
|
||||
}
|
||||
: undefined}
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
@@ -244,15 +276,16 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleUnpinned() {
|
||||
this._valuePinned = false;
|
||||
}
|
||||
|
||||
private _getAdditionalItems = (searchString?: string) =>
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [
|
||||
...(this.getItems
|
||||
? this.getItems(this._search, this.selectedSection)
|
||||
: []),
|
||||
];
|
||||
let items = [...this.getItems(this._search, this.selectedSection)];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) =>
|
||||
@@ -279,6 +312,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
};
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
if (item === "padding") {
|
||||
return html`<div class="bottom-padding"></div>`;
|
||||
}
|
||||
@@ -339,8 +375,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
Fuse.createIndex(["search_labels"], states)
|
||||
private _fuseIndex = memoizeOne(
|
||||
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
||||
);
|
||||
|
||||
private _filterChanged = (ev: Event) => {
|
||||
@@ -356,34 +393,26 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
||||
const fuse = new HaFuse(
|
||||
const index = this._fuseIndex(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchString.length, 2),
|
||||
},
|
||||
index
|
||||
this.searchKeys
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchString);
|
||||
let filteredItems = [...this._allItems];
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
) as (PickerComboBoxItem | string)[];
|
||||
|
||||
if (results) {
|
||||
const items: (PickerComboBoxItem | string)[] = results.map(
|
||||
(result) => result.item
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
filteredItems = items;
|
||||
if (!filteredItems.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
filteredItems.push(...additionalItems);
|
||||
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(
|
||||
searchString,
|
||||
@@ -590,7 +619,25 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||
typeof item === "string" ? item : item.id;
|
||||
typeof item === "string" ? item : item?.id;
|
||||
|
||||
private _getInitialSelectedIndex() {
|
||||
if (!this._virtualizerElement || !this.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const index = this._virtualizerElement.items.findIndex(
|
||||
(item) =>
|
||||
typeof item !== "string" &&
|
||||
(item as PickerComboBoxItem).id === this.value
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@@ -7,8 +8,10 @@ import {
|
||||
type CSSResultGroup,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { localizeContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
@@ -33,6 +36,10 @@ export class HaPickerField extends LitElement {
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public unknown = false;
|
||||
|
||||
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
|
||||
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@@ -41,6 +48,10 @@ export class HaPickerField extends LitElement {
|
||||
|
||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: HomeAssistant["localize"];
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.item?.focus();
|
||||
@@ -61,6 +72,12 @@ export class HaPickerField extends LitElement {
|
||||
${this.placeholder}
|
||||
</span>
|
||||
`}
|
||||
${this.unknown
|
||||
? html`<div slot="supporting-text" class="unknown">
|
||||
${this.unknownItemText ||
|
||||
this.localize("ui.components.combo-box.unknown_item")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${showClearIcon
|
||||
? html`
|
||||
<ha-icon-button
|
||||
@@ -142,6 +159,10 @@ export class HaPickerField extends LitElement {
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
:host([unknown]) ha-combo-box-item {
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
@@ -156,6 +177,10 @@ export class HaPickerField extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ interface ServiceComboBoxItem extends PickerComboBoxItem {
|
||||
service_id?: string;
|
||||
}
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "name", weight: 10 },
|
||||
{ name: "description", weight: 8 },
|
||||
{ name: "domainName", weight: 6 },
|
||||
{ name: "serviceId", weight: 3 },
|
||||
];
|
||||
|
||||
@customElement("ha-service-picker")
|
||||
class HaServicePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -141,6 +148,10 @@ class HaServicePicker extends LitElement {
|
||||
this.hass.localize,
|
||||
this.hass.services
|
||||
)}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.service-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
@@ -194,9 +205,7 @@ class HaServicePicker extends LitElement {
|
||||
secondary: description,
|
||||
domain_name: domainName,
|
||||
service_id: serviceId,
|
||||
search_labels: [serviceId, domainName, name, description].filter(
|
||||
Boolean
|
||||
),
|
||||
search_labels: { serviceId, domainName, name, description },
|
||||
sorting_label: serviceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,17 +15,30 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import {
|
||||
areaFloorComboBoxKeys,
|
||||
getAreasAndFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
} from "../data/area_floor";
|
||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { getDevices, type DevicePickerItem } from "../data/device_registry";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
type DevicePickerItem,
|
||||
} from "../data/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
|
||||
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
type EntityComboBoxItem,
|
||||
} from "../data/entity_registry";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
|
||||
import {
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
type LabelRegistryEntry,
|
||||
} from "../data/label_registry";
|
||||
import {
|
||||
areaMeetsFilter,
|
||||
deviceMeetsFilter,
|
||||
@@ -37,7 +50,11 @@ import {
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import {
|
||||
multiTermSearch,
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
@@ -113,16 +130,16 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _fuseIndexes = {
|
||||
area: memoizeOne((states: FloorComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, areaFloorComboBoxKeys)
|
||||
),
|
||||
entity: memoizeOne((states: EntityComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, entityComboBoxKeys)
|
||||
),
|
||||
device: memoizeOne((states: DevicePickerItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, deviceComboBoxKeys)
|
||||
),
|
||||
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, labelComboBoxKeys)
|
||||
),
|
||||
};
|
||||
|
||||
@@ -134,8 +151,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _createFuseIndex = (states) =>
|
||||
Fuse.createIndex(["search_labels"], states);
|
||||
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(keys, states);
|
||||
|
||||
protected render() {
|
||||
if (this.addOnTop) {
|
||||
@@ -735,8 +752,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
(item: EntityComboBoxItem) =>
|
||||
item.stateObj?.entity_id === searchTerm
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -765,7 +781,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterType && deviceItems.length) {
|
||||
@@ -799,7 +820,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -844,7 +867,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
labels = this._filterGroup("label", labels, searchTerm);
|
||||
labels = this._filterGroup(
|
||||
"label",
|
||||
labels,
|
||||
searchTerm,
|
||||
labelComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterType && labels.length) {
|
||||
@@ -863,40 +891,24 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
type: TargetType,
|
||||
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
|
||||
searchTerm: string,
|
||||
checkExact?: (
|
||||
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
|
||||
) => boolean
|
||||
weightedKeys: FuseWeightedKey[],
|
||||
sort = true
|
||||
) {
|
||||
const fuseIndex = this._fuseIndexes[type](items);
|
||||
const fuse = new HaFuse(
|
||||
items,
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchTerm.length, 2),
|
||||
},
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchTerm);
|
||||
let filteredItems = items;
|
||||
if (results) {
|
||||
filteredItems = results.map((result) => result.item);
|
||||
if (sort) {
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (!checkExact) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex((item) => checkExact(item));
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
|
||||
return filteredItems;
|
||||
return multiTermSearch(items, searchTerm, weightedKeys, fuseIndex, {
|
||||
ignoreLocation: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
|
||||
|
||||
@@ -17,6 +17,12 @@ interface UserComboBoxItem extends PickerComboBoxItem {
|
||||
user?: User;
|
||||
}
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "primary", weight: 10 },
|
||||
{ name: "search_labels.username", weight: 6 },
|
||||
{ name: "id", weight: 3 },
|
||||
];
|
||||
|
||||
@customElement("ha-user-picker")
|
||||
class HaUserPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -109,9 +115,7 @@ class HaUserPicker extends LitElement {
|
||||
id: user.id,
|
||||
primary: user.name,
|
||||
domain_name: user.name,
|
||||
search_labels: [user.name, user.id, user.username].filter(
|
||||
Boolean
|
||||
) as string[],
|
||||
search_labels: { username: user.username },
|
||||
sorting_label: user.name,
|
||||
user,
|
||||
}));
|
||||
@@ -134,6 +138,10 @@ class HaUserPicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.user-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import {
|
||||
@@ -26,7 +27,8 @@ export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
||||
export interface UnassignedAreasFloorComboBoxItem {
|
||||
id: undefined;
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -98,6 +100,29 @@ export const getAreasAndFloors = (
|
||||
excludeFloors
|
||||
) as FloorComboBoxItem[];
|
||||
|
||||
export const areaFloorComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.name",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.aliases",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "search_labels.floorName",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.relatedAreas",
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
name: "search_labels.id",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const getAreasAndFloorsItems = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
@@ -304,12 +329,12 @@ const getAreasAndFloorsItems = (
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
icon: floor.icon || undefined,
|
||||
search_labels: [
|
||||
floor.floor_id,
|
||||
floorName,
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
search_labels: {
|
||||
id: floor.floor_id,
|
||||
name: floorName || null,
|
||||
aliases: floor.aliases.join(", ") || null,
|
||||
relatedAreas: areaSearchLabels.join(" ") || null,
|
||||
},
|
||||
};
|
||||
|
||||
items.push(floorItem);
|
||||
@@ -322,11 +347,12 @@ const getAreasAndFloorsItems = (
|
||||
primary: areaName || area.area_id,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [
|
||||
area.area_id,
|
||||
...(areaName ? [areaName] : []),
|
||||
...area.aliases,
|
||||
],
|
||||
search_labels: {
|
||||
id: area.area_id,
|
||||
name: areaName || null,
|
||||
aliases: area.aliases.join(", ") || null,
|
||||
floorName: floorName || null,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -339,19 +365,24 @@ const getAreasAndFloorsItems = (
|
||||
|
||||
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
|
||||
const area = haAreas[areaId];
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
type: "area" as const,
|
||||
primary: areaName,
|
||||
primary: areaName || area.area_id,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
search_labels: {
|
||||
id: area.area_id,
|
||||
name: areaName || null,
|
||||
aliases: area.aliases.join(", ") || null,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (nested && unassignedAreaItems.length) {
|
||||
items.push({
|
||||
id: undefined,
|
||||
areas: unassignedAreaItems,
|
||||
} as UnassignedAreasFloorComboBoxItem);
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getDeviceContext } from "../common/entity/context/get_device_context";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { ConfigEntry } from "./config_entries";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
@@ -181,6 +182,25 @@ export interface DevicePickerItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
}
|
||||
|
||||
export const deviceComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.deviceName",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.areaName",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domainName",
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domain",
|
||||
weight: 4,
|
||||
},
|
||||
];
|
||||
|
||||
export const getDevices = (
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
@@ -311,9 +331,12 @@ export const getDevices = (
|
||||
secondary: areaName,
|
||||
domain: configEntry?.domain,
|
||||
domain_name: domainName,
|
||||
search_labels: [deviceName, areaName, domain, domainName].filter(
|
||||
Boolean
|
||||
) as string[],
|
||||
search_labels: {
|
||||
deviceName,
|
||||
areaName: areaName || null,
|
||||
domain: domain || null,
|
||||
domainName: domainName || null,
|
||||
},
|
||||
sorting_label: deviceName || "zzz",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
import { domainToName } from "./integration";
|
||||
@@ -335,6 +336,33 @@ export interface EntityComboBoxItem extends PickerComboBoxItem {
|
||||
stateObj?: HassEntity;
|
||||
}
|
||||
|
||||
export const entityComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.entityName",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.friendlyName",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "search_labels.deviceName",
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
name: "search_labels.areaName",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domainName",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.entityId",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export const getEntities = (
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
@@ -403,14 +431,14 @@ export const getEntities = (
|
||||
secondary: secondary,
|
||||
domain_name: domainName,
|
||||
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
|
||||
search_labels: [
|
||||
entityName,
|
||||
deviceName,
|
||||
areaName,
|
||||
domainName,
|
||||
friendlyName,
|
||||
entityId,
|
||||
].filter(Boolean) as string[],
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
domainName: domainName || null,
|
||||
friendlyName: friendlyName || null,
|
||||
entityId: entityId,
|
||||
},
|
||||
stateObj: stateObj,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
@@ -100,6 +101,21 @@ export const deleteLabelRegistryEntry = (
|
||||
label_id: labelId,
|
||||
});
|
||||
|
||||
export const labelComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.name",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.description",
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
name: "search_labels.id",
|
||||
weight: 4,
|
||||
},
|
||||
];
|
||||
|
||||
export const getLabels = (
|
||||
hassStates: HomeAssistant["states"],
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
@@ -273,9 +289,11 @@ export const getLabels = (
|
||||
icon: label.icon || undefined,
|
||||
icon_path: label.icon ? undefined : mdiLabel,
|
||||
sorting_label: label.name,
|
||||
search_labels: [label.name, label.label_id, label.description].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
),
|
||||
search_labels: {
|
||||
name: label.name,
|
||||
description: label.description,
|
||||
id: label.label_id,
|
||||
},
|
||||
}));
|
||||
|
||||
return items;
|
||||
|
||||
@@ -45,7 +45,7 @@ import { domainToName } from "../../data/integration";
|
||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../../panels/config/ha-panel-config";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import { multiTermSortedSearch } from "../../resources/fuseMultiTerm";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
@@ -58,7 +58,17 @@ import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "primaryText", weight: 10 },
|
||||
{ name: "altText", weight: 8 },
|
||||
{ name: "friendlyName", weight: 8 },
|
||||
{ name: "area", weight: 6 },
|
||||
{ name: "translatedDomain", weight: 5 },
|
||||
{ name: "entityId", weight: 4 }, // for technical search
|
||||
];
|
||||
|
||||
interface QuickBarItem extends ScorableTextItem {
|
||||
id: string;
|
||||
primaryText: string;
|
||||
iconPath?: string;
|
||||
action(data?: any): void;
|
||||
@@ -593,6 +603,7 @@ export class QuickBar extends LitElement {
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
const deviceItem = {
|
||||
id: device.id,
|
||||
primaryText: deviceName,
|
||||
deviceId: device.id,
|
||||
area: areaName,
|
||||
@@ -666,6 +677,7 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
|
||||
const entityItem = {
|
||||
id: `entity-${entityId}`,
|
||||
primaryText: primary,
|
||||
altText: secondary,
|
||||
icon: html`
|
||||
@@ -767,8 +779,9 @@ export class QuickBar extends LitElement {
|
||||
),
|
||||
});
|
||||
|
||||
return commands.map((command) => ({
|
||||
return commands.map((command, index) => ({
|
||||
...command,
|
||||
id: `command_${index}_${command.primaryText}`,
|
||||
categoryKey: "reload",
|
||||
strings: [`${command.categoryText} ${command.primaryText}`],
|
||||
}));
|
||||
@@ -777,10 +790,11 @@ export class QuickBar extends LitElement {
|
||||
private _generateServerControlCommands(): CommandItem[] {
|
||||
const serverActions = ["restart", "stop"] as const;
|
||||
|
||||
return serverActions.map((action) => {
|
||||
return serverActions.map((action, index) => {
|
||||
const categoryKey: CommandItem["categoryKey"] = "server_control";
|
||||
|
||||
const item = {
|
||||
id: `server_control_${index}_${action}`,
|
||||
primaryText: this.hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
{
|
||||
@@ -940,10 +954,11 @@ export class QuickBar extends LitElement {
|
||||
private _finalizeNavigationCommands(
|
||||
items: BaseNavigationCommand[]
|
||||
): CommandItem[] {
|
||||
return items.map((item) => {
|
||||
return items.map((item, index) => {
|
||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
||||
|
||||
const navItem = {
|
||||
id: `navigation_${index}_${item.path}`,
|
||||
iconPath: mdiEarth,
|
||||
categoryText: this.hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||
@@ -961,28 +976,20 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
private _fuseIndex = memoizeOne((items: QuickBarItem[]) =>
|
||||
Fuse.createIndex(
|
||||
[
|
||||
"primaryText",
|
||||
"altText",
|
||||
"friendlyName",
|
||||
"translatedDomain",
|
||||
"entityId", // for technical search
|
||||
],
|
||||
items
|
||||
)
|
||||
Fuse.createIndex(SEARCH_KEYS, items)
|
||||
);
|
||||
|
||||
private _filterItems = memoizeOne(
|
||||
(items: QuickBarItem[], filter: string): QuickBarItem[] => {
|
||||
const index = this._fuseIndex(items);
|
||||
const fuse = new HaFuse(items, {}, index);
|
||||
|
||||
const results = fuse.multiTermsSearch(filter.trim());
|
||||
if (!results || !results.length) {
|
||||
return items;
|
||||
}
|
||||
return results.map((result) => result.item);
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
filter,
|
||||
SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../components/ha-svg-icon";
|
||||
import "../components/ha-tab";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
@@ -113,11 +112,9 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("route")) {
|
||||
withViewTransition(() => {
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
`${this.route.prefix}${this.route.path}`.includes(tab.path)
|
||||
);
|
||||
});
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
`${this.route.prefix}${this.route.path}`.includes(tab.path)
|
||||
);
|
||||
}
|
||||
super.willUpdate(changedProperties);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ import { slugify } from "../../../common/string/slugify";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
@@ -226,32 +228,23 @@ class HaConfigAreaPage extends LitElement {
|
||||
></ha-icon>`
|
||||
: nothing}${area.name}`}
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
<ha-dropdown-item value="settings" .entry=${area}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.areas.edit_settings")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
@click=${this._deleteConfirm}
|
||||
>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.areas.editor.delete")}
|
||||
<ha-svg-icon class="warning" slot="graphic" .path=${mdiDelete}>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
|
||||
<div class="container">
|
||||
<div class="column">
|
||||
@@ -613,6 +606,20 @@ class HaConfigAreaPage extends LitElement {
|
||||
this._related = await findRelated(this.hass, "area", this.areaId);
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem & {
|
||||
entry: AreaRegistryEntry;
|
||||
};
|
||||
switch (item.value) {
|
||||
case "settings":
|
||||
this._openDialog(item.entry);
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _showSettings(ev: MouseEvent) {
|
||||
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
@@ -24,10 +24,12 @@ import {
|
||||
type AreasFloorHierarchy,
|
||||
} from "../../../common/areas/areas-floor-hierarchy";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
@@ -196,44 +198,43 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
${floor.name}
|
||||
</h2>
|
||||
<div class="actions">
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
.floor=${floor}
|
||||
@action=${this._handleFloorAction}
|
||||
@wa-select=${this._handleFloorAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
<ha-dropdown-item value="reorder">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiSort}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.reorder"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.edit_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="warning" graphic="icon"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.delete_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<ha-sortable
|
||||
@@ -273,23 +274,23 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
)}
|
||||
</h2>
|
||||
<div class="actions">
|
||||
<ha-button-menu
|
||||
@action=${this._handleUnassignedAreasAction}
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleUnassignedAreasAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
<ha-dropdown-item value="reorder">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiSort}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.reorder"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<ha-sortable
|
||||
@@ -533,23 +534,25 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
}, time);
|
||||
}
|
||||
|
||||
private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
|
||||
private _handleFloorAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
const floor = (ev.currentTarget as any).floor;
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
switch (item.value) {
|
||||
case "reorder":
|
||||
this._showReorderDialog();
|
||||
break;
|
||||
case 1:
|
||||
case "edit":
|
||||
this._editFloor(floor);
|
||||
break;
|
||||
case 2:
|
||||
case "delete":
|
||||
this._deleteFloor(floor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleUnassignedAreasAction(ev: CustomEvent<ActionDetail>) {
|
||||
if (ev.detail.index === 0) {
|
||||
private _handleUnassignedAreasAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
if (item.value === "reorder") {
|
||||
this._showReorderDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import { consume } from "@lit/context";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import {
|
||||
@@ -34,6 +33,7 @@ import "../../../../components/ha-section-title";
|
||||
import "../../../../components/ha-tree-indicator";
|
||||
import { ACTION_BUILDING_BLOCKS_GROUP } from "../../../../data/action";
|
||||
import {
|
||||
areaFloorComboBoxKeys,
|
||||
getAreasAndFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
@@ -42,23 +42,30 @@ import { CONDITION_BUILDING_BLOCKS_GROUP } from "../../../../data/condition";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import { labelsContext } from "../../../../data/context";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
type DevicePickerItem,
|
||||
} from "../../../../data/device_registry";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../../../data/entity_registry";
|
||||
import type { DomainManifestLookup } from "../../../../data/integration";
|
||||
import {
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
type LabelRegistryEntry,
|
||||
} from "../../../../data/label_registry";
|
||||
import {
|
||||
getTargetComboBoxItemType,
|
||||
TARGET_SEPARATOR,
|
||||
} from "../../../../data/target";
|
||||
import { HaFuse } from "../../../../resources/fuse";
|
||||
import {
|
||||
multiTermSearch,
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../../../../resources/fuseMultiTerm";
|
||||
import { loadVirtualizer } from "../../../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
@@ -76,6 +83,17 @@ const TARGET_SEARCH_SECTIONS = [
|
||||
"label",
|
||||
] as const;
|
||||
|
||||
export const ITEM_SEARCH_KEYS: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "name",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
weight: 7,
|
||||
},
|
||||
];
|
||||
|
||||
type SearchSection = "item" | "block" | "entity" | "device" | "area" | "label";
|
||||
|
||||
@customElement("ha-automation-add-search")
|
||||
@@ -434,27 +452,27 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||
typeof item === "string" ? item : item.id;
|
||||
|
||||
private _createFuseIndex = (states) =>
|
||||
Fuse.createIndex(["search_labels"], states);
|
||||
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(keys, states);
|
||||
|
||||
private _fuseIndexes = {
|
||||
area: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, areaFloorComboBoxKeys)
|
||||
),
|
||||
entity: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, entityComboBoxKeys)
|
||||
),
|
||||
device: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, deviceComboBoxKeys)
|
||||
),
|
||||
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, labelComboBoxKeys)
|
||||
),
|
||||
item: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, ITEM_SEARCH_KEYS)
|
||||
),
|
||||
block: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, ITEM_SEARCH_KEYS)
|
||||
),
|
||||
};
|
||||
|
||||
@@ -478,11 +496,12 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
if (!selectedSection || selectedSection === "item") {
|
||||
let items = this._convertItemsToComboBoxItems(automationItems, type);
|
||||
if (searchTerm) {
|
||||
items = this._filterGroup("item", items, searchTerm, {
|
||||
ignoreLocation: true,
|
||||
includeScore: true,
|
||||
minMatchCharLength: Math.min(2, this.filter.length),
|
||||
}) as AutomationItemComboBoxItem[];
|
||||
items = this._filterGroup(
|
||||
"item",
|
||||
items,
|
||||
searchTerm,
|
||||
ITEM_SEARCH_KEYS
|
||||
) as AutomationItemComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && items.length) {
|
||||
@@ -514,11 +533,12 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
blocks = this._filterGroup("block", blocks, searchTerm, {
|
||||
ignoreLocation: true,
|
||||
includeScore: true,
|
||||
minMatchCharLength: Math.min(2, this.filter.length),
|
||||
}) as AutomationItemComboBoxItem[];
|
||||
blocks = this._filterGroup(
|
||||
"block",
|
||||
blocks,
|
||||
searchTerm,
|
||||
ITEM_SEARCH_KEYS
|
||||
) as AutomationItemComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && blocks.length) {
|
||||
@@ -550,9 +570,7 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
undefined,
|
||||
(item: EntityComboBoxItem) =>
|
||||
item.stateObj?.entity_id === searchTerm
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -581,7 +599,12 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedSection && deviceItems.length) {
|
||||
@@ -617,7 +640,9 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -664,7 +689,12 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
labels = this._filterGroup("label", labels, searchTerm);
|
||||
labels = this._filterGroup(
|
||||
"label",
|
||||
labels,
|
||||
searchTerm,
|
||||
labelComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedSection && labels.length) {
|
||||
@@ -691,50 +721,28 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
| AutomationItemComboBoxItem
|
||||
)[],
|
||||
searchTerm: string,
|
||||
fuseOptions?: IFuseOptions<any>,
|
||||
checkExact?: (
|
||||
item:
|
||||
| FloorComboBoxItem
|
||||
| PickerComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| AutomationItemComboBoxItem
|
||||
) => boolean
|
||||
searchKeys: FuseWeightedKey[],
|
||||
sort = true
|
||||
) {
|
||||
const fuseIndex = this._fuseIndexes[type](items);
|
||||
const fuse = new HaFuse<
|
||||
| FloorComboBoxItem
|
||||
| PickerComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| AutomationItemComboBoxItem
|
||||
>(
|
||||
|
||||
if (sort) {
|
||||
return multiTermSortedSearch<PickerComboBoxItem>(
|
||||
items,
|
||||
searchTerm,
|
||||
searchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
|
||||
return multiTermSearch<PickerComboBoxItem>(
|
||||
items,
|
||||
fuseOptions || {
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchTerm.length, 2),
|
||||
},
|
||||
fuseIndex
|
||||
searchTerm,
|
||||
searchKeys,
|
||||
fuseIndex,
|
||||
{ ignoreLocation: true }
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchTerm);
|
||||
let filteredItems = items;
|
||||
if (results) {
|
||||
filteredItems = results.map((result) => result.item);
|
||||
}
|
||||
|
||||
if (!checkExact) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex((item) => checkExact(item));
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
private _toggleSection(ev: Event) {
|
||||
@@ -787,7 +795,11 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
iconPath,
|
||||
renderedIcon: icon,
|
||||
type,
|
||||
search_labels: [key, name, description],
|
||||
search_labels: {
|
||||
key,
|
||||
name,
|
||||
description,
|
||||
},
|
||||
}));
|
||||
|
||||
private _focusSearchList = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
@@ -23,7 +23,7 @@ import { differenceInDays } from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
@@ -36,7 +36,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -51,6 +50,9 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-blueprints";
|
||||
import "../../../components/ha-filter-categories";
|
||||
@@ -59,13 +61,9 @@ import "../../../components/ha-filter-entities";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import {
|
||||
@@ -175,8 +173,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@state() private _overflowAutomation?: AutomationItem;
|
||||
|
||||
@storage({ key: "automation-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@@ -204,8 +200,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@@ -368,12 +362,81 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (automation) => html`
|
||||
<ha-icon-button
|
||||
<ha-dropdown
|
||||
.automation=${automation}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._showOverflowMenu}
|
||||
></ha-icon-button>
|
||||
@wa-select=${this._handleRowOverflowMenu}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-info">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_info"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="show-settings">
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit-category">
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${automation.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="run-actions">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="show-trace">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTransitConnection}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_trace"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="duplicate">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.duplicate"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="toggle">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${automation.state === "off"
|
||||
? mdiToggleSwitch
|
||||
: mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
${automation.state === "off"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.enable"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.disable"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`,
|
||||
},
|
||||
};
|
||||
@@ -381,17 +444,38 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
return;
|
||||
private _handleRowOverflowMenu = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
const automation = (
|
||||
(ev.currentTarget as HTMLElement).closest("ha-dropdown") as any
|
||||
).automation;
|
||||
|
||||
switch (item.value) {
|
||||
case "show-info":
|
||||
this._showInfo(automation);
|
||||
break;
|
||||
case "show-settings":
|
||||
this._showSettings(automation);
|
||||
break;
|
||||
case "edit-category":
|
||||
this._editCategory(automation);
|
||||
break;
|
||||
case "run-actions":
|
||||
this._runActions(automation);
|
||||
break;
|
||||
case "show-trace":
|
||||
this._showTrace(automation);
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicate(automation);
|
||||
break;
|
||||
case "toggle":
|
||||
this._toggle(automation);
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm(automation);
|
||||
break;
|
||||
}
|
||||
this._overflowAutomation = ev.target.automation;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
};
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@@ -409,34 +493,38 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__no_category__"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_category__"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
@@ -446,14 +534,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
@@ -467,46 +555,50 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item>`;
|
||||
</ha-dropdown-item>`;
|
||||
})}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_label__"
|
||||
@click=${this._bulkCreateLabel}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_area"
|
||||
data-area=${area.area_id}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
slot="icon"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="__no_area__">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_area__"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -527,9 +619,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
@@ -541,16 +633,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
has-filters
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length
|
||||
}
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.localize,
|
||||
@@ -643,13 +733,31 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-blueprints>
|
||||
${
|
||||
!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -657,179 +765,123 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
</div>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-menu-item .clickAction=${this._handleBulkEnable}>
|
||||
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.enable"
|
||||
</ha-assist-chip>
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
: nothing}
|
||||
<ha-dropdown
|
||||
has-overflow
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._handleBulkDisable}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.disable"
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
${
|
||||
!this.automations.length
|
||||
? html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
|
||||
<h1>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_header"
|
||||
)}
|
||||
</h1>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_text_1"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_text_2",
|
||||
{ user: this.hass.user?.name || "Alice" }
|
||||
)}
|
||||
</p>
|
||||
<ha-button
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/automation/editor/"
|
||||
)}
|
||||
target="_blank"
|
||||
appearance="plain"
|
||||
rel="noreferrer"
|
||||
size="small"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.common.learn_more")}
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}> </ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
<ha-dropdown-item value="enable">
|
||||
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.enable"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="disable">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.disable"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${!this.automations.length
|
||||
? html`<div class="empty" slot="empty">
|
||||
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
|
||||
<h1>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_header"
|
||||
)}
|
||||
</h1>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_text_1"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.empty_text_2",
|
||||
{ user: this.hass.user?.name || "Alice" }
|
||||
)}
|
||||
</p>
|
||||
<ha-button
|
||||
href=${documentationUrl(this.hass, "/docs/automation/editor/")}
|
||||
target="_blank"
|
||||
appearance="plain"
|
||||
rel="noreferrer"
|
||||
size="small"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.common.learn_more")}
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}> </ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize(
|
||||
@@ -841,80 +893,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._showInfo}>
|
||||
<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._showSettings}>
|
||||
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._editCategory}>
|
||||
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._runActions}>
|
||||
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._showTrace}>
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_trace"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._duplicate}>
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._toggle}>
|
||||
<ha-svg-icon
|
||||
.path=${
|
||||
this._overflowAutomation?.state === "off"
|
||||
? mdiToggleSwitch
|
||||
: mdiToggleSwitchOffOutline
|
||||
}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${
|
||||
this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.disable"
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1071,33 +1049,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _showInfo = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
private _showInfo = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
|
||||
};
|
||||
|
||||
private _showSettings = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _showSettings = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
};
|
||||
|
||||
private _runActions = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _runActions = (automation: AutomationItem) => {
|
||||
triggerAutomationActions(this.hass, automation.entity_id);
|
||||
};
|
||||
|
||||
private _editCategory = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _editCategory = (automation: AutomationItem) => {
|
||||
const entityReg = this._entityReg.find(
|
||||
(reg) => reg.entity_id === automation.entity_id
|
||||
);
|
||||
@@ -1118,10 +1085,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _showTrace = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _showTrace = (automation: AutomationItem) => {
|
||||
if (!automation.attributes.id) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -1135,20 +1099,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
};
|
||||
|
||||
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _toggle = async (automation: AutomationItem): Promise<void> => {
|
||||
const service = automation.state === "off" ? "turn_on" : "turn_off";
|
||||
await this.hass.callService("automation", service, {
|
||||
entity_id: automation.entity_id,
|
||||
});
|
||||
};
|
||||
|
||||
private _deleteConfirm = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _deleteConfirm = async (automation: AutomationItem) => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete_confirm_title"
|
||||
@@ -1164,8 +1122,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private async _delete(automation) {
|
||||
private async _delete(automation: AutomationEntity) {
|
||||
try {
|
||||
if (!automation.attributes.id) {
|
||||
throw new Error("Automation ID is missing");
|
||||
}
|
||||
await deleteAutomation(this.hass, automation.attributes.id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== automation.entity_id
|
||||
@@ -1185,11 +1146,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _duplicate = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _duplicate = async (automation: AutomationEntity) => {
|
||||
try {
|
||||
if (!automation.attributes.id) {
|
||||
throw new Error("Automation ID is missing");
|
||||
}
|
||||
const config = await fetchAutomationFileConfig(
|
||||
this.hass,
|
||||
automation.attributes.id
|
||||
@@ -1261,11 +1222,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBulkCategory = async (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1292,8 +1248,9 @@ ${rejected
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
ev.stopPropagation();
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
const action = ev.currentTarget.dataset.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
@@ -1327,11 +1284,6 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1367,6 +1319,40 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "enable":
|
||||
this._handleBulkEnable();
|
||||
break;
|
||||
case "disable":
|
||||
this._handleBulkDisable();
|
||||
break;
|
||||
case "__create_category__":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "__no_category__":
|
||||
this._bulkAddCategory("");
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
case "__create_area__":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "__no_area__":
|
||||
this._bulkAddArea("");
|
||||
break;
|
||||
case "move_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private _handleBulkEnable = async () => {
|
||||
const promises: Promise<ServiceCallResponse>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1477,7 +1463,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { mdiClose, mdiFolderUpload } from "@mdi/js";
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-file-upload";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import {
|
||||
CORE_LOCAL_AGENT,
|
||||
HASSIO_LOCAL_AGENT,
|
||||
@@ -43,11 +41,12 @@ export class DialogUploadBackup
|
||||
|
||||
@state() private _formData?: BackupUploadFileFormData;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
@@ -56,11 +55,12 @@ export class DialogUploadBackup
|
||||
}
|
||||
this._formData = undefined;
|
||||
this._params = undefined;
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -74,52 +74,44 @@ export class DialogUploadBackup
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.title"
|
||||
)}
|
||||
?prevent-scrim-close=${this._uploading}
|
||||
@closed=${this._dialogClosed}
|
||||
.disableCancelAction=${this._uploading}
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._uploading}
|
||||
></ha-icon-button>
|
||||
|
||||
<span slot="title">
|
||||
${this.hass.localize("ui.panel.config.backup.dialogs.upload.title")}
|
||||
</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||
.localize=${this.hass.localize}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.input_label"
|
||||
)}
|
||||
.supports=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
||||
)}
|
||||
@file-picked=${this._filePicked}
|
||||
@files-cleared=${this._filesCleared}
|
||||
></ha-file-upload>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||
.localize=${this.hass.localize}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.input_label"
|
||||
)}
|
||||
.supports=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
||||
)}
|
||||
@file-picked=${this._filePicked}
|
||||
@files-cleared=${this._filesCleared}
|
||||
></ha-file-upload>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._uploading}
|
||||
>${this.hass.localize("ui.common.cancel")}</ha-button
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._upload}
|
||||
.disabled=${!this._formValid() || this._uploading}
|
||||
>
|
||||
@@ -127,8 +119,8 @@ export class DialogUploadBackup
|
||||
"ui.panel.config.backup.dialogs.upload.action"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -183,15 +175,9 @@ export class DialogUploadBackup
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 100%;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -120,9 +120,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
icon: category.icon || undefined,
|
||||
icon_path: category.icon ? undefined : mdiTag,
|
||||
sorting_label: category.name,
|
||||
search_labels: [category.name, category.category_id].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
),
|
||||
}));
|
||||
|
||||
return items;
|
||||
@@ -203,6 +200,9 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.category-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -6,8 +6,10 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-tip";
|
||||
import type {
|
||||
@@ -53,26 +55,26 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="reset">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.reset_cloud_data"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="download">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.download_support_package"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div class="content">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Home Assistant Cloud</span>
|
||||
@@ -297,13 +299,15 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "reset":
|
||||
this._deleteCloudData();
|
||||
break;
|
||||
case 1:
|
||||
case "download":
|
||||
this._downloadSupportPackage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-list";
|
||||
import "../../../../components/ha-list-item";
|
||||
@@ -44,26 +46,26 @@ export class CloudLoginPanel extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
header="Home Assistant Cloud"
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="reset">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.reset_cloud_data"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="download">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.download_support_package"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div class="content">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Home Assistant Cloud</span>
|
||||
@@ -164,13 +166,15 @@ export class CloudLoginPanel extends LitElement {
|
||||
fireEvent(this, "flash-message-changed", { value: "" });
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "reset":
|
||||
this._deleteCloudData();
|
||||
break;
|
||||
case 1:
|
||||
case "download":
|
||||
this._downloadSupportPackage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
@@ -6,13 +6,12 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-metric";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
@@ -73,24 +72,24 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu multi>
|
||||
<ha-dropdown @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-check-list-item
|
||||
left
|
||||
@request-selected=${this._toggleSkipped}
|
||||
.selected=${this._showSkipped}
|
||||
<ha-dropdown-item
|
||||
type="checkbox"
|
||||
.checked=${this._showSkipped}
|
||||
value="toggle_skipped"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-check-list-item>
|
||||
</ha-dropdown-item>
|
||||
${this._supervisorInfo
|
||||
? html`
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item
|
||||
@request-selected=${this._toggleBeta}
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item
|
||||
value="toggle_beta"
|
||||
.disabled=${this._supervisorInfo.channel === "dev"}
|
||||
>
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
@@ -98,10 +97,10 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
@@ -133,27 +132,21 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
}
|
||||
|
||||
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showSkipped = !this._showSkipped;
|
||||
}
|
||||
|
||||
private async _toggleBeta(
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
): Promise<void> {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
private _handleMenuAction(ev: CustomEvent): void {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "toggle_skipped":
|
||||
this._showSkipped = !this._showSkipped;
|
||||
break;
|
||||
case "toggle_beta":
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiCloudLock,
|
||||
mdiDotsVertical,
|
||||
@@ -13,11 +12,12 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
@@ -226,25 +226,25 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMagnify}
|
||||
@click=${this._showQuickBar}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu slot="actionItems" @action=${this._handleMenuAction}>
|
||||
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="check_updates">
|
||||
<ha-svg-icon slot="icon" .path=${mdiRefresh}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.updates.check_updates")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRefresh}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="restart">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.system_dashboard.restart_homeassistant"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPower}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
|
||||
<ha-config-section
|
||||
.narrow=${this.narrow}
|
||||
@@ -371,12 +371,13 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private async _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "check_updates":
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
break;
|
||||
case 1:
|
||||
case "restart":
|
||||
showRestartDialog(this);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiMenuDown,
|
||||
@@ -39,11 +38,15 @@ import type {
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
@@ -51,9 +54,6 @@ import "../../../components/ha-filter-integrations";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
@@ -702,6 +702,80 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="toggle_area"
|
||||
data-area=${area.area_id}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="__no_area__">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="__create_area__"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
.value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
})}
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._bulkCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { devicesOutput } = this._devicesAndFilterDomains(
|
||||
this.hass.devices,
|
||||
@@ -718,77 +792,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((deviceId) =>
|
||||
this.hass.devices[deviceId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item>`;
|
||||
})}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -906,7 +909,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-labels>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
? html`<ha-dropdown slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -918,12 +921,15 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-md-button-menu>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>
|
||||
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -935,10 +941,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
: nothing}
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
<ha-dropdown
|
||||
has-overflow
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
@@ -959,51 +969,32 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html` <ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._deleteSelected}
|
||||
<ha-dropdown-item
|
||||
value="delete"
|
||||
.disabled=${!this._selectedCanDelete.length}
|
||||
class="warning"
|
||||
variant="danger"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
@@ -1093,12 +1084,28 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
private _handleOverflowMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
switch (item.value) {
|
||||
case "delete":
|
||||
this._deleteSelected();
|
||||
break;
|
||||
case "__no_area__":
|
||||
this._bulkAddArea(null);
|
||||
break;
|
||||
case "__create_area__":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "toggle_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string | null) {
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
promises.push(
|
||||
@@ -1133,12 +1140,6 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
@@ -1178,6 +1179,13 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
ev.stopPropagation();
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.dataset.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private _deleteSelected = () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -1277,7 +1285,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiEye,
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -55,8 +56,9 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-domains";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
@@ -65,9 +67,6 @@ import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
@@ -114,7 +113,6 @@ import { isHelperDomain } from "../helpers/const";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
|
||||
export interface StateEntity extends Omit<
|
||||
EntityRegistryEntry,
|
||||
@@ -748,6 +746,48 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
.value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-dropdown-item>`;
|
||||
})}
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._handleCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || this._entities === undefined) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
@@ -772,53 +812,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
[...filteredDomains][0]
|
||||
);
|
||||
|
||||
const labelItems = html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
const partial =
|
||||
!selected &&
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
></ha-checkbox>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item>`;
|
||||
})}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${
|
||||
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||
}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
@@ -828,16 +828,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
{ number: filteredEntities.length }
|
||||
)}
|
||||
has-filters
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter)
|
||||
? filter.length
|
||||
: filter &&
|
||||
Object.values(filter).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length
|
||||
}
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter)
|
||||
? filter.length
|
||||
: filter &&
|
||||
Object.values(filter).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping ?? "device_full"}
|
||||
@@ -864,157 +862,120 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
slot="toolbar-icon"
|
||||
></ha-integration-overflow-menu>
|
||||
|
||||
|
||||
${
|
||||
!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${!this.narrow
|
||||
? html`<ha-dropdown slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`
|
||||
: nothing}
|
||||
<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`
|
||||
: nothing
|
||||
}
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</ha-assist-chip>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: nothing
|
||||
}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._enableSelected}>
|
||||
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._disableSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-dropdown-item value="enable">
|
||||
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="disable">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiToggleSwitchOffOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._unhideSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiEye}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.unhide_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._hideSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiEyeOff}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-dropdown-item value="unhide">
|
||||
<ha-svg-icon slot="icon" .path=${mdiEye}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.unhide_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="hide">
|
||||
<ha-svg-icon slot="icon" .path=${mdiEyeOff}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._restoreEntityIdSelected}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiRestore}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-dropdown-item value="restore_entity_id">
|
||||
<ha-svg-icon slot="icon" .path=${mdiRestore}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._removeSelected} class="warning">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.delete_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
</ha-md-button-menu>
|
||||
${
|
||||
Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry.length
|
||||
? html`<ha-alert slot="filter-pane">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.filtering_by_config_entry"
|
||||
)}
|
||||
${this._entries?.find(
|
||||
(entry) => entry.entry_id === this._filters.config_entry![0]
|
||||
)?.title || this._filters.config_entry[0]}${this._filters
|
||||
.config_entry.length === 1 &&
|
||||
Array.isArray(this._filters.sub_entry) &&
|
||||
this._filters.sub_entry.length
|
||||
? html` (${this._subEntries?.find(
|
||||
(entry) =>
|
||||
entry.subentry_id === this._filters.sub_entry![0]
|
||||
)?.title || this._filters.sub_entry[0]})`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
<ha-dropdown-item value="remove" variant="danger">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.delete_selected.button"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry.length
|
||||
? html`<ha-alert slot="filter-pane">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.filtering_by_config_entry"
|
||||
)}
|
||||
${this._entries?.find(
|
||||
(entry) => entry.entry_id === this._filters.config_entry![0]
|
||||
)?.title || this._filters.config_entry[0]}${this._filters
|
||||
.config_entry.length === 1 &&
|
||||
Array.isArray(this._filters.sub_entry) &&
|
||||
this._filters.sub_entry.length
|
||||
? html` (${this._subEntries?.find(
|
||||
(entry) => entry.subentry_id === this._filters.sub_entry![0]
|
||||
)?.title || this._filters.sub_entry[0]})`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-filter-floor-areas
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
@@ -1075,20 +1036,16 @@ ${
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-labels>
|
||||
${
|
||||
includeAddDeviceFab
|
||||
? html`<ha-fab
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.add_device"
|
||||
)}
|
||||
extended
|
||||
@click=${this._addDevice}
|
||||
slot="fab"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>`
|
||||
: nothing
|
||||
}
|
||||
${includeAddDeviceFab
|
||||
? html`<ha-fab
|
||||
.label=${this.hass.localize("ui.panel.config.devices.add_device")}
|
||||
extended
|
||||
@click=${this._addDevice}
|
||||
slot="fab"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>`
|
||||
: nothing}
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
@@ -1222,6 +1179,44 @@ ${
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleLabelMenuSelect(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem;
|
||||
|
||||
const label = item.value as string;
|
||||
const action = (item as HTMLElement).dataset.action as "add" | "remove";
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private _handleCreateLabel() {
|
||||
this._bulkCreateLabel();
|
||||
}
|
||||
|
||||
private _handleOverflowMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
|
||||
switch (item.value) {
|
||||
case "enable":
|
||||
this._enableSelected();
|
||||
break;
|
||||
case "disable":
|
||||
this._disableSelected();
|
||||
break;
|
||||
case "unhide":
|
||||
this._unhideSelected();
|
||||
break;
|
||||
case "hide":
|
||||
this._hideSelected();
|
||||
break;
|
||||
case "remove":
|
||||
this._removeSelected();
|
||||
break;
|
||||
case "restore_entity_id":
|
||||
this._restoreEntityIdSelected();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _enableSelected = async () => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -1345,12 +1340,6 @@ ${
|
||||
this._clearSelection();
|
||||
};
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
await this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1612,7 +1601,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
import { consume } from "@lit/context";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
mdiMenuDown,
|
||||
mdiPencilOff,
|
||||
mdiProgressHelper,
|
||||
mdiPlus,
|
||||
mdiProgressHelper,
|
||||
mdiTag,
|
||||
mdiTrashCan,
|
||||
mdiDownload,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
@@ -42,6 +45,9 @@ import type {
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-categories";
|
||||
import "../../../components/ha-filter-devices";
|
||||
@@ -50,10 +56,12 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import type { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import {
|
||||
createCategoryRegistryEntry,
|
||||
@@ -71,6 +79,10 @@ import type {
|
||||
DataTableFiltersItems,
|
||||
DataTableFiltersValues,
|
||||
} from "../../../data/data_table_filters";
|
||||
import {
|
||||
fetchDiagnosticHandlers,
|
||||
getConfigEntryDiagnosticsDownloadUrl,
|
||||
} from "../../../data/diagnostics";
|
||||
import type {
|
||||
EntityRegistryEntry,
|
||||
UpdateEntityRegistryEntryResult,
|
||||
@@ -81,6 +93,7 @@ import {
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
|
||||
import { HELPERS_CRUD } from "../../../data/helpers_crud";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
domainToName,
|
||||
@@ -104,23 +117,15 @@ import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { renderConfigEntryError } from "../integrations/ha-config-integration-page";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { isHelperDomain, type HelperDomain } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { HELPERS_CRUD } from "../../../data/helpers_crud";
|
||||
import {
|
||||
fetchDiagnosticHandlers,
|
||||
getConfigEntryDiagnosticsDownloadUrl,
|
||||
} from "../../../data/diagnostics";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
|
||||
interface HelperItem {
|
||||
id: string;
|
||||
@@ -132,6 +137,7 @@ interface HelperItem {
|
||||
configEntry?: ConfigEntry;
|
||||
entity?: HassEntity;
|
||||
category: string | undefined;
|
||||
area?: string;
|
||||
label_entries: LabelRegistryEntry[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -347,6 +353,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.area"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
},
|
||||
labels: {
|
||||
title: "",
|
||||
hidden: true,
|
||||
@@ -565,6 +577,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
entityRegistryByEntityId(entityReg)[item.entity_id];
|
||||
const labels = labelReg && entityRegEntry?.labels;
|
||||
const category = entityRegEntry?.categories.helpers;
|
||||
const areaId = entityRegEntry?.area_id;
|
||||
const area =
|
||||
areaId && this.hass.areas?.[areaId]
|
||||
? computeAreaName(this.hass.areas[areaId])
|
||||
: undefined;
|
||||
return {
|
||||
...item,
|
||||
localized_type:
|
||||
@@ -579,6 +596,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
category: category
|
||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||
: undefined,
|
||||
area,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -592,42 +610,35 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._stateItems === undefined ||
|
||||
this._entityEntries === undefined ||
|
||||
this._configEntries === undefined
|
||||
) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no_category">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create_category"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this._labelsForEntity(entityId).includes(label.label_id)
|
||||
@@ -637,14 +648,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this._labelsForEntity(entityId).includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
return html`<ha-dropdown-item
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
@@ -658,13 +669,25 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item> `;
|
||||
})}<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
</ha-dropdown-item>`;
|
||||
})}<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._bulkCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._stateItems === undefined ||
|
||||
this._entityEntries === undefined ||
|
||||
this._configEntries === undefined
|
||||
) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -766,7 +789,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-categories>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleMenuSelect}
|
||||
>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -778,11 +804,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -794,14 +820,15 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}`
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
@@ -819,50 +846,24 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || this.hass.dockedSidebar === "docked"
|
||||
? html` <ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-button-menu>`
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || this.hass.dockedSidebar === "docked"
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
</ha-dropdown>`
|
||||
: nothing}
|
||||
|
||||
<ha-integration-overflow-menu
|
||||
@@ -1005,12 +1006,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleBulkCategory = (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string) {
|
||||
private async _bulkAddCategory(category: string | null) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
@@ -1035,12 +1031,6 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1076,6 +1066,32 @@ ${rejected
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "no_category":
|
||||
this._bulkAddCategory(null);
|
||||
break;
|
||||
case "create_category":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLabelMenuSelect(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem;
|
||||
|
||||
const label = item.value as string;
|
||||
const action = (item as HTMLElement).dataset.action as "add" | "remove";
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -1416,7 +1432,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -4,8 +4,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -500,15 +502,18 @@ export class HassioNetwork extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<ha-button-menu
|
||||
@opened=${this._handleDNSMenuOpened}
|
||||
@closed=${this._handleDNSMenuClosed}
|
||||
<ha-dropdown
|
||||
@wa-show=${this._handleDNSMenuOpened}
|
||||
@wa-hide=${this._handleDNSMenuClosed}
|
||||
@wa-select=${this._handleDNSMenuSelect}
|
||||
.version=${version}
|
||||
class="add-nameserver"
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-button appearance="filled" size="small" slot="trigger">
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="small"
|
||||
slot="trigger"
|
||||
class="add-nameserver"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.add_dns_server"
|
||||
)}
|
||||
@@ -519,21 +524,21 @@ export class HassioNetwork extends LitElement {
|
||||
</ha-button>
|
||||
${Object.entries(PREDEFINED_DNS[version]).map(
|
||||
([name, addresses]) => html`
|
||||
<ha-list-item
|
||||
@click=${this._addPredefinedDNS}
|
||||
<ha-dropdown-item
|
||||
value="predefined"
|
||||
.version=${version}
|
||||
.addresses=${addresses}
|
||||
>
|
||||
${name}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
<ha-list-item @click=${this._addCustomDNS} .version=${version}>
|
||||
<ha-dropdown-item value="custom" .version=${version}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.custom_dns"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
@@ -747,27 +752,30 @@ export class HassioNetwork extends LitElement {
|
||||
this._dnsMenuOpen = false;
|
||||
}
|
||||
|
||||
private _addPredefinedDNS(ev: Event) {
|
||||
const source = ev.target as any;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
const addresses = source.addresses as string[];
|
||||
if (!this._interface![version]!.nameservers) {
|
||||
this._interface![version]!.nameservers = [];
|
||||
}
|
||||
this._interface![version]!.nameservers!.push(...addresses);
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
private _handleDNSMenuSelect(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem & {
|
||||
version: "ipv4" | "ipv6";
|
||||
addresses?: string[];
|
||||
};
|
||||
const version = item.version;
|
||||
|
||||
private _addCustomDNS(ev: Event) {
|
||||
const source = ev.target as any;
|
||||
const version = source.version as "ipv4" | "ipv6";
|
||||
if (!this._interface![version]!.nameservers) {
|
||||
this._interface![version]!.nameservers = [];
|
||||
if (item.value === "predefined" && item.addresses) {
|
||||
if (!this._interface![version]!.nameservers) {
|
||||
this._interface![version]!.nameservers = [];
|
||||
}
|
||||
this._interface![version]!.nameservers!.push(...item.addresses);
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_interface");
|
||||
return;
|
||||
}
|
||||
if (item.value === "custom") {
|
||||
if (!this._interface![version]!.nameservers) {
|
||||
this._interface![version]!.nameservers = [];
|
||||
}
|
||||
this._interface![version]!.nameservers!.push("");
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
this._interface![version]!.nameservers!.push("");
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _removeNameserver(ev: Event): void {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
@@ -31,6 +31,7 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -44,6 +45,9 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-categories";
|
||||
import "../../../components/ha-filter-devices";
|
||||
@@ -52,11 +56,7 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
@@ -106,7 +106,6 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
|
||||
type SceneItem = SceneEntity & {
|
||||
name: string;
|
||||
@@ -434,34 +433,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
|
||||
const labelItems = html` ${this._labels?.map((label) => {
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
@@ -471,14 +444,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
return html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
reducedTouchTarget
|
||||
@@ -492,46 +465,74 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item>`;
|
||||
</ha-dropdown-item>`;
|
||||
})}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._handleCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-label"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-category">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-category"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_area"
|
||||
data-area=${area.area_id}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
slot="icon"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-area">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="create-area">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -654,7 +655,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-categories>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -666,11 +670,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -682,11 +686,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -698,14 +705,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html` <ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
@@ -723,68 +731,32 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-button-menu>`
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
</ha-dropdown>`
|
||||
: nothing}
|
||||
${!this.scenes.length
|
||||
? html`<div class="empty" slot="empty">
|
||||
@@ -955,12 +927,52 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBulkCategory = (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
private _handleLabelMenuSelect = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem & {
|
||||
dataset: { action?: string };
|
||||
};
|
||||
const action = item.dataset.action as "add" | "remove";
|
||||
this._bulkLabel(item.value, action);
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string) {
|
||||
private _handleCreateLabel = () => {
|
||||
this._bulkCreateLabel();
|
||||
};
|
||||
|
||||
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
|
||||
switch (item.value) {
|
||||
case "create-category":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "no-category":
|
||||
this._bulkAddCategory(null);
|
||||
break;
|
||||
case "create-label":
|
||||
this._bulkCreateLabel();
|
||||
break;
|
||||
case "create-area":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "no-area":
|
||||
this._bulkAddArea(null);
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
case "move_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string | null) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
@@ -985,12 +997,6 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1021,12 +1027,7 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
private async _bulkAddArea(area: string | null) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
@@ -1231,7 +1232,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
@@ -32,8 +32,10 @@ import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-picker";
|
||||
@@ -227,78 +229,66 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
? computeStateName(this._scene)
|
||||
: this.hass.localize("ui.panel.config.scene.editor.default_name")}
|
||||
>
|
||||
<ha-button-menu
|
||||
slot="toolbar-icon"
|
||||
@action=${this._handleMenuAction}
|
||||
activatable
|
||||
>
|
||||
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-dropdown-item
|
||||
value="apply"
|
||||
.disabled=${!this.sceneId || this._mode === "live"}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.apply")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="info" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="settings" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
<ha-dropdown-item value="category" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.scene.picker.${this._getCategory(this._entityRegistryEntries, this._scene?.entity_id) ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="toggle_yaml">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${this._mode !== "yaml" ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
|
||||
<ha-dropdown-item value="duplicate" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate_scene"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item
|
||||
<ha-dropdown-item
|
||||
value="delete"
|
||||
.disabled=${!this.sceneId}
|
||||
class=${classMap({ warning: Boolean(this.sceneId) })}
|
||||
graphic="icon"
|
||||
.variant=${this.sceneId ? "danger" : "default"}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.delete_scene")}
|
||||
<ha-svg-icon
|
||||
class=${classMap({ warning: Boolean(this.sceneId) })}
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
|
||||
${this._mode === "yaml" ? this._renderYamlMode() : this._renderUiMode()}
|
||||
<ha-fab
|
||||
@@ -652,24 +642,25 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private async _handleMenuAction(ev: CustomEvent) {
|
||||
const item = ev.detail.item as HaDropdownItem;
|
||||
switch (item.value) {
|
||||
case "apply":
|
||||
activateScene(this.hass, this._scene!.entity_id);
|
||||
break;
|
||||
case 1:
|
||||
case "info":
|
||||
fireEvent(this, "hass-more-info", { entityId: this._scene!.entity_id });
|
||||
break;
|
||||
case 2:
|
||||
case "settings":
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._scene!.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
break;
|
||||
case 3:
|
||||
case "category":
|
||||
this._editCategory(this._scene!);
|
||||
break;
|
||||
case 4:
|
||||
case "toggle_yaml":
|
||||
if (this._mode === "yaml") {
|
||||
this._initEntities(this._config!);
|
||||
this._exitYamlMode();
|
||||
@@ -677,10 +668,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._enterYamlMode();
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
case "duplicate":
|
||||
this._duplicate();
|
||||
break;
|
||||
case 6:
|
||||
case "delete":
|
||||
this._deleteTapped();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
@@ -34,7 +34,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -47,6 +46,9 @@ import type {
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-blueprints";
|
||||
import "../../../components/ha-filter-categories";
|
||||
@@ -56,11 +58,8 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import {
|
||||
@@ -419,33 +418,35 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const categoryItems = html`${this._categories?.map(
|
||||
private _renderCategoryItems = (submenu = false) =>
|
||||
html`${this._categories?.map(
|
||||
(category) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${category.category_id}
|
||||
.clickAction=${this._handleBulkCategory}
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_category"
|
||||
data-category=${category.category_id}
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
|
||||
<div slot="headline">${category.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
|
||||
${category.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</div> </ha-md-menu-item
|
||||
><ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-category">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-category"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
private _renderLabelItems = (submenu = false) =>
|
||||
html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
@@ -455,15 +456,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected.some((entityId) =>
|
||||
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||
);
|
||||
return html`<ha-md-menu-item
|
||||
.value=${label.label_id}
|
||||
.action=${selected ? "remove" : "add"}
|
||||
@click=${this._handleBulkLabel}
|
||||
keep-open
|
||||
reducedTouchTarget
|
||||
>
|
||||
<ha-checkbox
|
||||
slot="start"
|
||||
return html`<ha-dropdown-item
|
||||
@click=${this._handleLabelMenuSelect}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value=${label.label_id}
|
||||
data-action=${selected ? "remove" : "add"}
|
||||
><ha-checkbox
|
||||
slot="icon"
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
></ha-checkbox>
|
||||
@@ -476,46 +475,47 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-md-menu-item>`;
|
||||
</ha-dropdown-item>`;
|
||||
})}
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-md-menu-item
|
||||
>`;
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item
|
||||
@click=${this._bulkCreateLabel}
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="create-label"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
private _renderAreaItems = (submenu = false) =>
|
||||
html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-md-menu-item
|
||||
.value=${area.area_id}
|
||||
.clickAction=${this._handleBulkArea}
|
||||
html`<ha-dropdown-item
|
||||
.slot=${submenu ? "submenu" : ""}
|
||||
value="move_area"
|
||||
data-area=${area.area_id}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
slot="icon"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-md-menu-item>`
|
||||
${area.name}
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>`;
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-area">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
|
||||
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="create-area">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</ha-dropdown-item>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
@@ -647,7 +647,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-blueprints>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
? html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -659,11 +662,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${categoryItems}
|
||||
</ha-md-button-menu>
|
||||
${this._renderCategoryItems()}
|
||||
</ha-dropdown>
|
||||
${labelsInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
: html`<ha-dropdown slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -675,11 +678,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-md-button-menu>`}
|
||||
${this._renderLabelItems()}
|
||||
</ha-dropdown>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-md-button-menu slot="selection-bar">
|
||||
: html`<ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -691,14 +697,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-md-button-menu>`}`
|
||||
${this._renderAreaItems()}
|
||||
</ha-dropdown>`}`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`
|
||||
<ha-md-button-menu has-overflow slot="selection-bar">
|
||||
${
|
||||
this.narrow
|
||||
? html` <ha-dropdown
|
||||
slot="selection-bar"
|
||||
@wa-select=${this._handleOverflowMenuSelect}
|
||||
>
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
@@ -716,68 +723,32 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${
|
||||
this.narrow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-md-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-button-menu>`
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.move_category"
|
||||
)}
|
||||
${this._renderCategoryItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
${this._renderLabelItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-dropdown-item>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
${this._renderAreaItems(true)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
</ha-dropdown>`
|
||||
: nothing}
|
||||
${!this.scripts.length
|
||||
? html` <div class="empty" slot="empty">
|
||||
@@ -995,9 +966,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleBulkCategory = (item) => {
|
||||
const category = item.value;
|
||||
this._bulkAddCategory(category);
|
||||
private _handleLabelMenuSelect = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem & {
|
||||
dataset: { action?: string };
|
||||
};
|
||||
const action = item.dataset.action as "add" | "remove";
|
||||
this._bulkLabel(item.value, action);
|
||||
};
|
||||
|
||||
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
|
||||
const item = ev.detail.item as HaDropdownItem & {
|
||||
dataset: { action?: string };
|
||||
};
|
||||
switch (item.value) {
|
||||
case "create-category":
|
||||
this._bulkCreateCategory();
|
||||
break;
|
||||
case "no-category":
|
||||
this._bulkAddCategory(null!);
|
||||
break;
|
||||
case "create-label":
|
||||
this._bulkCreateLabel();
|
||||
break;
|
||||
case "create-area":
|
||||
this._bulkCreateArea();
|
||||
break;
|
||||
case "no-area":
|
||||
this._bulkAddArea(null!);
|
||||
break;
|
||||
case "move_area":
|
||||
if (item.dataset.area) {
|
||||
this._bulkAddArea(item.dataset.area);
|
||||
}
|
||||
break;
|
||||
case "move_category":
|
||||
if (item.dataset.category) {
|
||||
this._bulkAddCategory(item.dataset.category);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private async _bulkAddCategory(category: string) {
|
||||
@@ -1025,12 +1033,6 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
this._bulkLabel(label, action);
|
||||
}
|
||||
|
||||
private async _bulkLabel(label: string, action: "add" | "remove") {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1235,11 +1237,6 @@ ${rejected
|
||||
});
|
||||
};
|
||||
|
||||
private _handleBulkArea = (item) => {
|
||||
const area = item.value;
|
||||
this._bulkAddArea(area);
|
||||
};
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
@@ -1322,7 +1319,7 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-md-button-menu ha-assist-chip {
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
ha-label {
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
import "../../components/ha-button-menu";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list-item";
|
||||
@@ -117,9 +116,7 @@ class PanelDeveloperTools extends LitElement {
|
||||
return;
|
||||
}
|
||||
if (newPage !== this._page) {
|
||||
withViewTransition(() => {
|
||||
navigate(`/developer-tools/${newPage}`);
|
||||
});
|
||||
navigate(`/developer-tools/${newPage}`);
|
||||
} else {
|
||||
scrollTo({ behavior: "smooth", top: 0 });
|
||||
}
|
||||
@@ -128,9 +125,7 @@ class PanelDeveloperTools extends LitElement {
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
withViewTransition(() => {
|
||||
navigate(`/developer-tools/debug`);
|
||||
});
|
||||
navigate(`/developer-tools/debug`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,16 +88,18 @@ export class HuiEnergyGasGraphCard
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${formatNumber(this._total, this.hass.locale)} ${this._unit}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._config.title
|
||||
? html` <div class="card-header">
|
||||
<span>${this._config.title}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${formatNumber(this._total, this.hass.locale)} ${this._unit}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
|
||||
@@ -90,16 +90,18 @@ export class HuiEnergySolarGraphCard
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${formatNumber(this._total, this.hass.locale)} kWh
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._config.title
|
||||
? html` <div class="card-header">
|
||||
<span>${this._config.title}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${formatNumber(this._total, this.hass.locale)} kWh
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
|
||||
@@ -103,19 +103,21 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_usage",
|
||||
{ num: formatNumber(this._total, this.hass.locale) }
|
||||
)}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._config.title
|
||||
? html` <div class="card-header">
|
||||
<span>${this._config.title}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_usage",
|
||||
{ num: formatNumber(this._total, this.hass.locale) }
|
||||
)}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
|
||||
@@ -88,16 +88,18 @@ export class HuiEnergyWaterGraphCard
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${formatNumber(this._total, this.hass.locale)} ${this._unit}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._config.title
|
||||
? html` <div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${formatNumber(this._total, this.hass.locale)} ${this._unit}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
|
||||
@@ -99,7 +99,6 @@ import type { HUIView } from "./views/hui-view";
|
||||
import "./views/hui-view-background";
|
||||
import "./views/hui-view-container";
|
||||
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
|
||||
interface ActionItem {
|
||||
icon: string;
|
||||
@@ -1246,9 +1245,7 @@ class HUIRoot extends LitElement {
|
||||
const viewIndex = Number(ev.detail.name);
|
||||
if (viewIndex !== this._curView) {
|
||||
const path = this.config.views[viewIndex].path || viewIndex;
|
||||
withViewTransition(() => {
|
||||
this._navigateToView(path);
|
||||
});
|
||||
this._navigateToView(path);
|
||||
} else if (!this._editMode) {
|
||||
scrollTo({ behavior: "smooth", top: 0 });
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import Fuse, {
|
||||
type Expression,
|
||||
type FuseIndex,
|
||||
type FuseResult,
|
||||
type FuseSearchOptions,
|
||||
type IFuseOptions,
|
||||
} from "fuse.js";
|
||||
|
||||
export interface FuseKey {
|
||||
getFn: null;
|
||||
id: string;
|
||||
path: string[];
|
||||
src: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: IFuseOptions<any> = {
|
||||
ignoreDiacritics: true,
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.3,
|
||||
minMatchCharLength: 2,
|
||||
};
|
||||
|
||||
export class HaFuse<T> extends Fuse<T> {
|
||||
public constructor(
|
||||
list: readonly T[],
|
||||
options?: IFuseOptions<T>,
|
||||
index?: FuseIndex<T>
|
||||
) {
|
||||
const mergedOptions = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
super(list, mergedOptions, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a multi-term search across the indexed data.
|
||||
* Splits the search string into individual terms and performs an AND operation between terms,
|
||||
* where each term is searched across all indexed keys with an OR operation. words with less than
|
||||
* 2 characters are ignored. If no valid terms are found, the search will return null.
|
||||
*
|
||||
* @param search - The search string to split into terms. Terms are space-separated.
|
||||
* @param options - Optional Fuse.js search options to customize the search behavior.
|
||||
* @typeParam R - The type of the result items. Defaults to T (the type of the indexed items).
|
||||
* @returns An array of FuseResult objects containing matched items and their matching information.
|
||||
* If no valid terms are found (after filtering by minimum length), returns all items with empty matches.
|
||||
*/
|
||||
public multiTermsSearch(
|
||||
search: string,
|
||||
options?: FuseSearchOptions
|
||||
): FuseResult<T>[] | null {
|
||||
const terms = search.toLowerCase().split(" ");
|
||||
|
||||
// @ts-expect-error options is not part of the Fuse type
|
||||
const { minMatchCharLength } = this.options as IFuseOptions<T>;
|
||||
|
||||
const filteredTerms = minMatchCharLength
|
||||
? terms.filter((term) => term.length >= minMatchCharLength)
|
||||
: terms;
|
||||
|
||||
if (filteredTerms.length === 0) {
|
||||
// If no valid terms are found, return null to indicate no search was performed
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = this.getIndex().toJSON();
|
||||
const keys = index.keys as unknown as FuseKey[]; // Fuse type for key is not correct
|
||||
|
||||
const expression: Expression = {
|
||||
$and: filteredTerms.map((term) => ({
|
||||
$or: keys.map((key) => ({ $path: key.path, $val: term })),
|
||||
})),
|
||||
};
|
||||
|
||||
return this.search(expression, options);
|
||||
}
|
||||
}
|
||||
254
src/resources/fuseMultiTerm.ts
Normal file
254
src/resources/fuseMultiTerm.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import type {
|
||||
Expression,
|
||||
FuseIndex,
|
||||
FuseOptionKey,
|
||||
FuseResult,
|
||||
IFuseOptions,
|
||||
} from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
export interface FuseWeightedKey {
|
||||
name: string | string[];
|
||||
weight: number;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: IFuseOptions<any> = {
|
||||
ignoreDiacritics: true,
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.3,
|
||||
minMatchCharLength: 2,
|
||||
};
|
||||
|
||||
const DEFAULT_MIN_CHAR_LENGTH = 2;
|
||||
|
||||
/**
|
||||
* Searches for a term within a collection of items using Fuse.js fuzzy search.
|
||||
* @param items - The array of items to search through.
|
||||
* @param search - The search term to look for.
|
||||
* @param fuseIndex - Optional pre-built Fuse index for improved performance.
|
||||
* @param options - Optional Fuse.js configuration options to override defaults.
|
||||
* @returns An array of search results matching the search term.
|
||||
*/
|
||||
function searchTerm<T>(
|
||||
items: T[],
|
||||
search: string | Expression,
|
||||
fuseIndex?: FuseIndex<T>,
|
||||
options?: IFuseOptions<T>,
|
||||
minMatchCharLength?: number
|
||||
) {
|
||||
const fuse = new Fuse<T>(
|
||||
items,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
minMatchCharLength:
|
||||
minMatchCharLength ??
|
||||
(typeof search === "string" && search.length < DEFAULT_MIN_CHAR_LENGTH
|
||||
? search.length
|
||||
: DEFAULT_MIN_CHAR_LENGTH),
|
||||
...(options || {}),
|
||||
},
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
return fuse.search(search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a multi-term search across an array of items using Fuse.js.
|
||||
* All search terms must match for an item to be included in the results.
|
||||
* Result is NOT sorted by relevance.
|
||||
*
|
||||
* @template T - The type of items being searched
|
||||
* @param items - The array of items to search through
|
||||
* @param search - The search string containing one or more space-separated terms
|
||||
* @param searchKeys - An array of weighted keys defining which properties to search
|
||||
* @param fuseIndex - Optional pre-built Fuse index for improved performance
|
||||
* @param options - Optional Fuse.js configuration options
|
||||
* @returns An array of items that match all search terms
|
||||
*/
|
||||
export function multiTermSearch<T>(
|
||||
items: T[],
|
||||
search: string,
|
||||
searchKeys: FuseOptionKey<T>[],
|
||||
fuseIndex?: FuseIndex<T>,
|
||||
options: IFuseOptions<T> = {}
|
||||
): T[] {
|
||||
const terms = search
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((t) => t.trim());
|
||||
|
||||
if (!terms.length) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// be sure that all terms are used in the search
|
||||
// just use DEFAULT_MIN_CHAR_LENGTH if the terms are at least that long
|
||||
let minLength = DEFAULT_MIN_CHAR_LENGTH;
|
||||
terms.forEach((term) => {
|
||||
if (term.length < minLength) {
|
||||
minLength = term.length;
|
||||
}
|
||||
});
|
||||
|
||||
const expression: Expression = {
|
||||
$and: terms.map((term) => ({
|
||||
$or: searchKeys.map((key) => ({
|
||||
$path:
|
||||
typeof key === "string"
|
||||
? key
|
||||
: Array.isArray(key)
|
||||
? key.join(".")
|
||||
: typeof key.name === "string"
|
||||
? key.name
|
||||
: key.name.join("."),
|
||||
$val: term,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
return searchTerm<T>(
|
||||
items,
|
||||
expression,
|
||||
fuseIndex,
|
||||
{
|
||||
...options,
|
||||
shouldSort: false,
|
||||
},
|
||||
minLength
|
||||
).map((r) => r.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a multi-term search across items using Fuse.js, returning results sorted by relevance.
|
||||
*
|
||||
* This function splits the search string into individual terms and searches for each term
|
||||
* independently. Results are aggregated and scored based on:
|
||||
* - Number of terms matched (items must match ALL terms to be included)
|
||||
* - Fuse.js match score for each term
|
||||
* - Weight of the matched keys
|
||||
*
|
||||
* @template T - The type of items being searched
|
||||
* @param items - The array of items to search through
|
||||
* @param search - The search string, which will be split by spaces into multiple terms
|
||||
* @param searchKeys - Array of weighted keys configuration for Fuse.js search
|
||||
* @param getItemId - Function to extract a unique identifier from each item
|
||||
* @param fuseIndex - Optional but highly recommended! Pre-built Fuse.js index for improved performance
|
||||
* @param options - Optional Fuse.js options to customize search behavior
|
||||
* @returns An array of items that match all search terms, sorted by relevance score (highest first).
|
||||
* Returns all items if search is empty, or empty array if not all terms have matches.
|
||||
*/
|
||||
export function multiTermSortedSearch<T>(
|
||||
items: T[],
|
||||
search: string,
|
||||
searchKeys: FuseWeightedKey[],
|
||||
getItemId: (item: T) => string,
|
||||
fuseIndex?: FuseIndex<T>,
|
||||
options: IFuseOptions<T> = {}
|
||||
) {
|
||||
const terms = search
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((t) => t.trim());
|
||||
|
||||
if (!terms.length) {
|
||||
return items;
|
||||
}
|
||||
|
||||
if (terms.length === 1) {
|
||||
return searchTerm<T>(items, terms[0], fuseIndex, options).map(
|
||||
(r) => r.item
|
||||
);
|
||||
}
|
||||
|
||||
const searchResults: Record<
|
||||
string,
|
||||
{ item: T; hits: number; score: number }
|
||||
> = {};
|
||||
|
||||
let termHits = 0;
|
||||
|
||||
terms.forEach((term) => {
|
||||
if (!term.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const termResults = searchTerm<T>(items, term, fuseIndex, {
|
||||
...options,
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
});
|
||||
|
||||
if (termResults.length) {
|
||||
termHits++;
|
||||
termResults.forEach((r) => {
|
||||
const itemId = getItemId(r.item);
|
||||
if (!searchResults[itemId]) {
|
||||
searchResults[itemId] = {
|
||||
item: r.item,
|
||||
hits: 0,
|
||||
score: 0,
|
||||
};
|
||||
}
|
||||
|
||||
searchResults[itemId].hits += 1;
|
||||
|
||||
const weight = _getMatchedKeyHighestWeight<T>(r, searchKeys);
|
||||
|
||||
const score = r.score ? 1 - r.score : 0;
|
||||
const weightedScore = score * weight;
|
||||
|
||||
searchResults[itemId].score += weightedScore;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// just return smth if for the full terms combination are results
|
||||
if (termHits !== terms.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter to only items that matched all terms
|
||||
const results = Object.values(searchResults).filter(
|
||||
({ hits }) => hits === terms.length
|
||||
);
|
||||
|
||||
// Sort by score descending
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
|
||||
return results.map(({ item }) => item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the highest weight among all matched keys in a Fuse.js search result.
|
||||
*
|
||||
* @typeParam T - The type of items being searched
|
||||
* @param result - A single Fuse.js search result containing match information
|
||||
* @param searchKeys - Array of weighted search keys configured for the Fuse instance
|
||||
* @returns The highest weight value among matched keys, or 1 if no matches exist or no weights are defined
|
||||
*/
|
||||
function _getMatchedKeyHighestWeight<T>(
|
||||
result: FuseResult<T>,
|
||||
searchKeys: FuseWeightedKey[]
|
||||
): number {
|
||||
if (!result.matches || result.matches.length === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Find the highest weighted key that matched
|
||||
let maxWeight = 1;
|
||||
for (const match of result.matches) {
|
||||
const keyConfig = searchKeys.find((k) => {
|
||||
if (typeof k.name === "string") {
|
||||
return k.name === match.key;
|
||||
}
|
||||
return k.name.join(".") === match.key;
|
||||
});
|
||||
if (keyConfig && keyConfig.weight && keyConfig.weight > maxWeight) {
|
||||
maxWeight = keyConfig.weight;
|
||||
}
|
||||
}
|
||||
|
||||
return maxWeight;
|
||||
}
|
||||
@@ -658,7 +658,8 @@
|
||||
"show_entities": "Show entities",
|
||||
"new_entity": "Create a new entity",
|
||||
"placeholder": "Select an entity",
|
||||
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper."
|
||||
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper.",
|
||||
"unknown": "Unknown entity selected"
|
||||
},
|
||||
"entity-name-picker": {
|
||||
"types": {
|
||||
@@ -779,7 +780,8 @@
|
||||
"user-picker": {
|
||||
"no_match": "No users found for {term}",
|
||||
"user": "User",
|
||||
"add_user": "Add user"
|
||||
"add_user": "Add user",
|
||||
"unknown": "Unknown user selected"
|
||||
},
|
||||
"blueprint-picker": {
|
||||
"select_blueprint": "Select a blueprint"
|
||||
@@ -793,7 +795,8 @@
|
||||
"device": "Device",
|
||||
"unnamed_device": "Unnamed device",
|
||||
"no_area": "No area",
|
||||
"placeholder": "Select a device"
|
||||
"placeholder": "Select a device",
|
||||
"unknown": "Unknown device selected"
|
||||
},
|
||||
"category-picker": {
|
||||
"clear": "Clear",
|
||||
@@ -805,6 +808,7 @@
|
||||
"add_new": "Add new category…",
|
||||
"no_categories": "No categories available",
|
||||
"no_match": "No categories found for {term}",
|
||||
"unknown": "Unknown category selected",
|
||||
"add_dialog": {
|
||||
"title": "Add new category",
|
||||
"text": "Enter the name of the new category.",
|
||||
@@ -831,7 +835,8 @@
|
||||
"add_new": "Add new area…",
|
||||
"no_areas": "No areas available",
|
||||
"no_match": "No areas found for {term}",
|
||||
"failed_create_area": "Failed to create area."
|
||||
"failed_create_area": "Failed to create area.",
|
||||
"unknown": "Unknown area selected"
|
||||
},
|
||||
"floor-picker": {
|
||||
"clear": "Clear",
|
||||
@@ -841,7 +846,8 @@
|
||||
"add_new": "Add new floor…",
|
||||
"no_floors": "No floors available",
|
||||
"no_match": "No floors found for {term}",
|
||||
"failed_create_floor": "Failed to create floor."
|
||||
"failed_create_floor": "Failed to create floor.",
|
||||
"unknown": "Unknown floor selected"
|
||||
},
|
||||
"area-filter": {
|
||||
"title": "Areas",
|
||||
@@ -858,7 +864,8 @@
|
||||
"no_match": "No statistics found for {term}",
|
||||
"no_state": "Entity without state",
|
||||
"missing_entity": "Why is my entity not listed?",
|
||||
"learn_more": "Learn more about statistics"
|
||||
"learn_more": "Learn more about statistics",
|
||||
"unknown": "Unknown statistic selected"
|
||||
},
|
||||
"addon-picker": {
|
||||
"addon": "Add-on",
|
||||
@@ -998,7 +1005,8 @@
|
||||
},
|
||||
"service-picker": {
|
||||
"action": "Action",
|
||||
"no_match": "No matching actions found"
|
||||
"no_match": "No matching actions found",
|
||||
"unknown": "Unknown action selected"
|
||||
},
|
||||
"service-control": {
|
||||
"required": "This field is required",
|
||||
@@ -1296,7 +1304,8 @@
|
||||
},
|
||||
"combo-box": {
|
||||
"no_match": "No matching items found",
|
||||
"no_items": "No items available"
|
||||
"no_items": "No items available",
|
||||
"unknown_item": "Unknown item"
|
||||
},
|
||||
"suggest_with_ai": {
|
||||
"label": "Suggest",
|
||||
@@ -3295,7 +3304,8 @@
|
||||
"entity_id": "Entity ID",
|
||||
"type": "Type",
|
||||
"editable": "Editable",
|
||||
"category": "Category"
|
||||
"category": "Category",
|
||||
"area": "Area"
|
||||
},
|
||||
"create_helper": "Create helper",
|
||||
"no_helpers": "Looks like you don't have any helpers yet!",
|
||||
|
||||
Reference in New Issue
Block a user