mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-29 20:42:05 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9c16fd72a | |||
| 559dc0a98e | |||
| 22c3025d8f | |||
| 27273e18d0 | |||
| a4f30f9086 | |||
| 787212b5f0 | |||
| 43eb7fd9c9 | |||
| 8b76d97e3a | |||
| cbae26ebec | |||
| f2792024c1 |
@@ -0,0 +1,37 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
devices: Set<string>;
|
||||
entities: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of related IDs for a given related result.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @returns The related ID sets.
|
||||
*/
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
});
|
||||
|
||||
/**
|
||||
* Stable partition sort: related items float to the top,
|
||||
* preserving relative order (e.g. Fuse score) within each group.
|
||||
* @param items - The items to sort.
|
||||
* @returns The sorted items.
|
||||
*/
|
||||
export const sortRelatedFirst = (
|
||||
items: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
@@ -10,10 +11,12 @@ import type {
|
||||
HomeAssistantRegistries,
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import type { ConfigEntry } from "../config_entries";
|
||||
import type { EntityRegistryEntry } from "../entity/entity_registry";
|
||||
import type { DomainManifestLookup } from "../integration";
|
||||
import type { LabelRegistryEntry } from "../label/label_registry";
|
||||
import type { ItemType } from "../search";
|
||||
|
||||
/**
|
||||
* Entity, device, area, and floor registries
|
||||
@@ -162,3 +165,30 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
export const authContext = createContext<HomeAssistant["auth"]>("auth");
|
||||
|
||||
// #endregion deprecated-contexts
|
||||
|
||||
// #region related-context
|
||||
|
||||
export interface RelatedContextItem {
|
||||
itemType: ItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved related entities/devices/areas for the current page context.
|
||||
* Set by `RelatedContextProvider` when a page fires `hass-related-context`.
|
||||
* Cleared on navigation.
|
||||
*/
|
||||
export const relatedContext = createContext<RelatedIdSets | undefined>(
|
||||
"related"
|
||||
);
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-related-context": RelatedContextItem | undefined;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-related-context": HASSDomEvent<RelatedContextItem | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion related-context
|
||||
|
||||
+2
-2
@@ -36,11 +36,11 @@ export type ItemType =
|
||||
| "script_blueprint";
|
||||
|
||||
export const findRelated = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
itemType: ItemType,
|
||||
itemId: string
|
||||
): Promise<RelatedResult> =>
|
||||
hass.callWS({
|
||||
hass.callWS<RelatedResult>({
|
||||
type: "search/related",
|
||||
item_type: itemType,
|
||||
item_id: itemId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mdiDevices } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -47,7 +48,9 @@ import {
|
||||
type ActionCommandComboBoxItem,
|
||||
type NavigationComboBoxItem,
|
||||
} from "../../data/quick_bar";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { sortRelatedFirst } from "../../common/search/related-context";
|
||||
import { relatedContext } from "../../data/context";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
@@ -70,6 +73,10 @@ const SEPARATOR = "________";
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _loading = true;
|
||||
@@ -80,8 +87,6 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _relatedResult?: RelatedResult;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
private get _showEntityId() {
|
||||
@@ -108,8 +113,6 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = effectiveQuickBarMode(this.hass.user, params.mode);
|
||||
this._showHint = params.showHint ?? false;
|
||||
|
||||
this._relatedResult = params.contextItem ? params.related : undefined;
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -432,7 +435,7 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = section as QuickBarSection | undefined;
|
||||
return this._getItemsMemoized(
|
||||
this._configEntryLookup,
|
||||
this._relatedResult,
|
||||
this._relatedIdSets,
|
||||
searchString,
|
||||
this._selectedSection
|
||||
);
|
||||
@@ -441,12 +444,11 @@ export class QuickBar extends LitElement {
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
relatedResult: RelatedResult | undefined,
|
||||
relatedIdSets: RelatedIdSets | undefined,
|
||||
filter?: string,
|
||||
section?: QuickBarSection
|
||||
) => {
|
||||
const items: (string | PickerComboBoxItem)[] = [];
|
||||
const relatedIdSets = this._getRelatedIdSets(relatedResult);
|
||||
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
@@ -498,7 +500,7 @@ export class QuickBar extends LitElement {
|
||||
let entityItems = this._getEntitiesMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.entities.size > 0) {
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
@@ -508,7 +510,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
entityItems = this._sortRelatedFirst(
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
@@ -537,7 +539,7 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.devices.size > 0) {
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => {
|
||||
const deviceId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
@@ -548,7 +550,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
deviceItems = this._sortRelatedFirst(
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
@@ -569,7 +571,7 @@ export class QuickBar extends LitElement {
|
||||
let areaItems = this._getAreasMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.areas.size > 0) {
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areaItems = areaItems.map((item) => {
|
||||
const areaId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
@@ -580,7 +582,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
areaItems = this._sortRelatedFirst(
|
||||
areaItems = sortRelatedFirst(
|
||||
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
@@ -601,12 +603,6 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getRelatedIdSets = memoizeOne((related?: RelatedResult) => ({
|
||||
entities: new Set(related?.entity || []),
|
||||
devices: new Set(related?.device || []),
|
||||
areas: new Set(related?.area || []),
|
||||
}));
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
|
||||
getEntities(
|
||||
hass,
|
||||
@@ -705,10 +701,13 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _sortBySortingLabel = (entityA, entityB) =>
|
||||
private _sortBySortingLabel = (
|
||||
entityA: PickerComboBoxItem,
|
||||
entityB: PickerComboBoxItem
|
||||
) =>
|
||||
caseInsensitiveStringCompare(
|
||||
(entityA as PickerComboBoxItem).sorting_label!,
|
||||
(entityB as PickerComboBoxItem).sorting_label!,
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
@@ -719,16 +718,6 @@ export class QuickBar extends LitElement {
|
||||
return this._sortBySortingLabel(a, b);
|
||||
});
|
||||
|
||||
private _sortRelatedFirst = (items: PickerComboBoxItem[]) =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
|
||||
// #endregion data
|
||||
|
||||
// #region interaction
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { closeDialog } from "../make-dialog-manager";
|
||||
|
||||
@@ -10,17 +9,10 @@ export type QuickBarSection =
|
||||
| "navigate"
|
||||
| "command";
|
||||
|
||||
export interface QuickBarContextItem {
|
||||
itemType: ItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarSection;
|
||||
showHint?: boolean;
|
||||
contextItem?: QuickBarContextItem;
|
||||
related?: RelatedResult;
|
||||
}
|
||||
|
||||
/** Non-admin users cannot scope the bar to command, device, or area (those sections are admin-only). */
|
||||
|
||||
@@ -20,6 +20,10 @@ import type {
|
||||
LocalizeKeys,
|
||||
} from "../../../../common/translations/localize";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import {
|
||||
sortRelatedFirst,
|
||||
type RelatedIdSets,
|
||||
} from "../../../../common/search/related-context";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/chips/ha-filter-chip";
|
||||
import "../../../../components/entity/state-badge";
|
||||
@@ -40,7 +44,7 @@ import {
|
||||
} from "../../../../data/area_floor_picker";
|
||||
import { CONDITION_BUILDING_BLOCKS_GROUP } from "../../../../data/condition";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import { labelsContext } from "../../../../data/context";
|
||||
import { labelsContext, relatedContext } from "../../../../data/context";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
@@ -129,6 +133,10 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
| "condition"
|
||||
| "action";
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
@state() private _searchSectionTitle?: string;
|
||||
|
||||
@state() private _selectedSearchSection?: SearchSection;
|
||||
@@ -194,7 +202,8 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
this.configEntryLookup,
|
||||
this.items,
|
||||
this.newTriggersAndConditions,
|
||||
this._selectedSearchSection
|
||||
this._selectedSearchSection,
|
||||
this._relatedIdSets
|
||||
);
|
||||
|
||||
let emptySearchTranslation: string | undefined;
|
||||
@@ -487,7 +496,8 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
automationItems: AddAutomationElementListItem[],
|
||||
newTriggersAndConditions: boolean,
|
||||
selectedSection?: SearchSection
|
||||
selectedSection?: SearchSection,
|
||||
relatedIdSets?: RelatedIdSets
|
||||
) => {
|
||||
const resultItems: (
|
||||
| string
|
||||
@@ -568,13 +578,26 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
`entity${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
(item as EntityComboBoxItem).stateObj?.entity_id || ""
|
||||
),
|
||||
})) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
entityItems = this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
entityComboBoxKeys
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
entityComboBoxKeys
|
||||
)
|
||||
) as EntityComboBoxItem[];
|
||||
} else if (relatedIdSets?.entities.size) {
|
||||
entityItems = sortRelatedFirst(entityItems) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && entityItems.length) {
|
||||
@@ -601,13 +624,26 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
`device${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.devices.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
)
|
||||
);
|
||||
} else if (relatedIdSets?.devices.size) {
|
||||
deviceItems = sortRelatedFirst(deviceItems);
|
||||
}
|
||||
|
||||
if (!selectedSection && deviceItems.length) {
|
||||
@@ -639,13 +675,31 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
undefined
|
||||
);
|
||||
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = areasAndFloors.map((item) => ({
|
||||
...item,
|
||||
isRelated:
|
||||
item.type === "area"
|
||||
? relatedIdSets.areas.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
)
|
||||
: false,
|
||||
})) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
)
|
||||
) as FloorComboBoxItem[];
|
||||
} else if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
areasAndFloors
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResult, LitElement, TemplateResult } from "lit";
|
||||
import type {
|
||||
CSSResult,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
@@ -136,6 +142,21 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("registryEntry")) {
|
||||
const areaId = this.registryEntry?.area_id;
|
||||
if (areaId) {
|
||||
fireEvent(this, "hass-related-context", {
|
||||
itemType: "area",
|
||||
itemId: areaId,
|
||||
});
|
||||
} else {
|
||||
fireEvent(this, "hass-related-context", undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected renderLoading(): TemplateResult {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
|
||||
@@ -379,7 +379,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
if (changedProps.has("deviceId")) {
|
||||
this._findRelated();
|
||||
// Broadcast device context for quick bar
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
fireEvent(this, "hass-related-context", {
|
||||
itemType: "device",
|
||||
itemId: this.deviceId,
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ import { subscribeLabelRegistry } from "../data/label/label_registry";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
import { LazyContextProvider } from "./lazy-context-provider";
|
||||
import { RelatedContextProvider } from "./related-context-provider";
|
||||
|
||||
export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
@@ -201,6 +202,8 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
}),
|
||||
};
|
||||
|
||||
private __relatedContextProvider = new RelatedContextProvider(this);
|
||||
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
for (const [key, value] of Object.entries(this.hass!)) {
|
||||
@@ -214,6 +217,8 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
for (const provider of Object.values(this.__lazyContextProviders)) {
|
||||
provider.setConnection(connection);
|
||||
}
|
||||
|
||||
this.__relatedContextProvider.connect();
|
||||
}
|
||||
|
||||
protected _updateHass(obj: Partial<HomeAssistant>) {
|
||||
@@ -244,5 +249,6 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
for (const provider of Object.values(this.__lazyContextProviders)) {
|
||||
provider.unsubscribe();
|
||||
}
|
||||
this.__relatedContextProvider.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,12 +5,7 @@ import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { ShortcutManager } from "../common/keyboard/shortcuts";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import { findRelated, type RelatedResult } from "../data/search";
|
||||
import type {
|
||||
QuickBarContextItem,
|
||||
QuickBarParams,
|
||||
QuickBarSection,
|
||||
} from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import type { QuickBarSection } from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import {
|
||||
closeQuickBar,
|
||||
showQuickBar,
|
||||
@@ -24,10 +19,8 @@ import type { HassElement } from "./hass-element";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-quick-bar": QuickBarParams;
|
||||
"hass-quick-bar-trigger": KeyboardEvent;
|
||||
"hass-enable-shortcuts": HomeAssistant["enableShortcuts"];
|
||||
"hass-quick-bar-context": QuickBarContextItem | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,52 +28,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
class extends superClass {
|
||||
private _quickBarOpen = false;
|
||||
|
||||
private _quickBarContext?: QuickBarContextItem;
|
||||
|
||||
private _quickBarContextRelated?: RelatedResult;
|
||||
|
||||
private _fetchRelatedMemoized = memoizeOne(
|
||||
(itemType: QuickBarContextItem["itemType"], itemId: string) =>
|
||||
findRelated(this.hass!, itemType, itemId)
|
||||
);
|
||||
|
||||
private _clearQuickBarContext = () => {
|
||||
this._quickBarContext = undefined;
|
||||
this._quickBarContextRelated = undefined;
|
||||
};
|
||||
|
||||
private _contextMatches = (context?: QuickBarContextItem) =>
|
||||
context?.itemType === this._quickBarContext?.itemType &&
|
||||
context?.itemId === this._quickBarContext?.itemId;
|
||||
|
||||
private _prefetchQuickBarContext = async (
|
||||
context?: QuickBarContextItem
|
||||
) => {
|
||||
this._quickBarContextRelated = undefined;
|
||||
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const related = await this._fetchRelatedMemoized(
|
||||
context.itemType,
|
||||
context.itemId
|
||||
);
|
||||
|
||||
if (this._contextMatches(context)) {
|
||||
this._quickBarContextRelated = related;
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Error prefetching quick bar related items", err);
|
||||
|
||||
if (this._contextMatches(context)) {
|
||||
this._quickBarContextRelated = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -111,20 +58,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener("hass-quick-bar-context", (ev) => {
|
||||
this._quickBarContext =
|
||||
ev.detail && "itemType" in ev.detail && "itemId" in ev.detail
|
||||
? ev.detail
|
||||
: undefined;
|
||||
this._prefetchQuickBarContext(this._quickBarContext);
|
||||
});
|
||||
|
||||
mainWindow.addEventListener(
|
||||
"location-changed",
|
||||
this._clearQuickBarContext
|
||||
);
|
||||
mainWindow.addEventListener("popstate", this._clearQuickBarContext);
|
||||
|
||||
mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => {
|
||||
switch (ev.detail.key) {
|
||||
case "e":
|
||||
@@ -161,15 +94,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
mainWindow.removeEventListener(
|
||||
"location-changed",
|
||||
this._clearQuickBarContext
|
||||
);
|
||||
mainWindow.removeEventListener("popstate", this._clearQuickBarContext);
|
||||
}
|
||||
|
||||
private _registerShortcut() {
|
||||
const shortcutManager = new ShortcutManager();
|
||||
shortcutManager.add({
|
||||
@@ -238,11 +162,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
showQuickBar(this, {
|
||||
mode,
|
||||
contextItem: this._quickBarContext,
|
||||
related: this._quickBarContextRelated,
|
||||
});
|
||||
showQuickBar(this, { mode });
|
||||
}
|
||||
|
||||
private _toggleQuickBar(e: KeyboardEvent, mode?: QuickBarSection) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { buildRelatedIdSets } from "../common/search/related-context";
|
||||
import { relatedContext, type RelatedContextItem } from "../data/context";
|
||||
import { findRelated } from "../data/search";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
/**
|
||||
* Standalone context provider for `relatedContext`.
|
||||
*
|
||||
* Listens for `hass-related-context` events fired by child components,
|
||||
* resolves the related entities/devices/areas via `findRelated`, and
|
||||
* provides the resolved `RelatedIdSets` to context consumers.
|
||||
*
|
||||
* Clears on actual page navigation (pathname change), not on dialog
|
||||
* history manipulation (`popstate` from dialog close).
|
||||
*
|
||||
* Instantiated from `context-mixin.ts` alongside other providers.
|
||||
*/
|
||||
export class RelatedContextProvider {
|
||||
private _relatedContext?: RelatedContextItem;
|
||||
|
||||
private _provider: ContextProvider<typeof relatedContext>;
|
||||
|
||||
private _contextPathname?: string;
|
||||
|
||||
private _fetchRelatedMemoized = memoizeOne(
|
||||
(itemType: RelatedContextItem["itemType"], itemId: string) =>
|
||||
findRelated(this._host.hass!, itemType, itemId)
|
||||
);
|
||||
|
||||
constructor(private _host: HassBaseEl) {
|
||||
this._provider = new ContextProvider(_host, { context: relatedContext });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners. Call from `firstUpdated` or `hassConnected`.
|
||||
*/
|
||||
public connect(): void {
|
||||
this._host.addEventListener("hass-related-context", this._onRelatedContext);
|
||||
mainWindow.addEventListener(
|
||||
"location-changed",
|
||||
this._maybeClearRelatedContext
|
||||
);
|
||||
mainWindow.addEventListener("popstate", this._maybeClearRelatedContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up event listeners. Call from `disconnectedCallback`.
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this._host.removeEventListener(
|
||||
"hass-related-context",
|
||||
this._onRelatedContext
|
||||
);
|
||||
mainWindow.removeEventListener(
|
||||
"location-changed",
|
||||
this._maybeClearRelatedContext
|
||||
);
|
||||
mainWindow.removeEventListener("popstate", this._maybeClearRelatedContext);
|
||||
}
|
||||
|
||||
private _onRelatedContext = (
|
||||
ev: HASSDomEvent<RelatedContextItem | undefined>
|
||||
): void => {
|
||||
this._relatedContext = ev.detail;
|
||||
this._contextPathname = mainWindow.location.pathname;
|
||||
this._resolveRelatedContext(this._relatedContext);
|
||||
};
|
||||
|
||||
/**
|
||||
* Only clear context when the actual page pathname changes.
|
||||
* Dialog open/close manipulates history state without changing the URL,
|
||||
* so we ignore those popstate/location-changed events.
|
||||
*/
|
||||
private _maybeClearRelatedContext = (): void => {
|
||||
if (
|
||||
this._contextPathname &&
|
||||
mainWindow.location.pathname === this._contextPathname
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._relatedContext = undefined;
|
||||
this._contextPathname = undefined;
|
||||
this._provider.setValue(undefined);
|
||||
};
|
||||
|
||||
private _contextMatches = (context?: RelatedContextItem): boolean =>
|
||||
context?.itemType === this._relatedContext?.itemType &&
|
||||
context?.itemId === this._relatedContext?.itemId;
|
||||
|
||||
private _resolveRelatedContext = async (
|
||||
context?: RelatedContextItem
|
||||
): Promise<void> => {
|
||||
if (!context || !this._host.hass) {
|
||||
this._provider.setValue(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const related = await this._fetchRelatedMemoized(
|
||||
context.itemType,
|
||||
context.itemId
|
||||
);
|
||||
if (this._contextMatches(context)) {
|
||||
this._provider.setValue(buildRelatedIdSets(related));
|
||||
}
|
||||
} catch (_err) {
|
||||
if (this._contextMatches(context)) {
|
||||
this._provider.setValue(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user