mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-22 16:22:17 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f01bb2bb7d |
@@ -1,5 +1,5 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
@@ -8,30 +8,14 @@ export interface RelatedIdSets {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of related IDs, merging in the current (queried) item.
|
||||
* `search/related` does not echo the queried item back, but it is the closest
|
||||
* related item (e.g. a card editor's own entity), so it is merged into the
|
||||
* matching group when it is an area, device, or entity.
|
||||
* Build a set of related IDs for a given related result.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @param current - The queried item to merge in.
|
||||
* @returns The related ID sets, including the current item.
|
||||
* @returns The related ID sets.
|
||||
*/
|
||||
export const buildRelatedIdSets = (
|
||||
related?: RelatedResult,
|
||||
current?: { itemType: ItemType; itemId: string }
|
||||
): RelatedIdSets => ({
|
||||
areas: new Set([
|
||||
...(related?.area || []),
|
||||
...(current?.itemType === "area" ? [current.itemId] : []),
|
||||
]),
|
||||
devices: new Set([
|
||||
...(related?.device || []),
|
||||
...(current?.itemType === "device" ? [current.itemId] : []),
|
||||
]),
|
||||
entities: new Set([
|
||||
...(related?.entity || []),
|
||||
...(current?.itemType === "entity" ? [current.itemId] : []),
|
||||
]),
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -7,14 +6,10 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { relatedContext } from "../../data/context";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
markEntitiesRelated,
|
||||
sortEntitiesByRelatedRank,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity/entity_picker";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -136,20 +131,6 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@state() private _pendingEntityId?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
private get _hasRelatedContext(): boolean {
|
||||
const related = this._relatedIdSets;
|
||||
return (
|
||||
!!related &&
|
||||
(related.entities.size > 0 ||
|
||||
related.devices.size > 0 ||
|
||||
related.areas.size > 0)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingEntityId &&
|
||||
@@ -343,22 +324,8 @@ export class HaEntityPicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _sortByRelatedContext = memoizeOne(
|
||||
(
|
||||
items: EntityComboBoxItem[],
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
language: string
|
||||
): EntityComboBoxItem[] =>
|
||||
sortEntitiesByRelatedRank(
|
||||
markEntitiesRelated(items, related, entities, devices),
|
||||
language
|
||||
)
|
||||
);
|
||||
|
||||
private _getItems = () => {
|
||||
const entityItems = this._getEntitiesMemoized(
|
||||
const items = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -369,23 +336,14 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeEntities,
|
||||
this.value
|
||||
);
|
||||
const sortedItems = this._hasRelatedContext
|
||||
? this._sortByRelatedContext(
|
||||
entityItems,
|
||||
this._relatedIdSets!,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.locale.language
|
||||
)
|
||||
: entityItems;
|
||||
if (this.extraOptions?.length) {
|
||||
const resolvedExtras = this.extraOptions.map((opt) => ({
|
||||
...opt,
|
||||
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
|
||||
}));
|
||||
return [...resolvedExtras, ...sortedItems];
|
||||
return [...resolvedExtras, ...items];
|
||||
}
|
||||
return sortedItems;
|
||||
return items;
|
||||
};
|
||||
|
||||
private _shouldHideClearIcon() {
|
||||
@@ -417,7 +375,6 @@ export class HaEntityPicker extends LitElement {
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.noSort=${this._hasRelatedContext}
|
||||
use-top-label
|
||||
.addButtonLabel=${this.addButton
|
||||
? (this.addButtonLabel ??
|
||||
@@ -436,23 +393,17 @@ export class HaEntityPicker extends LitElement {
|
||||
search,
|
||||
filteredItems
|
||||
) => {
|
||||
// Float related items to the top by closeness, keeping search relevance
|
||||
// order within each tier.
|
||||
const items = this._hasRelatedContext
|
||||
? sortEntitiesByRelatedRank(filteredItems)
|
||||
: filteredItems;
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = items.findIndex(
|
||||
const index = filteredItems.findIndex(
|
||||
(item) => item.stateObj?.entity_id === search
|
||||
);
|
||||
if (index === -1) {
|
||||
return items;
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = items.splice(index, 1);
|
||||
items.unshift(exactMatch);
|
||||
return items;
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
public async open() {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -14,8 +13,6 @@ import "./ha-list";
|
||||
|
||||
@customElement("ha-filter-states")
|
||||
export class HaFilterStates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -4,7 +4,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-labeled-slider";
|
||||
import { generateColorTemperatureGradient } from "../../dialogs/more-info/components/lights/light-color-temp-picker";
|
||||
import {
|
||||
@@ -15,8 +14,6 @@ import {
|
||||
|
||||
@customElement("ha-selector-color_temp")
|
||||
export class HaColorTempSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ColorTempSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@@ -84,7 +84,6 @@ export class HaLocationSelector extends LitElement {
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.locations=${this._location(this.selector, this.value)}
|
||||
@location-updated=${this._locationChanged}
|
||||
|
||||
@@ -3,15 +3,13 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { StringSelector } from "../../data/selector";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "../ha-textarea";
|
||||
import "../input/ha-input";
|
||||
import "../input/ha-input-multi";
|
||||
|
||||
@customElement("ha-selector-text")
|
||||
export class HaTextSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import type { ThemeMode } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
@@ -45,8 +45,6 @@ export interface MarkerLocation {
|
||||
|
||||
@customElement("ha-locations-editor")
|
||||
export class HaLocationsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public locations?: MarkerLocation[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@@ -7,8 +7,8 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaProgressRing extends ProgressRing {
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
public updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("size")) {
|
||||
switch (this.size) {
|
||||
|
||||
@@ -44,7 +44,6 @@ import type {
|
||||
IfActionTraceStep,
|
||||
TraceExtended,
|
||||
} from "../../data/trace";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-service-icon";
|
||||
import "./hat-graph-branch";
|
||||
@@ -76,8 +75,6 @@ export class HatScriptGraph extends LitElement {
|
||||
@query("hat-graph-node[active], hat-graph-branch[active]")
|
||||
private _activeNode?: HTMLElement;
|
||||
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
public renderedNodes: Record<string, NodeInfo> = {};
|
||||
|
||||
public trackedNodes: Record<string, NodeInfo> = {};
|
||||
@@ -457,7 +454,6 @@ export class HatScriptGraph extends LitElement {
|
||||
${node.action
|
||||
? html`<ha-service-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.service=${node.action}
|
||||
></ha-service-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
@@ -196,33 +196,4 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the related context to an entity (or clear it when no entity), so nearby
|
||||
* pickers float relevant entities.
|
||||
* @param node - The node to fire the event on.
|
||||
* @param context - The context to set, or undefined to clear.
|
||||
*/
|
||||
export const fireRelatedContext = (
|
||||
node: HTMLElement,
|
||||
context: RelatedContextItem | undefined
|
||||
): void => {
|
||||
fireEvent(node, "hass-related-context", context);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the related context to an entity (or clear it when no entity), so nearby
|
||||
* pickers float relevant entities. Fired by editors.
|
||||
* @param node - The node to fire the event on.
|
||||
* @param entityId - The entity to set, or undefined to clear.
|
||||
*/
|
||||
export const fireEntityRelatedContext = (
|
||||
node: HTMLElement,
|
||||
entityId: string | undefined
|
||||
): void => {
|
||||
fireRelatedContext(
|
||||
node,
|
||||
entityId ? { itemType: "entity", itemId: entityId } : undefined
|
||||
);
|
||||
};
|
||||
|
||||
// #endregion related-context
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { getEntityAreaId } from "../../common/entity/context/get_entity_context";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
@@ -15,7 +12,6 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
export interface EntityComboBoxItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
stateObj?: HassEntity;
|
||||
relatedRank?: number;
|
||||
}
|
||||
|
||||
export const entityComboBoxKeys: FuseWeightedKey[] = [
|
||||
@@ -190,72 +186,3 @@ export const getEntities = (
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const RELATED_RANK_UNRELATED = 3;
|
||||
|
||||
const entityRelatedRank = (
|
||||
entityId: string | undefined,
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): number => {
|
||||
if (!entityId) {
|
||||
return RELATED_RANK_UNRELATED;
|
||||
}
|
||||
if (related.entities.has(entityId)) {
|
||||
return 0;
|
||||
}
|
||||
const deviceId = entities[entityId]?.device_id;
|
||||
if (deviceId && related.devices.has(deviceId)) {
|
||||
return 1;
|
||||
}
|
||||
const areaId = getEntityAreaId(entityId, entities, devices);
|
||||
if (areaId && related.areas.has(areaId)) {
|
||||
return 2;
|
||||
}
|
||||
return RELATED_RANK_UNRELATED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Annotate entity items with their closeness to the related context, so they
|
||||
* can be floated to the top. The entity itself ranks closest, then its device,
|
||||
* then its area; anything unrelated keeps the lowest rank.
|
||||
*/
|
||||
export const markEntitiesRelated = (
|
||||
items: EntityComboBoxItem[],
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): EntityComboBoxItem[] =>
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
relatedRank: entityRelatedRank(
|
||||
item.stateObj?.entity_id,
|
||||
related,
|
||||
entities,
|
||||
devices
|
||||
),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Sort entity items by related closeness (entity, then device, then area, then
|
||||
* the rest). Pass `language` to break ties within a tier alphabetically by
|
||||
* label; omit it to keep the incoming order (e.g. search relevance).
|
||||
*/
|
||||
export const sortEntitiesByRelatedRank = (
|
||||
items: EntityComboBoxItem[],
|
||||
language?: string
|
||||
): EntityComboBoxItem[] =>
|
||||
[...items].sort((a, b) => {
|
||||
const rankDiff =
|
||||
(a.relatedRank ?? RELATED_RANK_UNRELATED) -
|
||||
(b.relatedRank ?? RELATED_RANK_UNRELATED);
|
||||
if (rankDiff !== 0 || language === undefined) {
|
||||
return rankDiff;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.sorting_label ?? "",
|
||||
b.sorting_label ?? "",
|
||||
language
|
||||
);
|
||||
});
|
||||
|
||||
@@ -176,7 +176,6 @@ class OnboardingLocation extends LitElement {
|
||||
</div>
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocations(
|
||||
this._location,
|
||||
this._places,
|
||||
|
||||
@@ -228,7 +228,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
<div class="main">
|
||||
<div class="graph">
|
||||
<hat-script-graph
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
|
||||
@@ -508,7 +508,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
|
||||
.value=${this._filters[TYPE_FILTER]}
|
||||
.states=${this._states(this.hass.localize, isHassio)}
|
||||
@@ -517,7 +516,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
></ha-filter-states>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.panel.config.backup.locations")}
|
||||
.value=${this._filters[LOCATIONS_FILTER]}
|
||||
.states=${this._locations(
|
||||
|
||||
@@ -2,7 +2,6 @@ import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isNavigationClick } from "../../../common/dom/is-navigation-click";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -92,11 +91,7 @@ class PanelDeveloperTools extends LitElement {
|
||||
panel=${tab.panel}
|
||||
.active=${page === tab.panel}
|
||||
>
|
||||
<a
|
||||
href="/config/developer-tools/${tab.panel}"
|
||||
@click=${this._handleTabAnchorClick}
|
||||
>${this.hass.localize(tab.translationKey)}</a
|
||||
>
|
||||
${this.hass.localize(tab.translationKey)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
@@ -110,14 +105,6 @@ class PanelDeveloperTools extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTabAnchorClick(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(href);
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePageSelected(ev: CustomEvent<{ name: string }>) {
|
||||
const newPage = ev.detail.name;
|
||||
if (!newPage) {
|
||||
@@ -155,16 +142,6 @@ class PanelDeveloperTools extends LitElement {
|
||||
--ha-tab-indicator-color: var(--app-header-text-color, white);
|
||||
--ha-tab-track-color: transparent;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
ha-tab-group-tab a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1em 1.5em;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -859,7 +859,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-integrations>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-states"]?.value}
|
||||
.states=${this._states(this.hass.localize)}
|
||||
.label=${this.hass.localize("ui.panel.config.devices.picker.state")}
|
||||
|
||||
@@ -1019,7 +1019,6 @@ export class HaConfigEntities extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-integrations>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.headers.status"
|
||||
)}
|
||||
|
||||
@@ -210,7 +210,6 @@ export class HaScriptTrace extends LitElement {
|
||||
<div class="main">
|
||||
<div class="graph">
|
||||
<hat-script-graph
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
|
||||
@@ -255,7 +255,6 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
? html`
|
||||
<div class="flex">
|
||||
<ha-locations-editor
|
||||
.hass=${this.hass}
|
||||
.locations=${this._getZones(
|
||||
this._storageItems,
|
||||
this._stateItems
|
||||
|
||||
@@ -49,33 +49,6 @@ export const computeShowHeaderToggle = <
|
||||
return !!config.show_header_toggle;
|
||||
};
|
||||
|
||||
export const migrateEntitiesCardConfig = (
|
||||
config: EntitiesCardConfig
|
||||
): EntitiesCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: (rest as EntityConfig).time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (LovelaceRowConfig | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("hui-entities-card")
|
||||
class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -180,12 +153,11 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
throw new Error("Entities must be specified");
|
||||
}
|
||||
|
||||
const migratedConfig = migrateEntitiesCardConfig(config);
|
||||
const entities = processConfigEntities(migratedConfig.entities);
|
||||
const entities = processConfigEntities(config.entities);
|
||||
|
||||
this._config = migratedConfig;
|
||||
this._config = config;
|
||||
this._configEntities = entities;
|
||||
this._showHeaderToggle = computeShowHeaderToggle(migratedConfig, entities);
|
||||
this._showHeaderToggle = computeShowHeaderToggle(config, entities);
|
||||
if (this._config.header) {
|
||||
this._headerElement = createHeaderFooterElement(
|
||||
this._config.header
|
||||
|
||||
@@ -32,34 +32,6 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import "../components/hui-warning-element";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { GlanceCardConfig, GlanceConfigEntity } from "./types";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../common/const";
|
||||
|
||||
export const migrateGlanceCardConfig = (
|
||||
config: GlanceCardConfig
|
||||
): GlanceCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: rest.time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (GlanceConfigEntity | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("hui-glance-card")
|
||||
export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
@@ -106,15 +78,14 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public setConfig(config: GlanceCardConfig): void {
|
||||
const migratedConfig = migrateGlanceCardConfig(config);
|
||||
this._config = {
|
||||
show_name: true,
|
||||
show_state: true,
|
||||
show_icon: true,
|
||||
state_color: true,
|
||||
...migratedConfig,
|
||||
...config,
|
||||
};
|
||||
const entities = processConfigEntities(migratedConfig.entities).map(
|
||||
const entities = processConfigEntities(config.entities).map(
|
||||
(entityConf) => ({
|
||||
hold_action: { action: "more-info" } as MoreInfoActionConfig,
|
||||
...entityConf,
|
||||
@@ -136,8 +107,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
const columns =
|
||||
migratedConfig.columns || Math.min(migratedConfig.entities.length, 5);
|
||||
const columns = config.columns || Math.min(config.entities.length, 5);
|
||||
this.style.setProperty("--glance-column-width", `${100 / columns}%`);
|
||||
|
||||
this._configEntities = entities;
|
||||
@@ -286,7 +256,6 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
const name = this.hass!.formatEntityName(stateObj, entityConf.name);
|
||||
const domain = computeDomain(entityConf.entity);
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -320,18 +289,17 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
${this._config!.show_state && entityConf.show_state !== false
|
||||
? html`
|
||||
<div>
|
||||
${(TIMESTAMP_STATE_DOMAINS.has(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
stateObj.attributes.device_class
|
||||
))) &&
|
||||
${computeDomain(entityConf.entity) === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
stateObj.attributes.device_class
|
||||
) &&
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.state !== UNKNOWN
|
||||
? html`
|
||||
<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${new Date(stateObj.state)}
|
||||
.format=${entityConf.time_format ??
|
||||
.format=${entityConf.format ??
|
||||
(stateObj.attributes.device_class ===
|
||||
SENSOR_DEVICE_CLASS_UPTIME
|
||||
? "total"
|
||||
|
||||
@@ -334,7 +334,7 @@ export interface GlanceConfigEntity extends ConfigEntity {
|
||||
image?: string;
|
||||
show_state?: boolean;
|
||||
state_color?: boolean;
|
||||
time_format?: TimestampRenderingFormat;
|
||||
format?: TimestampRenderingFormat;
|
||||
}
|
||||
|
||||
export interface GlanceCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { RelatedContextItem } from "../../../data/context";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { getConfigEntityId } from "./get-config-entity-id";
|
||||
|
||||
export const getConfigRelatedContext = (
|
||||
config: LovelaceCardConfig
|
||||
): RelatedContextItem | undefined => {
|
||||
if (config.type === "area" && typeof config.area === "string") {
|
||||
return { itemType: "area", itemId: config.area };
|
||||
}
|
||||
const entityId = getConfigEntityId(config);
|
||||
return entityId ? { itemType: "entity", itemId: entityId } : undefined;
|
||||
};
|
||||
@@ -60,7 +60,7 @@ const LAZY_LOAD_TYPES = {
|
||||
attribute: () => import("../special-rows/hui-attribute-row"),
|
||||
text: () => import("../special-rows/hui-text-row"),
|
||||
};
|
||||
export const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
_domain_not_found: "simple",
|
||||
alert: "toggle",
|
||||
automation: "toggle",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEntityRelatedContext } from "../../../../data/context";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import { stripDefaults } from "../../../../common/util/strip-defaults";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
@@ -32,7 +31,6 @@ import {
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../badges/hui-badge";
|
||||
import { getConfigEntityId } from "../../common/get-config-entity-id";
|
||||
import "../../sections/hui-section";
|
||||
import { addBadge, replaceBadge } from "../config-util";
|
||||
import { getBadgeDefaultConfig } from "../get-badge-default-config";
|
||||
@@ -141,38 +139,25 @@ export class HuiDialogEditBadge
|
||||
this._badgeConfig = undefined;
|
||||
this._error = undefined;
|
||||
this._documentationURL = undefined;
|
||||
this._updateRelatedContext(undefined);
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (!changedProps.has("_badgeConfig")) {
|
||||
if (
|
||||
!this._badgeConfig ||
|
||||
this._documentationURL !== undefined ||
|
||||
!changedProps.has("_badgeConfig")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._badgeConfig && this._documentationURL === undefined) {
|
||||
const oldConfig = changedProps.get("_badgeConfig") as LovelaceBadgeConfig;
|
||||
const oldConfig = changedProps.get("_badgeConfig") as LovelaceBadgeConfig;
|
||||
|
||||
if (oldConfig?.type !== this._badgeConfig.type) {
|
||||
this._documentationURL = this._badgeConfig.type
|
||||
? getBadgeDocumentationURL(this.hass, this._badgeConfig.type)
|
||||
: undefined;
|
||||
}
|
||||
if (oldConfig?.type !== this._badgeConfig!.type) {
|
||||
this._documentationURL = this._badgeConfig!.type
|
||||
? getBadgeDocumentationURL(this.hass, this._badgeConfig!.type)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
this._updateRelatedContext(
|
||||
this._badgeConfig ? getConfigEntityId(this._badgeConfig) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
private _relatedEntityId?: string;
|
||||
|
||||
private _updateRelatedContext(entityId: string | undefined): void {
|
||||
if (entityId === this._relatedEntityId) {
|
||||
return;
|
||||
}
|
||||
this._relatedEntityId = entityId;
|
||||
fireEntityRelatedContext(this, entityId);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -6,10 +6,6 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import {
|
||||
fireRelatedContext,
|
||||
type RelatedContextItem,
|
||||
} from "../../../../data/context";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import { stripDefaults } from "../../../../common/util/strip-defaults";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
@@ -37,7 +33,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import { getConfigRelatedContext } from "../../common/get-config-related-context";
|
||||
import "../../sections/hui-section";
|
||||
import { getCardDefaultConfig } from "../get-card-default-config";
|
||||
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
@@ -130,7 +125,6 @@ export class HuiDialogEditCard
|
||||
this._cardConfig = undefined;
|
||||
this._error = undefined;
|
||||
this._documentationURL = undefined;
|
||||
this._updateRelatedContext(undefined);
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -147,21 +141,6 @@ export class HuiDialogEditCard
|
||||
this._cardConfig!.type
|
||||
);
|
||||
}
|
||||
|
||||
this._updateRelatedContext(getConfigRelatedContext(this._cardConfig));
|
||||
}
|
||||
|
||||
private _relatedContext?: RelatedContextItem;
|
||||
|
||||
private _updateRelatedContext(context: RelatedContextItem | undefined): void {
|
||||
if (
|
||||
context?.itemType === this._relatedContext?.itemType &&
|
||||
context?.itemId === this._relatedContext?.itemId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._relatedContext = context;
|
||||
fireRelatedContext(this, context);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -29,10 +29,7 @@ import "../../../../components/ha-theme-picker";
|
||||
import "../../../../components/input/ha-input";
|
||||
import { isCustomType } from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
computeShowHeaderToggle,
|
||||
migrateEntitiesCardConfig,
|
||||
} from "../../cards/hui-entities-card";
|
||||
import { computeShowHeaderToggle } from "../../cards/hui-entities-card";
|
||||
import type { EntitiesCardConfig } from "../../cards/types";
|
||||
import { processConfigEntities } from "../../common/process-config-entities";
|
||||
import { timeFormatConfigStruct } from "../../components/types";
|
||||
@@ -122,7 +119,7 @@ const attributeEntitiesRowConfigStruct = object({
|
||||
suffix: optional(string()),
|
||||
name: optional(string()),
|
||||
icon: optional(string()),
|
||||
time_format: optional(timeFormatConfigStruct),
|
||||
format: optional(timeFormatConfigStruct),
|
||||
});
|
||||
|
||||
const textEntitiesRowConfigStruct = object({
|
||||
@@ -210,10 +207,9 @@ export class HuiEntitiesCardEditor
|
||||
@state() private _subElementEditorConfig?: SubElementEditorConfig;
|
||||
|
||||
public setConfig(config: EntitiesCardConfig): void {
|
||||
const migratedConfig = migrateEntitiesCardConfig(config);
|
||||
assert(migratedConfig, cardConfigStruct);
|
||||
this._config = migratedConfig;
|
||||
this._configEntities = processEditorEntities(migratedConfig.entities);
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = processEditorEntities(config.entities);
|
||||
}
|
||||
|
||||
private _showHeaderToggle = memoizeOne((config: EntitiesCardConfig) => {
|
||||
|
||||
@@ -15,8 +15,6 @@ import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../../../data/sensor";
|
||||
import type { EntitiesCardEntityConfig } from "../../cards/types";
|
||||
import type { LovelaceRowEditor } from "../../types";
|
||||
import { entitiesConfigStruct } from "../structs/entities-struct";
|
||||
import { DOMAIN_TO_ELEMENT_TYPE } from "../../create-element/create-row-element";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../../common/const";
|
||||
|
||||
const SECONDARY_INFO_VALUES = {
|
||||
none: {},
|
||||
@@ -90,7 +88,7 @@ export class HuiGenericEntityRowEditor
|
||||
...(showTimeFormat
|
||||
? ([
|
||||
{
|
||||
name: "time_format",
|
||||
name: "format",
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
@@ -107,17 +105,13 @@ export class HuiGenericEntityRowEditor
|
||||
}
|
||||
|
||||
const entity = this._config.entity;
|
||||
const domain = entity ? computeDomain(entity) : "";
|
||||
const simpleEntity =
|
||||
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
|
||||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
|
||||
const showTimeFormat = simpleEntity
|
||||
? TIMESTAMP_STATE_DOMAINS.has(domain)
|
||||
: domain === "event" ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
));
|
||||
const domain = entity ? computeDomain(entity) : undefined;
|
||||
const showTimeFormat =
|
||||
domain === "event" ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
));
|
||||
|
||||
const schema =
|
||||
this.schema ||
|
||||
@@ -146,6 +140,10 @@ export class HuiGenericEntityRowEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.entity-row.${schema.name}`
|
||||
);
|
||||
case "format":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.time_format`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
|
||||
@@ -11,17 +11,11 @@ import {
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
SchemaUnion,
|
||||
HaFormSchema,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { migrateGlanceCardConfig } from "../../cards/hui-glance-card";
|
||||
import type { ConfigEntity, GlanceCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-entity-editor";
|
||||
import type { EntityConfig } from "../../entity-rows/types";
|
||||
@@ -32,8 +26,6 @@ import { processEditorEntities } from "../process-editor-entities";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { entitiesConfigStruct } from "../structs/entities-struct";
|
||||
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../../common/const";
|
||||
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../../../data/sensor";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@@ -49,6 +41,59 @@ const cardConfigStruct = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const SUB_FORM = {
|
||||
schema: [
|
||||
{ name: "entity", selector: { entity: {} }, required: true },
|
||||
{
|
||||
name: "name",
|
||||
selector: { entity_name: {} },
|
||||
context: {
|
||||
entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{ name: "show_last_changed", selector: { boolean: {} } },
|
||||
{ name: "show_state", selector: { boolean: {} }, default: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map((action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})),
|
||||
},
|
||||
] as const,
|
||||
};
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
@@ -86,97 +131,22 @@ export class HuiGlanceCardEditor
|
||||
@state() private _configEntities?: ConfigEntity[];
|
||||
|
||||
public setConfig(config: GlanceCardConfig): void {
|
||||
const migratedConfig = migrateGlanceCardConfig(config);
|
||||
assert(migratedConfig, cardConfigStruct);
|
||||
this._config = migratedConfig;
|
||||
this._configEntities = processEditorEntities(migratedConfig.entities);
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._configEntities = processEditorEntities(config.entities);
|
||||
}
|
||||
|
||||
private _subForm = memoizeOne((showTimeFormat?: boolean) => ({
|
||||
schema: [
|
||||
{ name: "entity", selector: { entity: {} }, required: true },
|
||||
{
|
||||
name: "name",
|
||||
selector: { entity_name: {} },
|
||||
context: {
|
||||
entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{ name: "show_last_changed", selector: { boolean: {} } },
|
||||
{ name: "show_state", selector: { boolean: {} }, default: true },
|
||||
],
|
||||
},
|
||||
...(showTimeFormat
|
||||
? ([
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map(
|
||||
(action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})
|
||||
),
|
||||
},
|
||||
] as const,
|
||||
}));
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._subElementEditorConfig) {
|
||||
const entity =
|
||||
this._configEntities![this._subElementEditorConfig.index!]?.entity;
|
||||
const domain = entity ? computeDomain(entity) : "";
|
||||
const showTimeFormat =
|
||||
TIMESTAMP_STATE_DOMAINS.has(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
|
||||
this.hass.states[entity]?.attributes.device_class
|
||||
));
|
||||
return html`
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._subElementEditorConfig}
|
||||
.form=${this._subForm(showTimeFormat)}
|
||||
.form=${SUB_FORM}
|
||||
@go-back=${this._goBack}
|
||||
@config-changed=${this._handleSubEntityChanged}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ export const entitiesConfigStruct = union([
|
||||
icon: optional(string()),
|
||||
image: optional(string()),
|
||||
secondary_info: optional(string()),
|
||||
time_format: optional(timeFormatConfigStruct),
|
||||
format: optional(timeFormatConfigStruct),
|
||||
state_color: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
|
||||
@@ -73,7 +73,7 @@ class HuiEventEntityRow extends LitElement implements LovelaceRow {
|
||||
: html`<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${new Date(stateObj.state)}
|
||||
.format=${this._config.time_format}
|
||||
.format=${this._config.format}
|
||||
capitalize
|
||||
></hui-timestamp-display>`}
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,12 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../components/hui-timestamp-display";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { TimestampRenderingFormat } from "../components/types";
|
||||
import type { LovelaceRow } from "./types";
|
||||
|
||||
interface SensorEntityConfig extends EntitiesCardEntityConfig {}
|
||||
interface SensorEntityConfig extends EntitiesCardEntityConfig {
|
||||
format?: TimestampRenderingFormat;
|
||||
}
|
||||
|
||||
@customElement("hui-sensor-entity-row")
|
||||
class HuiSensorEntityRow extends LitElement implements LovelaceRow {
|
||||
@@ -59,7 +62,7 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow {
|
||||
<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${new Date(stateObj.state)}
|
||||
.format=${this._config.time_format ??
|
||||
.format=${this._config.format ??
|
||||
(stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_UPTIME
|
||||
? "total"
|
||||
: undefined)}
|
||||
|
||||
@@ -5,12 +5,8 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import "../components/hui-timestamp-display";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { LovelaceRow } from "./types";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../common/const";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
|
||||
@customElement("hui-simple-entity-row")
|
||||
class HuiSimpleEntityRow extends LitElement implements LovelaceRow {
|
||||
@@ -46,18 +42,7 @@ class HuiSimpleEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
${TIMESTAMP_STATE_DOMAINS.has(computeDomain(this._config.entity)) &&
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.state !== UNKNOWN
|
||||
? html`
|
||||
<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${new Date(stateObj.state)}
|
||||
.format=${this._config.time_format}
|
||||
capitalize
|
||||
></hui-timestamp-display>
|
||||
`
|
||||
: this.hass.formatEntityState(stateObj)}
|
||||
${this.hass.formatEntityState(stateObj)}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface EntityConfig {
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
icon?: string;
|
||||
image?: string;
|
||||
time_format?: TimestampRenderingFormat;
|
||||
}
|
||||
|
||||
export interface ConfirmableRowConfig extends EntityConfig {
|
||||
@@ -108,4 +107,5 @@ export interface AttributeRowConfig extends EntityConfig {
|
||||
attribute: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
format?: TimestampRenderingFormat;
|
||||
}
|
||||
|
||||
@@ -50,18 +50,18 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
|
||||
|
||||
const attribute = stateObj.attributes[this._config.attribute];
|
||||
let date: Date | undefined;
|
||||
if (this._config.time_format) {
|
||||
if (this._config.format) {
|
||||
date = new Date(attribute);
|
||||
}
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
${this._config.prefix}
|
||||
${this._config.time_format && checkValidDate(date)
|
||||
${this._config.format && checkValidDate(date)
|
||||
? html` <hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${date}
|
||||
.format=${this._config.time_format}
|
||||
.format=${this._config.format}
|
||||
capitalize
|
||||
></hui-timestamp-display>`
|
||||
: attribute !== undefined
|
||||
|
||||
@@ -78,12 +78,9 @@ export class RelatedContextProvider {
|
||||
private _onRelatedContext = (
|
||||
ev: HASSDomEvent<RelatedContextItem | undefined>
|
||||
): void => {
|
||||
// `fireEvent` coerces an undefined detail to `{}`, so a clear arrives
|
||||
// without an itemId; normalise that back to undefined.
|
||||
const context = ev.detail?.itemId ? ev.detail : undefined;
|
||||
this._relatedContext = context;
|
||||
this._contextPathname = context ? mainWindow.location.pathname : undefined;
|
||||
this._resolveRelatedContext(context);
|
||||
this._relatedContext = ev.detail;
|
||||
this._contextPathname = mainWindow.location.pathname;
|
||||
this._resolveRelatedContext(this._relatedContext);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -121,7 +118,7 @@ export class RelatedContextProvider {
|
||||
context.itemId
|
||||
);
|
||||
if (this._contextMatches(context)) {
|
||||
this._setValue(buildRelatedIdSets(related, context));
|
||||
this._setValue(buildRelatedIdSets(related));
|
||||
}
|
||||
} catch (_err) {
|
||||
if (this._contextMatches(context)) {
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildRelatedIdSets,
|
||||
sortRelatedFirst,
|
||||
} from "../../../src/common/search/related-context";
|
||||
import type { RelatedIdSets } from "../../../src/common/search/related-context";
|
||||
import type { PickerComboBoxItem } from "../../../src/components/ha-picker-combo-box";
|
||||
import type { RelatedResult } from "../../../src/data/search";
|
||||
|
||||
const toArrays = (value: RelatedIdSets) => ({
|
||||
entities: [...value.entities].sort(),
|
||||
devices: [...value.devices].sort(),
|
||||
areas: [...value.areas].sort(),
|
||||
});
|
||||
|
||||
const item = (id: string, isRelated?: boolean): PickerComboBoxItem => ({
|
||||
id,
|
||||
primary: id,
|
||||
...(isRelated === undefined ? {} : { isRelated }),
|
||||
});
|
||||
|
||||
const orderOf = (items: PickerComboBoxItem[]) => items.map((i) => i.id);
|
||||
|
||||
describe("buildRelatedIdSets", () => {
|
||||
it("builds empty sets with no arguments", () => {
|
||||
expect(toArrays(buildRelatedIdSets())).toEqual({
|
||||
entities: [],
|
||||
devices: [],
|
||||
areas: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("builds sets from a related result", () => {
|
||||
const related: RelatedResult = {
|
||||
entity: ["light.kitchen"],
|
||||
device: ["dev1"],
|
||||
area: ["area1"],
|
||||
};
|
||||
expect(toArrays(buildRelatedIdSets(related))).toEqual({
|
||||
entities: ["light.kitchen"],
|
||||
devices: ["dev1"],
|
||||
areas: ["area1"],
|
||||
});
|
||||
});
|
||||
|
||||
it("merges a current entity into the entities group", () => {
|
||||
const related: RelatedResult = { device: ["dev1"], area: ["area1"] };
|
||||
const result = buildRelatedIdSets(related, {
|
||||
itemType: "entity",
|
||||
itemId: "light.ac",
|
||||
});
|
||||
expect(toArrays(result)).toEqual({
|
||||
entities: ["light.ac"],
|
||||
devices: ["dev1"],
|
||||
areas: ["area1"],
|
||||
});
|
||||
});
|
||||
|
||||
it("merges a current device into the devices group", () => {
|
||||
const result = buildRelatedIdSets(undefined, {
|
||||
itemType: "device",
|
||||
itemId: "dev1",
|
||||
});
|
||||
expect([...result.devices]).toEqual(["dev1"]);
|
||||
});
|
||||
|
||||
it("merges a current area into the areas group", () => {
|
||||
const result = buildRelatedIdSets(undefined, {
|
||||
itemType: "area",
|
||||
itemId: "area1",
|
||||
});
|
||||
expect([...result.areas]).toEqual(["area1"]);
|
||||
});
|
||||
|
||||
it("ignores a current item whose type is not tracked", () => {
|
||||
const result = buildRelatedIdSets(undefined, {
|
||||
itemType: "automation",
|
||||
itemId: "auto1",
|
||||
});
|
||||
expect(toArrays(result)).toEqual({ entities: [], devices: [], areas: [] });
|
||||
});
|
||||
|
||||
it("deduplicates a current item already in the related result", () => {
|
||||
const related: RelatedResult = { entity: ["light.ac"] };
|
||||
const result = buildRelatedIdSets(related, {
|
||||
itemType: "entity",
|
||||
itemId: "light.ac",
|
||||
});
|
||||
expect([...result.entities]).toEqual(["light.ac"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortRelatedFirst", () => {
|
||||
it("floats related items above unrelated ones", () => {
|
||||
const items = [
|
||||
item("a", false),
|
||||
item("b", true),
|
||||
item("c", false),
|
||||
item("d", true),
|
||||
];
|
||||
expect(orderOf(sortRelatedFirst(items))).toEqual(["b", "d", "a", "c"]);
|
||||
});
|
||||
|
||||
it("preserves relative order within each group (stable)", () => {
|
||||
const items = [
|
||||
item("r1", true),
|
||||
item("u1", false),
|
||||
item("r2", true),
|
||||
item("u2", false),
|
||||
item("r3", true),
|
||||
];
|
||||
expect(orderOf(sortRelatedFirst(items))).toEqual([
|
||||
"r1",
|
||||
"r2",
|
||||
"r3",
|
||||
"u1",
|
||||
"u2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats a missing isRelated flag as unrelated", () => {
|
||||
const items = [item("plain"), item("related", true)];
|
||||
expect(orderOf(sortRelatedFirst(items))).toEqual(["related", "plain"]);
|
||||
});
|
||||
|
||||
it("keeps order when nothing is related", () => {
|
||||
const items = [item("a"), item("b"), item("c")];
|
||||
expect(orderOf(sortRelatedFirst(items))).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("does not mutate the input array", () => {
|
||||
const items = [item("a", false), item("b", true)];
|
||||
sortRelatedFirst(items);
|
||||
expect(orderOf(items)).toEqual(["a", "b"]);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { RelatedIdSets } from "../../../src/common/search/related-context";
|
||||
import {
|
||||
getEntities,
|
||||
markEntitiesRelated,
|
||||
sortEntitiesByRelatedRank,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../../src/data/entity/entity_picker";
|
||||
import { getEntities } from "../../../src/data/entity/entity_picker";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
const makeHass = (entityIds: string[]): HomeAssistant => {
|
||||
@@ -104,128 +98,3 @@ describe("getEntities", () => {
|
||||
expect(items[0].id).toBe("entity-sensor.temp");
|
||||
});
|
||||
});
|
||||
|
||||
const item = (entityId: string): EntityComboBoxItem => ({
|
||||
id: entityId,
|
||||
primary: entityId,
|
||||
sorting_label: entityId,
|
||||
stateObj: { entity_id: entityId } as any,
|
||||
});
|
||||
|
||||
const emptyRelated = (): RelatedIdSets => ({
|
||||
areas: new Set(),
|
||||
devices: new Set(),
|
||||
entities: new Set(),
|
||||
});
|
||||
|
||||
// entity.in_area sits on device "dev" in area "area";
|
||||
// entity.in_device sits on device "dev"; entity.lonely has no device or area.
|
||||
const relatedHass = {
|
||||
entities: {
|
||||
"light.in_area": { entity_id: "light.in_area", device_id: "dev" },
|
||||
"light.in_device": { entity_id: "light.in_device", device_id: "dev2" },
|
||||
"light.lonely": { entity_id: "light.lonely" },
|
||||
},
|
||||
devices: {
|
||||
dev: { id: "dev", area_id: "area" },
|
||||
dev2: { id: "dev2" },
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
describe("markEntitiesRelated", () => {
|
||||
it("ranks the entity itself closest", () => {
|
||||
const related = emptyRelated();
|
||||
related.entities.add("light.lonely");
|
||||
const [marked] = markEntitiesRelated(
|
||||
[item("light.lonely")],
|
||||
related,
|
||||
relatedHass.entities,
|
||||
relatedHass.devices
|
||||
);
|
||||
expect(marked.relatedRank).toBe(0);
|
||||
});
|
||||
|
||||
it("ranks an entity by its device when not directly related", () => {
|
||||
const related = emptyRelated();
|
||||
related.devices.add("dev2");
|
||||
const [marked] = markEntitiesRelated(
|
||||
[item("light.in_device")],
|
||||
related,
|
||||
relatedHass.entities,
|
||||
relatedHass.devices
|
||||
);
|
||||
expect(marked.relatedRank).toBe(1);
|
||||
});
|
||||
|
||||
it("ranks an entity by its (device-inherited) area", () => {
|
||||
const related = emptyRelated();
|
||||
related.areas.add("area");
|
||||
const [marked] = markEntitiesRelated(
|
||||
[item("light.in_area")],
|
||||
related,
|
||||
relatedHass.entities,
|
||||
relatedHass.devices
|
||||
);
|
||||
expect(marked.relatedRank).toBe(2);
|
||||
});
|
||||
|
||||
it("marks unrelated entities with the lowest rank", () => {
|
||||
const related = emptyRelated();
|
||||
related.entities.add("light.other");
|
||||
const [marked] = markEntitiesRelated(
|
||||
[item("light.lonely")],
|
||||
related,
|
||||
relatedHass.entities,
|
||||
relatedHass.devices
|
||||
);
|
||||
expect(marked.relatedRank).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortEntitiesByRelatedRank", () => {
|
||||
it("sorts by closeness: entity, then device, then area, then the rest", () => {
|
||||
const related = emptyRelated();
|
||||
related.entities.add("light.in_device"); // direct entity match wins
|
||||
related.devices.add("dev"); // covers light.in_area via its device
|
||||
related.areas.add("area");
|
||||
const marked = markEntitiesRelated(
|
||||
[item("light.lonely"), item("light.in_area"), item("light.in_device")],
|
||||
related,
|
||||
relatedHass.entities,
|
||||
relatedHass.devices
|
||||
);
|
||||
const sorted = sortEntitiesByRelatedRank(marked, "en");
|
||||
expect(sorted.map((i) => i.id)).toEqual([
|
||||
"light.in_device", // rank 0 (entity)
|
||||
"light.in_area", // rank 1 (device)
|
||||
"light.lonely", // rank 3 (unrelated)
|
||||
]);
|
||||
});
|
||||
|
||||
it("breaks ties alphabetically by label when a language is given", () => {
|
||||
const sorted = sortEntitiesByRelatedRank(
|
||||
[item("light.zebra"), item("light.apple")],
|
||||
"en"
|
||||
);
|
||||
expect(sorted.map((i) => i.id)).toEqual(["light.apple", "light.zebra"]);
|
||||
});
|
||||
|
||||
it("keeps incoming order within a tier when no language is given", () => {
|
||||
const sorted = sortEntitiesByRelatedRank([
|
||||
item("light.zebra"),
|
||||
item("light.apple"),
|
||||
]);
|
||||
expect(sorted.map((i) => i.id)).toEqual(["light.zebra", "light.apple"]);
|
||||
});
|
||||
|
||||
it("falls back to plain alphabetical when nothing is related", () => {
|
||||
const marked = markEntitiesRelated(
|
||||
[item("light.zebra"), item("light.apple")],
|
||||
emptyRelated(),
|
||||
relatedHass.entities,
|
||||
relatedHass.devices
|
||||
);
|
||||
const sorted = sortEntitiesByRelatedRank(marked, "en");
|
||||
expect(sorted.map((i) => i.id)).toEqual(["light.apple", "light.zebra"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getConfigRelatedContext } from "../../../../src/panels/lovelace/common/get-config-related-context";
|
||||
|
||||
describe("getConfigRelatedContext", () => {
|
||||
it("returns an area context for area cards", () => {
|
||||
expect(getConfigRelatedContext({ type: "area", area: "kitchen" })).toEqual({
|
||||
itemType: "area",
|
||||
itemId: "kitchen",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an entity context for entity cards", () => {
|
||||
expect(
|
||||
getConfigRelatedContext({ type: "entity", entity: "light.kitchen" })
|
||||
).toEqual({
|
||||
itemType: "entity",
|
||||
itemId: "light.kitchen",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the area context for area cards with entity-like fields", () => {
|
||||
expect(
|
||||
getConfigRelatedContext({
|
||||
type: "area",
|
||||
area: "kitchen",
|
||||
entity: "light.kitchen",
|
||||
})
|
||||
).toEqual({
|
||||
itemType: "area",
|
||||
itemId: "kitchen",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined when no related context can be derived", () => {
|
||||
expect(getConfigRelatedContext({ type: "markdown" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,213 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { RelatedIdSets } from "../../src/common/search/related-context";
|
||||
import type { RelatedContextItem } from "../../src/data/context";
|
||||
import { findRelated } from "../../src/data/search";
|
||||
import type { RelatedResult } from "../../src/data/search";
|
||||
import type { HassBaseEl } from "../../src/state/hass-base-mixin";
|
||||
import { RelatedContextProvider } from "../../src/state/related-context-provider";
|
||||
|
||||
vi.mock("../../src/data/search", () => ({
|
||||
findRelated: vi.fn(),
|
||||
}));
|
||||
|
||||
const findRelatedMock = vi.mocked(findRelated);
|
||||
|
||||
const tick = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
|
||||
const deferred = <T>() => {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
const toArrays = (value: RelatedIdSets | undefined) =>
|
||||
value && {
|
||||
entities: [...value.entities].sort(),
|
||||
devices: [...value.devices].sort(),
|
||||
areas: [...value.areas].sort(),
|
||||
};
|
||||
|
||||
describe("RelatedContextProvider", () => {
|
||||
let host: HassBaseEl;
|
||||
let provider: RelatedContextProvider;
|
||||
|
||||
// Read the value the provider has published to its context consumers.
|
||||
const published = () =>
|
||||
(provider as unknown as { _provider: { value: RelatedIdSets | undefined } })
|
||||
._provider.value;
|
||||
|
||||
const createProvider = (hass: unknown) => {
|
||||
const el = document.createElement("div");
|
||||
(el as unknown as { hass: unknown }).hass = hass;
|
||||
host = el as unknown as HassBaseEl;
|
||||
provider = new RelatedContextProvider(host);
|
||||
provider.connect();
|
||||
};
|
||||
|
||||
const dispatch = (detail: unknown) => {
|
||||
const event = new Event("hass-related-context");
|
||||
(event as unknown as { detail: unknown }).detail = detail;
|
||||
host.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const fireItem = (item: RelatedContextItem) => dispatch(item);
|
||||
|
||||
const navigate = (path: string) => {
|
||||
window.history.pushState({}, "", path);
|
||||
window.dispatchEvent(new Event("popstate"));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
findRelatedMock.mockReset();
|
||||
window.haContext = {};
|
||||
window.history.pushState({}, "", "/lovelace");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
provider?.disconnect();
|
||||
delete window.haContext;
|
||||
});
|
||||
|
||||
it("resolves an item and publishes the related set, merging the item itself", async () => {
|
||||
findRelatedMock.mockResolvedValue({ device: ["dev1"], area: ["area1"] });
|
||||
createProvider({});
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
|
||||
expect(findRelatedMock).toHaveBeenCalledWith(
|
||||
host.hass,
|
||||
"entity",
|
||||
"light.ac"
|
||||
);
|
||||
expect(toArrays(published())).toEqual({
|
||||
entities: ["light.ac"],
|
||||
devices: ["dev1"],
|
||||
areas: ["area1"],
|
||||
});
|
||||
expect(toArrays(window.haContext?.related)).toEqual({
|
||||
entities: ["light.ac"],
|
||||
devices: ["dev1"],
|
||||
areas: ["area1"],
|
||||
});
|
||||
});
|
||||
|
||||
it("clears when the detail has no itemId (fireEvent coerces undefined to {})", async () => {
|
||||
findRelatedMock.mockResolvedValue({ device: ["dev1"] });
|
||||
createProvider({});
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
expect(published()).toBeDefined();
|
||||
|
||||
findRelatedMock.mockClear();
|
||||
dispatch({});
|
||||
await tick();
|
||||
|
||||
expect(published()).toBeUndefined();
|
||||
expect(window.haContext?.related).toBeUndefined();
|
||||
expect(findRelatedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("publishes nothing when hass is not available", async () => {
|
||||
createProvider(undefined);
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
|
||||
expect(findRelatedMock).not.toHaveBeenCalled();
|
||||
expect(published()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("publishes nothing when the related lookup fails", async () => {
|
||||
findRelatedMock.mockRejectedValue(new Error("boom"));
|
||||
createProvider({});
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
|
||||
expect(published()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores a stale in-flight resolve superseded by a newer item", async () => {
|
||||
const first = deferred<RelatedResult>();
|
||||
const second = deferred<RelatedResult>();
|
||||
findRelatedMock
|
||||
.mockReturnValueOnce(first.promise)
|
||||
.mockReturnValueOnce(second.promise);
|
||||
createProvider({});
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.first" });
|
||||
fireItem({ itemType: "entity", itemId: "light.second" });
|
||||
|
||||
second.resolve({ device: ["dev2"] });
|
||||
await tick();
|
||||
expect(toArrays(published())!.entities).toEqual(["light.second"]);
|
||||
|
||||
// The older lookup resolves late and must not overwrite the newer value.
|
||||
first.resolve({ device: ["dev1"] });
|
||||
await tick();
|
||||
expect(toArrays(published())!.entities).toEqual(["light.second"]);
|
||||
expect(toArrays(published())!.devices).toEqual(["dev2"]);
|
||||
});
|
||||
|
||||
it("memoizes repeated identical items", async () => {
|
||||
findRelatedMock.mockResolvedValue({ device: ["dev1"] });
|
||||
createProvider({});
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
|
||||
expect(findRelatedMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("clears the context on a real navigation (pathname change)", async () => {
|
||||
findRelatedMock.mockResolvedValue({ device: ["dev1"] });
|
||||
createProvider({});
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
expect(published()).toBeDefined();
|
||||
|
||||
navigate("/config/devices");
|
||||
await tick();
|
||||
|
||||
expect(published()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps the context on a same-path popstate (dialog history)", async () => {
|
||||
findRelatedMock.mockResolvedValue({ device: ["dev1"] });
|
||||
createProvider({});
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
const before = toArrays(published());
|
||||
|
||||
// popstate without a pathname change (dialog open/close)
|
||||
window.dispatchEvent(new Event("popstate"));
|
||||
await tick();
|
||||
|
||||
expect(toArrays(published())).toEqual(before);
|
||||
});
|
||||
|
||||
it("stops responding to events after disconnect", async () => {
|
||||
findRelatedMock.mockResolvedValue({ device: ["dev1"] });
|
||||
createProvider({});
|
||||
provider.disconnect();
|
||||
|
||||
fireItem({ itemType: "entity", itemId: "light.ac" });
|
||||
await tick();
|
||||
|
||||
expect(findRelatedMock).not.toHaveBeenCalled();
|
||||
expect(published()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user