From e731f060f138293660df66ce060cf90fe0fe7029 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 3 Dec 2024 07:50:07 +0100 Subject: [PATCH] Add device search to quick bar (#23095) * Add device search to quick bar * Process code review --- src/dialogs/quick-bar/ha-quick-bar.ts | 171 +++++++++++++----- .../quick-bar/show-dialog-quick-bar.ts | 8 +- .../config/dashboard/ha-config-dashboard.ts | 9 +- src/panels/lovelace/hui-root.ts | 9 +- src/state/quick-bar-mixin.ts | 23 ++- src/translations/en.json | 5 +- 6 files changed, 164 insertions(+), 61 deletions(-) diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index e96cb6f23704..73b0107b7f50 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -3,13 +3,14 @@ import type { ListItem } from "@material/mwc-list/mwc-list-item"; import { mdiClose, mdiConsoleLine, + mdiDevices, mdiEarth, mdiMagnify, mdiReload, mdiServerNetwork, } from "@mdi/js"; import type { TemplateResult } from "lit"; -import { LitElement, css, html, nothing } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; @@ -38,7 +39,7 @@ import { haStyleDialog, haStyleScrollbar } from "../../resources/styles"; import { loadVirtualizer } from "../../resources/virtualizer"; import type { HomeAssistant } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; -import type { QuickBarParams } from "./show-dialog-quick-bar"; +import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; interface QuickBarItem extends ScorableTextItem { primaryText: string; @@ -56,9 +57,17 @@ interface EntityItem extends QuickBarItem { icon?: TemplateResult; } +interface DeviceItem extends QuickBarItem { + deviceId: string; + area?: string; +} + const isCommandItem = (item: QuickBarItem): item is CommandItem => (item as CommandItem).categoryKey !== undefined; +const isDeviceItem = (item: QuickBarItem): item is DeviceItem => + (item as DeviceItem).deviceId !== undefined; + interface QuickBarNavigationItem extends CommandItem { path: string; } @@ -77,20 +86,22 @@ export class QuickBar extends LitElement { @state() private _entityItems?: EntityItem[]; + @state() private _deviceItems?: DeviceItem[]; + @state() private _filter = ""; @state() private _search = ""; @state() private _open = false; - @state() private _commandMode = false; - @state() private _opened = false; @state() private _narrow = false; @state() private _hint?: string; + @state() private _mode = QuickBarMode.Entity; + @query("ha-textfield", false) private _filterInputField?: HTMLElement; private _focusSet = false; @@ -98,7 +109,7 @@ export class QuickBar extends LitElement { private _focusListElement?: ListItem | null; public async showDialog(params: QuickBarParams) { - this._commandMode = params.commandMode || this._toggleIfAlreadyOpened(); + this._mode = params.mode || QuickBarMode.Entity; this._hint = params.hint; this._narrow = matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" @@ -125,8 +136,20 @@ export class QuickBar extends LitElement { } private _getItems = memoizeOne( - (commandMode: boolean, commandItems, entityItems, filter: string) => { - const items = commandMode ? commandItems : entityItems; + ( + mode: QuickBarMode, + commandItems, + entityItems, + deviceItems, + filter: string + ) => { + let items = entityItems; + + if (mode === QuickBarMode.Command) { + items = commandItems; + } else if (mode === QuickBarMode.Device) { + items = deviceItems; + } if (items && filter && filter !== " ") { return this._filterItems(items, filter); @@ -141,12 +164,28 @@ export class QuickBar extends LitElement { } const items: QuickBarItem[] | undefined = this._getItems( - this._commandMode, + this._mode, this._commandItems, this._entityItems, + this._deviceItems, this._filter ); + const translationKey = + this._mode === QuickBarMode.Device ? "devices" : "entities"; + const placeholder = this.hass.localize( + `ui.dialogs.quick-bar.filter_placeholder.${translationKey}` + ); + + const commandMode = this._mode === QuickBarMode.Command; + const deviceMode = this._mode === QuickBarMode.Device; + const icon = commandMode + ? mdiConsoleLine + : deviceMode + ? mdiDevices + : mdiMagnify; + const searchPrefix = commandMode ? ">" : deviceMode ? "#" : ""; + return html` ${this._search}` : this._search} + .placeholder=${placeholder} + aria-label=${placeholder} + .value="${searchPrefix}${this._search}" icon .iconTrailing=${this._search !== undefined || this._narrow} @input=${this._handleSearchChange} @keydown=${this._handleInputKeyDown} @focus=${this._setFocusFirstListItem} > - ${this._commandMode - ? html` - - ` - : html` - - `} + ${this._search || this._narrow ? html`
@@ -232,8 +257,7 @@ export class QuickBar extends LitElement { height: this._narrow ? "calc(100vh - 56px)" : `${Math.min( - items.length * (this._commandMode ? 56 : 72) + - 26, + items.length * (commandMode ? 56 : 72) + 26, 500 )}px`, })} @@ -252,9 +276,11 @@ export class QuickBar extends LitElement { } private async _initializeItemsIfNeeded() { - if (this._commandMode) { + if (this._mode === QuickBarMode.Command) { this._commandItems = this._commandItems || (await this._generateCommandItems()); + } else if (this._mode === QuickBarMode.Device) { + this._deviceItems = this._deviceItems || this._generateDeviceItems(); } else { this._entityItems = this._entityItems || this._generateEntityItems(); } @@ -279,11 +305,37 @@ export class QuickBar extends LitElement { if (!item) { return nothing; } - return isCommandItem(item) - ? this._renderCommandItem(item, index) - : this._renderEntityItem(item as EntityItem, index); + + if (isDeviceItem(item)) { + return this._renderDeviceItem(item, index); + } + + if (isCommandItem(item)) { + return this._renderCommandItem(item, index); + } + + return this._renderEntityItem(item as EntityItem, index); }; + private _renderDeviceItem(item: DeviceItem, index?: number) { + return html` + + ${item.primaryText} + ${item.area + ? html` + ${item.area} + ` + : nothing} + + `; + } + private _renderEntityItem(item: EntityItem, index?: number) { return html` ")) { - newCommandMode = true; + newMode = QuickBarMode.Command; + newSearch = newFilter.substring(1); + } else if (newFilter.startsWith("#")) { + newMode = QuickBarMode.Device; newSearch = newFilter.substring(1); } else { - newCommandMode = false; + newMode = QuickBarMode.Entity; newSearch = newFilter; } - if (oldCommandMode === newCommandMode && oldSearch === newSearch) { + if (oldMode === newMode && oldSearch === newSearch) { return; } - this._commandMode = newCommandMode; + this._mode = newMode; this._search = newSearch; if (this._hint) { this._hint = undefined; } - if (oldCommandMode !== this._commandMode) { + if (oldMode !== this._mode) { this._focusSet = false; this._initializeItemsIfNeeded(); this._filter = this._search; @@ -464,6 +519,32 @@ export class QuickBar extends LitElement { ); } + private _generateDeviceItems(): DeviceItem[] { + return Object.keys(this.hass.devices) + .map((deviceId) => { + const device = this.hass.devices[deviceId]; + const area = this.hass.areas[device.area_id!]; + const deviceItem = { + primaryText: device.name!, + deviceId: device.id, + area: area?.name, + action: () => navigate(`/config/devices/device/${device.id}`), + }; + + return { + ...deviceItem, + strings: [deviceItem.primaryText], + }; + }) + .sort((a, b) => + caseInsensitiveStringCompare( + a.primaryText, + b.primaryText, + this.hass.locale.language + ) + ); + } + private _generateEntityItems(): EntityItem[] { return Object.keys(this.hass.states) .map((entityId) => { @@ -746,10 +827,6 @@ export class QuickBar extends LitElement { }); } - private _toggleIfAlreadyOpened() { - return this._opened ? !this._commandMode : false; - } - private _filterItems = memoizeOne( (items: QuickBarItem[], filter: string): QuickBarItem[] => fuzzyFilterSort(filter.trimLeft(), items) diff --git a/src/dialogs/quick-bar/show-dialog-quick-bar.ts b/src/dialogs/quick-bar/show-dialog-quick-bar.ts index 01e2af978edf..2609ac8b3589 100644 --- a/src/dialogs/quick-bar/show-dialog-quick-bar.ts +++ b/src/dialogs/quick-bar/show-dialog-quick-bar.ts @@ -1,8 +1,14 @@ import { fireEvent } from "../../common/dom/fire_event"; +export const enum QuickBarMode { + Command = "command", + Device = "device", + Entity = "entity", +} + export interface QuickBarParams { entityFilter?: string; - commandMode?: boolean; + mode?: QuickBarMode; hint?: string; } diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 1e18778c10cf..591ce9cf4230 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -8,7 +8,7 @@ import { } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; @@ -33,7 +33,10 @@ import { checkForEntityUpdates, filterUpdateEntitiesWithInstall, } from "../../../data/update"; -import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; +import { + QuickBarMode, + showQuickBar, +} from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; import type { PageNavigation } from "../../../layouts/hass-tabs-subpage"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; @@ -325,7 +328,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { private _showQuickBar(): void { showQuickBar(this, { - commandMode: true, + mode: QuickBarMode.Command, hint: this.hass.enableShortcuts ? this.hass.localize("ui.dialogs.quick-bar.key_c_hint") : undefined, diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 0fe8d4f6de18..3fd083ccf448 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -18,7 +18,7 @@ import { import "@polymer/paper-tabs/paper-tab"; import "@polymer/paper-tabs/paper-tabs"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; @@ -59,7 +59,10 @@ import { showAlertDialog, showConfirmationDialog, } from "../../dialogs/generic/show-dialog-box"; -import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar"; +import { + QuickBarMode, + showQuickBar, +} from "../../dialogs/quick-bar/show-dialog-quick-bar"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant, PanelInfo } from "../../types"; @@ -662,7 +665,7 @@ class HUIRoot extends LitElement { private _showQuickBar(): void { showQuickBar(this, { - commandMode: false, + mode: QuickBarMode.Entity, hint: this.hass.enableShortcuts ? this.hass.localize("ui.tips.key_e_hint") : undefined, diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index 837bb3c44713..30a96e7e9366 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -4,7 +4,10 @@ import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../common/config/is_component_loaded"; import { mainWindow } from "../common/dom/get_main_window"; import type { QuickBarParams } from "../dialogs/quick-bar/show-dialog-quick-bar"; -import { showQuickBar } from "../dialogs/quick-bar/show-dialog-quick-bar"; +import { + QuickBarMode, + showQuickBar, +} from "../dialogs/quick-bar/show-dialog-quick-bar"; import type { Constructor, HomeAssistant } from "../types"; import { storeState } from "../util/ha-pref-storage"; import { showToast } from "../util/toast"; @@ -36,7 +39,10 @@ export default >(superClass: T) => this._showQuickBar(ev.detail); break; case "c": - this._showQuickBar(ev.detail, true); + this._showQuickBar(ev.detail, QuickBarMode.Command); + break; + case "d": + this._showQuickBar(ev.detail, QuickBarMode.Device); break; case "m": this._createMyLink(ev.detail); @@ -54,14 +60,16 @@ export default >(superClass: T) => tinykeys(window, { // Those are for latin keyboards that have e, c, m keys e: (ev) => this._showQuickBar(ev), - c: (ev) => this._showQuickBar(ev, true), + c: (ev) => this._showQuickBar(ev, QuickBarMode.Command), m: (ev) => this._createMyLink(ev), a: (ev) => this._showVoiceCommandDialog(ev), + d: (ev) => this._showQuickBar(ev, QuickBarMode.Device), // Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts) KeyE: (ev) => this._showQuickBar(ev), - KeyC: (ev) => this._showQuickBar(ev, true), + KeyC: (ev) => this._showQuickBar(ev, QuickBarMode.Command), KeyM: (ev) => this._createMyLink(ev), KeyA: (ev) => this._showVoiceCommandDialog(ev), + KeyD: (ev) => this._showQuickBar(ev, QuickBarMode.Device), }); } @@ -86,7 +94,10 @@ export default >(superClass: T) => showVoiceCommandDialog(this, this.hass!, { pipeline_id: "last_used" }); } - private _showQuickBar(e: KeyboardEvent, commandMode = false) { + private _showQuickBar( + e: KeyboardEvent, + mode: QuickBarMode = QuickBarMode.Entity + ) { if (!this._canShowQuickBar(e)) { return; } @@ -96,7 +107,7 @@ export default >(superClass: T) => } e.preventDefault(); - showQuickBar(this, { commandMode }); + showQuickBar(this, { mode }); } private async _createMyLink(e: KeyboardEvent) { diff --git a/src/translations/en.json b/src/translations/en.json index 469d55924768..06e64f486483 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1195,7 +1195,10 @@ "addon_info": "{addon} Info" } }, - "filter_placeholder": "Search entities", + "filter_placeholder": { + "entities": "Search entities", + "devices": "Search devices" + }, "title": "Quick search", "key_c_hint": "Press 'c' on any page to open the command dialog", "nothing_found": "Nothing found!"