mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +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-entity";
|
||||
import "./ha-selector-file";
|
||||
import "./ha-selector-navigation";
|
||||
import "./ha-selector-number";
|
||||
import "./ha-selector-object";
|
||||
import "./ha-selector-select";
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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: {} } },
|
||||
],
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user