mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-19 04:11:00 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb5cca794 | ||
|
|
6ddbb68444 | ||
|
|
906cd7bbec | ||
|
|
30467c3a51 | ||
|
|
fc760b9325 |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../data/page_navigation";
|
||||
import type { HomeAssistant, HomeAssistantConfig } from "../../types";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { isComponentLoaded } from "./is_component_loaded";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../data/page_navigation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { canShowPage } from "./can_show_page";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import "./ha-icon-next";
|
||||
import "./ha-svg-icon";
|
||||
import "./item/ha-list-item-button";
|
||||
import "./list/ha-list-nav";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../data/page_navigation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-config-navigation-list")
|
||||
|
||||
@@ -8,8 +8,6 @@ import "./ha-ripple";
|
||||
export class HaTab extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public active = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
// A short note next to the name, like how many updates wait there
|
||||
@@ -47,7 +45,7 @@ export class HaTab extends LitElement {
|
||||
div {
|
||||
padding: 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
@@ -59,11 +57,6 @@ export class HaTab extends LitElement {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:host(:not([narrow])) div {
|
||||
flex-direction: row;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0 var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
@@ -85,29 +78,10 @@ export class HaTab extends LitElement {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host(:not([narrow])[active]) div {
|
||||
:host([active]) div {
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([narrow]) div {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
::slotted([slot="icon"]) {
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
|
||||
:host(:not([narrow])) ::slotted([slot="icon"]) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div:focus-visible:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
||||
@@ -232,8 +232,11 @@ export class HaToast extends LitElement {
|
||||
inset-block-start: auto;
|
||||
inset-inline-end: auto;
|
||||
inset-block-end: calc(
|
||||
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) +
|
||||
var(--ha-toast-bottom-offset, 0px)
|
||||
max(
|
||||
var(--safe-area-inset-bottom, 0px),
|
||||
var(--ha-bottom-bar-height, 0px)
|
||||
) +
|
||||
var(--ha-space-4) + var(--ha-toast-bottom-offset, 0px)
|
||||
);
|
||||
inset-inline-start: 50%;
|
||||
margin: 0;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
translationKey?: string;
|
||||
component?: string | string[];
|
||||
name?: string;
|
||||
core?: boolean;
|
||||
/** Hide from non-admin users in filtered navigation and quick bar. */
|
||||
adminOnly?: boolean;
|
||||
iconPath?: string;
|
||||
iconSecondaryPath?: string;
|
||||
iconViewBox?: string;
|
||||
description?: string;
|
||||
iconColor?: string;
|
||||
// Shown next to the name of the tab
|
||||
badge?: string;
|
||||
info?: any;
|
||||
filter?: (hass: Pick<HomeAssistant, "entities">) => boolean;
|
||||
}
|
||||
+3
-3
@@ -9,7 +9,7 @@ import {
|
||||
mdiPlayBoxMultiple,
|
||||
} from "@mdi/js";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "./page_navigation";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
export const APP_PANEL = "app";
|
||||
@@ -109,8 +109,8 @@ export const getConfigSubpageTitle = (
|
||||
): string | undefined => {
|
||||
const sections = Object.entries(configSections);
|
||||
|
||||
// Prefer pages with a full translation key or a name. The dashboard
|
||||
// sections also list pages like /config/integrations under a broader title.
|
||||
// Prefer pages with a full translation key or a name. The Settings page
|
||||
// entries list pages like /config/integrations again with a short key.
|
||||
for (const [, pages] of sections) {
|
||||
const pageNav = pages.find((nav) => path.startsWith(nav.path));
|
||||
if (!pageNav) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "../common/config/filter_navigation_pages";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "./page_navigation";
|
||||
import { configSections } from "../panels/config/config-sections";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@@ -57,6 +57,7 @@ export const demoConfig: HassConfig = {
|
||||
"date",
|
||||
"fan",
|
||||
"automation",
|
||||
"blueprint",
|
||||
"weather",
|
||||
"climate",
|
||||
"stt",
|
||||
|
||||
+18
-12
@@ -155,9 +155,18 @@ class HassSubpage extends LitElement {
|
||||
1px - var(--header-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
) - max(
|
||||
var(--safe-area-inset-bottom, 0px),
|
||||
var(--ha-bottom-bar-height, 0px)
|
||||
)
|
||||
);
|
||||
padding-bottom: max(
|
||||
0px,
|
||||
var(--safe-area-inset-bottom, 0px) - var(
|
||||
--ha-bottom-bar-height,
|
||||
0px
|
||||
)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom, 0px);
|
||||
margin-right: var(--safe-area-inset-right);
|
||||
overflow-y: auto;
|
||||
overflow: auto;
|
||||
@@ -184,7 +193,13 @@ class HassSubpage extends LitElement {
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
inset-inline-end: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
inset-inline-start: initial;
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
bottom: calc(
|
||||
16px +
|
||||
max(
|
||||
var(--safe-area-inset-bottom, 0px),
|
||||
var(--ha-bottom-bar-height, 0px)
|
||||
)
|
||||
);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -192,15 +207,6 @@ class HassSubpage extends LitElement {
|
||||
gap: var(--ha-space-2);
|
||||
--ha-button-box-shadow: var(--ha-box-shadow-l);
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: calc(24px + var(--safe-area-inset-bottom, 0px));
|
||||
right: calc(24px + var(--safe-area-inset-right, 0px));
|
||||
inset-inline-end: calc(24px + var(--safe-area-inset-right, 0px));
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
mdiUnfoldLessHorizontal,
|
||||
mdiUnfoldMoreHorizontal,
|
||||
} from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent, type HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
@@ -41,15 +40,11 @@ import "../components/ha-icon-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import "../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../components/input/ha-input-search";
|
||||
import {
|
||||
configContext,
|
||||
entitiesContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { PageNavigation } from "../data/page_navigation";
|
||||
import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./hass-tabs-subpage";
|
||||
import type { PageNavigation } from "./hass-tabs-subpage";
|
||||
|
||||
@customElement("hass-tabs-subpage-data-table")
|
||||
export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
@@ -58,14 +53,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@@ -109,13 +96,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
@property({ attribute: "has-fab", type: Boolean, reflect: true })
|
||||
public hasFab = false;
|
||||
|
||||
/**
|
||||
* Show tabs on top or at bottom (narrow) of the page.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@property({ attribute: "show-tabs", type: Boolean, reflect: true })
|
||||
public showTabs = false;
|
||||
|
||||
/**
|
||||
* Add an extra row at the bottom of the data table
|
||||
* @type {TemplateResult}
|
||||
@@ -243,18 +223,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (
|
||||
changedProperties.has("tabs") ||
|
||||
changedProperties.has("_hassConfig") ||
|
||||
changedProperties.has("_entities")
|
||||
) {
|
||||
this.showTabs =
|
||||
this.tabs.filter((page) =>
|
||||
canShowPage({ ...this._hassConfig, entities: this._entities }, page)
|
||||
).length > 1;
|
||||
}
|
||||
|
||||
protected willUpdate() {
|
||||
if (this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
@@ -786,7 +755,10 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--data-table-border-width: 0;
|
||||
--data-table-empty-row-height: var(--safe-area-inset-bottom, 0px);
|
||||
--data-table-empty-row-height: max(
|
||||
0px,
|
||||
var(--safe-area-inset-bottom, 0px) - var(--ha-bottom-bar-height, 0px)
|
||||
);
|
||||
/* Same insets as the content padding of hass-tabs-subpage */
|
||||
--data-table-inset-right: var(--safe-area-inset-right, 0px);
|
||||
}
|
||||
@@ -805,22 +777,20 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
display: block;
|
||||
}
|
||||
/* Last content row should keep the same padding above the fab as the fab
|
||||
has to the bottom (16px standard fab bottom padding) + the safe-area inset. */
|
||||
has to the bottom (16px standard fab bottom padding) + the safe-area inset
|
||||
not already covered by a bottom bar. */
|
||||
:host([has-fab]) ha-data-table {
|
||||
--data-table-empty-row-height: calc(
|
||||
48px + 16px * 2 + var(--safe-area-inset-bottom, 0px)
|
||||
48px + 16px * 2 +
|
||||
max(
|
||||
0px,
|
||||
var(--safe-area-inset-bottom, 0px) - var(
|
||||
--ha-bottom-bar-height,
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
/* In narrow view with tabs shown at the bottom, the tab bar already
|
||||
accounts for safe-area-inset-bottom. No extra empty-row height is needed. */
|
||||
:host([narrow][show-tabs]:not([has-fab])) ha-data-table {
|
||||
--data-table-empty-row-height: 0px;
|
||||
}
|
||||
/* Reserve space for fab + doubled narrow-mode bottom padding (28px * 2)
|
||||
when using narrow layout with bottom tabs. */
|
||||
:host([narrow][show-tabs][has-fab]) ha-data-table {
|
||||
--data-table-empty-row-height: calc(48px + 28px * 2);
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
height: calc(
|
||||
@@ -836,6 +806,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
:host([narrow]) hass-tabs-subpage {
|
||||
--main-title-margin: 0;
|
||||
/* The empty row of the data table clears the safe area */
|
||||
--tabs-subpage-content-padding-bottom: 0px;
|
||||
}
|
||||
:host([narrow]) {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
|
||||
+184
-113
@@ -16,19 +16,25 @@ import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { getHistoryState, navigate } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../common/translations/localize";
|
||||
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
|
||||
import { handleBackClick } from "./back-navigation";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import "../components/ha-tab";
|
||||
import "../components/ha-tab-group";
|
||||
import "../components/ha-tab-group-tab";
|
||||
import {
|
||||
configContext,
|
||||
entitiesContext,
|
||||
internationalizationContext,
|
||||
narrowViewportContext,
|
||||
} from "../data/context";
|
||||
import type { PageNavigation } from "../data/page_navigation";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
|
||||
@@ -37,25 +43,6 @@ const normalizePathname = (pathname: string): string =>
|
||||
? pathname.slice(0, -1)
|
||||
: pathname;
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
translationKey?: string;
|
||||
component?: string | string[];
|
||||
name?: string;
|
||||
core?: boolean;
|
||||
/** Hide from non-admin users in filtered navigation and quick bar. */
|
||||
adminOnly?: boolean;
|
||||
iconPath?: string;
|
||||
iconSecondaryPath?: string;
|
||||
iconViewBox?: string;
|
||||
description?: string;
|
||||
iconColor?: string;
|
||||
// Shown next to the name of the tab
|
||||
badge?: string;
|
||||
info?: any;
|
||||
filter?: (hass: Pick<HomeAssistant, "entities">) => boolean;
|
||||
}
|
||||
|
||||
@customElement("hass-tabs-subpage")
|
||||
export class HassTabsSubpage extends LitElement {
|
||||
// Unread, kept for callers that still pass it until they move to contexts
|
||||
@@ -102,7 +89,6 @@ export class HassTabsSubpage extends LitElement {
|
||||
|
||||
/**
|
||||
* Whether tabs are shown (2 or more tabs visible).
|
||||
* When both, show-tabs and narrow are true, tabs are shown as bottom bar.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "show-tabs", reflect: true })
|
||||
@@ -115,45 +101,31 @@ export class HassTabsSubpage extends LitElement {
|
||||
// @ts-ignore
|
||||
@restoreScroll(".content") private _savedScrollPos?: number;
|
||||
|
||||
private _getTabs = memoizeOne(
|
||||
private _shownTabs = memoizeOne(
|
||||
(
|
||||
tabs: PageNavigation[],
|
||||
activeTab: PageNavigation | undefined,
|
||||
_components,
|
||||
_language,
|
||||
_userData,
|
||||
_narrow,
|
||||
localizeFunc,
|
||||
entities: ContextType<typeof entitiesContext>
|
||||
) => {
|
||||
const shownTabs = tabs.filter((page) =>
|
||||
) =>
|
||||
tabs.filter((page) =>
|
||||
canShowPage({ ...this._hassConfig, entities }, page)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
if (shownTabs.length < 2) {
|
||||
this.showTabs = false;
|
||||
if (shownTabs.length === 1) {
|
||||
const page = shownTabs[0];
|
||||
return [
|
||||
page.translationKey ? localizeFunc(page.translationKey) : page.name,
|
||||
];
|
||||
}
|
||||
return [""];
|
||||
}
|
||||
|
||||
this.showTabs = true;
|
||||
return shownTabs.map(
|
||||
private _renderTabs = memoizeOne(
|
||||
(
|
||||
shownTabs: PageNavigation[],
|
||||
activeTab: PageNavigation | undefined,
|
||||
localizeFunc: LocalizeFunc
|
||||
) =>
|
||||
shownTabs.map(
|
||||
(page) => html`
|
||||
<a href=${page.path} @click=${this._tabClicked}>
|
||||
<ha-tab
|
||||
.active=${page.path === activeTab?.path}
|
||||
.narrow=${this._narrow}
|
||||
.badge=${page.badge}
|
||||
.name=${
|
||||
page.translationKey
|
||||
? localizeFunc(page.translationKey)
|
||||
: page.name
|
||||
}
|
||||
.name=${this._tabName(page, localizeFunc)}
|
||||
>
|
||||
${
|
||||
page.iconPath
|
||||
@@ -166,37 +138,83 @@ export class HassTabsSubpage extends LitElement {
|
||||
</ha-tab>
|
||||
</a>
|
||||
`
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
private _renderTabRow = memoizeOne(
|
||||
(
|
||||
shownTabs: PageNavigation[],
|
||||
activeTab: PageNavigation | undefined,
|
||||
localizeFunc: LocalizeFunc
|
||||
) => html`
|
||||
<ha-tab-group
|
||||
class="tab-row"
|
||||
activation="manual"
|
||||
without-scroll-controls
|
||||
@wa-tab-show=${this._tabShown}
|
||||
>
|
||||
${shownTabs.map(
|
||||
(page) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${page.path}
|
||||
.active=${page.path === activeTab?.path}
|
||||
>
|
||||
<a href=${page.path} tabindex="-1" @click=${this._tabRowClicked}>
|
||||
${this._tabName(page, localizeFunc)}
|
||||
${
|
||||
page.badge
|
||||
? html`<span class="badge">${page.badge}</span>`
|
||||
: nothing
|
||||
}
|
||||
</a>
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
`
|
||||
);
|
||||
|
||||
private _tabName(page: PageNavigation, localizeFunc: LocalizeFunc) {
|
||||
return page.translationKey
|
||||
? localizeFunc(page.translationKey as LocalizeKeys)
|
||||
: page.name;
|
||||
}
|
||||
|
||||
private _getShownTabs() {
|
||||
return this._shownTabs(
|
||||
this.tabs,
|
||||
this._hassConfig.config.components,
|
||||
this._hassConfig.userData,
|
||||
this._entities
|
||||
);
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues<this>) {
|
||||
this.toggleAttribute("narrow", this._narrow);
|
||||
|
||||
if (changedProperties.has("route")) {
|
||||
if (changedProperties.has("route") || changedProperties.has("tabs")) {
|
||||
const currentPath = `${this.route.prefix}${this.route.path}`;
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
this._isActiveTabPath(tab.path, currentPath)
|
||||
);
|
||||
}
|
||||
this.showTabs = this._getShownTabs().length > 1;
|
||||
super.willUpdate(changedProperties);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const tabs = this._getTabs(
|
||||
this.tabs,
|
||||
this._activeTab,
|
||||
this._hassConfig.config.components,
|
||||
this._i18n.language,
|
||||
this._hassConfig.userData,
|
||||
this._narrow,
|
||||
this.localizeFunc || this._i18n.localize,
|
||||
this._entities
|
||||
);
|
||||
const localizeFunc = this.localizeFunc || this._i18n.localize;
|
||||
const shownTabs = this._getShownTabs();
|
||||
const titleTab = this.showTabs ? this._activeTab : shownTabs[0];
|
||||
const title = titleTab ? this._tabName(titleTab, localizeFunc) : "";
|
||||
const tabRow = this.showTabs && this._narrow;
|
||||
const backPath = sanitizeNavigationPath(this.backPath);
|
||||
|
||||
return html`
|
||||
<div class="toolbar ${classMap({ narrow: this._narrow })}">
|
||||
<div
|
||||
class="toolbar ${classMap({ narrow: this._narrow, "has-tab-row": tabRow })}"
|
||||
>
|
||||
<slot name="toolbar">
|
||||
<div class="toolbar-content">
|
||||
${
|
||||
@@ -212,13 +230,19 @@ export class HassTabsSubpage extends LitElement {
|
||||
${
|
||||
this._narrow || !this.showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!this.showTabs ? tabs[0] : ""}</slot>
|
||||
<slot name="header">${title}</slot>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
this.showTabs && !this._narrow
|
||||
? html`<div id="tabbar">${tabs}</div>`
|
||||
? html`<div id="tabbar">
|
||||
${this._renderTabs(
|
||||
shownTabs,
|
||||
this._activeTab,
|
||||
localizeFunc
|
||||
)}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
<div id="toolbar-icon">
|
||||
@@ -226,12 +250,12 @@ export class HassTabsSubpage extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
${
|
||||
this.showTabs && this._narrow
|
||||
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
${
|
||||
tabRow
|
||||
? this._renderTabRow(shownTabs, this._activeTab, localizeFunc)
|
||||
: nothing
|
||||
}
|
||||
<div class="container">
|
||||
${
|
||||
this.pane
|
||||
@@ -302,12 +326,37 @@ export class HassTabsSubpage extends LitElement {
|
||||
await navigate(href, { replace: true });
|
||||
}
|
||||
|
||||
// Plain clicks are left to the tab group, which ignores the click that ends a
|
||||
// drag scroll. Other clicks keep the browser's link behavior.
|
||||
private _tabRowClicked(ev: MouseEvent): void {
|
||||
const href = isNavigationClick(ev);
|
||||
if (!href) {
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
// Without a matching tab the group marks the first tab active, so it
|
||||
// would not fire wa-tab-show for it.
|
||||
if (!this._activeTab) {
|
||||
ev.stopPropagation();
|
||||
navigate(href, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Click and keyboard activation in the tab row
|
||||
private _tabShown(ev: CustomEvent<{ name: string }>) {
|
||||
const path = ev.detail.name;
|
||||
if (path && path !== this._activeTab?.path) {
|
||||
navigate(path, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
@@ -319,9 +368,8 @@ export class HassTabsSubpage extends LitElement {
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: calc(
|
||||
100% - var(--header-height, 0px) - var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
ha-menu-button {
|
||||
@@ -341,6 +389,10 @@ export class HassTabsSubpage extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.toolbar.has-tab-row {
|
||||
border-bottom: none;
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
@@ -359,12 +411,10 @@ export class HassTabsSubpage extends LitElement {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#tabbar {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
font-size: var(--ha-font-size-m);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -374,25 +424,35 @@ export class HassTabsSubpage extends LitElement {
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
#tabbar.bottom-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0 calc(16px + var(--safe-area-inset-right))
|
||||
var(--safe-area-inset-bottom)
|
||||
calc(16px + var(--safe-area-inset-left));
|
||||
box-sizing: border-box;
|
||||
.tab-row {
|
||||
flex-shrink: 0;
|
||||
padding-left: var(--safe-area-inset-left, 0px);
|
||||
padding-right: var(--safe-area-inset-right, 0px);
|
||||
color: var(--sidebar-text-color);
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
justify-content: space-around;
|
||||
z-index: 2;
|
||||
font-size: var(--ha-font-size-s);
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
--ha-tab-track-color: transparent;
|
||||
}
|
||||
|
||||
#tabbar:not(.bottom-bar) {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
.tab-row ha-tab-group-tab::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
.tab-row a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.badge {
|
||||
padding: 0 var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
white-space: nowrap;
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
:host(:not([narrow])) #toolbar-icon {
|
||||
@@ -427,23 +487,37 @@ export class HassTabsSubpage extends LitElement {
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-bottom: var(--ha-bottom-bar-height, 0px);
|
||||
}
|
||||
:host([narrow]) .content {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
:host([narrow][show-tabs]) .content {
|
||||
/* Bottom bar reuses header height */
|
||||
margin-bottom: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-bottom, 0px)
|
||||
/* The fab spacer already clears the safe area. Pages that clear it
|
||||
themselves, like data tables, set the variable to 0px. */
|
||||
:host([narrow]:not([has-fab])) .content {
|
||||
padding-bottom: var(
|
||||
--tabs-subpage-content-padding-bottom,
|
||||
max(
|
||||
0px,
|
||||
var(--safe-area-inset-bottom, 0px) - var(
|
||||
--ha-bottom-bar-height,
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.content .fab-bottom-space {
|
||||
height: calc(64px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
:host([narrow][show-tabs]) .content .fab-bottom-space {
|
||||
height: calc(80px + var(--safe-area-inset-bottom, 0px));
|
||||
height: calc(
|
||||
64px +
|
||||
max(
|
||||
0px,
|
||||
var(--safe-area-inset-bottom, 0px) - var(
|
||||
--ha-bottom-bar-height,
|
||||
0px
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#fab {
|
||||
@@ -451,7 +525,13 @@ export class HassTabsSubpage extends LitElement {
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
inset-inline-end: calc(16px + var(--safe-area-inset-right));
|
||||
inset-inline-start: initial;
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
bottom: calc(
|
||||
16px +
|
||||
max(
|
||||
var(--safe-area-inset-bottom, 0px),
|
||||
var(--ha-bottom-bar-height, 0px)
|
||||
)
|
||||
);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -459,15 +539,6 @@ export class HassTabsSubpage extends LitElement {
|
||||
gap: var(--ha-space-2);
|
||||
--ha-button-box-shadow: var(--ha-box-shadow-l);
|
||||
}
|
||||
:host([narrow][show-tabs]) #fab {
|
||||
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
inset-inline-end: 24px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
|
||||
.pane {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
|
||||
@@ -291,8 +291,11 @@ class NotificationManager extends LitElement {
|
||||
inset-block-start: auto;
|
||||
inset-inline-end: auto;
|
||||
inset-block-end: calc(
|
||||
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) +
|
||||
var(--notification-stack-bottom-offset, 0px)
|
||||
max(
|
||||
var(--safe-area-inset-bottom, 0px),
|
||||
var(--ha-bottom-bar-height, 0px)
|
||||
) +
|
||||
var(--ha-space-4) + var(--notification-stack-bottom-offset, 0px)
|
||||
);
|
||||
inset-inline-start: 50%;
|
||||
margin: 0;
|
||||
|
||||
@@ -156,7 +156,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.tabs=${configSections.integration_credentials}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._getApplicationCredentials(
|
||||
|
||||
@@ -22,7 +22,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../data/page_navigation";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
showAreaRegistryDetailDialog,
|
||||
@@ -167,7 +167,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.isWide=${this.isWide}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.areas}
|
||||
.tabs=${configPageTabs["/config/areas"]}
|
||||
.route=${this.route}
|
||||
has-fab
|
||||
>
|
||||
|
||||
@@ -121,7 +121,7 @@ import {
|
||||
getLabelsTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
getAssistantsSortableKey,
|
||||
@@ -500,7 +500,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
back-path="/config"
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.tabs=${configPageTabs["/config/automation"]}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.search",
|
||||
{ number: automations.length }
|
||||
|
||||
@@ -54,7 +54,7 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { showAddBlueprintDialog } from "./show-dialog-import-blueprint";
|
||||
|
||||
type BlueprintMetaDataPath = BlueprintMetaData & {
|
||||
@@ -329,7 +329,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.tabs=${configPageTabs["/config/blueprint"]}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._processedBlueprints(
|
||||
this.blueprints,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
mdiAccessPointNetwork,
|
||||
mdiAccount,
|
||||
mdiApps,
|
||||
mdiBackupRestore,
|
||||
mdiBadgeAccountHorizontal,
|
||||
mdiBluetooth,
|
||||
@@ -41,7 +42,7 @@ import {
|
||||
mdiZWave,
|
||||
} from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../data/page_navigation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { mdiMqttLogo } from "../../resources/mqtt-logo-svg";
|
||||
|
||||
@@ -55,36 +56,144 @@ const getHasDomainCheck = (domain: string) => {
|
||||
checkRegistry(hass.entities);
|
||||
};
|
||||
|
||||
// Pages that the Settings page lists one by one, with their entry's
|
||||
// translation key under ui.panel.config.dashboard
|
||||
const dashboardEntryKeys = {
|
||||
"/config/integrations": "integrations",
|
||||
"/config/devices": "device_list",
|
||||
"/config/entities": "entities",
|
||||
"/config/helpers": "helpers",
|
||||
"/config/automation": "automation_list",
|
||||
"/config/scene": "scenes",
|
||||
"/config/script": "scripts",
|
||||
"/config/blueprint": "blueprints",
|
||||
"/config/areas": "area_list",
|
||||
"/config/labels": "labels",
|
||||
"/config/zone": "zones",
|
||||
} as const;
|
||||
|
||||
export type ConfigEntryPagePath = keyof typeof dashboardEntryKeys;
|
||||
|
||||
type ConfigEntryPage = PageNavigation & { path: ConfigEntryPagePath };
|
||||
|
||||
const devicesPages: ConfigEntryPage[] = [
|
||||
{
|
||||
component: "integrations",
|
||||
path: "/config/integrations",
|
||||
translationKey: "ui.panel.config.integrations.caption",
|
||||
iconPath: mdiPuzzle,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "devices",
|
||||
path: "/config/devices",
|
||||
translationKey: "ui.panel.config.devices.caption",
|
||||
iconPath: mdiDevices,
|
||||
iconColor: "#0277BD",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "entities",
|
||||
path: "/config/entities",
|
||||
translationKey: "ui.panel.config.entities.caption",
|
||||
iconPath: mdiShape,
|
||||
iconColor: "#7E57C2",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "helpers",
|
||||
path: "/config/helpers",
|
||||
translationKey: "ui.panel.config.helpers.caption",
|
||||
iconPath: mdiTools,
|
||||
iconColor: "#4D2EA4",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
];
|
||||
|
||||
const automationsPages: ConfigEntryPage[] = [
|
||||
{
|
||||
component: "automation",
|
||||
path: "/config/automation",
|
||||
translationKey: "ui.panel.config.automation.caption",
|
||||
iconPath: mdiRobot,
|
||||
iconColor: "#518C43",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "scene",
|
||||
path: "/config/scene",
|
||||
translationKey: "ui.panel.config.scene.caption",
|
||||
iconPath: mdiPalette,
|
||||
iconColor: "#00796B",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "script",
|
||||
path: "/config/script",
|
||||
translationKey: "ui.panel.config.script.caption",
|
||||
iconPath: mdiScriptText,
|
||||
iconColor: "#1B5E20",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "blueprint",
|
||||
path: "/config/blueprint",
|
||||
translationKey: "ui.panel.config.blueprint.caption",
|
||||
iconPath: mdiPaletteSwatch,
|
||||
iconColor: "#827717",
|
||||
adminOnly: true,
|
||||
},
|
||||
];
|
||||
|
||||
const areasPages: ConfigEntryPage[] = [
|
||||
{
|
||||
component: "areas",
|
||||
path: "/config/areas",
|
||||
translationKey: "ui.panel.config.areas.caption",
|
||||
iconPath: mdiSofa,
|
||||
iconColor: "#E48629",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "labels",
|
||||
path: "/config/labels",
|
||||
translationKey: "ui.panel.config.labels.caption",
|
||||
iconPath: mdiLabel,
|
||||
iconColor: "#D84315",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "zone",
|
||||
path: "/config/zone",
|
||||
translationKey: "ui.panel.config.zone.caption",
|
||||
iconPath: mdiMapMarkerRadius,
|
||||
iconColor: "#A1662F",
|
||||
adminOnly: true,
|
||||
},
|
||||
];
|
||||
|
||||
const dashboardEntries = (pages: ConfigEntryPage[]): PageNavigation[] =>
|
||||
pages.map((page) => ({
|
||||
...page,
|
||||
translationKey: dashboardEntryKeys[page.path],
|
||||
}));
|
||||
|
||||
export const configSections: Record<string, PageNavigation[]> = {
|
||||
dashboard_devices: dashboardEntries(devicesPages),
|
||||
dashboard_automations: dashboardEntries(automationsPages),
|
||||
dashboard_areas: dashboardEntries(areasPages),
|
||||
dashboard: [
|
||||
{
|
||||
path: "/config/integrations",
|
||||
translationKey: "devices",
|
||||
iconPath: mdiDevices,
|
||||
iconColor: "#0D47A1",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
path: "/config/automation",
|
||||
translationKey: "automations",
|
||||
iconPath: mdiRobot,
|
||||
iconColor: "#518C43",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
path: "/config/areas",
|
||||
translationKey: "areas",
|
||||
iconPath: mdiSofa,
|
||||
iconColor: "#E48629",
|
||||
component: "zone",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
path: "/config/apps",
|
||||
translationKey: "apps",
|
||||
iconPath: mdiPuzzle,
|
||||
iconPath: mdiApps,
|
||||
iconColor: "#F1C447",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
@@ -287,78 +396,8 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
devices: [
|
||||
{
|
||||
component: "integrations",
|
||||
path: "/config/integrations",
|
||||
translationKey: "ui.panel.config.integrations.caption",
|
||||
iconPath: mdiPuzzle,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "devices",
|
||||
path: "/config/devices",
|
||||
translationKey: "ui.panel.config.devices.caption",
|
||||
iconPath: mdiDevices,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "entities",
|
||||
path: "/config/entities",
|
||||
translationKey: "ui.panel.config.entities.caption",
|
||||
iconPath: mdiShape,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "helpers",
|
||||
path: "/config/helpers",
|
||||
translationKey: "ui.panel.config.helpers.caption",
|
||||
iconPath: mdiTools,
|
||||
iconColor: "#4D2EA4",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
automations: [
|
||||
{
|
||||
component: "automation",
|
||||
path: "/config/automation",
|
||||
translationKey: "ui.panel.config.automation.caption",
|
||||
iconPath: mdiRobot,
|
||||
iconColor: "#518C43",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "scene",
|
||||
path: "/config/scene",
|
||||
translationKey: "ui.panel.config.scene.caption",
|
||||
iconPath: mdiPalette,
|
||||
iconColor: "#518C43",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "script",
|
||||
path: "/config/script",
|
||||
translationKey: "ui.panel.config.script.caption",
|
||||
iconPath: mdiScriptText,
|
||||
iconColor: "#518C43",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "blueprint",
|
||||
path: "/config/blueprint",
|
||||
translationKey: "ui.panel.config.blueprint.caption",
|
||||
iconPath: mdiPaletteSwatch,
|
||||
iconColor: "#518C43",
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
devices: devicesPages,
|
||||
automations: automationsPages,
|
||||
tags: [
|
||||
{
|
||||
component: "tag",
|
||||
@@ -476,34 +515,7 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
areas: [
|
||||
{
|
||||
component: "areas",
|
||||
path: "/config/areas",
|
||||
translationKey: "ui.panel.config.areas.caption",
|
||||
iconPath: mdiSofa,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "labels",
|
||||
path: "/config/labels",
|
||||
translationKey: "ui.panel.config.labels.caption",
|
||||
iconPath: mdiLabel,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "zone",
|
||||
path: "/config/zone",
|
||||
translationKey: "ui.panel.config.zone.caption",
|
||||
iconPath: mdiMapMarkerRadius,
|
||||
iconColor: "#E48629",
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
areas: areasPages,
|
||||
general: [
|
||||
{
|
||||
path: "/config/general",
|
||||
@@ -611,3 +623,13 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const singlePageTabs = (pages: ConfigEntryPage[]) =>
|
||||
Object.fromEntries(pages.map((page) => [page.path, [page]]));
|
||||
|
||||
// Pages reached directly from the Settings page, each as its own only tab
|
||||
export const configPageTabs = {
|
||||
...singlePageTabs(devicesPages),
|
||||
...singlePageTabs(automationsPages),
|
||||
...singlePageTabs(areasPages),
|
||||
} as Record<ConfigEntryPagePath, PageNavigation[]>;
|
||||
@@ -10,6 +10,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { filterNavigationPages } from "../../../common/config/filter_navigation_pages";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { ctrlOrCmdLabel } from "../../../common/keyboard/ctrl-or-cmd";
|
||||
import "../../../components/ha-card";
|
||||
@@ -36,7 +37,7 @@ import {
|
||||
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../data/page_navigation";
|
||||
import { ChildPanelReady } from "../../../layouts/panel-ready";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -180,6 +181,9 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
? pages.filter((page) => page.path !== "/config/apps")
|
||||
: pages;
|
||||
return [
|
||||
configSections.dashboard_devices,
|
||||
configSections.dashboard_automations,
|
||||
configSections.dashboard_areas,
|
||||
isCloudLoaded
|
||||
? filterApps([
|
||||
{
|
||||
@@ -349,7 +353,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
this.hass.userData?.apps_info_dismissed,
|
||||
isComponentLoaded(this.hass.config, "hassio")
|
||||
).map((categoryPages) =>
|
||||
categoryPages.length === 0
|
||||
filterNavigationPages(this.hass, categoryPages).length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
|
||||
@@ -8,7 +8,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../data/page_navigation";
|
||||
import {
|
||||
childPanelReadyContext,
|
||||
type RegisterChildPanelReady,
|
||||
|
||||
@@ -97,7 +97,7 @@ import {
|
||||
getLabelsTableColumn,
|
||||
getModifiedAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
@@ -876,7 +876,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.tabs=${configPageTabs["/config/devices"]}
|
||||
.route=${this.route}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.search",
|
||||
|
||||
@@ -38,7 +38,7 @@ import "./components/ha-energy-gas-settings";
|
||||
import "./components/ha-energy-water-settings";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../data/page_navigation";
|
||||
import { showEnergyCustomiseDialog } from "./dialogs/show-dialog-energy-customise";
|
||||
|
||||
const INITIAL_CONFIG: EnergyPreferences = {
|
||||
|
||||
@@ -116,7 +116,7 @@ import {
|
||||
getLabelsTableColumn,
|
||||
getModifiedAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import type { Helper } from "../helpers/const";
|
||||
import { isHelperDomain } from "../helpers/const";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
@@ -811,7 +811,7 @@ export class HaConfigEntities extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.tabs=${configPageTabs["/config/entities"]}
|
||||
.columns=${this._columns(this.hass.localize, filteredEntities)}
|
||||
.data=${filteredEntities}
|
||||
.searchLabel=${this.hass.localize(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../data/page_navigation";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
export const hardwareTabs = (hass: HomeAssistant): PageNavigation[] => {
|
||||
|
||||
@@ -120,7 +120,7 @@ import {
|
||||
getEntityIdTableColumn,
|
||||
getLabelsTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { renderConfigEntryError } from "../integrations/ha-config-integration-page";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
@@ -650,7 +650,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.tabs=${configPageTabs["/config/helpers"]}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.search",
|
||||
{ number: helpers.length }
|
||||
|
||||
@@ -68,7 +68,7 @@ import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { isHelperDomain } from "../helpers/const";
|
||||
import "./ha-config-flow-card";
|
||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
@@ -532,7 +532,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
.hass=${this.hass}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.tabs=${configPageTabs["/config/integrations"]}
|
||||
has-fab
|
||||
>
|
||||
${
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import {
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../data/page_navigation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import {
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../data/page_navigation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-relative-time";
|
||||
import type { InfraredDevice } from "../../../../../data/infrared";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../data/page_navigation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import {
|
||||
type RadioFrequencyTransmitter,
|
||||
} from "../../../../../data/radio_frequency";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../data/page_navigation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { fetchZHADevice } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-error-screen";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../data/page_navigation";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import "./device-page/zha-device-bindings-pane";
|
||||
@@ -299,8 +299,7 @@ class ZHADevicePage extends LitElement {
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: var(--ha-space-4) var(--ha-space-4)
|
||||
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-20);
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
||||
@@ -14,7 +14,7 @@ import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import type { ZHAGroup } from "../../../../../data/zha";
|
||||
import { fetchGroups } from "../../../../../data/zha";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../data/page_navigation";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
getCreatedAtTableColumn,
|
||||
getModifiedAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { showLabelDetailDialog } from "./show-dialog-label-detail";
|
||||
|
||||
type ConfigTranslationKey = FlattenObjectKeys<
|
||||
@@ -257,7 +257,7 @@ export class HaConfigLabels extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.areas}
|
||||
.tabs=${configPageTabs["/config/labels"]}
|
||||
.columns=${this._columns(this.hass.localize, this.narrow)}
|
||||
.data=${this._data(this._labels)}
|
||||
.loading=${this._loading}
|
||||
|
||||
@@ -107,7 +107,7 @@ import {
|
||||
getLabelsTableColumn,
|
||||
renderRelativeTimeColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
getAssistantsSortableKey,
|
||||
@@ -460,7 +460,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.tabs=${configPageTabs["/config/scene"]}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.search",
|
||||
{ number: scenes.length }
|
||||
|
||||
@@ -112,7 +112,7 @@ import {
|
||||
getLabelsTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
getAssistantsSortableKey,
|
||||
@@ -431,7 +431,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.tabs=${configPageTabs["/config/script"]}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.script.picker.search",
|
||||
{ number: scripts.length }
|
||||
|
||||
@@ -53,7 +53,7 @@ import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../config-sections";
|
||||
import { configPageTabs } from "../config-sections";
|
||||
import { showHomeZoneDetailDialog } from "./show-dialog-home-zone-detail";
|
||||
import { showZoneDetailDialog } from "./show-dialog-zone-detail";
|
||||
|
||||
@@ -361,7 +361,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.areas}
|
||||
.tabs=${configPageTabs["/config/zone"]}
|
||||
has-fab
|
||||
>
|
||||
${
|
||||
|
||||
@@ -44,7 +44,7 @@ import type { HaIconButton } from "../../../components/ha-icon-button";
|
||||
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../data/page_navigation";
|
||||
import type { HomeAssistantUI, Route } from "../../../types";
|
||||
import { showMarketplaceAddFromLink } from "../tools/add-from-link";
|
||||
import "../components/ha-marketplace-discover";
|
||||
|
||||
@@ -12,7 +12,7 @@ import "./ha-pick-theme-row";
|
||||
import { isExternal } from "../../data/external";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import "../../layouts/hass-subpage";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../data/page_navigation";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import { showEditProfileDialog } from "./show-dialog-edit-profile";
|
||||
|
||||
@@ -8,6 +8,9 @@ export const mainStyles = css`
|
||||
/* for header */
|
||||
--header-height: 56px;
|
||||
|
||||
/* Space taken by bottom chrome, including the safe-area inset it covers */
|
||||
--ha-bottom-bar-height: 0px;
|
||||
|
||||
/* opacity for dark text on a light background */
|
||||
--dark-divider-opacity: 0.12;
|
||||
--dark-disabled-opacity: 0.38; /* or hint text or icon */
|
||||
|
||||
@@ -2825,13 +2825,37 @@
|
||||
},
|
||||
"header": "Configure Home Assistant",
|
||||
"dashboard": {
|
||||
"devices": {
|
||||
"main": "Devices & services",
|
||||
"secondary": "Integrations, devices, entities, and helpers"
|
||||
"integrations": {
|
||||
"main": "Integrations",
|
||||
"secondary": "Connect your devices and services"
|
||||
},
|
||||
"automations": {
|
||||
"main": "Automations & scenes",
|
||||
"secondary": "Automations, scenes, scripts, and blueprints"
|
||||
"device_list": {
|
||||
"main": "Devices",
|
||||
"secondary": "Browse and manage your devices"
|
||||
},
|
||||
"entities": {
|
||||
"main": "Entities",
|
||||
"secondary": "Manage sensors, switches, and other entities"
|
||||
},
|
||||
"helpers": {
|
||||
"main": "Helpers",
|
||||
"secondary": "Create toggles, timers, and other building blocks"
|
||||
},
|
||||
"automation_list": {
|
||||
"main": "Automations",
|
||||
"secondary": "Make your home respond to events"
|
||||
},
|
||||
"scenes": {
|
||||
"main": "Scenes",
|
||||
"secondary": "Capture and restore the state of your devices"
|
||||
},
|
||||
"scripts": {
|
||||
"main": "Scripts",
|
||||
"secondary": "Run sequences of actions on demand"
|
||||
},
|
||||
"blueprints": {
|
||||
"main": "Blueprints",
|
||||
"secondary": "Create automations and scripts from reusable templates"
|
||||
},
|
||||
"backup": {
|
||||
"main": "Backup",
|
||||
@@ -2869,9 +2893,17 @@
|
||||
"main": "People",
|
||||
"secondary": "Manage who can access your home"
|
||||
},
|
||||
"areas": {
|
||||
"main": "Areas, labels & zones",
|
||||
"secondary": "Manage locations in and around your house"
|
||||
"area_list": {
|
||||
"main": "Areas",
|
||||
"secondary": "Organize your devices by room and floor"
|
||||
},
|
||||
"labels": {
|
||||
"main": "Labels",
|
||||
"secondary": "Group your devices, entities, and automations"
|
||||
},
|
||||
"zones": {
|
||||
"main": "Zones",
|
||||
"secondary": "Define locations like home, work, or school"
|
||||
},
|
||||
"companion": {
|
||||
"main": "Companion app",
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
isLoadedIntegration,
|
||||
isCore,
|
||||
} from "../../../src/common/config/can_show_page";
|
||||
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../src/data/page_navigation";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
describe("canShowPage", () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getConfigSubpageTitle } from "../../src/data/panel";
|
||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../src/data/page_navigation";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
const translations: Record<string, string> = {
|
||||
"ui.panel.config.dashboard.devices.main": "Devices & services",
|
||||
"ui.panel.config.dashboard.integrations.main": "Integrations entry",
|
||||
"ui.panel.config.dashboard.system.main": "System",
|
||||
"ui.panel.config.dashboard.matter.main": "Matter",
|
||||
"ui.panel.config.integrations.caption": "Integrations",
|
||||
@@ -18,8 +18,8 @@ const hass = {
|
||||
|
||||
// Mirrors how config-sections.ts mixes short and full translation keys.
|
||||
const configSections: Record<string, PageNavigation[]> = {
|
||||
dashboard: [
|
||||
{ path: "/config/integrations", translationKey: "devices" },
|
||||
dashboard_devices: [
|
||||
{ path: "/config/integrations", translationKey: "integrations" },
|
||||
] as PageNavigation[],
|
||||
dashboard_3: [
|
||||
{ path: "/config/system", translationKey: "system" },
|
||||
|
||||
@@ -15,9 +15,17 @@ import { e2ePanelRouteAssertions } from "./ha-test-panels";
|
||||
// ── Config dashboard links ───────────────────────────────────────────────────
|
||||
|
||||
export const configLinks: LinkSmokeCase[] = [
|
||||
{ href: "/config/integrations", label: "Devices & services" },
|
||||
{ href: "/config/automation", label: "Automations & scenes" },
|
||||
{ href: "/config/areas", label: "Areas, labels & zones" },
|
||||
{ href: "/config/integrations", label: "Integrations" },
|
||||
{ href: "/config/devices", label: "Devices" },
|
||||
{ href: "/config/entities", label: "Entities" },
|
||||
{ href: "/config/helpers", label: "Helpers" },
|
||||
{ href: "/config/automation", label: "Automations" },
|
||||
{ href: "/config/scene", label: "Scenes" },
|
||||
{ href: "/config/script", label: "Scripts" },
|
||||
{ href: "/config/blueprint", label: "Blueprints" },
|
||||
{ href: "/config/areas", label: "Areas" },
|
||||
{ href: "/config/labels", label: "Labels" },
|
||||
{ href: "/config/zone", label: "Zones" },
|
||||
{ href: "/config/apps", label: "Apps" },
|
||||
{ href: "/config/lovelace/dashboards", label: "Dashboards" },
|
||||
{ href: "/config/connectivity", label: "Connectivity" },
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { LitElement } from "lit";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { narrowViewportContext } from "../../src/data/context";
|
||||
import type { PageNavigation } from "../../src/data/page_navigation";
|
||||
import { provideHass } from "../../src/fake_data/provide_hass";
|
||||
import type { HassTabsSubpage } from "../../src/layouts/hass-tabs-subpage";
|
||||
|
||||
@@ -7,27 +10,46 @@ import type { HassTabsSubpage } from "../../src/layouts/hass-tabs-subpage";
|
||||
vi.mock("../../src/components/ha-icon-button-arrow-prev", () => ({}));
|
||||
vi.mock("../../src/components/ha-menu-button", () => ({}));
|
||||
vi.mock("../../src/components/ha-tab", () => ({}));
|
||||
customElements.define("ha-icon-button-arrow-prev", class extends LitElement {});
|
||||
customElements.define("ha-menu-button", class extends LitElement {});
|
||||
customElements.define("ha-tab", class extends LitElement {});
|
||||
vi.mock("../../src/components/ha-tab-group", () => ({}));
|
||||
vi.mock("../../src/components/ha-tab-group-tab", () => ({}));
|
||||
[
|
||||
"ha-icon-button-arrow-prev",
|
||||
"ha-menu-button",
|
||||
"ha-tab",
|
||||
"ha-tab-group",
|
||||
"ha-tab-group-tab",
|
||||
].forEach((tag) => customElements.define(tag, class extends LitElement {}));
|
||||
vi.mock("../../src/common/navigate", () => ({
|
||||
getHistoryState: () => undefined,
|
||||
navigate: vi.fn(),
|
||||
}));
|
||||
const { navigate } = await import("../../src/common/navigate");
|
||||
await import("../../src/layouts/hass-tabs-subpage");
|
||||
|
||||
let host: HTMLDivElement | undefined;
|
||||
|
||||
const mount = async (backPath: string) => {
|
||||
const mountElement = async (
|
||||
props: Partial<HassTabsSubpage>,
|
||||
narrow = false
|
||||
) => {
|
||||
host = document.createElement("div");
|
||||
provideHass(host, { localize: (key: string) => key });
|
||||
provideHass(host, { localize: (key: string) => `localized:${key}` });
|
||||
new ContextProvider(host, {
|
||||
context: narrowViewportContext,
|
||||
initialValue: narrow,
|
||||
});
|
||||
document.body.append(host);
|
||||
const element = document.createElement(
|
||||
"hass-tabs-subpage"
|
||||
) as HassTabsSubpage;
|
||||
Object.assign(element, {
|
||||
route: { prefix: "", path: "" },
|
||||
tabs: [],
|
||||
backPath,
|
||||
});
|
||||
Object.assign(element, { route: { prefix: "", path: "" }, tabs: [] }, props);
|
||||
host.append(element);
|
||||
await element.updateComplete;
|
||||
return element;
|
||||
};
|
||||
|
||||
const mount = async (backPath: string) => {
|
||||
const element = await mountElement({ backPath });
|
||||
return element.shadowRoot!.querySelector("ha-icon-button-arrow-prev") as
|
||||
(LitElement & { href?: string }) | null;
|
||||
};
|
||||
@@ -52,3 +74,99 @@ describe("hass-tabs-subpage back path", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("hass-tabs-subpage tabs", () => {
|
||||
const areas: PageNavigation = {
|
||||
path: "/config/areas/dashboard",
|
||||
translationKey: "areas",
|
||||
core: true,
|
||||
};
|
||||
const floors: PageNavigation = {
|
||||
path: "/config/floors",
|
||||
name: "Floors",
|
||||
core: true,
|
||||
};
|
||||
|
||||
it("uses the tab matching the route as narrow title", async () => {
|
||||
const element = await mountElement(
|
||||
{
|
||||
tabs: [areas, floors],
|
||||
route: { prefix: "/config/floors", path: "" },
|
||||
},
|
||||
true
|
||||
);
|
||||
const title = () =>
|
||||
element.shadowRoot!.querySelector(".main-title")!.textContent!.trim();
|
||||
expect(title()).toEqual("Floors");
|
||||
|
||||
element.route = { prefix: "/config/areas", path: "/dashboard" };
|
||||
await element.updateComplete;
|
||||
expect(title()).toEqual("localized:areas");
|
||||
});
|
||||
|
||||
it.each<[string, PageNavigation[], boolean]>([
|
||||
["one tab", [areas], false],
|
||||
["two tabs", [areas, floors], true],
|
||||
[
|
||||
"a tab hidden by its integration",
|
||||
[areas, { ...floors, core: false, component: "not_loaded" }],
|
||||
false,
|
||||
],
|
||||
[
|
||||
"a tab hidden by its filter",
|
||||
[areas, { ...floors, filter: () => false }],
|
||||
false,
|
||||
],
|
||||
])("shows tabs with %s", async (_label, tabs, expected) => {
|
||||
const element = await mountElement({ tabs });
|
||||
expect(element.showTabs).toBe(expected);
|
||||
expect(element.hasAttribute("show-tabs")).toBe(expected);
|
||||
});
|
||||
|
||||
const showInTabRow = async (path: string) => {
|
||||
vi.mocked(navigate).mockClear();
|
||||
const element = await mountElement(
|
||||
{
|
||||
tabs: [areas, floors],
|
||||
route: { prefix: "/config/floors", path: "" },
|
||||
},
|
||||
true
|
||||
);
|
||||
element
|
||||
.shadowRoot!.querySelector(".tab-row")!
|
||||
.dispatchEvent(
|
||||
new CustomEvent("wa-tab-show", { detail: { name: path } })
|
||||
);
|
||||
};
|
||||
|
||||
it("does not navigate when the tab row shows the active tab", async () => {
|
||||
await showInTabRow("/config/floors");
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("navigates when the tab row shows another tab", async () => {
|
||||
await showInTabRow("/config/areas/dashboard");
|
||||
expect(navigate).toHaveBeenCalledExactlyOnceWith(
|
||||
"/config/areas/dashboard",
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("navigates on a tab row click when no tab matches the route", async () => {
|
||||
vi.mocked(navigate).mockClear();
|
||||
const element = await mountElement(
|
||||
{
|
||||
tabs: [areas, floors],
|
||||
route: { prefix: "/config/other", path: "" },
|
||||
},
|
||||
true
|
||||
);
|
||||
element.shadowRoot!.querySelector<HTMLAnchorElement>(".tab-row a")!.click();
|
||||
expect(navigate).toHaveBeenCalledExactlyOnceWith(
|
||||
"/config/areas/dashboard",
|
||||
{ replace: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user