mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-15 04:27:28 +00:00
Compare commits
16 Commits
show-proto
...
config-ent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca2344eb10 | ||
|
|
6c9f51071c | ||
|
|
0c1f57e2ce | ||
|
|
e573f2012e | ||
|
|
05ca8253f0 | ||
|
|
071161e82d | ||
|
|
9cd38c7128 | ||
|
|
6322c19a45 | ||
|
|
74b51b77fe | ||
|
|
b80481b53e | ||
|
|
2ce1eaf8c6 | ||
|
|
4030ce3f88 | ||
|
|
41cabde393 | ||
|
|
47d1fdf673 | ||
|
|
e59e83fffe | ||
|
|
b896b78876 |
29
src/common/util/configuration-url.ts
Normal file
29
src/common/util/configuration-url.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export interface ProcessedConfigurationUrl {
|
||||||
|
url: string;
|
||||||
|
isLocal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a processed configuration URL, converting homeassistant:// URLs to local paths
|
||||||
|
* and determining if it should be opened locally or in a new tab.
|
||||||
|
*
|
||||||
|
* @param configurationUrl - The configuration URL to process
|
||||||
|
* @returns Processed URL and whether it's a local link, or null if URL is empty
|
||||||
|
*/
|
||||||
|
export const getConfigurationUrl = (
|
||||||
|
configurationUrl: string | null | undefined
|
||||||
|
): ProcessedConfigurationUrl | null => {
|
||||||
|
if (!configurationUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHomeAssistant = configurationUrl.startsWith("homeassistant://");
|
||||||
|
const url = isHomeAssistant
|
||||||
|
? configurationUrl.replace("homeassistant://", "/")
|
||||||
|
: configurationUrl;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
isLocal: isHomeAssistant,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -16,8 +16,10 @@ import memoizeOne from "memoize-one";
|
|||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
import { stringCompare } from "../../common/string/compare";
|
||||||
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { groupBy } from "../../common/util/group-by";
|
import { groupBy } from "../../common/util/group-by";
|
||||||
|
import { nextRender } from "../../common/util/render-status";
|
||||||
import { haStyleScrollbar } from "../../resources/styles";
|
import { haStyleScrollbar } from "../../resources/styles";
|
||||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
@@ -26,8 +28,6 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
|
||||||
import { nextRender } from "../../common/util/render-status";
|
|
||||||
|
|
||||||
export interface RowClickedEvent {
|
export interface RowClickedEvent {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { expose } from "comlink";
|
import { expose } from "comlink";
|
||||||
import Fuse from "fuse.js";
|
import Fuse, { type FuseOptionKey } from "fuse.js";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||||
import { HaFuse } from "../../resources/fuse";
|
import { multiTermSearch } from "../../resources/fuseMultiTerm";
|
||||||
import type {
|
import type {
|
||||||
ClonedDataTableColumnData,
|
ClonedDataTableColumnData,
|
||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
@@ -11,9 +11,10 @@ import type {
|
|||||||
SortingDirection,
|
SortingDirection,
|
||||||
} from "./ha-data-table";
|
} from "./ha-data-table";
|
||||||
|
|
||||||
const fuseIndex = memoizeOne(
|
const getSearchKeys = memoizeOne(
|
||||||
(data: DataTableRowData[], columns: SortableColumnContainer) => {
|
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
|
||||||
const searchKeys = new Set<string>();
|
const searchKeys = new Set<string>();
|
||||||
|
|
||||||
Object.entries(columns).forEach(([key, column]) => {
|
Object.entries(columns).forEach(([key, column]) => {
|
||||||
if (column.filterable) {
|
if (column.filterable) {
|
||||||
searchKeys.add(
|
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 = (
|
const filterData = (
|
||||||
data: DataTableRowData[],
|
data: DataTableRowData[],
|
||||||
columns: SortableColumnContainer,
|
columns: SortableColumnContainer,
|
||||||
@@ -38,21 +44,13 @@ const filterData = (
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = fuseIndex(data, columns);
|
const keys = getSearchKeys(columns);
|
||||||
|
|
||||||
const fuse = new HaFuse(
|
const index = fuseIndex(data, keys);
|
||||||
data,
|
|
||||||
{ shouldSort: false, minMatchCharLength: 1 },
|
|
||||||
index
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchResults = fuse.multiTermsSearch(filter);
|
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
|
||||||
|
threshold: 0.2, // reduce fuzzy matches in data tables
|
||||||
if (searchResults) {
|
});
|
||||||
return searchResults.map((result) => result.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortData = (
|
const sortData = (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { computeDeviceName } from "../../common/entity/compute_device_name";
|
|||||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
|
deviceComboBoxKeys,
|
||||||
getDevices,
|
getDevices,
|
||||||
type DevicePickerItem,
|
type DevicePickerItem,
|
||||||
type DeviceRegistryEntry,
|
type DeviceRegistryEntry,
|
||||||
@@ -216,6 +217,10 @@ export class HaDevicePicker extends LitElement {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
|
.searchKeys=${deviceComboBoxKeys}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.device-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
|||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||||
import {
|
import {
|
||||||
|
entityComboBoxKeys,
|
||||||
getEntities,
|
getEntities,
|
||||||
type EntityComboBoxItem,
|
type EntityComboBoxItem,
|
||||||
} from "../../data/entity_registry";
|
} from "../../data/entity_registry";
|
||||||
@@ -288,10 +289,14 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
@value-changed=${this._valueChanged}
|
.searchKeys=${entityComboBoxKeys}
|
||||||
.addButtonLabel=${this.addButton
|
.addButtonLabel=${this.addButton
|
||||||
? this.hass.localize("ui.components.entity.entity-picker.add")
|
? this.hass.localize("ui.components.entity.entity-picker.add")
|
||||||
: undefined}
|
: undefined}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.unknown"
|
||||||
|
)}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -38,9 +38,21 @@ type StatisticItemType = "entity" | "external" | "no_state";
|
|||||||
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
||||||
statistic_id?: string;
|
statistic_id?: string;
|
||||||
stateObj?: HassEntity;
|
stateObj?: HassEntity;
|
||||||
|
domainName?: string;
|
||||||
type?: StatisticItemType;
|
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")
|
@customElement("ha-statistic-picker")
|
||||||
export class HaStatisticPicker extends LitElement {
|
export class HaStatisticPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -233,7 +245,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
),
|
),
|
||||||
type,
|
type,
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, id],
|
|
||||||
icon_path: mdiShape,
|
icon_path: mdiShape,
|
||||||
});
|
});
|
||||||
} else if (type === "external") {
|
} else if (type === "external") {
|
||||||
@@ -246,7 +257,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: domainName,
|
secondary: domainName,
|
||||||
type,
|
type,
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, domainName, id],
|
search_labels: { label, domainName },
|
||||||
icon_path: mdiChartLine,
|
icon_path: mdiChartLine,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -280,13 +291,12 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
search_labels: [
|
search_labels: {
|
||||||
entityName,
|
entityName: entityName || null,
|
||||||
deviceName,
|
deviceName: deviceName || null,
|
||||||
areaName,
|
areaName: areaName || null,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
id,
|
},
|
||||||
].filter(Boolean) as string[],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -361,13 +371,13 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
search_labels: [
|
search_labels: {
|
||||||
entityName,
|
entityName: entityName || null,
|
||||||
deviceName,
|
deviceName: deviceName || null,
|
||||||
areaName,
|
areaName: areaName || null,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
statisticId,
|
statisticId,
|
||||||
].filter(Boolean) as string[],
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +404,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: domainName,
|
secondary: domainName,
|
||||||
type: "external",
|
type: "external",
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, domainName, statisticId],
|
search_labels: { label, domainName, statisticId },
|
||||||
icon_path: mdiChartLine,
|
icon_path: mdiChartLine,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -409,7 +419,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
||||||
type: "no_state",
|
type: "no_state",
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, statisticId],
|
search_labels: { label, statisticId },
|
||||||
icon_path: mdiShape,
|
icon_path: mdiShape,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -475,6 +485,10 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</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 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")
|
@customElement("ha-area-picker")
|
||||||
export class HaAreaPicker extends LitElement {
|
export class HaAreaPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -291,12 +297,12 @@ export class HaAreaPicker extends LitElement {
|
|||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||||
sorting_label: areaName,
|
sorting_label: areaName,
|
||||||
search_labels: [
|
search_labels: {
|
||||||
areaName,
|
areaName: areaName || null,
|
||||||
floorName,
|
floorName: floorName || null,
|
||||||
area.area_id,
|
id: area.area_id,
|
||||||
...area.aliases,
|
aliases: area.aliases.join(" "),
|
||||||
].filter((v): v is string => Boolean(v)),
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -379,6 +385,10 @@ export class HaAreaPicker extends LitElement {
|
|||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.addButtonLabel=${this.addButtonLabel}
|
.addButtonLabel=${this.addButtonLabel}
|
||||||
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.area-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
})}"
|
})}"
|
||||||
breaks
|
breaks
|
||||||
cache
|
cache
|
||||||
|
assist
|
||||||
.content=${message.text}
|
.content=${message.text}
|
||||||
>
|
>
|
||||||
</ha-markdown>
|
</ha-markdown>
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ import "./ha-svg-icon";
|
|||||||
|
|
||||||
const ADD_NEW_ID = "___ADD_NEW___";
|
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 {
|
interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||||
floor?: FloorRegistryEntry;
|
floor?: FloorRegistryEntry;
|
||||||
}
|
}
|
||||||
@@ -286,9 +292,11 @@ export class HaFloorPicker extends LitElement {
|
|||||||
primary: floorName,
|
primary: floorName,
|
||||||
floor: floor,
|
floor: floor,
|
||||||
sorting_label: floor.level?.toString() || "zzzzz",
|
sorting_label: floor.level?.toString() || "zzzzz",
|
||||||
search_labels: [floorName, floor.floor_id, ...floor.aliases].filter(
|
search_labels: {
|
||||||
(v): v is string => Boolean(v)
|
floorName,
|
||||||
),
|
floor_id: floor.floor_id,
|
||||||
|
aliases: floor.aliases.join(" "),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -393,6 +401,10 @@ export class HaFloorPicker extends LitElement {
|
|||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.floor-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { mdiPlaylistPlus } from "@mdi/js";
|
|||||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { tinykeys } from "tinykeys";
|
import { tinykeys } from "tinykeys";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-bottom-sheet";
|
import "./ha-bottom-sheet";
|
||||||
import "./ha-button";
|
import "./ha-button";
|
||||||
@@ -46,8 +48,9 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
public hideClearIcon = false;
|
public hideClearIcon = false;
|
||||||
|
|
||||||
|
/** To prevent lags, getItems needs to be memoized */
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public getItems?: (
|
public getItems!: (
|
||||||
searchString?: string,
|
searchString?: string,
|
||||||
section?: string
|
section?: string
|
||||||
) => (PickerComboBoxItem | string)[];
|
) => (PickerComboBoxItem | string)[];
|
||||||
@@ -64,6 +67,9 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public searchKeys?: FuseWeightedKey[];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public notFoundLabel?: string | ((search: string) => string);
|
public notFoundLabel?: string | ((search: string) => string);
|
||||||
|
|
||||||
@@ -107,6 +113,8 @@ export class HaGenericPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
|
||||||
|
|
||||||
@query(".container") private _containerElement?: HTMLDivElement;
|
@query(".container") private _containerElement?: HTMLDivElement;
|
||||||
|
|
||||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||||
@@ -156,6 +164,8 @@ export class HaGenericPicker extends LitElement {
|
|||||||
type="button"
|
type="button"
|
||||||
class=${this._opened ? "opened" : ""}
|
class=${this._opened ? "opened" : ""}
|
||||||
compact
|
compact
|
||||||
|
.unknown=${this._unknownValue(this.value, this.getItems())}
|
||||||
|
.unknownItemText=${this.unknownItemText}
|
||||||
aria-label=${ifDefined(this.label)}
|
aria-label=${ifDefined(this.label)}
|
||||||
@click=${this.open}
|
@click=${this.open}
|
||||||
@clear=${this._clear}
|
@clear=${this._clear}
|
||||||
@@ -229,10 +239,23 @@ export class HaGenericPicker extends LitElement {
|
|||||||
.sections=${this.sections}
|
.sections=${this.sections}
|
||||||
.sectionTitleFunction=${this.sectionTitleFunction}
|
.sectionTitleFunction=${this.sectionTitleFunction}
|
||||||
.selectedSection=${this.selectedSection}
|
.selectedSection=${this.selectedSection}
|
||||||
|
.searchKeys=${this.searchKeys}
|
||||||
></ha-picker-combo-box>
|
></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() {
|
private _renderHelper() {
|
||||||
return this.helper
|
return this.helper
|
||||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { LabelRegistryEntry } from "../data/label_registry";
|
|||||||
import {
|
import {
|
||||||
createLabelRegistryEntry,
|
createLabelRegistryEntry,
|
||||||
getLabels,
|
getLabels,
|
||||||
|
labelComboBoxKeys,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../data/label_registry";
|
} from "../data/label_registry";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
@@ -237,6 +238,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
|
.searchKeys=${labelComboBoxKeys}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
|
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
|
||||||
|
|||||||
@@ -40,14 +40,12 @@ export const getLanguageOptions = (
|
|||||||
return {
|
return {
|
||||||
id: lang,
|
id: lang,
|
||||||
primary,
|
primary,
|
||||||
search_labels: [primary],
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (locale) {
|
} else if (locale) {
|
||||||
options = languages.map((lang) => ({
|
options = languages.map((lang) => ({
|
||||||
id: lang,
|
id: lang,
|
||||||
primary: formatLanguageCode(lang, locale),
|
primary: formatLanguageCode(lang, locale),
|
||||||
search_labels: [formatLanguageCode(lang, locale)],
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,13 +70,15 @@ export class HaMarkdown extends LitElement {
|
|||||||
a {
|
a {
|
||||||
color: var(--markdown-link-color, var(--primary-color));
|
color: var(--markdown-link-color, var(--primary-color));
|
||||||
}
|
}
|
||||||
|
:host([assist]) img {
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
transition: height 0.2s ease-in-out;
|
||||||
|
}
|
||||||
img {
|
img {
|
||||||
background-color: var(--markdown-image-background-color);
|
background-color: var(--markdown-image-background-color);
|
||||||
border-radius: var(--markdown-image-border-radius);
|
border-radius: var(--markdown-image-border-radius);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
transition: height 0.2s ease-in-out;
|
|
||||||
}
|
}
|
||||||
p:first-child > img:first-child {
|
p:first-child > img:first-child {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import { tinykeys } from "tinykeys";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
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 { haStyleScrollbar } from "../resources/styles";
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -26,11 +29,26 @@ import "./ha-icon";
|
|||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
import type { HaTextField } from "./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 {
|
export interface PickerComboBoxItem {
|
||||||
id: string;
|
id: string;
|
||||||
primary: string;
|
primary: string;
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
search_labels?: string[];
|
search_labels?: Record<string, string | null>;
|
||||||
sorting_label?: string;
|
sorting_label?: string;
|
||||||
icon_path?: string;
|
icon_path?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
@@ -77,10 +95,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public searchKeys?: FuseWeightedKey[];
|
||||||
|
|
||||||
@state() private _listScrolled = false;
|
@state() private _listScrolled = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public getItems?: (
|
public getItems!: (
|
||||||
searchString?: string,
|
searchString?: string,
|
||||||
section?: string
|
section?: string
|
||||||
) => (PickerComboBoxItem | string)[];
|
) => (PickerComboBoxItem | string)[];
|
||||||
@@ -133,6 +154,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _sectionTitle?: string;
|
@state() private _sectionTitle?: string;
|
||||||
|
|
||||||
|
@state() private _valuePinned = true;
|
||||||
|
|
||||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||||
|
|
||||||
private _selectedItemIndex = -1;
|
private _selectedItemIndex = -1;
|
||||||
@@ -194,6 +217,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
.renderItem=${this._renderItem}
|
.renderItem=${this._renderItem}
|
||||||
style="min-height: 36px;"
|
style="min-height: 36px;"
|
||||||
class=${this._listScrolled ? "scrolled" : ""}
|
class=${this._listScrolled ? "scrolled" : ""}
|
||||||
|
.layout=${this.value && this._valuePinned
|
||||||
|
? {
|
||||||
|
pin: {
|
||||||
|
index: this._getInitialSelectedIndex(),
|
||||||
|
block: "center",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
|
@unpinned=${this._handleUnpinned}
|
||||||
@scroll=${this._onScrollList}
|
@scroll=${this._onScrollList}
|
||||||
@focus=${this._focusList}
|
@focus=${this._focusList}
|
||||||
@visibilityChanged=${this._visibilityChanged}
|
@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) =>
|
private _getAdditionalItems = (searchString?: string) =>
|
||||||
this.getAdditionalItems?.(searchString) || [];
|
this.getAdditionalItems?.(searchString) || [];
|
||||||
|
|
||||||
private _getItems = () => {
|
private _getItems = () => {
|
||||||
let items = [
|
let items = [...this.getItems(this._search, this.selectedSection)];
|
||||||
...(this.getItems
|
|
||||||
? this.getItems(this._search, this.selectedSection)
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!this.sections?.length) {
|
if (!this.sections?.length) {
|
||||||
items = items.sort((entityA, entityB) =>
|
items = items.sort((entityA, entityB) =>
|
||||||
@@ -279,6 +312,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||||
|
if (!item) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
if (item === "padding") {
|
if (item === "padding") {
|
||||||
return html`<div class="bottom-padding"></div>`;
|
return html`<div class="bottom-padding"></div>`;
|
||||||
}
|
}
|
||||||
@@ -339,8 +375,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
fireEvent(this, "value-changed", { value: newValue });
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
|
private _fuseIndex = memoizeOne(
|
||||||
Fuse.createIndex(["search_labels"], states)
|
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
||||||
|
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filterChanged = (ev: Event) => {
|
private _filterChanged = (ev: Event) => {
|
||||||
@@ -356,34 +393,26 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
const index = this._fuseIndex(
|
||||||
const fuse = new HaFuse(
|
|
||||||
this._allItems as PickerComboBoxItem[],
|
this._allItems as PickerComboBoxItem[],
|
||||||
{
|
this.searchKeys
|
||||||
shouldSort: false,
|
|
||||||
minMatchCharLength: Math.min(searchString.length, 2),
|
|
||||||
},
|
|
||||||
index
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(searchString);
|
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||||
let filteredItems = [...this._allItems];
|
this._allItems as PickerComboBoxItem[],
|
||||||
|
searchString,
|
||||||
|
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||||
|
(item) => item.id,
|
||||||
|
index
|
||||||
|
) as (PickerComboBoxItem | string)[];
|
||||||
|
|
||||||
if (results) {
|
if (!filteredItems.length) {
|
||||||
const items: (PickerComboBoxItem | string)[] = results.map(
|
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||||
(result) => result.item
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!items.length) {
|
|
||||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalItems = this._getAdditionalItems();
|
|
||||||
items.push(...additionalItems);
|
|
||||||
|
|
||||||
filteredItems = items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const additionalItems = this._getAdditionalItems();
|
||||||
|
filteredItems.push(...additionalItems);
|
||||||
|
|
||||||
if (this.searchFn) {
|
if (this.searchFn) {
|
||||||
filteredItems = this.searchFn(
|
filteredItems = this.searchFn(
|
||||||
searchString,
|
searchString,
|
||||||
@@ -590,7 +619,25 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
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() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { consume } from "@lit/context";
|
||||||
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -7,8 +8,10 @@ import {
|
|||||||
type CSSResultGroup,
|
type CSSResultGroup,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from "lit";
|
} 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 { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { localizeContext } from "../data/context";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
@@ -33,6 +36,10 @@ export class HaPickerField extends LitElement {
|
|||||||
|
|
||||||
@property() public placeholder?: string;
|
@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 })
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
public hideClearIcon = false;
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@@ -41,6 +48,10 @@ export class HaPickerField extends LitElement {
|
|||||||
|
|
||||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: localizeContext, subscribe: true })
|
||||||
|
private localize!: HomeAssistant["localize"];
|
||||||
|
|
||||||
public async focus() {
|
public async focus() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this.item?.focus();
|
await this.item?.focus();
|
||||||
@@ -61,6 +72,12 @@ export class HaPickerField extends LitElement {
|
|||||||
${this.placeholder}
|
${this.placeholder}
|
||||||
</span>
|
</span>
|
||||||
`}
|
`}
|
||||||
|
${this.unknown
|
||||||
|
? html`<div slot="supporting-text" class="unknown">
|
||||||
|
${this.unknownItemText ||
|
||||||
|
this.localize("ui.components.combo-box.unknown_item")}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
${showClearIcon
|
${showClearIcon
|
||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@@ -142,6 +159,10 @@ export class HaPickerField extends LitElement {
|
|||||||
background-color: var(--mdc-theme-primary);
|
background-color: var(--mdc-theme-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([unknown]) ha-combo-box-item {
|
||||||
|
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||||
|
}
|
||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
margin: 0 -8px;
|
margin: 0 -8px;
|
||||||
--mdc-icon-button-size: 32px;
|
--mdc-icon-button-size: 32px;
|
||||||
@@ -156,6 +177,10 @@ export class HaPickerField extends LitElement {
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unknown {
|
||||||
|
color: var(--ha-color-on-warning-normal);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ interface ServiceComboBoxItem extends PickerComboBoxItem {
|
|||||||
service_id?: string;
|
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")
|
@customElement("ha-service-picker")
|
||||||
class HaServicePicker extends LitElement {
|
class HaServicePicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -141,6 +148,10 @@ class HaServicePicker extends LitElement {
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.hass.services
|
this.hass.services
|
||||||
)}
|
)}
|
||||||
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.service-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
@@ -194,9 +205,7 @@ class HaServicePicker extends LitElement {
|
|||||||
secondary: description,
|
secondary: description,
|
||||||
domain_name: domainName,
|
domain_name: domainName,
|
||||||
service_id: serviceId,
|
service_id: serviceId,
|
||||||
search_labels: [serviceId, domainName, name, description].filter(
|
search_labels: { serviceId, domainName, name, description },
|
||||||
Boolean
|
|
||||||
),
|
|
||||||
sorting_label: serviceId,
|
sorting_label: serviceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,30 @@ import { fireEvent } from "../common/dom/fire_event";
|
|||||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import {
|
import {
|
||||||
|
areaFloorComboBoxKeys,
|
||||||
getAreasAndFloors,
|
getAreasAndFloors,
|
||||||
type AreaFloorValue,
|
type AreaFloorValue,
|
||||||
type FloorComboBoxItem,
|
type FloorComboBoxItem,
|
||||||
} from "../data/area_floor";
|
} from "../data/area_floor";
|
||||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||||
import { labelsContext } from "../data/context";
|
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 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 { domainToName } from "../data/integration";
|
||||||
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
|
import {
|
||||||
|
getLabels,
|
||||||
|
labelComboBoxKeys,
|
||||||
|
type LabelRegistryEntry,
|
||||||
|
} from "../data/label_registry";
|
||||||
import {
|
import {
|
||||||
areaMeetsFilter,
|
areaMeetsFilter,
|
||||||
deviceMeetsFilter,
|
deviceMeetsFilter,
|
||||||
@@ -37,7 +50,11 @@ import {
|
|||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||||
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
|
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 type { HomeAssistant } from "../types";
|
||||||
import { brandsUrl } from "../util/brands-url";
|
import { brandsUrl } from "../util/brands-url";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
@@ -113,16 +130,16 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private _fuseIndexes = {
|
private _fuseIndexes = {
|
||||||
area: memoizeOne((states: FloorComboBoxItem[]) =>
|
area: memoizeOne((states: FloorComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, areaFloorComboBoxKeys)
|
||||||
),
|
),
|
||||||
entity: memoizeOne((states: EntityComboBoxItem[]) =>
|
entity: memoizeOne((states: EntityComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, entityComboBoxKeys)
|
||||||
),
|
),
|
||||||
device: memoizeOne((states: DevicePickerItem[]) =>
|
device: memoizeOne((states: DevicePickerItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, deviceComboBoxKeys)
|
||||||
),
|
),
|
||||||
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, labelComboBoxKeys)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,8 +151,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createFuseIndex = (states) =>
|
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
|
||||||
Fuse.createIndex(["search_labels"], states);
|
Fuse.createIndex(keys, states);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.addOnTop) {
|
if (this.addOnTop) {
|
||||||
@@ -735,8 +752,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
"entity",
|
"entity",
|
||||||
entityItems,
|
entityItems,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
(item: EntityComboBoxItem) =>
|
entityComboBoxKeys
|
||||||
item.stateObj?.entity_id === searchTerm
|
|
||||||
) as EntityComboBoxItem[];
|
) as EntityComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +781,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
deviceItems = this._filterGroup(
|
||||||
|
"device",
|
||||||
|
deviceItems,
|
||||||
|
searchTerm,
|
||||||
|
deviceComboBoxKeys
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filterType && deviceItems.length) {
|
if (!filterType && deviceItems.length) {
|
||||||
@@ -799,7 +820,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
areasAndFloors = this._filterGroup(
|
areasAndFloors = this._filterGroup(
|
||||||
"area",
|
"area",
|
||||||
areasAndFloors,
|
areasAndFloors,
|
||||||
searchTerm
|
searchTerm,
|
||||||
|
areaFloorComboBoxKeys,
|
||||||
|
false
|
||||||
) as FloorComboBoxItem[];
|
) as FloorComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,7 +867,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
labels = this._filterGroup("label", labels, searchTerm);
|
labels = this._filterGroup(
|
||||||
|
"label",
|
||||||
|
labels,
|
||||||
|
searchTerm,
|
||||||
|
labelComboBoxKeys
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filterType && labels.length) {
|
if (!filterType && labels.length) {
|
||||||
@@ -863,40 +891,24 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
type: TargetType,
|
type: TargetType,
|
||||||
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
|
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
checkExact?: (
|
weightedKeys: FuseWeightedKey[],
|
||||||
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
|
sort = true
|
||||||
) => boolean
|
|
||||||
) {
|
) {
|
||||||
const fuseIndex = this._fuseIndexes[type](items);
|
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);
|
if (sort) {
|
||||||
let filteredItems = items;
|
return multiTermSortedSearch(
|
||||||
if (results) {
|
items,
|
||||||
filteredItems = results.map((result) => result.item);
|
searchTerm,
|
||||||
|
weightedKeys,
|
||||||
|
(item) => item.id,
|
||||||
|
fuseIndex
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkExact) {
|
return multiTermSearch(items, searchTerm, weightedKeys, fuseIndex, {
|
||||||
return filteredItems;
|
ignoreLocation: true,
|
||||||
}
|
});
|
||||||
|
|
||||||
// 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 _getAdditionalItems = () => this._getCreateItems(this.createDomains);
|
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ interface UserComboBoxItem extends PickerComboBoxItem {
|
|||||||
user?: User;
|
user?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SEARCH_KEYS = [
|
||||||
|
{ name: "primary", weight: 10 },
|
||||||
|
{ name: "search_labels.username", weight: 6 },
|
||||||
|
{ name: "id", weight: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
@customElement("ha-user-picker")
|
@customElement("ha-user-picker")
|
||||||
class HaUserPicker extends LitElement {
|
class HaUserPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -109,9 +115,7 @@ class HaUserPicker extends LitElement {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
primary: user.name,
|
primary: user.name,
|
||||||
domain_name: user.name,
|
domain_name: user.name,
|
||||||
search_labels: [user.name, user.id, user.username].filter(
|
search_labels: { username: user.username },
|
||||||
Boolean
|
|
||||||
) as string[],
|
|
||||||
sorting_label: user.name,
|
sorting_label: user.name,
|
||||||
user,
|
user,
|
||||||
}));
|
}));
|
||||||
@@ -134,6 +138,10 @@ class HaUserPicker extends LitElement {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.user-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { computeDomain } from "../common/entity/compute_domain";
|
|||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
|
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { AreaRegistryEntry } from "./area_registry";
|
import type { AreaRegistryEntry } from "./area_registry";
|
||||||
import {
|
import {
|
||||||
@@ -26,7 +27,8 @@ export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
|||||||
areas: FloorComboBoxItem[];
|
areas: FloorComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
export interface UnassignedAreasFloorComboBoxItem {
|
||||||
|
id: undefined;
|
||||||
areas: FloorComboBoxItem[];
|
areas: FloorComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +100,29 @@ export const getAreasAndFloors = (
|
|||||||
excludeFloors
|
excludeFloors
|
||||||
) as FloorComboBoxItem[];
|
) 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 = (
|
const getAreasAndFloorsItems = (
|
||||||
states: HomeAssistant["states"],
|
states: HomeAssistant["states"],
|
||||||
haFloors: HomeAssistant["floors"],
|
haFloors: HomeAssistant["floors"],
|
||||||
@@ -304,12 +329,12 @@ const getAreasAndFloorsItems = (
|
|||||||
primary: floorName,
|
primary: floorName,
|
||||||
floor: floor,
|
floor: floor,
|
||||||
icon: floor.icon || undefined,
|
icon: floor.icon || undefined,
|
||||||
search_labels: [
|
search_labels: {
|
||||||
floor.floor_id,
|
id: floor.floor_id,
|
||||||
floorName,
|
name: floorName || null,
|
||||||
...floor.aliases,
|
aliases: floor.aliases.join(", ") || null,
|
||||||
...areaSearchLabels,
|
relatedAreas: areaSearchLabels.join(" ") || null,
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
items.push(floorItem);
|
items.push(floorItem);
|
||||||
@@ -322,11 +347,12 @@ const getAreasAndFloorsItems = (
|
|||||||
primary: areaName || area.area_id,
|
primary: areaName || area.area_id,
|
||||||
area: area,
|
area: area,
|
||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
search_labels: [
|
search_labels: {
|
||||||
area.area_id,
|
id: area.area_id,
|
||||||
...(areaName ? [areaName] : []),
|
name: areaName || null,
|
||||||
...area.aliases,
|
aliases: area.aliases.join(", ") || null,
|
||||||
],
|
floorName: floorName || null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,19 +365,24 @@ const getAreasAndFloorsItems = (
|
|||||||
|
|
||||||
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
|
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
|
||||||
const area = haAreas[areaId];
|
const area = haAreas[areaId];
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
const areaName = computeAreaName(area);
|
||||||
return {
|
return {
|
||||||
id: formatId({ id: area.area_id, type: "area" }),
|
id: formatId({ id: area.area_id, type: "area" }),
|
||||||
type: "area" as const,
|
type: "area" as const,
|
||||||
primary: areaName,
|
primary: areaName || area.area_id,
|
||||||
area: area,
|
area: area,
|
||||||
icon: area.icon || undefined,
|
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) {
|
if (nested && unassignedAreaItems.length) {
|
||||||
items.push({
|
items.push({
|
||||||
|
id: undefined,
|
||||||
areas: unassignedAreaItems,
|
areas: unassignedAreaItems,
|
||||||
} as UnassignedAreasFloorComboBoxItem);
|
} as UnassignedAreasFloorComboBoxItem);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface ConfigEntry {
|
|||||||
reason: string | null;
|
reason: string | null;
|
||||||
error_reason_translation_key: string | null;
|
error_reason_translation_key: string | null;
|
||||||
error_reason_translation_placeholders: Record<string, string> | null;
|
error_reason_translation_placeholders: Record<string, string> | null;
|
||||||
|
configuration_url?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubEntry {
|
export interface SubEntry {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getDeviceContext } from "../common/entity/context/get_device_context";
|
|||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
|
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { ConfigEntry } from "./config_entries";
|
import type { ConfigEntry } from "./config_entries";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
@@ -181,6 +182,25 @@ export interface DevicePickerItem extends PickerComboBoxItem {
|
|||||||
domain_name?: string;
|
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 = (
|
export const getDevices = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
configEntryLookup: Record<string, ConfigEntry>,
|
configEntryLookup: Record<string, ConfigEntry>,
|
||||||
@@ -311,9 +331,12 @@ export const getDevices = (
|
|||||||
secondary: areaName,
|
secondary: areaName,
|
||||||
domain: configEntry?.domain,
|
domain: configEntry?.domain,
|
||||||
domain_name: domainName,
|
domain_name: domainName,
|
||||||
search_labels: [deviceName, areaName, domain, domainName].filter(
|
search_labels: {
|
||||||
Boolean
|
deviceName,
|
||||||
) as string[],
|
areaName: areaName || null,
|
||||||
|
domain: domain || null,
|
||||||
|
domainName: domainName || null,
|
||||||
|
},
|
||||||
sorting_label: deviceName || "zzz",
|
sorting_label: deviceName || "zzz",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
|
|||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
|
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
@@ -335,6 +336,33 @@ export interface EntityComboBoxItem extends PickerComboBoxItem {
|
|||||||
stateObj?: HassEntity;
|
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 = (
|
export const getEntities = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
includeDomains?: string[],
|
includeDomains?: string[],
|
||||||
@@ -403,14 +431,14 @@ export const getEntities = (
|
|||||||
secondary: secondary,
|
secondary: secondary,
|
||||||
domain_name: domainName,
|
domain_name: domainName,
|
||||||
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
|
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
|
||||||
search_labels: [
|
search_labels: {
|
||||||
entityName,
|
entityName: entityName || null,
|
||||||
deviceName,
|
deviceName: deviceName || null,
|
||||||
areaName,
|
areaName: areaName || null,
|
||||||
domainName,
|
domainName: domainName || null,
|
||||||
friendlyName,
|
friendlyName: friendlyName || null,
|
||||||
entityId,
|
entityId: entityId,
|
||||||
].filter(Boolean) as string[],
|
},
|
||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { stringCompare } from "../common/string/compare";
|
|||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||||
|
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import {
|
import {
|
||||||
getDeviceEntityDisplayLookup,
|
getDeviceEntityDisplayLookup,
|
||||||
@@ -100,6 +101,21 @@ export const deleteLabelRegistryEntry = (
|
|||||||
label_id: labelId,
|
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 = (
|
export const getLabels = (
|
||||||
hassStates: HomeAssistant["states"],
|
hassStates: HomeAssistant["states"],
|
||||||
hassAreas: HomeAssistant["areas"],
|
hassAreas: HomeAssistant["areas"],
|
||||||
@@ -273,9 +289,11 @@ export const getLabels = (
|
|||||||
icon: label.icon || undefined,
|
icon: label.icon || undefined,
|
||||||
icon_path: label.icon ? undefined : mdiLabel,
|
icon_path: label.icon ? undefined : mdiLabel,
|
||||||
sorting_label: label.name,
|
sorting_label: label.name,
|
||||||
search_labels: [label.name, label.label_id, label.description].filter(
|
search_labels: {
|
||||||
(v): v is string => Boolean(v)
|
name: label.name,
|
||||||
),
|
description: label.description,
|
||||||
|
id: label.label_id,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import { domainToName } from "../../data/integration";
|
|||||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||||
import { configSections } from "../../panels/config/ha-panel-config";
|
import { configSections } from "../../panels/config/ha-panel-config";
|
||||||
import { HaFuse } from "../../resources/fuse";
|
import { multiTermSortedSearch } from "../../resources/fuseMultiTerm";
|
||||||
import {
|
import {
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
haStyleDialogFixedTop,
|
haStyleDialogFixedTop,
|
||||||
@@ -58,7 +58,17 @@ import { showConfirmationDialog } from "../generic/show-dialog-box";
|
|||||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||||
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
|
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 {
|
interface QuickBarItem extends ScorableTextItem {
|
||||||
|
id: string;
|
||||||
primaryText: string;
|
primaryText: string;
|
||||||
iconPath?: string;
|
iconPath?: string;
|
||||||
action(data?: any): void;
|
action(data?: any): void;
|
||||||
@@ -593,6 +603,7 @@ export class QuickBar extends LitElement {
|
|||||||
const areaName = area ? computeAreaName(area) : undefined;
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
const deviceItem = {
|
const deviceItem = {
|
||||||
|
id: device.id,
|
||||||
primaryText: deviceName,
|
primaryText: deviceName,
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
area: areaName,
|
area: areaName,
|
||||||
@@ -666,6 +677,7 @@ export class QuickBar extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const entityItem = {
|
const entityItem = {
|
||||||
|
id: `entity-${entityId}`,
|
||||||
primaryText: primary,
|
primaryText: primary,
|
||||||
altText: secondary,
|
altText: secondary,
|
||||||
icon: html`
|
icon: html`
|
||||||
@@ -767,8 +779,9 @@ export class QuickBar extends LitElement {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
return commands.map((command) => ({
|
return commands.map((command, index) => ({
|
||||||
...command,
|
...command,
|
||||||
|
id: `command_${index}_${command.primaryText}`,
|
||||||
categoryKey: "reload",
|
categoryKey: "reload",
|
||||||
strings: [`${command.categoryText} ${command.primaryText}`],
|
strings: [`${command.categoryText} ${command.primaryText}`],
|
||||||
}));
|
}));
|
||||||
@@ -777,10 +790,11 @@ export class QuickBar extends LitElement {
|
|||||||
private _generateServerControlCommands(): CommandItem[] {
|
private _generateServerControlCommands(): CommandItem[] {
|
||||||
const serverActions = ["restart", "stop"] as const;
|
const serverActions = ["restart", "stop"] as const;
|
||||||
|
|
||||||
return serverActions.map((action) => {
|
return serverActions.map((action, index) => {
|
||||||
const categoryKey: CommandItem["categoryKey"] = "server_control";
|
const categoryKey: CommandItem["categoryKey"] = "server_control";
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
|
id: `server_control_${index}_${action}`,
|
||||||
primaryText: this.hass.localize(
|
primaryText: this.hass.localize(
|
||||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||||
{
|
{
|
||||||
@@ -940,10 +954,11 @@ export class QuickBar extends LitElement {
|
|||||||
private _finalizeNavigationCommands(
|
private _finalizeNavigationCommands(
|
||||||
items: BaseNavigationCommand[]
|
items: BaseNavigationCommand[]
|
||||||
): CommandItem[] {
|
): CommandItem[] {
|
||||||
return items.map((item) => {
|
return items.map((item, index) => {
|
||||||
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
const categoryKey: CommandItem["categoryKey"] = "navigation";
|
||||||
|
|
||||||
const navItem = {
|
const navItem = {
|
||||||
|
id: `navigation_${index}_${item.path}`,
|
||||||
iconPath: mdiEarth,
|
iconPath: mdiEarth,
|
||||||
categoryText: this.hass.localize(
|
categoryText: this.hass.localize(
|
||||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||||
@@ -961,28 +976,20 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _fuseIndex = memoizeOne((items: QuickBarItem[]) =>
|
private _fuseIndex = memoizeOne((items: QuickBarItem[]) =>
|
||||||
Fuse.createIndex(
|
Fuse.createIndex(SEARCH_KEYS, items)
|
||||||
[
|
|
||||||
"primaryText",
|
|
||||||
"altText",
|
|
||||||
"friendlyName",
|
|
||||||
"translatedDomain",
|
|
||||||
"entityId", // for technical search
|
|
||||||
],
|
|
||||||
items
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filterItems = memoizeOne(
|
private _filterItems = memoizeOne(
|
||||||
(items: QuickBarItem[], filter: string): QuickBarItem[] => {
|
(items: QuickBarItem[], filter: string): QuickBarItem[] => {
|
||||||
const index = this._fuseIndex(items);
|
const index = this._fuseIndex(items);
|
||||||
const fuse = new HaFuse(items, {}, index);
|
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(filter.trim());
|
return multiTermSortedSearch(
|
||||||
if (!results || !results.length) {
|
items,
|
||||||
return items;
|
filter,
|
||||||
}
|
SEARCH_KEYS,
|
||||||
return results.map((result) => result.item);
|
(item) => item.id,
|
||||||
|
index
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import "../components/ha-svg-icon";
|
|||||||
import "../components/ha-tab";
|
import "../components/ha-tab";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant, Route } from "../types";
|
import type { HomeAssistant, Route } from "../types";
|
||||||
import { withViewTransition } from "../common/util/view-transition";
|
|
||||||
|
|
||||||
export interface PageNavigation {
|
export interface PageNavigation {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -113,11 +112,9 @@ class HassTabsSubpage extends LitElement {
|
|||||||
|
|
||||||
public willUpdate(changedProperties: PropertyValues) {
|
public willUpdate(changedProperties: PropertyValues) {
|
||||||
if (changedProperties.has("route")) {
|
if (changedProperties.has("route")) {
|
||||||
withViewTransition(() => {
|
this._activeTab = this.tabs.find((tab) =>
|
||||||
this._activeTab = this.tabs.find((tab) =>
|
`${this.route.prefix}${this.route.path}`.includes(tab.path)
|
||||||
`${this.route.prefix}${this.route.path}`.includes(tab.path)
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { mdiClose, mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
|
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { LitElement, css, html, nothing } 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 { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import {
|
import {
|
||||||
type AreasFloorHierarchy,
|
type AreasFloorHierarchy,
|
||||||
@@ -11,12 +11,10 @@ import {
|
|||||||
} from "../../../common/areas/areas-floor-hierarchy";
|
} from "../../../common/areas/areas-floor-hierarchy";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
import "../../../components/ha-dialog-header";
|
import "../../../components/ha-dialog-footer";
|
||||||
import "../../../components/ha-floor-icon";
|
import "../../../components/ha-floor-icon";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-wa-dialog";
|
||||||
import "../../../components/ha-md-dialog";
|
|
||||||
import type { HaMdDialog } from "../../../components/ha-md-dialog";
|
|
||||||
import "../../../components/ha-md-list";
|
import "../../../components/ha-md-list";
|
||||||
import "../../../components/ha-md-list-item";
|
import "../../../components/ha-md-list-item";
|
||||||
import "../../../components/ha-sortable";
|
import "../../../components/ha-sortable";
|
||||||
@@ -49,8 +47,6 @@ class DialogAreasFloorsOrder extends LitElement {
|
|||||||
|
|
||||||
@state() private _saving = false;
|
@state() private _saving = false;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
_params: AreasFloorsOrderDialogParams
|
_params: AreasFloorsOrderDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -66,7 +62,8 @@ class DialogAreasFloorsOrder extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._dialog?.close();
|
this._saving = false;
|
||||||
|
this._open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed(): void {
|
private _dialogClosed(): void {
|
||||||
@@ -77,7 +74,7 @@ class DialogAreasFloorsOrder extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._open || !this._hierarchy) {
|
if (!this._hierarchy) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,17 +86,13 @@ class DialogAreasFloorsOrder extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
<ha-wa-dialog
|
||||||
<ha-dialog-header slot="headline">
|
.hass=${this.hass}
|
||||||
<ha-icon-button
|
.open=${this._open}
|
||||||
slot="navigationIcon"
|
header-title=${dialogTitle}
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
@closed=${this._dialogClosed}
|
||||||
.path=${mdiClose}
|
>
|
||||||
@click=${this.closeDialog}
|
<div class="content">
|
||||||
></ha-icon-button>
|
|
||||||
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
|
|
||||||
</ha-dialog-header>
|
|
||||||
<div slot="content" class="content">
|
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
handle-selector=".floor-handle"
|
handle-selector=".floor-handle"
|
||||||
draggable-selector=".floor"
|
draggable-selector=".floor"
|
||||||
@@ -116,15 +109,23 @@ class DialogAreasFloorsOrder extends LitElement {
|
|||||||
</ha-sortable>
|
</ha-sortable>
|
||||||
${this._renderUnassignedAreas()}
|
${this._renderUnassignedAreas()}
|
||||||
</div>
|
</div>
|
||||||
<div slot="actions">
|
<ha-dialog-footer slot="footer">
|
||||||
<ha-button @click=${this.closeDialog} appearance="plain">
|
<ha-button
|
||||||
|
slot="secondaryAction"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
appearance="plain"
|
||||||
|
>
|
||||||
${this.hass.localize("ui.common.cancel")}
|
${this.hass.localize("ui.common.cancel")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
<ha-button @click=${this._save} .disabled=${this._saving}>
|
<ha-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._save}
|
||||||
|
.disabled=${this._saving}
|
||||||
|
>
|
||||||
${this.hass.localize("ui.common.save")}
|
${this.hass.localize("ui.common.save")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</ha-dialog-footer>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,15 +392,13 @@ class DialogAreasFloorsOrder extends LitElement {
|
|||||||
haStyle,
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-md-dialog {
|
ha-wa-dialog {
|
||||||
min-width: 600px;
|
|
||||||
max-height: 90%;
|
max-height: 90%;
|
||||||
--dialog-content-padding: 8px 24px;
|
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 600px), all and (max-height: 500px) {
|
@media all and (max-width: 580px), all and (max-height: 500px) {
|
||||||
ha-md-dialog {
|
ha-wa-dialog {
|
||||||
--md-dialog-container-shape: 0;
|
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
|||||||
import { consume } from "@lit/context";
|
import { consume } from "@lit/context";
|
||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
||||||
import type { IFuseOptions } from "fuse.js";
|
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +33,7 @@ import "../../../../components/ha-section-title";
|
|||||||
import "../../../../components/ha-tree-indicator";
|
import "../../../../components/ha-tree-indicator";
|
||||||
import { ACTION_BUILDING_BLOCKS_GROUP } from "../../../../data/action";
|
import { ACTION_BUILDING_BLOCKS_GROUP } from "../../../../data/action";
|
||||||
import {
|
import {
|
||||||
|
areaFloorComboBoxKeys,
|
||||||
getAreasAndFloors,
|
getAreasAndFloors,
|
||||||
type AreaFloorValue,
|
type AreaFloorValue,
|
||||||
type FloorComboBoxItem,
|
type FloorComboBoxItem,
|
||||||
@@ -42,23 +42,30 @@ import { CONDITION_BUILDING_BLOCKS_GROUP } from "../../../../data/condition";
|
|||||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||||
import { labelsContext } from "../../../../data/context";
|
import { labelsContext } from "../../../../data/context";
|
||||||
import {
|
import {
|
||||||
|
deviceComboBoxKeys,
|
||||||
getDevices,
|
getDevices,
|
||||||
type DevicePickerItem,
|
type DevicePickerItem,
|
||||||
} from "../../../../data/device_registry";
|
} from "../../../../data/device_registry";
|
||||||
import {
|
import {
|
||||||
|
entityComboBoxKeys,
|
||||||
getEntities,
|
getEntities,
|
||||||
type EntityComboBoxItem,
|
type EntityComboBoxItem,
|
||||||
} from "../../../../data/entity_registry";
|
} from "../../../../data/entity_registry";
|
||||||
import type { DomainManifestLookup } from "../../../../data/integration";
|
import type { DomainManifestLookup } from "../../../../data/integration";
|
||||||
import {
|
import {
|
||||||
getLabels,
|
getLabels,
|
||||||
|
labelComboBoxKeys,
|
||||||
type LabelRegistryEntry,
|
type LabelRegistryEntry,
|
||||||
} from "../../../../data/label_registry";
|
} from "../../../../data/label_registry";
|
||||||
import {
|
import {
|
||||||
getTargetComboBoxItemType,
|
getTargetComboBoxItemType,
|
||||||
TARGET_SEPARATOR,
|
TARGET_SEPARATOR,
|
||||||
} from "../../../../data/target";
|
} from "../../../../data/target";
|
||||||
import { HaFuse } from "../../../../resources/fuse";
|
import {
|
||||||
|
multiTermSearch,
|
||||||
|
multiTermSortedSearch,
|
||||||
|
type FuseWeightedKey,
|
||||||
|
} from "../../../../resources/fuseMultiTerm";
|
||||||
import { loadVirtualizer } from "../../../../resources/virtualizer";
|
import { loadVirtualizer } from "../../../../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type {
|
import type {
|
||||||
@@ -76,6 +83,17 @@ const TARGET_SEARCH_SECTIONS = [
|
|||||||
"label",
|
"label",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export const ITEM_SEARCH_KEYS: FuseWeightedKey[] = [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
weight: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
weight: 7,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
type SearchSection = "item" | "block" | "entity" | "device" | "area" | "label";
|
type SearchSection = "item" | "block" | "entity" | "device" | "area" | "label";
|
||||||
|
|
||||||
@customElement("ha-automation-add-search")
|
@customElement("ha-automation-add-search")
|
||||||
@@ -434,27 +452,27 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||||
typeof item === "string" ? item : item.id;
|
typeof item === "string" ? item : item.id;
|
||||||
|
|
||||||
private _createFuseIndex = (states) =>
|
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
|
||||||
Fuse.createIndex(["search_labels"], states);
|
Fuse.createIndex(keys, states);
|
||||||
|
|
||||||
private _fuseIndexes = {
|
private _fuseIndexes = {
|
||||||
area: memoizeOne((states: PickerComboBoxItem[]) =>
|
area: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, areaFloorComboBoxKeys)
|
||||||
),
|
),
|
||||||
entity: memoizeOne((states: PickerComboBoxItem[]) =>
|
entity: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, entityComboBoxKeys)
|
||||||
),
|
),
|
||||||
device: memoizeOne((states: PickerComboBoxItem[]) =>
|
device: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, deviceComboBoxKeys)
|
||||||
),
|
),
|
||||||
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, labelComboBoxKeys)
|
||||||
),
|
),
|
||||||
item: memoizeOne((states: PickerComboBoxItem[]) =>
|
item: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||||
this._createFuseIndex(states)
|
this._createFuseIndex(states, ITEM_SEARCH_KEYS)
|
||||||
),
|
),
|
||||||
block: memoizeOne((states: PickerComboBoxItem[]) =>
|
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") {
|
if (!selectedSection || selectedSection === "item") {
|
||||||
let items = this._convertItemsToComboBoxItems(automationItems, type);
|
let items = this._convertItemsToComboBoxItems(automationItems, type);
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
items = this._filterGroup("item", items, searchTerm, {
|
items = this._filterGroup(
|
||||||
ignoreLocation: true,
|
"item",
|
||||||
includeScore: true,
|
items,
|
||||||
minMatchCharLength: Math.min(2, this.filter.length),
|
searchTerm,
|
||||||
}) as AutomationItemComboBoxItem[];
|
ITEM_SEARCH_KEYS
|
||||||
|
) as AutomationItemComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedSection && items.length) {
|
if (!selectedSection && items.length) {
|
||||||
@@ -514,11 +533,12 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
blocks = this._filterGroup("block", blocks, searchTerm, {
|
blocks = this._filterGroup(
|
||||||
ignoreLocation: true,
|
"block",
|
||||||
includeScore: true,
|
blocks,
|
||||||
minMatchCharLength: Math.min(2, this.filter.length),
|
searchTerm,
|
||||||
}) as AutomationItemComboBoxItem[];
|
ITEM_SEARCH_KEYS
|
||||||
|
) as AutomationItemComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedSection && blocks.length) {
|
if (!selectedSection && blocks.length) {
|
||||||
@@ -550,9 +570,7 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
"entity",
|
"entity",
|
||||||
entityItems,
|
entityItems,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
undefined,
|
entityComboBoxKeys
|
||||||
(item: EntityComboBoxItem) =>
|
|
||||||
item.stateObj?.entity_id === searchTerm
|
|
||||||
) as EntityComboBoxItem[];
|
) as EntityComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,7 +599,12 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
deviceItems = this._filterGroup(
|
||||||
|
"device",
|
||||||
|
deviceItems,
|
||||||
|
searchTerm,
|
||||||
|
deviceComboBoxKeys
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedSection && deviceItems.length) {
|
if (!selectedSection && deviceItems.length) {
|
||||||
@@ -617,7 +640,9 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
areasAndFloors = this._filterGroup(
|
areasAndFloors = this._filterGroup(
|
||||||
"area",
|
"area",
|
||||||
areasAndFloors,
|
areasAndFloors,
|
||||||
searchTerm
|
searchTerm,
|
||||||
|
areaFloorComboBoxKeys,
|
||||||
|
false
|
||||||
) as FloorComboBoxItem[];
|
) as FloorComboBoxItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,7 +689,12 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
labels = this._filterGroup("label", labels, searchTerm);
|
labels = this._filterGroup(
|
||||||
|
"label",
|
||||||
|
labels,
|
||||||
|
searchTerm,
|
||||||
|
labelComboBoxKeys
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedSection && labels.length) {
|
if (!selectedSection && labels.length) {
|
||||||
@@ -691,50 +721,28 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
| AutomationItemComboBoxItem
|
| AutomationItemComboBoxItem
|
||||||
)[],
|
)[],
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
fuseOptions?: IFuseOptions<any>,
|
searchKeys: FuseWeightedKey[],
|
||||||
checkExact?: (
|
sort = true
|
||||||
item:
|
|
||||||
| FloorComboBoxItem
|
|
||||||
| PickerComboBoxItem
|
|
||||||
| EntityComboBoxItem
|
|
||||||
| AutomationItemComboBoxItem
|
|
||||||
) => boolean
|
|
||||||
) {
|
) {
|
||||||
const fuseIndex = this._fuseIndexes[type](items);
|
const fuseIndex = this._fuseIndexes[type](items);
|
||||||
const fuse = new HaFuse<
|
|
||||||
| FloorComboBoxItem
|
if (sort) {
|
||||||
| PickerComboBoxItem
|
return multiTermSortedSearch<PickerComboBoxItem>(
|
||||||
| EntityComboBoxItem
|
items,
|
||||||
| AutomationItemComboBoxItem
|
searchTerm,
|
||||||
>(
|
searchKeys,
|
||||||
|
(item) => item.id,
|
||||||
|
fuseIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiTermSearch<PickerComboBoxItem>(
|
||||||
items,
|
items,
|
||||||
fuseOptions || {
|
searchTerm,
|
||||||
shouldSort: false,
|
searchKeys,
|
||||||
minMatchCharLength: Math.min(searchTerm.length, 2),
|
fuseIndex,
|
||||||
},
|
{ ignoreLocation: true }
|
||||||
fuseIndex
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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) {
|
private _toggleSection(ev: Event) {
|
||||||
@@ -787,7 +795,11 @@ export class HaAutomationAddSearch extends LitElement {
|
|||||||
iconPath,
|
iconPath,
|
||||||
renderedIcon: icon,
|
renderedIcon: icon,
|
||||||
type,
|
type,
|
||||||
search_labels: [key, name, description],
|
search_labels: {
|
||||||
|
key,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
private _focusSearchList = () => {
|
private _focusSearchList = () => {
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
|
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { LitElement, css, html, nothing } 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 { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-dialog-footer";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import "../../../../components/ha-icon-button-prev";
|
import "../../../../components/ha-icon-button-prev";
|
||||||
import "../../../../components/ha-md-dialog";
|
import "../../../../components/ha-wa-dialog";
|
||||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
|
||||||
import "../../../../components/ha-md-list";
|
import "../../../../components/ha-md-list";
|
||||||
import "../../../../components/ha-md-list-item";
|
import "../../../../components/ha-md-list-item";
|
||||||
import "../../../../components/ha-password-field";
|
import "../../../../components/ha-password-field";
|
||||||
@@ -31,20 +30,18 @@ type Step = (typeof STEPS)[number];
|
|||||||
class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _open = false;
|
||||||
|
|
||||||
@state() private _step?: Step;
|
@state() private _step?: Step;
|
||||||
|
|
||||||
@state() private _params?: ChangeBackupEncryptionKeyDialogParams;
|
@state() private _params?: ChangeBackupEncryptionKeyDialogParams;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog!: HaMdDialog;
|
|
||||||
|
|
||||||
@state() private _newEncryptionKey?: string;
|
@state() private _newEncryptionKey?: string;
|
||||||
|
|
||||||
public showDialog(params: ChangeBackupEncryptionKeyDialogParams): void {
|
public showDialog(params: ChangeBackupEncryptionKeyDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._step = STEPS[0];
|
this._step = STEPS[0];
|
||||||
this._opened = true;
|
this._open = true;
|
||||||
this._newEncryptionKey = generateEncryptionKey();
|
this._newEncryptionKey = generateEncryptionKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,10 +49,10 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
if (this._params!.cancel) {
|
if (this._params!.cancel) {
|
||||||
this._params!.cancel();
|
this._params!.cancel();
|
||||||
}
|
}
|
||||||
if (this._opened) {
|
if (this._open) {
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
this._opened = false;
|
this._open = false;
|
||||||
this._step = undefined;
|
this._step = undefined;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._newEncryptionKey = undefined;
|
this._newEncryptionKey = undefined;
|
||||||
@@ -64,7 +61,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
|
|
||||||
private _done() {
|
private _done() {
|
||||||
this._params?.submit!(true);
|
this._params?.submit!(true);
|
||||||
this._dialog.close();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _previousStep() {
|
private _previousStep() {
|
||||||
@@ -84,10 +81,6 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._opened || !this._params) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialogTitle =
|
const dialogTitle =
|
||||||
this._step === "current" || this._step === "new"
|
this._step === "current" || this._step === "new"
|
||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
@@ -96,36 +89,40 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
<ha-wa-dialog
|
||||||
<ha-dialog-header slot="headline">
|
.hass=${this.hass}
|
||||||
${this._step === "new"
|
.open=${this._open}
|
||||||
? html`
|
header-title=${dialogTitle}
|
||||||
<ha-icon-button-prev
|
prevent-scrim-close
|
||||||
slot="navigationIcon"
|
@closed=${this.closeDialog}
|
||||||
@click=${this._previousStep}
|
>
|
||||||
></ha-icon-button-prev>
|
${this._step === "new"
|
||||||
`
|
? html`
|
||||||
: html`
|
<ha-icon-button-prev
|
||||||
<ha-icon-button
|
slot="headerNavigationIcon"
|
||||||
slot="navigationIcon"
|
@click=${this._previousStep}
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
></ha-icon-button-prev>
|
||||||
.path=${mdiClose}
|
`
|
||||||
@click=${this.closeDialog}
|
: html`
|
||||||
></ha-icon-button>
|
<ha-icon-button
|
||||||
`}
|
slot="headerNavigationIcon"
|
||||||
<span slot="title">${dialogTitle}</span>
|
data-dialog="close"
|
||||||
</ha-dialog-header>
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
<div slot="content">${this._renderStepContent()}</div>
|
.path=${mdiClose}
|
||||||
<div slot="actions">
|
></ha-icon-button>
|
||||||
|
`}
|
||||||
|
${this._renderStepContent()}
|
||||||
|
<ha-dialog-footer slot="footer">
|
||||||
${this._step === "current"
|
${this._step === "current"
|
||||||
? html`
|
? html`
|
||||||
<ha-button @click=${this._nextStep}>
|
<ha-button slot="primaryAction" @click=${this._nextStep}>
|
||||||
${this.hass.localize("ui.common.next")}
|
${this.hass.localize("ui.common.next")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
`
|
`
|
||||||
: this._step === "new"
|
: this._step === "new"
|
||||||
? html`
|
? html`
|
||||||
<ha-button
|
<ha-button
|
||||||
|
slot="primaryAction"
|
||||||
@click=${this._submit}
|
@click=${this._submit}
|
||||||
.disabled=${!this._newEncryptionKey}
|
.disabled=${!this._newEncryptionKey}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -136,14 +133,14 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
</ha-button>
|
</ha-button>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<ha-button @click=${this._done}>
|
<ha-button slot="primaryAction" @click=${this._done}>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.change_encryption_key.actions.done"
|
"ui.panel.config.backup.dialogs.change_encryption_key.actions.done"
|
||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
`}
|
`}
|
||||||
</div>
|
</ha-dialog-footer>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,10 +284,8 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
haStyle,
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-md-dialog {
|
ha-wa-dialog {
|
||||||
width: 90vw;
|
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||||
max-width: 560px;
|
|
||||||
--dialog-content-padding: 8px 24px;
|
|
||||||
}
|
}
|
||||||
ha-md-list {
|
ha-md-list {
|
||||||
background: none;
|
background: none;
|
||||||
@@ -321,14 +316,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
flex: none;
|
flex: none;
|
||||||
margin: -16px;
|
margin: -16px;
|
||||||
}
|
}
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
|
||||||
ha-md-dialog {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
div[slot="content"] {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { mdiClose } from "@mdi/js";
|
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { LitElement, css, html, nothing } 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 { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-dialog-header";
|
|
||||||
import "../../../../components/ha-icon-button";
|
|
||||||
import "../../../../components/ha-icon-next";
|
|
||||||
import "../../../../components/ha-md-dialog";
|
|
||||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
|
||||||
import "../../../../components/ha-md-list";
|
|
||||||
import "../../../../components/ha-md-list-item";
|
|
||||||
import "../../../../components/ha-svg-icon";
|
|
||||||
import "../../../../components/ha-password-field";
|
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-dialog-footer";
|
||||||
|
import "../../../../components/ha-wa-dialog";
|
||||||
|
import "../../../../components/ha-password-field";
|
||||||
import {
|
import {
|
||||||
canDecryptBackupOnDownload,
|
canDecryptBackupOnDownload,
|
||||||
getPreferredAgentForDownload,
|
getPreferredAgentForDownload,
|
||||||
@@ -27,102 +21,96 @@ import type { DownloadDecryptedBackupDialogParams } from "./show-dialog-download
|
|||||||
class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _open = false;
|
||||||
|
|
||||||
@state() private _params?: DownloadDecryptedBackupDialogParams;
|
@state() private _params?: DownloadDecryptedBackupDialogParams;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
|
||||||
|
|
||||||
@state() private _encryptionKey = "";
|
@state() private _encryptionKey = "";
|
||||||
|
|
||||||
@state() private _error = "";
|
@state() private _error = "";
|
||||||
|
|
||||||
public showDialog(params: DownloadDecryptedBackupDialogParams): void {
|
public showDialog(params: DownloadDecryptedBackupDialogParams): void {
|
||||||
this._opened = true;
|
this._open = true;
|
||||||
this._params = params;
|
this._params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._dialog?.close();
|
this._open = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed() {
|
private _dialogClosed() {
|
||||||
if (this._opened) {
|
if (this._open) {
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
this._opened = false;
|
this._open = false;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._encryptionKey = "";
|
this._encryptionKey = "";
|
||||||
this._error = "";
|
this._error = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._opened || !this._params) {
|
if (!this._params) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog open @closed=${this._dialogClosed} disable-cancel-action>
|
<ha-wa-dialog
|
||||||
<ha-dialog-header slot="headline">
|
.hass=${this.hass}
|
||||||
<ha-icon-button
|
.open=${this._open}
|
||||||
slot="navigationIcon"
|
header-title=${this.hass.localize(
|
||||||
@click=${this.closeDialog}
|
"ui.panel.config.backup.dialogs.download.title"
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
)}
|
||||||
.path=${mdiClose}
|
prevent-scrim-close
|
||||||
></ha-icon-button>
|
@closed=${this._dialogClosed}
|
||||||
<span slot="title">
|
>
|
||||||
${this.hass.localize(
|
<p>
|
||||||
"ui.panel.config.backup.dialogs.download.title"
|
${this.hass.localize(
|
||||||
)}
|
"ui.panel.config.backup.dialogs.download.description"
|
||||||
</span>
|
)}
|
||||||
</ha-dialog-header>
|
</p>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download_backup_encrypted",
|
||||||
|
{
|
||||||
|
download_it_encrypted: html`<button
|
||||||
|
class="link"
|
||||||
|
@click=${this._downloadEncrypted}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download_it_encrypted"
|
||||||
|
)}
|
||||||
|
</button>`,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div slot="content">
|
<ha-password-field
|
||||||
<p>
|
.label=${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.panel.config.backup.dialogs.download.encryption_key"
|
||||||
"ui.panel.config.backup.dialogs.download.description"
|
)}
|
||||||
)}
|
@input=${this._keyChanged}
|
||||||
</p>
|
></ha-password-field>
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.download.download_backup_encrypted",
|
|
||||||
{
|
|
||||||
download_it_encrypted: html`<button
|
|
||||||
class="link"
|
|
||||||
@click=${this._downloadEncrypted}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.download.download_it_encrypted"
|
|
||||||
)}
|
|
||||||
</button>`,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ha-password-field
|
${this._error
|
||||||
.label=${this.hass.localize(
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
"ui.panel.config.backup.dialogs.download.encryption_key"
|
: nothing}
|
||||||
)}
|
<ha-dialog-footer slot="footer">
|
||||||
@input=${this._keyChanged}
|
<ha-button
|
||||||
></ha-password-field>
|
slot="secondaryAction"
|
||||||
|
appearance="plain"
|
||||||
${this._error
|
@click=${this._cancel}
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
>
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
<div slot="actions">
|
|
||||||
<ha-button appearance="plain" @click=${this._cancel}>
|
|
||||||
${this.hass.localize("ui.common.cancel")}
|
${this.hass.localize("ui.common.cancel")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
|
|
||||||
<ha-button @click=${this._submit}>
|
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.download.download"
|
"ui.panel.config.backup.dialogs.download.download"
|
||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</ha-dialog-footer>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,17 +179,8 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
|||||||
haStyle,
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-md-dialog {
|
ha-wa-dialog {
|
||||||
--dialog-content-padding: 8px 24px;
|
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
|
||||||
ha-md-dialog {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
div[slot="content"] {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.link {
|
button.link {
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
|
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { LitElement, css, html, nothing } 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 { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-dialog-footer";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import "../../../../components/ha-icon-button-prev";
|
import "../../../../components/ha-icon-button-prev";
|
||||||
import "../../../../components/ha-md-dialog";
|
import "../../../../components/ha-wa-dialog";
|
||||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
|
||||||
import "../../../../components/ha-md-list";
|
import "../../../../components/ha-md-list";
|
||||||
import "../../../../components/ha-md-list-item";
|
import "../../../../components/ha-md-list-item";
|
||||||
import "../../../../components/ha-password-field";
|
import "../../../../components/ha-password-field";
|
||||||
@@ -24,89 +23,83 @@ import type { ShowBackupEncryptionKeyDialogParams } from "./show-dialog-show-bac
|
|||||||
class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _params?: ShowBackupEncryptionKeyDialogParams;
|
@state() private _open = false;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog!: HaMdDialog;
|
@state() private _params?: ShowBackupEncryptionKeyDialogParams;
|
||||||
|
|
||||||
public showDialog(params: ShowBackupEncryptionKeyDialogParams): void {
|
public showDialog(params: ShowBackupEncryptionKeyDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
|
this._open = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
|
if (this._open) {
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
this._open = false;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _closeDialog() {
|
|
||||||
this._dialog.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
<ha-wa-dialog
|
||||||
<ha-dialog-header slot="headline">
|
.hass=${this.hass}
|
||||||
|
.open=${this._open}
|
||||||
|
header-title=${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.show_encryption_key.title"
|
||||||
|
)}
|
||||||
|
prevent-scrim-close
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="headerNavigationIcon"
|
||||||
|
data-dialog="close"
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.show_encryption_key.description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div class="encryption-key">
|
||||||
|
<p>${this._params?.currentKey}</p>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
.path=${mdiContentCopy}
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
@click=${this._copyKeyToClipboard}
|
||||||
.path=${mdiClose}
|
|
||||||
@click=${this._closeDialog}
|
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<span slot="title">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.show_encryption_key.title"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</ha-dialog-header>
|
|
||||||
<div slot="content">
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.show_encryption_key.description"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<div class="encryption-key">
|
|
||||||
<p>${this._params?.currentKey}</p>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiContentCopy}
|
|
||||||
@click=${this._copyKeyToClipboard}
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
<ha-md-list>
|
|
||||||
<ha-md-list-item>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="supporting-text">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<ha-button
|
|
||||||
size="small"
|
|
||||||
appearance="plain"
|
|
||||||
slot="end"
|
|
||||||
@click=${this._download}
|
|
||||||
>
|
|
||||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
</ha-md-list-item>
|
|
||||||
</ha-md-list>
|
|
||||||
</div>
|
</div>
|
||||||
<div slot="actions">
|
<ha-md-list>
|
||||||
<ha-button @click=${this._closeDialog}>
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-button slot="end" @click=${this._download}>
|
||||||
|
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>
|
||||||
|
<ha-dialog-footer slot="footer">
|
||||||
|
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||||
${this.hass.localize("ui.common.close")}
|
${this.hass.localize("ui.common.close")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</ha-dialog-footer>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,10 +128,8 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
haStyle,
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-md-dialog {
|
ha-wa-dialog {
|
||||||
width: 90vw;
|
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||||
max-width: 560px;
|
|
||||||
--dialog-content-padding: 8px 24px;
|
|
||||||
}
|
}
|
||||||
ha-md-list {
|
ha-md-list {
|
||||||
background: none;
|
background: none;
|
||||||
@@ -169,14 +160,7 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
flex: none;
|
flex: none;
|
||||||
margin: -16px;
|
margin: -16px;
|
||||||
}
|
}
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
|
||||||
ha-md-dialog {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
div[slot="content"] {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { mdiClose, mdiFolderUpload } from "@mdi/js";
|
import { mdiFolderUpload } from "@mdi/js";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { css, html, LitElement, nothing } 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 { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||||
import {
|
import {
|
||||||
fireEvent,
|
fireEvent,
|
||||||
type HASSDomEvent,
|
type HASSDomEvent,
|
||||||
} from "../../../../common/dom/fire_event";
|
} from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-expansion-panel";
|
import "../../../../components/ha-dialog-footer";
|
||||||
import "../../../../components/ha-file-upload";
|
import "../../../../components/ha-file-upload";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-wa-dialog";
|
||||||
import "../../../../components/ha-md-dialog";
|
|
||||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
|
||||||
import {
|
import {
|
||||||
CORE_LOCAL_AGENT,
|
CORE_LOCAL_AGENT,
|
||||||
HASSIO_LOCAL_AGENT,
|
HASSIO_LOCAL_AGENT,
|
||||||
@@ -43,11 +41,12 @@ export class DialogUploadBackup
|
|||||||
|
|
||||||
@state() private _formData?: BackupUploadFileFormData;
|
@state() private _formData?: BackupUploadFileFormData;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
@state() private _open = false;
|
||||||
|
|
||||||
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||||
|
this._open = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed() {
|
private _dialogClosed() {
|
||||||
@@ -56,11 +55,12 @@ export class DialogUploadBackup
|
|||||||
}
|
}
|
||||||
this._formData = undefined;
|
this._formData = undefined;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
|
this._open = false;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._dialog?.close();
|
this._open = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,52 +74,44 @@ export class DialogUploadBackup
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog
|
<ha-wa-dialog
|
||||||
open
|
.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}
|
@closed=${this._dialogClosed}
|
||||||
.disableCancelAction=${this._uploading}
|
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="headline">
|
${this._error
|
||||||
<ha-icon-button
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
slot="navigationIcon"
|
: nothing}
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
<ha-file-upload
|
||||||
.path=${mdiClose}
|
.hass=${this.hass}
|
||||||
@click=${this.closeDialog}
|
.uploading=${this._uploading}
|
||||||
.disabled=${this._uploading}
|
.icon=${mdiFolderUpload}
|
||||||
></ha-icon-button>
|
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||||
|
.localize=${this.hass.localize}
|
||||||
<span slot="title">
|
.label=${this.hass.localize(
|
||||||
${this.hass.localize("ui.panel.config.backup.dialogs.upload.title")}
|
"ui.panel.config.backup.dialogs.upload.input_label"
|
||||||
</span>
|
)}
|
||||||
</ha-dialog-header>
|
.supports=${this.hass.localize(
|
||||||
<div slot="content">
|
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
||||||
${this._error
|
)}
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
@file-picked=${this._filePicked}
|
||||||
: nothing}
|
@files-cleared=${this._filesCleared}
|
||||||
<ha-file-upload
|
></ha-file-upload>
|
||||||
.hass=${this.hass}
|
<ha-dialog-footer slot="footer">
|
||||||
.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">
|
|
||||||
<ha-button
|
<ha-button
|
||||||
|
slot="secondaryAction"
|
||||||
appearance="plain"
|
appearance="plain"
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
.disabled=${this._uploading}
|
.disabled=${this._uploading}
|
||||||
>${this.hass.localize("ui.common.cancel")}</ha-button
|
|
||||||
>
|
>
|
||||||
|
${this.hass.localize("ui.common.cancel")}
|
||||||
|
</ha-button>
|
||||||
<ha-button
|
<ha-button
|
||||||
|
slot="primaryAction"
|
||||||
@click=${this._upload}
|
@click=${this._upload}
|
||||||
.disabled=${!this._formValid() || this._uploading}
|
.disabled=${!this._formValid() || this._uploading}
|
||||||
>
|
>
|
||||||
@@ -127,8 +119,8 @@ export class DialogUploadBackup
|
|||||||
"ui.panel.config.backup.dialogs.upload.action"
|
"ui.panel.config.backup.dialogs.upload.action"
|
||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</ha-dialog-footer>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,15 +175,9 @@ export class DialogUploadBackup
|
|||||||
haStyle,
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-md-dialog {
|
|
||||||
max-width: 500px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
ha-alert {
|
ha-alert {
|
||||||
display: block;
|
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: category.icon || undefined,
|
||||||
icon_path: category.icon ? undefined : mdiTag,
|
icon_path: category.icon ? undefined : mdiTag,
|
||||||
sorting_label: category.name,
|
sorting_label: category.name,
|
||||||
search_labels: [category.name, category.category_id].filter(
|
|
||||||
(v): v is string => Boolean(v)
|
|
||||||
),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -203,6 +200,9 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.category-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
|
|||||||
import { stringCompare } from "../../../common/string/compare";
|
import { stringCompare } from "../../../common/string/compare";
|
||||||
import { slugify } from "../../../common/string/slugify";
|
import { slugify } from "../../../common/string/slugify";
|
||||||
import { groupBy } from "../../../common/util/group-by";
|
import { groupBy } from "../../../common/util/group-by";
|
||||||
|
import { getConfigurationUrl } from "../../../common/util/configuration-url";
|
||||||
import "../../../components/entity/ha-battery-icon";
|
import "../../../components/entity/ha-battery-icon";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
@@ -1090,17 +1091,12 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
|
|
||||||
const deviceActions: DeviceAction[] = [];
|
const deviceActions: DeviceAction[] = [];
|
||||||
|
|
||||||
const configurationUrlIsHomeAssistant =
|
const processedUrl = getConfigurationUrl(device.configuration_url);
|
||||||
device.configuration_url?.startsWith("homeassistant://") || false;
|
|
||||||
|
|
||||||
const configurationUrl = configurationUrlIsHomeAssistant
|
if (processedUrl) {
|
||||||
? device.configuration_url!.replace("homeassistant://", "/")
|
|
||||||
: device.configuration_url;
|
|
||||||
|
|
||||||
if (configurationUrl) {
|
|
||||||
deviceActions.push({
|
deviceActions.push({
|
||||||
href: configurationUrl,
|
href: processedUrl.url,
|
||||||
target: configurationUrlIsHomeAssistant ? undefined : "_blank",
|
target: processedUrl.isLocal ? undefined : "_blank",
|
||||||
icon: mdiCog,
|
icon: mdiCog,
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
"ui.panel.config.devices.open_configuration_url"
|
"ui.panel.config.devices.open_configuration_url"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
|||||||
import { storage } from "../../../common/decorators/storage";
|
import { storage } from "../../../common/decorators/storage";
|
||||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||||
|
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import type {
|
import type {
|
||||||
LocalizeFunc,
|
LocalizeFunc,
|
||||||
@@ -132,6 +133,7 @@ interface HelperItem {
|
|||||||
configEntry?: ConfigEntry;
|
configEntry?: ConfigEntry;
|
||||||
entity?: HassEntity;
|
entity?: HassEntity;
|
||||||
category: string | undefined;
|
category: string | undefined;
|
||||||
|
area?: string;
|
||||||
label_entries: LabelRegistryEntry[];
|
label_entries: LabelRegistryEntry[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -347,6 +349,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
|
area: {
|
||||||
|
title: localize("ui.panel.config.helpers.picker.headers.area"),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
groupable: true,
|
||||||
|
},
|
||||||
labels: {
|
labels: {
|
||||||
title: "",
|
title: "",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
@@ -565,6 +573,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
entityRegistryByEntityId(entityReg)[item.entity_id];
|
entityRegistryByEntityId(entityReg)[item.entity_id];
|
||||||
const labels = labelReg && entityRegEntry?.labels;
|
const labels = labelReg && entityRegEntry?.labels;
|
||||||
const category = entityRegEntry?.categories.helpers;
|
const category = entityRegEntry?.categories.helpers;
|
||||||
|
const areaId = entityRegEntry?.area_id;
|
||||||
|
const area =
|
||||||
|
areaId && this.hass.areas?.[areaId]
|
||||||
|
? computeAreaName(this.hass.areas[areaId])
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
localized_type:
|
localized_type:
|
||||||
@@ -579,6 +592,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
category: category
|
category: category
|
||||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||||
: undefined,
|
: undefined,
|
||||||
|
area,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
mdiDownload,
|
mdiDownload,
|
||||||
mdiHandExtendedOutline,
|
mdiHandExtendedOutline,
|
||||||
|
mdiOpenInNew,
|
||||||
mdiPlayCircleOutline,
|
mdiPlayCircleOutline,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiProgressHelper,
|
mdiProgressHelper,
|
||||||
@@ -73,6 +74,7 @@ import "./ha-config-entry-device-row";
|
|||||||
import { renderConfigEntryError } from "./ha-config-integration-page";
|
import { renderConfigEntryError } from "./ha-config-integration-page";
|
||||||
import "./ha-config-sub-entry-row";
|
import "./ha-config-sub-entry-row";
|
||||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||||
|
import { getConfigurationUrl } from "../../../common/util/configuration-url";
|
||||||
import { showToast } from "../../../util/toast";
|
import { showToast } from "../../../util/toast";
|
||||||
|
|
||||||
@customElement("ha-config-entry-row")
|
@customElement("ha-config-entry-row")
|
||||||
@@ -213,34 +215,53 @@ class HaConfigEntryRow extends LitElement {
|
|||||||
? html`<ha-button slot="end" @click=${this._handleEnable}>
|
? html`<ha-button slot="end" @click=${this._handleEnable}>
|
||||||
${this.hass.localize("ui.common.enable")}
|
${this.hass.localize("ui.common.enable")}
|
||||||
</ha-button>`
|
</ha-button>`
|
||||||
: configPanel &&
|
: (() => {
|
||||||
(item.domain !== "matter" ||
|
const processedUrl = getConfigurationUrl(item.configuration_url);
|
||||||
isDevVersion(this.hass.config.version)) &&
|
|
||||||
!stateText
|
return html`
|
||||||
? html`<a
|
${processedUrl
|
||||||
slot="end"
|
? html`<a
|
||||||
href=${`/${configPanel}?config_entry=${item.entry_id}`}
|
slot="end"
|
||||||
><ha-icon-button
|
href=${processedUrl.url}
|
||||||
.path=${mdiCogOutline}
|
target=${processedUrl.isLocal ? undefined : "_blank"}
|
||||||
.label=${this.hass.localize(
|
rel=${processedUrl.isLocal ? undefined : "noreferrer"}
|
||||||
"ui.panel.config.integrations.config_entry.configure"
|
>
|
||||||
)}
|
<ha-icon-button
|
||||||
>
|
.path=${mdiOpenInNew}
|
||||||
</ha-icon-button
|
.label=${processedUrl.url}
|
||||||
></a>`
|
></ha-icon-button>
|
||||||
: item.supports_options
|
</a>`
|
||||||
? html`
|
: nothing}
|
||||||
<ha-icon-button
|
${configPanel &&
|
||||||
slot="end"
|
(item.domain !== "matter" ||
|
||||||
@click=${this._showOptions}
|
isDevVersion(this.hass.config.version)) &&
|
||||||
.path=${mdiCogOutline}
|
!stateText
|
||||||
.label=${this.hass.localize(
|
? html`<a
|
||||||
"ui.panel.config.integrations.config_entry.configure"
|
slot="end"
|
||||||
)}
|
href=${`/${configPanel}?config_entry=${item.entry_id}`}
|
||||||
>
|
><ha-icon-button
|
||||||
</ha-icon-button>
|
.path=${mdiCogOutline}
|
||||||
`
|
.label=${this.hass.localize(
|
||||||
: nothing}
|
"ui.panel.config.integrations.config_entry.configure"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-icon-button
|
||||||
|
></a>`
|
||||||
|
: item.supports_options
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
slot="end"
|
||||||
|
@click=${this._showOptions}
|
||||||
|
.path=${mdiCogOutline}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.configure"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
})()}
|
||||||
<ha-md-button-menu positioning="popover" slot="end">
|
<ha-md-button-menu positioning="popover" slot="end">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { css, html, LitElement, nothing } 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 { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
import type { HaMdDialog } from "../../../components/ha-md-dialog";
|
import "../../../components/ha-dialog-footer";
|
||||||
import "../../../components/ha-md-dialog";
|
import "../../../components/ha-wa-dialog";
|
||||||
import "../../../components/ha-md-list";
|
import "../../../components/ha-md-list";
|
||||||
import "../../../components/ha-md-list-item";
|
import "../../../components/ha-md-list-item";
|
||||||
import type { HaSwitch } from "../../../components/ha-switch";
|
import type { HaSwitch } from "../../../components/ha-switch";
|
||||||
@@ -30,13 +30,14 @@ export class DialogLabsPreviewFeatureEnable
|
|||||||
|
|
||||||
@state() private _createBackup = false;
|
@state() private _createBackup = false;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
@state() private _open = false;
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: LabsPreviewFeatureEnableDialogParams
|
params: LabsPreviewFeatureEnableDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._createBackup = false;
|
this._createBackup = false;
|
||||||
|
this._open = true;
|
||||||
this._fetchBackupConfig();
|
this._fetchBackupConfig();
|
||||||
if (isComponentLoaded(this.hass, "hassio")) {
|
if (isComponentLoaded(this.hass, "hassio")) {
|
||||||
this._fetchUpdateBackupConfig();
|
this._fetchUpdateBackupConfig();
|
||||||
@@ -44,7 +45,7 @@ export class DialogLabsPreviewFeatureEnable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): boolean {
|
public closeDialog(): boolean {
|
||||||
this._dialog?.close();
|
this._open = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,72 +144,69 @@ export class DialogLabsPreviewFeatureEnable
|
|||||||
const createBackupTexts = this._computeCreateBackupTexts();
|
const createBackupTexts = this._computeCreateBackupTexts();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
<ha-wa-dialog
|
||||||
<span slot="headline">
|
.hass=${this.hass}
|
||||||
${this.hass.localize("ui.panel.config.labs.enable_title")}
|
.open=${this._open}
|
||||||
</span>
|
header-title=${this.hass.localize("ui.panel.config.labs.enable_title")}
|
||||||
<div slot="content">
|
@closed=${this._dialogClosed}
|
||||||
<p>
|
>
|
||||||
${this.hass.localize(
|
<p>
|
||||||
`component.${this._params.preview_feature.domain}.preview_features.${this._params.preview_feature.preview_feature}.enable_confirmation`
|
${this.hass.localize(
|
||||||
) || this.hass.localize("ui.panel.config.labs.enable_confirmation")}
|
`component.${this._params.preview_feature.domain}.preview_features.${this._params.preview_feature.preview_feature}.enable_confirmation`
|
||||||
</p>
|
) || this.hass.localize("ui.panel.config.labs.enable_confirmation")}
|
||||||
</div>
|
</p>
|
||||||
<div slot="actions">
|
${createBackupTexts
|
||||||
${createBackupTexts
|
? html`
|
||||||
? html`
|
<ha-md-list>
|
||||||
<ha-md-list>
|
<ha-md-list-item>
|
||||||
<ha-md-list-item>
|
<span slot="headline">${createBackupTexts.title}</span>
|
||||||
<span slot="headline">${createBackupTexts.title}</span>
|
${createBackupTexts.description
|
||||||
${createBackupTexts.description
|
? html`
|
||||||
? html`
|
<span slot="supporting-text">
|
||||||
<span slot="supporting-text">
|
${createBackupTexts.description}
|
||||||
${createBackupTexts.description}
|
</span>
|
||||||
</span>
|
`
|
||||||
`
|
: nothing}
|
||||||
: nothing}
|
<ha-switch
|
||||||
<ha-switch
|
slot="end"
|
||||||
slot="end"
|
.checked=${this._createBackup}
|
||||||
.checked=${this._createBackup}
|
@change=${this._createBackupChanged}
|
||||||
@change=${this._createBackupChanged}
|
></ha-switch>
|
||||||
></ha-switch>
|
</ha-md-list-item>
|
||||||
</ha-md-list-item>
|
</ha-md-list>
|
||||||
</ha-md-list>
|
`
|
||||||
`
|
: nothing}
|
||||||
: nothing}
|
<ha-dialog-footer slot="footer">
|
||||||
<div>
|
<ha-button
|
||||||
<ha-button appearance="plain" @click=${this._handleCancel}>
|
slot="secondaryAction"
|
||||||
${this.hass.localize("ui.common.cancel")}
|
appearance="plain"
|
||||||
</ha-button>
|
@click=${this._handleCancel}
|
||||||
<ha-button
|
>
|
||||||
appearance="filled"
|
${this.hass.localize("ui.common.cancel")}
|
||||||
variant="brand"
|
</ha-button>
|
||||||
@click=${this._handleConfirm}
|
<ha-button
|
||||||
>
|
slot="primaryAction"
|
||||||
${this.hass.localize("ui.panel.config.labs.enable")}
|
appearance="filled"
|
||||||
</ha-button>
|
variant="brand"
|
||||||
</div>
|
@click=${this._handleConfirm}
|
||||||
</div>
|
>
|
||||||
</ha-md-dialog>
|
${this.hass.localize("ui.panel.config.labs.enable")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog-footer>
|
||||||
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly styles = css`
|
static readonly styles = css`
|
||||||
ha-md-dialog {
|
ha-wa-dialog {
|
||||||
--dialog-content-padding: var(--ha-space-6);
|
--dialog-content-padding: var(--ha-space-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0 var(--ha-space-6) var(--ha-space-6);
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
div[slot="actions"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-md-list {
|
ha-md-list {
|
||||||
background: none;
|
background: none;
|
||||||
--md-list-item-leading-space: var(--ha-space-6);
|
--md-list-item-leading-space: var(--ha-space-6);
|
||||||
@@ -217,13 +215,6 @@ export class DialogLabsPreviewFeatureEnable
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
border-top: var(--ha-border-width-sm) solid var(--divider-color);
|
border-top: var(--ha-border-width-sm) solid var(--divider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
div[slot="actions"] > div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--ha-space-2);
|
|
||||||
padding: var(--ha-space-4) var(--ha-space-6);
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-md-dialog";
|
import "../../../components/ha-wa-dialog";
|
||||||
import "../../../components/ha-spinner";
|
import "../../../components/ha-spinner";
|
||||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
@@ -39,36 +39,36 @@ export class DialogLabsProgress
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog
|
<ha-wa-dialog
|
||||||
|
.hass=${this.hass}
|
||||||
.open=${this._open}
|
.open=${this._open}
|
||||||
disable-cancel-action
|
prevent-scrim-close
|
||||||
@closed=${this._handleClosed}
|
@closed=${this._handleClosed}
|
||||||
>
|
>
|
||||||
<div slot="content">
|
<div slot="header"></div>
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<ha-spinner></ha-spinner>
|
<ha-spinner></ha-spinner>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="heading">
|
<p class="heading">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.labs.progress.creating_backup"
|
"ui.panel.config.labs.progress.creating_backup"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
this._params.enabled
|
this._params.enabled
|
||||||
? "ui.panel.config.labs.progress.backing_up_before_enabling"
|
? "ui.panel.config.labs.progress.backing_up_before_enabling"
|
||||||
: "ui.panel.config.labs.progress.backing_up_before_disabling"
|
: "ui.panel.config.labs.progress.backing_up_before_disabling"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ha-md-dialog>
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly styles = css`
|
static readonly styles = css`
|
||||||
ha-md-dialog {
|
ha-wa-dialog {
|
||||||
--dialog-content-padding: var(--ha-space-6);
|
--dialog-content-padding: var(--ha-space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
|||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { withViewTransition } from "../../common/util/view-transition";
|
|
||||||
import "../../components/ha-button-menu";
|
import "../../components/ha-button-menu";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-list-item";
|
||||||
@@ -117,9 +116,7 @@ class PanelDeveloperTools extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPage !== this._page) {
|
if (newPage !== this._page) {
|
||||||
withViewTransition(() => {
|
navigate(`/developer-tools/${newPage}`);
|
||||||
navigate(`/developer-tools/${newPage}`);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
scrollTo({ behavior: "smooth", top: 0 });
|
scrollTo({ behavior: "smooth", top: 0 });
|
||||||
}
|
}
|
||||||
@@ -128,9 +125,7 @@ class PanelDeveloperTools extends LitElement {
|
|||||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||||
switch (ev.detail.index) {
|
switch (ev.detail.index) {
|
||||||
case 0:
|
case 0:
|
||||||
withViewTransition(() => {
|
navigate(`/developer-tools/debug`);
|
||||||
navigate(`/developer-tools/debug`);
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ import type { HUIView } from "./views/hui-view";
|
|||||||
import "./views/hui-view-background";
|
import "./views/hui-view-background";
|
||||||
import "./views/hui-view-container";
|
import "./views/hui-view-container";
|
||||||
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||||
import { withViewTransition } from "../../common/util/view-transition";
|
|
||||||
|
|
||||||
interface ActionItem {
|
interface ActionItem {
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -1246,9 +1245,7 @@ class HUIRoot extends LitElement {
|
|||||||
const viewIndex = Number(ev.detail.name);
|
const viewIndex = Number(ev.detail.name);
|
||||||
if (viewIndex !== this._curView) {
|
if (viewIndex !== this._curView) {
|
||||||
const path = this.config.views[viewIndex].path || viewIndex;
|
const path = this.config.views[viewIndex].path || viewIndex;
|
||||||
withViewTransition(() => {
|
this._navigateToView(path);
|
||||||
this._navigateToView(path);
|
|
||||||
});
|
|
||||||
} else if (!this._editMode) {
|
} else if (!this._editMode) {
|
||||||
scrollTo({ behavior: "smooth", top: 0 });
|
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",
|
"show_entities": "Show entities",
|
||||||
"new_entity": "Create a new entity",
|
"new_entity": "Create a new entity",
|
||||||
"placeholder": "Select an 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": {
|
"entity-name-picker": {
|
||||||
"types": {
|
"types": {
|
||||||
@@ -779,7 +780,8 @@
|
|||||||
"user-picker": {
|
"user-picker": {
|
||||||
"no_match": "No users found for {term}",
|
"no_match": "No users found for {term}",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"add_user": "Add user"
|
"add_user": "Add user",
|
||||||
|
"unknown": "Unknown user selected"
|
||||||
},
|
},
|
||||||
"blueprint-picker": {
|
"blueprint-picker": {
|
||||||
"select_blueprint": "Select a blueprint"
|
"select_blueprint": "Select a blueprint"
|
||||||
@@ -793,7 +795,8 @@
|
|||||||
"device": "Device",
|
"device": "Device",
|
||||||
"unnamed_device": "Unnamed device",
|
"unnamed_device": "Unnamed device",
|
||||||
"no_area": "No area",
|
"no_area": "No area",
|
||||||
"placeholder": "Select a device"
|
"placeholder": "Select a device",
|
||||||
|
"unknown": "Unknown device selected"
|
||||||
},
|
},
|
||||||
"category-picker": {
|
"category-picker": {
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -805,6 +808,7 @@
|
|||||||
"add_new": "Add new category…",
|
"add_new": "Add new category…",
|
||||||
"no_categories": "No categories available",
|
"no_categories": "No categories available",
|
||||||
"no_match": "No categories found for {term}",
|
"no_match": "No categories found for {term}",
|
||||||
|
"unknown": "Unknown category selected",
|
||||||
"add_dialog": {
|
"add_dialog": {
|
||||||
"title": "Add new category",
|
"title": "Add new category",
|
||||||
"text": "Enter the name of the new category.",
|
"text": "Enter the name of the new category.",
|
||||||
@@ -831,7 +835,8 @@
|
|||||||
"add_new": "Add new area…",
|
"add_new": "Add new area…",
|
||||||
"no_areas": "No areas available",
|
"no_areas": "No areas available",
|
||||||
"no_match": "No areas found for {term}",
|
"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": {
|
"floor-picker": {
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -841,7 +846,8 @@
|
|||||||
"add_new": "Add new floor…",
|
"add_new": "Add new floor…",
|
||||||
"no_floors": "No floors available",
|
"no_floors": "No floors available",
|
||||||
"no_match": "No floors found for {term}",
|
"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": {
|
"area-filter": {
|
||||||
"title": "Areas",
|
"title": "Areas",
|
||||||
@@ -858,7 +864,8 @@
|
|||||||
"no_match": "No statistics found for {term}",
|
"no_match": "No statistics found for {term}",
|
||||||
"no_state": "Entity without state",
|
"no_state": "Entity without state",
|
||||||
"missing_entity": "Why is my entity not listed?",
|
"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-picker": {
|
||||||
"addon": "Add-on",
|
"addon": "Add-on",
|
||||||
@@ -998,7 +1005,8 @@
|
|||||||
},
|
},
|
||||||
"service-picker": {
|
"service-picker": {
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"no_match": "No matching actions found"
|
"no_match": "No matching actions found",
|
||||||
|
"unknown": "Unknown action selected"
|
||||||
},
|
},
|
||||||
"service-control": {
|
"service-control": {
|
||||||
"required": "This field is required",
|
"required": "This field is required",
|
||||||
@@ -1296,7 +1304,8 @@
|
|||||||
},
|
},
|
||||||
"combo-box": {
|
"combo-box": {
|
||||||
"no_match": "No matching items found",
|
"no_match": "No matching items found",
|
||||||
"no_items": "No items available"
|
"no_items": "No items available",
|
||||||
|
"unknown_item": "Unknown item"
|
||||||
},
|
},
|
||||||
"suggest_with_ai": {
|
"suggest_with_ai": {
|
||||||
"label": "Suggest",
|
"label": "Suggest",
|
||||||
@@ -3295,7 +3304,8 @@
|
|||||||
"entity_id": "Entity ID",
|
"entity_id": "Entity ID",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"editable": "Editable",
|
"editable": "Editable",
|
||||||
"category": "Category"
|
"category": "Category",
|
||||||
|
"area": "Area"
|
||||||
},
|
},
|
||||||
"create_helper": "Create helper",
|
"create_helper": "Create helper",
|
||||||
"no_helpers": "Looks like you don't have any helpers yet!",
|
"no_helpers": "Looks like you don't have any helpers yet!",
|
||||||
|
|||||||
Reference in New Issue
Block a user