From bd1b458a61337255f15bac62b22dc93de33aa529 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 20 Nov 2025 12:25:41 +0100 Subject: [PATCH 01/14] Simplify sidebar logic --- src/components/ha-sidebar.ts | 159 ++++++++++----------- src/data/panel.ts | 34 ++++- src/dialogs/sidebar/dialog-edit-sidebar.ts | 49 ++++--- 3 files changed, 133 insertions(+), 109 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index d811b82e4b78..1118ab4152ae 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -33,7 +33,13 @@ import { computeRTL } from "../common/util/compute_rtl"; import { throttle } from "../common/util/throttle"; import { subscribeFrontendUserData } from "../data/frontend"; import type { ActionHandlerDetail } from "../data/lovelace/action_handler"; -import { getDefaultPanelUrlPath } from "../data/panel"; +import { + FIXED_PANELS, + getDefaultPanelUrlPath, + getPanelIcon, + getPanelIconPath, + getPanelTitle, +} from "../data/panel"; import type { PersistentNotification } from "../data/persistent_notification"; import { subscribeNotifications } from "../data/persistent_notification"; import { subscribeRepairsIssueRegistry } from "../data/repairs"; @@ -54,7 +60,7 @@ import "./ha-spinner"; import "./ha-svg-icon"; import "./user/ha-user-badge"; -const SHOW_AFTER_SPACER = ["config", "developer-tools"]; +const SHOW_AFTER_SPACER = ["developer-tools"]; const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body; @@ -67,7 +73,7 @@ const SORT_VALUE_URL_PATHS = { config: 11, }; -export const PANEL_ICONS = { +export const PANEL_ICON_PATHS = { calendar: mdiCalendar, "developer-tools": mdiHammer, energy: mdiLightningBolt, @@ -155,7 +161,11 @@ export const computePanels = memoizeOne( const beforeSpacer: PanelInfo[] = []; const afterSpacer: PanelInfo[] = []; - Object.values(panels).forEach((panel) => { + const allPanels = Object.values(panels).filter( + (panel) => !FIXED_PANELS.includes(panel.url_path) + ); + + allPanels.forEach((panel) => { if ( hiddenPanels.includes(panel.url_path) || (!panel.title && panel.url_path !== defaultPanel) || @@ -252,9 +262,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { } // Show the supervisor as being part of configuration - const selectedPanel = this.route.path?.startsWith("/hassio/") - ? "config" - : this.hass.panelUrl; + const selectedPanel = this.hass.panelUrl; // prettier-ignore return html` @@ -413,7 +421,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { this.hass.locale ); - // prettier-ignore return html` - ${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)} + ${this._renderPanels(beforeSpacer, selectedPanel)} ${this._renderSpacer()} - ${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)} - ${this._renderExternalConfiguration()} + ${this._renderPanels(afterSpacer, selectedPanel)} + ${this.hass.user?.is_admin + ? this._renderConfiguration(selectedPanel) + : this._renderExternalConfiguration()} `; } - private _renderPanels( - panels: PanelInfo[], - selectedPanel: string, - defaultPanel: string - ) { + private _renderPanels(panels: PanelInfo[], selectedPanel: string) { return panels.map((panel) => - this._renderPanel( - panel.url_path, - panel.url_path === defaultPanel - ? panel.title || this.hass.localize("panel.states") - : this.hass.localize(`panel.${panel.title}`) || panel.title, - panel.icon, - panel.url_path === defaultPanel && !panel.icon - ? PANEL_ICONS.lovelace - : panel.url_path in PANEL_ICONS - ? PANEL_ICONS[panel.url_path] - : undefined, - selectedPanel - ) + this._renderPanel(panel, panel.url_path === selectedPanel) ); } - private _renderPanel( - urlPath: string, - title: string | null, - icon: string | null | undefined, - iconPath: string | null | undefined, - selectedPanel: string - ) { - return urlPath === "config" - ? this._renderConfiguration(title, selectedPanel) - : html` - - ${iconPath - ? html`` - : html``} - ${title} - - `; + private _renderPanel(panel: PanelInfo, isSelected: boolean) { + const title = getPanelTitle(this.hass, panel); + const urlPath = panel.url_path; + const icon = getPanelIcon(panel); + const iconPath = getPanelIconPath(panel); + + return html` + + ${iconPath + ? html`` + : html``} + ${title} + + `; } private _renderDivider() { @@ -487,10 +475,15 @@ class HaSidebar extends SubscribeMixin(LitElement) { return html`
`; } - private _renderConfiguration(title: string | null, selectedPanel: string) { + private _renderConfiguration(selectedPanel: string) { + if (!this.hass.user?.is_admin) { + return nothing; + } + const isSelected = + selectedPanel === "config" || this.route.path?.startsWith("/hassio/"); return html` ` - : ""} - ${title} + : nothing} + ${this.hass.localize("panel.config")} ${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0) ? html` ${this._updatesCount + this._issuesCount} ` - : ""} + : nothing} `; } @@ -535,19 +530,20 @@ class HaSidebar extends SubscribeMixin(LitElement) { ? html` ${notificationCount} ` - : ""} + : nothing} ${this.hass.localize("ui.notification_drawer.title")} ${this.alwaysExpand && notificationCount > 0 ? html`${notificationCount}` - : ""} + : nothing} `; } private _renderUserItem(selectedPanel: string) { const isRTL = computeRTL(this.hass); + const isSelected = selectedPanel === "profile"; return html` - - ${this.hass.user ? this.hass.user.name : ""} + + ${this.hass.user ? this.hass.user.name : ""} + `; } private _renderExternalConfiguration() { - return html`${!this.hass.user?.is_admin && - this.hass.auth.external?.config.hasSettingsScreen - ? html` - - - - ${this.hass.localize("ui.sidebar.external_app_configuration")} - - - ` - : ""}`; + if (!this.hass.auth.external?.config.hasSettingsScreen) { + return nothing; + } + return html` + + + + ${this.hass.localize("ui.sidebar.external_app_configuration")} + + + `; } private _handleExternalAppConfiguration(ev: Event) { diff --git a/src/data/panel.ts b/src/data/panel.ts index 08ad42d8925f..ec6fc192ad43 100644 --- a/src/data/panel.ts +++ b/src/data/panel.ts @@ -1,3 +1,15 @@ +import { + mdiAccount, + mdiCalendar, + mdiChartBox, + mdiClipboardList, + mdiFormatListBulletedType, + mdiHammer, + mdiLightningBolt, + mdiPlayBoxMultiple, + mdiTooltipAccount, + mdiViewDashboard, +} from "@mdi/js"; import type { HomeAssistant, PanelInfo } from "../types"; /** Panel to show when no panel is picked. */ @@ -60,7 +72,7 @@ export const getPanelTitleFromUrlPath = ( return getPanelTitle(hass, panel); }; -export const getPanelIcon = (panel: PanelInfo): string | null => { +export const getPanelIcon = (panel: PanelInfo): string | undefined => { if (!panel.icon) { switch (panel.component_name) { case "profile": @@ -70,5 +82,23 @@ export const getPanelIcon = (panel: PanelInfo): string | null => { } } - return panel.icon; + return panel.icon || undefined; }; + +export const PANEL_ICON_PATHS = { + calendar: mdiCalendar, + "developer-tools": mdiHammer, + energy: mdiLightningBolt, + history: mdiChartBox, + logbook: mdiFormatListBulletedType, + lovelace: mdiViewDashboard, + profile: mdiAccount, + map: mdiTooltipAccount, + "media-browser": mdiPlayBoxMultiple, + todo: mdiClipboardList, +}; + +export const getPanelIconPath = (panel: PanelInfo): string | undefined => + PANEL_ICON_PATHS[panel.url_path]; + +export const FIXED_PANELS = ["profile", "config"]; diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts index 413c6fed602c..951e5f0a8573 100644 --- a/src/dialogs/sidebar/dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts @@ -12,15 +12,20 @@ import "../../components/ha-items-display-editor"; import type { DisplayValue } from "../../components/ha-items-display-editor"; import "../../components/ha-md-dialog"; import type { HaMdDialog } from "../../components/ha-md-dialog"; -import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar"; +import { computePanels } from "../../components/ha-sidebar"; import "../../components/ha-spinner"; import { fetchFrontendUserData, saveFrontendUserData, } from "../../data/frontend"; +import { + getDefaultPanelUrlPath, + getPanelIcon, + getPanelIconPath, + getPanelTitle, +} from "../../data/panel"; import type { HomeAssistant } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; -import { getDefaultPanelUrlPath } from "../../data/panel"; @customElement("dialog-edit-sidebar") class DialogEditSidebar extends LitElement { @@ -119,34 +124,28 @@ class DialogEditSidebar extends LitElement { const items = [ ...beforeSpacer, ...panels.filter((panel) => this._hidden!.includes(panel.url_path)), - ...afterSpacer.filter((panel) => panel.url_path !== "config"), + ...afterSpacer, ].map((panel) => ({ value: panel.url_path, - label: - panel.url_path === defaultPanel - ? panel.title || this.hass.localize("panel.states") - : this.hass.localize(`panel.${panel.title}`) || panel.title || "?", - icon: panel.icon || undefined, - iconPath: - panel.url_path === defaultPanel && !panel.icon - ? PANEL_ICONS.lovelace - : panel.url_path in PANEL_ICONS - ? PANEL_ICONS[panel.url_path] - : undefined, + label: getPanelTitle(this.hass, panel) || panel.title, + icon: getPanelIcon(panel), + iconPath: getPanelIconPath(panel), disableSorting: panel.url_path === "developer-tools", })); - return html` - `; + return html` + + + `; } protected render() { From 308d6f9788a5c32ebf4de07cd23848835f7a9cc8 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 20 Nov 2025 14:40:59 +0100 Subject: [PATCH 02/14] Add reset to default sidebar button --- src/data/frontend.ts | 4 +-- src/dialogs/sidebar/dialog-edit-sidebar.ts | 41 +++++++++++++++++++++- src/translations/en.json | 4 ++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/data/frontend.ts b/src/data/frontend.ts index d4c3d00e27d2..7746cefb38e2 100644 --- a/src/data/frontend.ts +++ b/src/data/frontend.ts @@ -7,8 +7,8 @@ export interface CoreFrontendUserData { } export interface SidebarFrontendUserData { - panelOrder: string[]; - hiddenPanels: string[]; + panelOrder?: string[]; + hiddenPanels?: string[]; } export interface CoreFrontendSystemData { diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts index 951e5f0a8573..f624bdcb9539 100644 --- a/src/dialogs/sidebar/dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts @@ -1,5 +1,5 @@ import "@material/mwc-linear-progress/mwc-linear-progress"; -import { mdiClose } from "@mdi/js"; +import { mdiClose, mdiDotsVertical, mdiRestart } from "@mdi/js"; import { css, html, LitElement, nothing, type TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -10,10 +10,13 @@ import "../../components/ha-fade-in"; import "../../components/ha-icon-button"; import "../../components/ha-items-display-editor"; import type { DisplayValue } from "../../components/ha-items-display-editor"; +import "../../components/ha-md-button-menu"; import "../../components/ha-md-dialog"; import type { HaMdDialog } from "../../components/ha-md-dialog"; +import "../../components/ha-md-menu-item"; import { computePanels } from "../../components/ha-sidebar"; import "../../components/ha-spinner"; +import "../../components/ha-svg-icon"; import { fetchFrontendUserData, saveFrontendUserData, @@ -170,6 +173,22 @@ class DialogEditSidebar extends LitElement { >${this.hass.localize("ui.sidebar.edit_subtitle")}` : nothing} + + + + + ${this.hass.localize("ui.sidebar.reset_to_defaults")} + +
${this._renderContent()}
@@ -193,6 +212,26 @@ class DialogEditSidebar extends LitElement { this._hidden = [...hidden]; } + private _resetToDefaults = async () => { + const confirmation = await showConfirmationDialog(this, { + text: this.hass.localize("ui.sidebar.reset_confirmation"), + confirmText: this.hass.localize("ui.common.reset"), + }); + + if (!confirmation) { + return; + } + + this._order = []; + this._hidden = []; + try { + await saveFrontendUserData(this.hass.connection, "sidebar", {}); + } catch (err: any) { + this._error = err.message || err; + } + this.closeDialog(); + }; + private async _save() { if (this._migrateToUserData) { const confirmation = await showConfirmationDialog(this, { diff --git a/src/translations/en.json b/src/translations/en.json index 5e96e900c591..a2ea406c71f9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2217,7 +2217,9 @@ "sidebar_toggle": "Sidebar toggle", "edit_sidebar": "Edit sidebar", "edit_subtitle": "Synced on all devices", - "migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device." + "migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.", + "reset_to_defaults": "Reset to defaults", + "reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels." }, "panel": { "home": { From d0e1b787d629dafdeac2e45c1f00b7918cf56ff4 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 20 Nov 2025 15:17:20 +0100 Subject: [PATCH 03/14] Refactor panel configuration --- .../ha-config-lovelace-dashboards.ts | 143 +++++------------- 1 file changed, 40 insertions(+), 103 deletions(-) diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 55d00db66037..2f3a6042a70f 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -10,7 +10,6 @@ import type { PropertyValues } from "lit"; import { LitElement, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoize from "memoize-one"; -import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { storage } from "../../../../common/decorators/storage"; import { navigate } from "../../../../common/navigate"; import { stringCompare } from "../../../../common/string/compare"; @@ -45,7 +44,11 @@ import { fetchDashboards, updateDashboard, } from "../../../../data/lovelace/dashboard"; -import { DEFAULT_PANEL } from "../../../../data/panel"; +import { + DEFAULT_PANEL, + getPanelIcon, + getPanelTitle, +} from "../../../../data/panel"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-tabs-subpage-data-table"; @@ -62,6 +65,7 @@ type DataTableItem = Pick< > & { default: boolean; filename: string; + localized_type: string; type: string; }; @@ -112,7 +116,7 @@ export class HaConfigLovelaceDashboards extends LitElement { state: false, subscribe: false, }) - private _activeGrouping?: string = "type"; + private _activeGrouping?: string = "localized_type"; @storage({ key: "lovelace-dashboards-table-collapsed", @@ -183,7 +187,7 @@ export class HaConfigLovelaceDashboards extends LitElement { }, }; - columns.type = { + columns.localized_type = { title: localize( "ui.panel.config.lovelace.dashboards.picker.headers.type" ), @@ -253,18 +257,14 @@ export class HaConfigLovelaceDashboards extends LitElement { .hass=${this.hass} narrow .items=${[ - ...(this._canEdit(dashboard.url_path) - ? [ - { - path: mdiPencil, - label: this.hass.localize( - "ui.panel.config.lovelace.dashboards.picker.edit" - ), - action: () => this._handleEdit(dashboard), - }, - ] - : []), - ...(this._canDelete(dashboard.url_path) + { + path: mdiPencil, + label: this.hass.localize( + "ui.panel.config.lovelace.dashboards.picker.edit" + ), + action: () => this._handleEdit(dashboard), + }, + ...(dashboard.type === "user_created" ? [ { label: this.hass.localize( @@ -288,92 +288,43 @@ export class HaConfigLovelaceDashboards extends LitElement { private _getItems = memoize( (dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => { - const defaultMode = ( - this.hass.panels?.lovelace?.config as LovelacePanelConfig - ).mode; + const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig) + .mode; const isDefault = defaultUrlPath === "lovelace"; const result: DataTableItem[] = [ { icon: "mdi:view-dashboard", title: this.hass.localize("panel.states"), default: isDefault, - show_in_sidebar: isDefault, + show_in_sidebar: true, require_admin: false, url_path: "lovelace", - mode: defaultMode, - filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "", - type: this._localizeType("built_in"), + mode: mode, + filename: mode === "yaml" ? "ui-lovelace.yaml" : "", + type: "built_in", + localized_type: this._localizeType("built_in"), }, ]; - if (isComponentLoaded(this.hass, "energy")) { - result.push({ - icon: "mdi:lightning-bolt", - title: this.hass.localize(`ui.panel.config.dashboard.energy.main`), - show_in_sidebar: true, - mode: "storage", - url_path: "energy", - filename: "", - default: false, - require_admin: false, - type: this._localizeType("built_in"), - }); - } - - if (this.hass.panels.light) { - result.push({ - icon: this.hass.panels.light.icon || "mdi:lamps", - title: this.hass.localize("panel.light"), - show_in_sidebar: true, - mode: "storage", - url_path: "light", - filename: "", - default: false, - require_admin: false, - type: this._localizeType("built_in"), - }); - } - if (this.hass.panels.security) { - result.push({ - icon: this.hass.panels.security.icon || "mdi:security", - title: this.hass.localize("panel.security"), - show_in_sidebar: true, - mode: "storage", - url_path: "security", - filename: "", - default: false, - require_admin: false, - type: this._localizeType("built_in"), - }); - } - - if (this.hass.panels.climate) { - result.push({ - icon: this.hass.panels.climate.icon || "mdi:home-thermometer", - title: this.hass.localize("panel.climate"), - show_in_sidebar: true, - mode: "storage", - url_path: "climate", - filename: "", - default: false, - require_admin: false, - type: this._localizeType("built_in"), - }); - } - - if (this.hass.panels.home) { - result.push({ - icon: this.hass.panels.home.icon || "mdi:home", - title: this.hass.localize("panel.home"), + ["home", "light", "security", "climate", "energy"].forEach((panel) => { + const panelInfo = this.hass.panels[panel]; + if (!panel) { + return; + } + const item: DataTableItem = { + icon: getPanelIcon(panelInfo), + title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path, show_in_sidebar: true, mode: "storage", - url_path: "home", + url_path: panelInfo.url_path, filename: "", - default: false, + default: defaultUrlPath === panelInfo.url_path, require_admin: false, - type: this._localizeType("built_in"), - }); - } + type: "built_in", + localized_type: this._localizeType("built_in"), + }; + result.push(item); + }); result.push( ...dashboards @@ -386,7 +337,8 @@ export class HaConfigLovelaceDashboards extends LitElement { filename: "", ...dashboard, default: defaultUrlPath === dashboard.url_path, - type: this._localizeType("user_created"), + type: "user_created", + localized_type: this._localizeType("user_created"), }) satisfies DataTableItem ) ); @@ -486,17 +438,6 @@ export class HaConfigLovelaceDashboards extends LitElement { this._openDetailDialog(dashboard, urlPath); } - private _canDelete(urlPath: string) { - return ![ - "lovelace", - "energy", - "light", - "security", - "climate", - "home", - ].includes(urlPath); - } - private _canEdit(urlPath: string) { return !["light", "security", "climate", "home"].includes(urlPath); } @@ -581,10 +522,6 @@ export class HaConfigLovelaceDashboards extends LitElement { private async _deleteDashboard( dashboard: LovelaceDashboard ): Promise { - if (!this._canDelete(dashboard.url_path)) { - return false; - } - const confirm = await showConfirmationDialog(this, { title: this.hass!.localize( "ui.panel.config.lovelace.dashboards.confirm_delete_title", From 5789ef17594ec8f77822f1411c2ee4764ac96b13 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 20 Nov 2025 16:14:08 +0100 Subject: [PATCH 04/14] Allow to set every dashboard as default --- src/components/ha-icon-overflow-menu.ts | 3 +- src/components/ha-md-menu-item.ts | 5 +++ .../ha-config-lovelace-dashboards.ts | 34 ++++++++++++++----- src/translations/en.json | 1 + 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/components/ha-icon-overflow-menu.ts b/src/components/ha-icon-overflow-menu.ts index dbbe30762cb0..12a6701f3100 100644 --- a/src/components/ha-icon-overflow-menu.ts +++ b/src/components/ha-icon-overflow-menu.ts @@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement { .path=${item.path} > ${item.label} - ` + ` )} ` : html` @@ -103,6 +103,7 @@ export class HaIconOverflowMenu extends LitElement { :host { display: flex; justify-content: flex-end; + cursor: initial; } div[role="separator"] { border-right: 1px solid var(--divider-color); diff --git a/src/components/ha-md-menu-item.ts b/src/components/ha-md-menu-item.ts index 06a43c7340ef..92727524a628 100644 --- a/src/components/ha-md-menu-item.ts +++ b/src/components/ha-md-menu-item.ts @@ -36,6 +36,11 @@ export class HaMdMenuItem extends MenuItemEl { ::slotted([slot="headline"]) { text-wrap: nowrap; } + :host([disabled]) { + opacity: 1; + --md-menu-item-label-text-color: var(--disabled-text-color); + --md-menu-item-leading-icon-color: var(--disabled-text-color); + } `, ]; } diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 2f3a6042a70f..55279f0cf266 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -1,8 +1,9 @@ import { mdiCheck, - mdiCheckCircleOutline, mdiDelete, mdiDotsVertical, + mdiHomeCircleOutline, + mdiHomeEdit, mdiPencil, mdiPlus, } from "@mdi/js"; @@ -28,6 +29,7 @@ import "../../../../components/ha-md-button-menu"; import "../../../../components/ha-md-list-item"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-tooltip"; +import { saveFrontendSystemData } from "../../../../data/frontend"; import type { LovelacePanelConfig } from "../../../../data/lovelace"; import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types"; import { @@ -171,7 +173,7 @@ export class HaConfigLovelaceDashboards extends LitElement { this._handleEdit(dashboard), + action: () => this._handleSetAsDefault(dashboard), + disabled: dashboard.default, }, ...(dashboard.type === "user_created" ? [ + { + path: mdiPencil, + label: this.hass.localize( + "ui.panel.config.lovelace.dashboards.picker.edit" + ), + action: () => this._handleEdit(dashboard), + }, { label: this.hass.localize( "ui.panel.config.lovelace.dashboards.picker.delete" @@ -438,9 +448,15 @@ export class HaConfigLovelaceDashboards extends LitElement { this._openDetailDialog(dashboard, urlPath); } - private _canEdit(urlPath: string) { - return !["light", "security", "climate", "home"].includes(urlPath); - } + private _handleSetAsDefault = async (item: DataTableItem) => { + if (item.default) { + return; + } + await saveFrontendSystemData(this.hass.connection, "core", { + ...this.hass.systemData, + defaultPanel: item.url_path === DEFAULT_PANEL ? undefined : item.url_path, + }); + }; private _handleDelete = async (item: DataTableItem) => { const dashboard = this._dashboards.find( diff --git a/src/translations/en.json b/src/translations/en.json index a2ea406c71f9..bb06ebca12ff 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3510,6 +3510,7 @@ "edit": "Edit", "delete": "Delete", "add_dashboard": "Add dashboard", + "set_as_default": "Set as default", "type": { "user_created": "User created", "built_in": "Built-in" From 9f5c3f01f79497cd27c77a2354e2c59e60adb131 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 20 Nov 2025 17:58:34 +0100 Subject: [PATCH 05/14] Improve sidebar logic with default panel --- src/components/ha-items-display-editor.ts | 29 ++++++++++-------- src/components/ha-sidebar.ts | 24 ++++++++------- src/data/panel.ts | 1 + src/dialogs/sidebar/dialog-edit-sidebar.ts | 35 +++++++++++++++------- 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index e8128c93bf43..e0cbcdf4e028 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -27,6 +27,7 @@ export interface DisplayItem { label: string; description?: string; disableSorting?: boolean; + disableHiding?: boolean; } export interface DisplayValue { @@ -101,6 +102,7 @@ export class HaItemDisplayEditor extends LitElement { icon, iconPath, disableSorting, + disableHiding, } = item; return html` ` : nothing} - + ${isVisible && !disableHiding + ? html`` + : nothing} ${isVisible && !disableSorting ? html` { + const isDefaultPanel = panel.url_path === defaultPanel; + if ( - hiddenPanels.includes(panel.url_path) || - (!panel.title && panel.url_path !== defaultPanel) || - (panel.default_visible === false && - !panelsOrder.includes(panel.url_path)) + !isDefaultPanel && + (!panel.title || + hiddenPanels.includes(panel.url_path) || + (panel.default_visible === false && + !panelsOrder.includes(panel.url_path))) ) { return; } - (SHOW_AFTER_SPACER.includes(panel.url_path) + (SHOW_AFTER_SPACER_PANELS.includes(panel.url_path) ? afterSpacer : beforeSpacer ).push(panel); @@ -405,9 +407,9 @@ class HaSidebar extends SubscribeMixin(LitElement) { private _renderAllPanels(selectedPanel: string) { if (!this._panelOrder || !this._hiddenPanels) { return html` - + + + `; } diff --git a/src/data/panel.ts b/src/data/panel.ts index ec6fc192ad43..b1a86d8893f2 100644 --- a/src/data/panel.ts +++ b/src/data/panel.ts @@ -102,3 +102,4 @@ export const getPanelIconPath = (panel: PanelInfo): string | undefined => PANEL_ICON_PATHS[panel.url_path]; export const FIXED_PANELS = ["profile", "config"]; +export const SHOW_AFTER_SPACER_PANELS = ["developer-tools"]; diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts index f624bdcb9539..6e1ab7b19593 100644 --- a/src/dialogs/sidebar/dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts @@ -9,7 +9,10 @@ import "../../components/ha-dialog-header"; import "../../components/ha-fade-in"; import "../../components/ha-icon-button"; import "../../components/ha-items-display-editor"; -import type { DisplayValue } from "../../components/ha-items-display-editor"; +import type { + DisplayItem, + DisplayValue, +} from "../../components/ha-items-display-editor"; import "../../components/ha-md-button-menu"; import "../../components/ha-md-dialog"; import type { HaMdDialog } from "../../components/ha-md-dialog"; @@ -26,6 +29,7 @@ import { getPanelIcon, getPanelIconPath, getPanelTitle, + SHOW_AFTER_SPACER_PANELS, } from "../../data/panel"; import type { HomeAssistant } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; @@ -113,27 +117,38 @@ class DialogEditSidebar extends LitElement { this.hass.locale ); - // Add default hidden panels that are missing in hidden + const orderSet = new Set(this._order); + const hiddenSet = new Set(this._hidden); + for (const panel of panels) { if ( panel.default_visible === false && - !this._order.includes(panel.url_path) && - !this._hidden.includes(panel.url_path) + !orderSet.has(panel.url_path) && + !hiddenSet.has(panel.url_path) ) { - this._hidden.push(panel.url_path); + hiddenSet.add(panel.url_path); } } + if (hiddenSet.has(defaultPanel)) { + hiddenSet.delete(defaultPanel); + } + + const hiddenPanels = Array.from(hiddenSet); + const items = [ ...beforeSpacer, - ...panels.filter((panel) => this._hidden!.includes(panel.url_path)), + ...panels.filter((panel) => hiddenPanels.includes(panel.url_path)), ...afterSpacer, - ].map((panel) => ({ + ].map((panel) => ({ value: panel.url_path, - label: getPanelTitle(this.hass, panel) || panel.title, + label: + (getPanelTitle(this.hass, panel) || panel.url_path) + + `${defaultPanel === panel.url_path ? " (default)" : ""}`, icon: getPanelIcon(panel), iconPath: getPanelIconPath(panel), - disableSorting: panel.url_path === "developer-tools", + disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path), + disableHiding: panel.url_path === defaultPanel, })); return html` @@ -141,7 +156,7 @@ class DialogEditSidebar extends LitElement { .hass=${this.hass} .value=${{ order: this._order, - hidden: this._hidden, + hidden: hiddenPanels, }} .items=${items} @value-changed=${this._changed} From 1abd133afd08164f502e23cb84eb7a8356580d47 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 20 Nov 2025 18:12:02 +0100 Subject: [PATCH 06/14] Simplify lovelace dashboard logic --- .../dialog-lovelace-dashboard-detail.ts | 64 ++----------------- src/translations/en.json | 2 +- 2 files changed, 7 insertions(+), 59 deletions(-) diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts index a113f06b47b0..01ce8be93756 100644 --- a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts @@ -8,16 +8,13 @@ import "../../../../components/ha-button"; import { createCloseHeading } from "../../../../components/ha-dialog"; import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; -import { saveFrontendSystemData } from "../../../../data/frontend"; import type { LovelaceDashboard, LovelaceDashboardCreateParams, LovelaceDashboardMutableParams, } from "../../../../data/lovelace/dashboard"; -import { DEFAULT_PANEL } from "../../../../data/panel"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers"; import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail"; @customElement("dialog-lovelace-dashboard-detail") @@ -61,9 +58,9 @@ export class DialogLovelaceDashboardDetail extends LitElement { if (!this._params || !this._data) { return nothing; } - const defaultPanelUrlPath = - this.hass.systemData?.default_panel || DEFAULT_PANEL; + const titleInvalid = !this._data.title || !this._data.title.trim(); + const isLovelaceDashboard = this._params.urlPath === "lovelace"; return html` ` - : ""} - - ${this._params.urlPath === defaultPanelUrlPath - ? this.hass.localize( - "ui.panel.config.lovelace.dashboards.detail.remove_default" - ) - : this.hass.localize( - "ui.panel.config.lovelace.dashboards.detail.set_default" - )} - + : nothing} ` - : ""} + : nothing} Date: Thu, 20 Nov 2025 18:36:21 +0100 Subject: [PATCH 07/14] Simplify navigation picker --- src/components/ha-navigation-picker.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/ha-navigation-picker.ts b/src/components/ha-navigation-picker.ts index 3eba515c62a1..6a7eb26aa63e 100644 --- a/src/components/ha-navigation-picker.ts +++ b/src/components/ha-navigation-picker.ts @@ -6,7 +6,7 @@ import { fireEvent } from "../common/dom/fire_event"; import { titleCase } from "../common/string/title-case"; import { fetchConfig } from "../data/lovelace/config/types"; import type { LovelaceViewRawConfig } from "../data/lovelace/config/view"; -import { getDefaultPanelUrlPath } from "../data/panel"; +import { getPanelIcon, getPanelTitle } from "../data/panel"; import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types"; import "./ha-combo-box"; import type { HaComboBox } from "./ha-combo-box"; @@ -43,13 +43,8 @@ const createViewNavigationItem = ( const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({ path: `/${panel.url_path}`, - icon: panel.icon ?? "mdi:view-dashboard", - title: - panel.url_path === getDefaultPanelUrlPath(hass) - ? hass.localize("panel.states") - : hass.localize(`panel.${panel.title}`) || - panel.title || - (panel.url_path ? titleCase(panel.url_path) : ""), + icon: getPanelIcon(panel), + title: getPanelTitle(hass, panel) || "", }); @customElement("ha-navigation-picker") From 62fa300f75f4ba211f941c615309fa53f0aa5fd0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 20 Nov 2025 18:41:20 +0100 Subject: [PATCH 08/14] Add other panels to profile --- src/components/ha-navigation-picker.ts | 2 +- .../dashboards/ha-config-lovelace-dashboards.ts | 10 +++++++++- src/panels/profile/ha-pick-dashboard-row.ts | 16 +++++++++++++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/components/ha-navigation-picker.ts b/src/components/ha-navigation-picker.ts index 6a7eb26aa63e..bb97181aac82 100644 --- a/src/components/ha-navigation-picker.ts +++ b/src/components/ha-navigation-picker.ts @@ -43,7 +43,7 @@ const createViewNavigationItem = ( const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({ path: `/${panel.url_path}`, - icon: getPanelIcon(panel), + icon: getPanelIcon(panel) || "mdi:view-dashboard", title: getPanelTitle(hass, panel) || "", }); diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 55279f0cf266..0614d9bbb7bc 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -61,6 +61,14 @@ import { lovelaceTabs } from "../ha-config-lovelace"; import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy"; import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; +export const PANEL_DASHBOARDS = [ + "home", + "light", + "security", + "climate", + "energy", +] as string[]; + type DataTableItem = Pick< LovelaceDashboard, "icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path" @@ -316,7 +324,7 @@ export class HaConfigLovelaceDashboards extends LitElement { }, ]; - ["home", "light", "security", "climate", "energy"].forEach((panel) => { + PANEL_DASHBOARDS.forEach((panel) => { const panelInfo = this.hass.panels[panel]; if (!panel) { return; diff --git a/src/panels/profile/ha-pick-dashboard-row.ts b/src/panels/profile/ha-pick-dashboard-row.ts index bb4163cf736e..d469b2e2ba75 100644 --- a/src/panels/profile/ha-pick-dashboard-row.ts +++ b/src/panels/profile/ha-pick-dashboard-row.ts @@ -8,6 +8,9 @@ import type { LovelaceDashboard } from "../../data/lovelace/dashboard"; import { fetchDashboards } from "../../data/lovelace/dashboard"; import type { HomeAssistant } from "../../types"; import { saveFrontendUserData } from "../../data/frontend"; +import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards"; +import { getPanelTitle } from "../../data/panel"; +import "../../components/ha-divider"; const USE_SYSTEM_VALUE = "___use_system___"; @@ -47,12 +50,19 @@ class HaPickDashboardRow extends LitElement { ${this.hass.localize("ui.panel.profile.dashboard.system")} + ${this.hass.localize("ui.panel.profile.dashboard.lovelace")} - - ${this.hass.localize("ui.panel.profile.dashboard.home")} - + ${PANEL_DASHBOARDS.map((panel) => { + const panelInfo = this.hass.panels[panel]; + return html` + + ${panelInfo ? getPanelTitle(this.hass, panelInfo) : panel} + + `; + })} + ${this._dashboards.map((dashboard) => { if (!this.hass.user!.is_admin && dashboard.require_admin) { return ""; From 3448601dd6530be03316a2d539a7d4edc43f2f66 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 21 Nov 2025 18:11:32 +0100 Subject: [PATCH 09/14] Remove duplicated panel icon paths --- src/components/ha-sidebar.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index bf42cc476c3d..a6fc0af867ec 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -1,18 +1,9 @@ import { mdiBell, - mdiCalendar, mdiCellphoneCog, - mdiChartBox, - mdiClipboardList, mdiCog, - mdiFormatListBulletedType, - mdiHammer, - mdiLightningBolt, mdiMenu, mdiMenuOpen, - mdiPlayBoxMultiple, - mdiTooltipAccount, - mdiViewDashboard, } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; @@ -72,18 +63,6 @@ const SORT_VALUE_URL_PATHS = { config: 11, }; -export const PANEL_ICON_PATHS = { - calendar: mdiCalendar, - "developer-tools": mdiHammer, - energy: mdiLightningBolt, - history: mdiChartBox, - logbook: mdiFormatListBulletedType, - lovelace: mdiViewDashboard, - map: mdiTooltipAccount, - "media-browser": mdiPlayBoxMultiple, - todo: mdiClipboardList, -}; - const panelSorter = ( reverseSort: string[], defaultPanel: string, From 302c77f28481654b936b75e5d640a2ee9a7d7909 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 24 Nov 2025 11:01:48 +0100 Subject: [PATCH 10/14] Fix disable hiding condition --- src/components/ha-items-display-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index e0cbcdf4e028..e4f251d7c433 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -157,7 +157,7 @@ export class HaItemDisplayEditor extends LitElement {
` : nothing} - ${isVisible && !disableHiding + ${!isVisible || !disableHiding ? html` Date: Mon, 24 Nov 2025 11:06:15 +0100 Subject: [PATCH 11/14] Fix dashboard picker --- src/panels/profile/ha-pick-dashboard-row.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/panels/profile/ha-pick-dashboard-row.ts b/src/panels/profile/ha-pick-dashboard-row.ts index d469b2e2ba75..4df5e24e38b8 100644 --- a/src/panels/profile/ha-pick-dashboard-row.ts +++ b/src/panels/profile/ha-pick-dashboard-row.ts @@ -1,16 +1,16 @@ import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import "../../components/ha-divider"; import "../../components/ha-list-item"; import "../../components/ha-select"; import "../../components/ha-settings-row"; +import { saveFrontendUserData } from "../../data/frontend"; import type { LovelaceDashboard } from "../../data/lovelace/dashboard"; import { fetchDashboards } from "../../data/lovelace/dashboard"; -import type { HomeAssistant } from "../../types"; -import { saveFrontendUserData } from "../../data/frontend"; -import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards"; import { getPanelTitle } from "../../data/panel"; -import "../../components/ha-divider"; +import type { HomeAssistant, PanelInfo } from "../../types"; +import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards"; const USE_SYSTEM_VALUE = "___use_system___"; @@ -55,10 +55,15 @@ class HaPickDashboardRow extends LitElement { ${this.hass.localize("ui.panel.profile.dashboard.lovelace")} ${PANEL_DASHBOARDS.map((panel) => { - const panelInfo = this.hass.panels[panel]; + const panelInfo = this.hass.panels[panel] as + | PanelInfo + | undefined; + if (!panelInfo) { + return nothing; + } return html` - - ${panelInfo ? getPanelTitle(this.hass, panelInfo) : panel} + + ${getPanelTitle(this.hass, panelInfo)} `; })} From 17f55e8aeae9da4f8489192a999e5d225c11d388 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 25 Nov 2025 09:32:53 +0100 Subject: [PATCH 12/14] Update default panel --- .../config/lovelace/dashboards/ha-config-lovelace-dashboards.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 0614d9bbb7bc..642ebcaccbf6 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -462,7 +462,7 @@ export class HaConfigLovelaceDashboards extends LitElement { } await saveFrontendSystemData(this.hass.connection, "core", { ...this.hass.systemData, - defaultPanel: item.url_path === DEFAULT_PANEL ? undefined : item.url_path, + default_panel: item.url_path, }); }; From bc9a50d65e6729e376881dfa207dcf102dc5a3e1 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 25 Nov 2025 09:44:59 +0100 Subject: [PATCH 13/14] Add confirm dialog --- .../dashboards/ha-config-lovelace-dashboards.ts | 17 +++++++++++++++++ src/translations/en.json | 4 +--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 642ebcaccbf6..eae81814626a 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -460,6 +460,23 @@ export class HaConfigLovelaceDashboards extends LitElement { if (item.default) { return; } + + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title" + ), + text: this.hass.localize( + "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text" + ), + confirmText: this.hass.localize("ui.common.ok"), + dismissText: this.hass.localize("ui.common.cancel"), + destructive: false, + }); + + if (!confirm) { + return; + } + await saveFrontendSystemData(this.hass.connection, "core", { ...this.hass.systemData, default_panel: item.url_path, diff --git a/src/translations/en.json b/src/translations/en.json index de0a93864afa..e43531522a98 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3536,9 +3536,7 @@ "set_default": "Set as default", "remove_default": "Remove as default", "set_default_confirm_title": "Set as default dashboard?", - "set_default_confirm_text": "This will replace the current default dashboard. Users can still override their default dashboard in their profile settings.", - "remove_default_confirm_title": "Remove default dashboard?", - "remove_default_confirm_text": "The default dashboard will be changed to Overview for every user. Users can still override their default dashboard in their profile settings." + "set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile." } }, "resources": { From d8b27bb30aefd5c05b46dbc18536995c1184f746 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 25 Nov 2025 11:31:32 +0100 Subject: [PATCH 14/14] Apply suggestion from @bramkragten --- src/components/ha-sidebar.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index a6fc0af867ec..108a9450cf93 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -242,7 +242,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { return nothing; } - // Show the supervisor as being part of configuration const selectedPanel = this.hass.panelUrl; // prettier-ignore