mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-09 18:36:35 +00:00
Add device search to quick bar (#23095)
* Add device search to quick bar * Process code review
This commit is contained in:
parent
c3942d244d
commit
e731f060f1
@ -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`
|
||||
<ha-dialog
|
||||
.heading=${this.hass.localize("ui.dialogs.quick-bar.title")}
|
||||
@ -158,34 +197,20 @@ export class QuickBar extends LitElement {
|
||||
<div slot="heading" class="heading">
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.filter_placeholder"
|
||||
)}
|
||||
.value=${this._commandMode ? `>${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`
|
||||
<ha-svg-icon
|
||||
slot="leadingIcon"
|
||||
class="prefix"
|
||||
.path=${mdiConsoleLine}
|
||||
.path=${icon}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="leadingIcon"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${this._search || this._narrow
|
||||
? html`
|
||||
<div slot="trailingIcon">
|
||||
@ -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`
|
||||
<ha-list-item
|
||||
.twoline=${Boolean(item.area)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
>
|
||||
<span>${item.primaryText}</span>
|
||||
${item.area
|
||||
? html`
|
||||
<span slot="secondary" class="item-text secondary"
|
||||
>${item.area}</span
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
||||
return html`
|
||||
<ha-list-item
|
||||
@ -376,31 +428,34 @@ export class QuickBar extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
const newFilter = (ev.currentTarget as any).value;
|
||||
const oldCommandMode = this._commandMode;
|
||||
const oldMode = this._mode;
|
||||
const oldSearch = this._search;
|
||||
let newCommandMode: boolean;
|
||||
let newMode: QuickBarMode;
|
||||
let newSearch: string;
|
||||
|
||||
if (newFilter.startsWith(">")) {
|
||||
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<QuickBarItem>(filter.trimLeft(), items)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 <T extends Constructor<HassElement>>(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 <T extends Constructor<HassElement>>(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 <T extends Constructor<HassElement>>(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 <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
showQuickBar(this, { commandMode });
|
||||
showQuickBar(this, { mode });
|
||||
}
|
||||
|
||||
private async _createMyLink(e: KeyboardEvent) {
|
||||
|
@ -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!"
|
||||
|
Loading…
x
Reference in New Issue
Block a user