Add navigation picker for dashboards (#13826)

* Add navigation picker for dashboards

* Rename to navigation

* Fix empty title and path

* Use hass panels instead of fetching dashboards

* Apply suggestions
This commit is contained in:
Paul Bottein 2022-09-21 14:42:51 +02:00 committed by GitHub
parent 977fdd9fbb
commit eac13980ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 300 additions and 6 deletions

View File

@ -0,0 +1,4 @@
export const titleCase = (s) =>
s.replace(/^_*(.)|_+(.)/g, (_s, c, d) =>
c ? c.toUpperCase() : " " + d.toUpperCase()
);

View File

@ -0,0 +1,221 @@
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
import {
fetchConfig,
LovelaceConfig,
LovelaceViewConfig,
} from "../data/lovelace";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant, PanelInfo } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon";
type NavigationItem = {
path: string;
icon: string;
title: string;
};
const DEFAULT_ITEMS: NavigationItem[] = [{ path: "", icon: "", title: "" }];
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
<mwc-list-item graphic="icon" .twoline=${!!item.title}>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
<span>${item.title || item.path}</span>
<span slot="secondary">${item.path}</span>
</mwc-list-item>
`;
const createViewNavigationItem = (
prefix: string,
view: LovelaceViewConfig,
index: number
) => ({
path: `/${prefix}/${view.path ?? index}`,
icon: view.icon ?? "mdi:view-compact",
title: view.title ?? (view.path ? titleCase(view.path) : `${index}`),
});
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
path: `/${panel.url_path}`,
icon: panel.icon ?? "mdi:view-dashboard",
title:
panel.url_path === hass.defaultPanel
? hass.localize("panel.states")
: hass.localize(`panel.${panel.title}`) ||
panel.title ||
(panel.url_path ? titleCase(panel.url_path) : ""),
});
@customElement("ha-navigation-picker")
export class HaNavigationPicker extends LitElement {
@property() public hass?: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() private _opened = false;
private navigationItemsLoaded = false;
private navigationItems: NavigationItem[] = DEFAULT_ITEMS;
@query("ha-combo-box", true) private comboBox!: HaComboBox;
protected render(): TemplateResult {
return html`
<ha-combo-box
.hass=${this.hass}
item-value-path="path"
item-label-path="path"
.value=${this._value}
allow-custom-value
.filteredItems=${this.navigationItems}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
`;
}
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
if (this._opened && !this.navigationItemsLoaded) {
this._loadNavigationItems();
}
}
private async _loadNavigationItems() {
this.navigationItemsLoaded = true;
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
id,
...panel,
}));
const lovelacePanels = panels.filter(
(panel) => panel.component_name === "lovelace"
);
const viewConfigs = await Promise.all(
lovelacePanels.map((panel) =>
fetchConfig(
this.hass!.connection,
// path should be null to fetch default lovelace panel
panel.url_path === "lovelace" ? null : panel.url_path,
true
)
.then((config) => [panel.id, config] as [string, LovelaceConfig])
.catch((_) => [panel.id, undefined] as [string, undefined])
)
);
const panelViewConfig = new Map(viewConfigs);
this.navigationItems = [];
for (const panel of panels) {
this.navigationItems.push(createPanelNavigationItem(this.hass!, panel));
const config = panelViewConfig.get(panel.id);
if (!config) continue;
config.views.forEach((view, index) =>
this.navigationItems.push(
createViewNavigationItem(panel.url_path, view, index)
)
);
}
this.comboBox.filteredItems = this.navigationItems;
}
protected shouldUpdate(changedProps: PropertyValues) {
return !this._opened || changedProps.has("_opened");
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
this._setValue(ev.detail.value);
}
private _setValue(value: string) {
this.value = value;
fireEvent(
this,
"value-changed",
{ value: this._value },
{
bubbles: false,
composed: false,
}
);
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
const characterCount = filterString.length;
if (characterCount >= 2) {
const filteredItems: NavigationItem[] = [];
this.navigationItems.forEach((item) => {
if (
item.path.toLowerCase().includes(filterString) ||
item.title.toLowerCase().includes(filterString)
) {
filteredItems.push(item);
}
});
if (filteredItems.length > 0) {
this.comboBox.filteredItems = filteredItems;
} else {
this.comboBox.filteredItems = [];
}
} else {
this.comboBox.filteredItems = this.navigationItems;
}
}
private get _value() {
return this.value || "";
}
static get styles() {
return css`
ha-icon,
ha-svg-icon {
color: var(--primary-text-color);
position: relative;
bottom: 0px;
}
*[slot="prefix"] {
margin-right: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-navigation-picker": HaNavigationPicker;
}
}

View File

@ -0,0 +1,47 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { NavigationSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-navigation-picker";
@customElement("ha-selector-navigation")
export class HaNavigationSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: NavigationSelector;
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-navigation-picker
.hass=${this.hass}
.label=${this.label}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
@value-changed=${this._valueChanged}
></ha-navigation-picker>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", { value: ev.detail.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-navigation": HaNavigationSelector;
}
}

View File

@ -16,6 +16,7 @@ import "./ha-selector-device";
import "./ha-selector-duration";
import "./ha-selector-entity";
import "./ha-selector-file";
import "./ha-selector-navigation";
import "./ha-selector-number";
import "./ha-selector-object";
import "./ha-selector-select";

View File

@ -21,6 +21,7 @@ export type Selector =
| IconSelector
| LocationSelector
| MediaSelector
| NavigationSelector
| NumberSelector
| ObjectSelector
| SelectSelector
@ -171,6 +172,11 @@ export interface MediaSelectorValue {
};
}
export interface NavigationSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
navigation: {};
}
export interface NumberSelector {
number: {
min?: number;

View File

@ -14,6 +14,7 @@ import {
import { ServiceAction } from "../../../data/script";
import { HomeAssistant } from "../../../types";
import { EditorTarget } from "../editor/types";
import "../../../components/ha-navigation-picker";
@customElement("hui-action-editor")
export class HuiActionEditor extends LitElement {
@ -89,14 +90,14 @@ export class HuiActionEditor extends LitElement {
</div>
${this.config?.action === "navigate"
? html`
<ha-textfield
label=${this.hass!.localize(
<ha-navigation-picker
.hass=${this.hass}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.action-editor.navigation_path"
)}
.value=${this._navigation_path}
.configValue=${"navigation_path"}
@input=${this._valueChanged}
></ha-textfield>
@value-changed=${this._navigateValueChanged}
></ha-navigation-picker>
`
: ""}
${this.config?.action === "url"
@ -193,6 +194,16 @@ export class HuiActionEditor extends LitElement {
fireEvent(this, "value-changed", { value });
}
private _navigateValueChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = {
...this.config!,
navigation_path: ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return css`
.dropdown {

View File

@ -26,7 +26,11 @@ const SCHEMA = [
name: "",
type: "grid",
schema: [
{ name: "navigation_path", required: false, selector: { text: {} } },
{
name: "navigation_path",
required: false,
selector: { navigation: {} },
},
{ name: "theme", required: false, selector: { theme: {} } },
],
},