mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-26 10:46:35 +00:00
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:
parent
977fdd9fbb
commit
eac13980ff
4
src/common/string/title-case.ts
Normal file
4
src/common/string/title-case.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const titleCase = (s) =>
|
||||||
|
s.replace(/^_*(.)|_+(.)/g, (_s, c, d) =>
|
||||||
|
c ? c.toUpperCase() : " " + d.toUpperCase()
|
||||||
|
);
|
221
src/components/ha-navigation-picker.ts
Normal file
221
src/components/ha-navigation-picker.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
47
src/components/ha-selector/ha-selector-navigation.ts
Normal file
47
src/components/ha-selector/ha-selector-navigation.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ import "./ha-selector-device";
|
|||||||
import "./ha-selector-duration";
|
import "./ha-selector-duration";
|
||||||
import "./ha-selector-entity";
|
import "./ha-selector-entity";
|
||||||
import "./ha-selector-file";
|
import "./ha-selector-file";
|
||||||
|
import "./ha-selector-navigation";
|
||||||
import "./ha-selector-number";
|
import "./ha-selector-number";
|
||||||
import "./ha-selector-object";
|
import "./ha-selector-object";
|
||||||
import "./ha-selector-select";
|
import "./ha-selector-select";
|
||||||
|
@ -21,6 +21,7 @@ export type Selector =
|
|||||||
| IconSelector
|
| IconSelector
|
||||||
| LocationSelector
|
| LocationSelector
|
||||||
| MediaSelector
|
| MediaSelector
|
||||||
|
| NavigationSelector
|
||||||
| NumberSelector
|
| NumberSelector
|
||||||
| ObjectSelector
|
| ObjectSelector
|
||||||
| SelectSelector
|
| SelectSelector
|
||||||
@ -171,6 +172,11 @@ export interface MediaSelectorValue {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NavigationSelector {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
navigation: {};
|
||||||
|
}
|
||||||
|
|
||||||
export interface NumberSelector {
|
export interface NumberSelector {
|
||||||
number: {
|
number: {
|
||||||
min?: number;
|
min?: number;
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
import { ServiceAction } from "../../../data/script";
|
import { ServiceAction } from "../../../data/script";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { EditorTarget } from "../editor/types";
|
import { EditorTarget } from "../editor/types";
|
||||||
|
import "../../../components/ha-navigation-picker";
|
||||||
|
|
||||||
@customElement("hui-action-editor")
|
@customElement("hui-action-editor")
|
||||||
export class HuiActionEditor extends LitElement {
|
export class HuiActionEditor extends LitElement {
|
||||||
@ -89,14 +90,14 @@ export class HuiActionEditor extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${this.config?.action === "navigate"
|
${this.config?.action === "navigate"
|
||||||
? html`
|
? html`
|
||||||
<ha-textfield
|
<ha-navigation-picker
|
||||||
label=${this.hass!.localize(
|
.hass=${this.hass}
|
||||||
|
.label=${this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.action-editor.navigation_path"
|
"ui.panel.lovelace.editor.action-editor.navigation_path"
|
||||||
)}
|
)}
|
||||||
.value=${this._navigation_path}
|
.value=${this._navigation_path}
|
||||||
.configValue=${"navigation_path"}
|
@value-changed=${this._navigateValueChanged}
|
||||||
@input=${this._valueChanged}
|
></ha-navigation-picker>
|
||||||
></ha-textfield>
|
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
${this.config?.action === "url"
|
${this.config?.action === "url"
|
||||||
@ -193,6 +194,16 @@ export class HuiActionEditor extends LitElement {
|
|||||||
fireEvent(this, "value-changed", { value });
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
.dropdown {
|
.dropdown {
|
||||||
|
@ -26,7 +26,11 @@ const SCHEMA = [
|
|||||||
name: "",
|
name: "",
|
||||||
type: "grid",
|
type: "grid",
|
||||||
schema: [
|
schema: [
|
||||||
{ name: "navigation_path", required: false, selector: { text: {} } },
|
{
|
||||||
|
name: "navigation_path",
|
||||||
|
required: false,
|
||||||
|
selector: { navigation: {} },
|
||||||
|
},
|
||||||
{ name: "theme", required: false, selector: { theme: {} } },
|
{ name: "theme", required: false, selector: { theme: {} } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user