From 32b3c833372c5672a6dc4bbcc34d7299bd50c15a Mon Sep 17 00:00:00 2001
From: Wendelin <12148533+wendevlin@users.noreply.github.com>
Date: Wed, 21 May 2025 13:42:43 +0200
Subject: [PATCH] Edit sidebar in a dialog (#25532)
---
src/components/ha-items-display-editor.ts | 29 ++-
src/components/ha-sidebar.ts | 213 +++---------------
src/dialogs/sidebar/dialog-edit-sidebar.ts | 159 +++++++++++++
.../sidebar/show-dialog-edit-sidebar.ts | 18 ++
src/layouts/home-assistant-main.ts | 36 +--
.../profile/ha-profile-section-general.ts | 12 +-
src/resources/ha-sidebar-edit-style.ts | 73 ------
src/translations/en.json | 4 +-
8 files changed, 264 insertions(+), 280 deletions(-)
create mode 100644 src/dialogs/sidebar/dialog-edit-sidebar.ts
create mode 100644 src/dialogs/sidebar/show-dialog-edit-sidebar.ts
delete mode 100644 src/resources/ha-sidebar-edit-style.ts
diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts
index af7fb2ba1f..d0e1319be0 100644
--- a/src/components/ha-items-display-editor.ts
+++ b/src/components/ha-items-display-editor.ts
@@ -25,6 +25,7 @@ export interface DisplayItem {
value: string;
label: string;
description?: string;
+ disableSorting?: boolean;
}
export interface DisplayValue {
@@ -50,6 +51,9 @@ export class HaItemDisplayEditor extends LitElement {
@property({ type: Boolean, attribute: "show-navigation-button" })
public showNavigationButton = false;
+ @property({ type: Boolean, attribute: "dont-sort-visible" })
+ public dontSortVisible = false;
+
@property({ attribute: false })
public value: DisplayValue = {
order: [],
@@ -122,9 +126,15 @@ export class HaItemDisplayEditor extends LitElement {
private _visibleItems = memoizeOne(
(items: DisplayItem[], hidden: string[], order: string[]) => {
const compare = orderCompare(order);
- return items
- .filter((item) => !hidden.includes(item.value))
- .sort((a, b) => compare(a.value, b.value));
+
+ const visibleItems = items.filter((item) => !hidden.includes(item.value));
+ if (this.dontSortVisible) {
+ return visibleItems;
+ }
+
+ return items.sort((a, b) =>
+ a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
+ );
}
);
@@ -160,7 +170,14 @@ export class HaItemDisplayEditor extends LitElement {
(item) => item.value,
(item: DisplayItem, _idx) => {
const isVisible = !this.value.hidden.includes(item.value);
- const { label, value, description, icon, iconPath } = item;
+ const {
+ label,
+ value,
+ description,
+ icon,
+ iconPath,
+ disableSorting,
+ } = item;
return html`
${label}
${description
? html`${description}`
: nothing}
- ${isVisible
+ ${isVisible && !disableSorting
? html`
${!this.narrow
@@ -389,11 +364,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
>
`
: ""}
- ${this.editMode
- ? html`
- ${this.hass.localize("ui.sidebar.done")}
- `
- : html`Home Assistant
`}
+ Home Assistant
`;
}
@@ -401,14 +372,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
- this._panelOrder,
- this._hiddenPanels,
+ this.panelOrder,
+ this.hiddenPanels,
this.hass.locale
);
// prettier-ignore
return html`
-
- ${this.editMode
- ? this._renderPanelsEdit(beforeSpacer, selectedPanel)
- : this._renderPanels(beforeSpacer, selectedPanel)}
+ ${this._renderPanels(beforeSpacer, selectedPanel)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer, selectedPanel)}
${this._renderExternalConfiguration()}
-
`;
}
- private _renderPanels(
- panels: PanelInfo[],
- selectedPanel: string,
- sortable = false
- ) {
+ private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
@@ -444,36 +407,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
- selectedPanel,
- sortable
+ selectedPanel
)
);
}
- private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
- return html`
- ${this._renderPanels(beforeSpacer, selectedPanel, true)}
- ${this._renderSpacer()}${this._renderHiddenPanels()}
- `;
- }
-
private _renderPanel(
urlPath: string,
title: string | null,
icon: string | null | undefined,
iconPath: string | null | undefined,
- selectedPanel: string,
- sortable = false
+ selectedPanel: string
) {
return urlPath === "config"
? this._renderConfiguration(title, selectedPanel)
: html`
`
: html``}
${title}
- ${this.editMode
- ? html``
- : nothing}
`;
}
- private _panelMoved(ev: CustomEvent) {
- ev.stopPropagation();
- const { oldIndex, newIndex } = ev.detail;
-
- const [beforeSpacer] = computePanels(
- this.hass.panels,
- this.hass.defaultPanel,
- this._panelOrder,
- this._hiddenPanels,
- this.hass.locale
- );
-
- const panelOrder = beforeSpacer.map((panel) => panel.url_path);
- const panel = panelOrder.splice(oldIndex, 1)[0];
- panelOrder.splice(newIndex, 0, panel);
-
- this._panelOrder = panelOrder;
- }
-
- private _renderHiddenPanels() {
- return html`${this._hiddenPanels.length
- ? html`${this._hiddenPanels.map((url) => {
- const panel = this.hass.panels[url];
- if (!panel) {
- return "";
- }
- return html`
- ${panel.url_path === this.hass.defaultPanel && !panel.icon
- ? html``
- : panel.url_path in PANEL_ICONS
- ? html``
- : html``}
- ${panel.url_path === this.hass.defaultPanel
- ? this.hass.localize("panel.states")
- : this.hass.localize(`panel.${panel.title}`) ||
- panel.title}
-
- `;
- })}
- ${this._renderSpacer()}`
- : ""}`;
- }
-
private _renderDivider() {
return html`
`;
}
@@ -677,48 +559,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return;
}
- fireEvent(this, "hass-edit-sidebar", { editMode: true });
+ showEditSidebarDialog(this, {
+ saveCallback: this._saveSidebar,
+ });
}
- private async _editModeActivated() {
- await this._loadEditStyle();
- }
-
- private async _loadEditStyle() {
- if (this._editStyleLoaded) return;
-
- const editStylesImport = await import("../resources/ha-sidebar-edit-style");
-
- const style = document.createElement("style");
- style.innerHTML = (editStylesImport.sidebarEditStyle as CSSResult).cssText;
- this.shadowRoot!.appendChild(style);
-
- await this.updateComplete;
- }
-
- private _closeEditMode() {
- fireEvent(this, "hass-edit-sidebar", { editMode: false });
- }
-
- private async _hidePanel(ev: Event) {
- ev.preventDefault();
- const panel = (ev.currentTarget as any).panel;
- if (this._hiddenPanels.includes(panel)) {
- return;
- }
- // Make a copy for Memoize
- this._hiddenPanels = [...this._hiddenPanels, panel];
- // Remove it from the panel order
- this._panelOrder = this._panelOrder.filter((order) => order !== panel);
- }
-
- private async _unhidePanel(ev: Event) {
- ev.preventDefault();
- const panel = (ev.currentTarget as any).panel;
- this._hiddenPanels = this._hiddenPanels.filter(
- (hidden) => hidden !== panel
- );
- }
+ private _saveSidebar = (order: string[], hidden: string[]) => {
+ fireEvent(this, "hass-edit-sidebar", {
+ order,
+ hidden,
+ });
+ };
private _itemMouseEnter(ev: MouseEvent) {
// On keypresses on the listbox, we're going to ignore mouse enter events
@@ -875,12 +726,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
:host([expanded]) .title {
display: initial;
}
- :host([expanded]) .menu mwc-button {
- margin: 0 8px;
- }
- .menu mwc-button {
- width: 100%;
- }
.hidden-panel {
display: none;
}
diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts
new file mode 100644
index 0000000000..efc42284cc
--- /dev/null
+++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts
@@ -0,0 +1,159 @@
+import "@material/mwc-linear-progress/mwc-linear-progress";
+import { mdiClose } from "@mdi/js";
+import { css, html, LitElement, nothing } from "lit";
+import { customElement, property, query, state } from "lit/decorators";
+import memoizeOne from "memoize-one";
+import { fireEvent } from "../../common/dom/fire_event";
+import "../../components/ha-dialog-header";
+import "../../components/ha-icon-button";
+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 type { HomeAssistant } from "../../types";
+import type { EditSidebarDialogParams } from "./show-dialog-edit-sidebar";
+
+@customElement("dialog-edit-sidebar")
+class DialogEditSidebar extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _open = false;
+
+ @query("ha-md-dialog") private _dialog?: HaMdDialog;
+
+ @state() private _order: string[] = [];
+
+ @state() private _hidden: string[] = [];
+
+ private _saveCallback?: (order: string[], hidden: string[]) => void;
+
+ public async showDialog(params: EditSidebarDialogParams): Promise {
+ this._open = true;
+
+ const storedOrder = localStorage.getItem("sidebarPanelOrder");
+ const storedHidden = localStorage.getItem("sidebarHiddenPanels");
+
+ this._order = storedOrder ? JSON.parse(storedOrder) : this._order;
+ this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden;
+ this._saveCallback = params.saveCallback;
+ }
+
+ private _dialogClosed(): void {
+ this._open = false;
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+
+ public closeDialog(): void {
+ this._dialog?.close();
+ }
+
+ private _panels = memoizeOne((panels: HomeAssistant["panels"]) =>
+ panels ? Object.values(panels) : []
+ );
+
+ protected render() {
+ if (!this._open) {
+ return nothing;
+ }
+
+ const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar");
+
+ const panels = this._panels(this.hass.panels);
+
+ const [beforeSpacer, afterSpacer] = computePanels(
+ this.hass.panels,
+ this.hass.defaultPanel,
+ this._order,
+ this._hidden,
+ this.hass.locale
+ );
+
+ const items = [
+ ...beforeSpacer,
+ ...panels.filter((panel) => this._hidden.includes(panel.url_path)),
+ ...afterSpacer.filter((panel) => panel.url_path !== "config"),
+ ].map((panel) => ({
+ value: panel.url_path,
+ label:
+ panel.url_path === this.hass.defaultPanel
+ ? panel.title || this.hass.localize("panel.states")
+ : this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
+ icon: panel.icon || undefined,
+ iconPath:
+ panel.url_path === this.hass.defaultPanel && !panel.icon
+ ? PANEL_ICONS.lovelace
+ : panel.url_path in PANEL_ICONS
+ ? PANEL_ICONS[panel.url_path]
+ : undefined,
+ disableSorting: panel.url_path === "developer-tools",
+ }));
+
+ return html`
+
+
+
+ ${dialogTitle}
+
+
+
+
+
+
+
+ ${this.hass.localize("ui.common.cancel")}
+
+
+ ${this.hass.localize("ui.common.save")}
+
+
+
+ `;
+ }
+
+ private _changed(ev: CustomEvent<{ value: DisplayValue }>): void {
+ const { order = [], hidden = [] } = ev.detail.value;
+ this._order = [...order];
+ this._hidden = [...hidden];
+ }
+
+ private _save(): void {
+ this._saveCallback?.(this._order ?? [], this._hidden ?? []);
+ this.closeDialog();
+ }
+
+ static styles = css`
+ ha-md-dialog {
+ min-width: 600px;
+ max-height: 90%;
+ }
+
+ @media all and (max-width: 600px), all and (max-height: 500px) {
+ ha-md-dialog {
+ --md-dialog-container-shape: 0;
+ min-width: 100%;
+ min-height: 100%;
+ }
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-edit-sidebar": DialogEditSidebar;
+ }
+}
diff --git a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts
new file mode 100644
index 0000000000..4a88bafd6b
--- /dev/null
+++ b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts
@@ -0,0 +1,18 @@
+import { fireEvent } from "../../common/dom/fire_event";
+
+export interface EditSidebarDialogParams {
+ saveCallback: (order: string[], hidden: string[]) => void;
+}
+
+export const loadEditSidebarDialog = () => import("./dialog-edit-sidebar");
+
+export const showEditSidebarDialog = (
+ element: HTMLElement,
+ dialogParams: EditSidebarDialogParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-edit-sidebar",
+ dialogImport: loadEditSidebarDialog,
+ dialogParams,
+ });
+};
diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts
index 831ac020c8..eeb66d9fa1 100644
--- a/src/layouts/home-assistant-main.ts
+++ b/src/layouts/home-assistant-main.ts
@@ -10,6 +10,7 @@ import { showNotificationDrawer } from "../dialogs/notifications/show-notificati
import type { HomeAssistant, Route } from "../types";
import "./partial-panel-resolver";
import { computeRTLDirection } from "../common/util/compute_rtl";
+import { storage } from "../common/decorators/storage";
declare global {
// for fire event
@@ -25,7 +26,8 @@ declare global {
}
interface EditSideBarEvent {
- editMode: boolean;
+ order: string[];
+ hidden: string[];
}
@customElement("home-assistant-main")
@@ -42,6 +44,22 @@ export class HomeAssistantMain extends LitElement {
@state() private _drawerOpen = false;
+ @state()
+ @storage({
+ key: "sidebarPanelOrder",
+ state: true,
+ subscribe: true,
+ })
+ private _panelOrder: string[] = [];
+
+ @state()
+ @storage({
+ key: "sidebarHiddenPanels",
+ state: true,
+ subscribe: true,
+ })
+ private _hiddenPanels: string[] = [];
+
constructor() {
super();
listenMediaQuery("(max-width: 870px)", (matches) => {
@@ -63,7 +81,8 @@ export class HomeAssistantMain extends LitElement {
.hass=${this.hass}
.narrow=${sidebarNarrow}
.route=${this.route}
- .editMode=${this._sidebarEditMode}
+ .panelOrder=${this._panelOrder}
+ .hiddenPanels=${this._hiddenPanels}
.alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"}
>
) => {
- this._sidebarEditMode = ev.detail.editMode;
-
- if (this._sidebarEditMode) {
- if (this._sidebarNarrow) {
- this._drawerOpen = true;
- } else {
- fireEvent(this, "hass-dock-sidebar", {
- dock: "docked",
- });
- }
- }
+ this._panelOrder = ev.detail.order;
+ this._hiddenPanels = ev.detail.hidden;
}
);
diff --git a/src/panels/profile/ha-profile-section-general.ts b/src/panels/profile/ha-profile-section-general.ts
index 4479d5dff0..4e9ebea2cb 100644
--- a/src/panels/profile/ha-profile-section-general.ts
+++ b/src/panels/profile/ha-profile-section-general.ts
@@ -10,6 +10,7 @@ import { isExternal } from "../../data/external";
import type { CoreFrontendUserData } from "../../data/frontend";
import { subscribeFrontendUserData } from "../../data/frontend";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
+import { showEditSidebarDialog } from "../../dialogs/sidebar/show-dialog-edit-sidebar";
import "../../layouts/hass-tabs-subpage";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
@@ -247,9 +248,18 @@ class HaProfileSectionGeneral extends LitElement {
}
private _customizeSidebar() {
- fireEvent(this, "hass-edit-sidebar", { editMode: true });
+ showEditSidebarDialog(this, {
+ saveCallback: this._saveSidebar,
+ });
}
+ private _saveSidebar = (order: string[], hidden: string[]) => {
+ fireEvent(this, "hass-edit-sidebar", {
+ order,
+ hidden,
+ });
+ };
+
private _handleLogOut() {
showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.profile.logout_title"),
diff --git a/src/resources/ha-sidebar-edit-style.ts b/src/resources/ha-sidebar-edit-style.ts
deleted file mode 100644
index fa09d6fb8a..0000000000
--- a/src/resources/ha-sidebar-edit-style.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { css } from "lit";
-
-export const sidebarEditStyle = css`
- ha-sortable ha-md-list-item.draggable:nth-child(2n) {
- animation-name: keyframes1;
- animation-iteration-count: infinite;
- transform-origin: 50% 10%;
- animation-delay: -0.75s;
- animation-duration: 0.25s;
- }
-
- ha-sortable ha-md-list-item.draggable:nth-child(2n-1) {
- animation-name: keyframes2;
- animation-iteration-count: infinite;
- animation-direction: alternate;
- transform-origin: 30% 5%;
- animation-delay: -0.5s;
- animation-duration: 0.33s;
- }
-
- ha-sortable ha-md-list-item.draggable {
- cursor: grab;
- }
-
- .hidden-panel {
- display: flex !important;
- }
-
- @keyframes keyframes1 {
- 0% {
- transform: rotate(-1deg);
- animation-timing-function: ease-in;
- }
-
- 50% {
- transform: rotate(1.5deg);
- animation-timing-function: ease-out;
- }
- }
-
- @keyframes keyframes2 {
- 0% {
- transform: rotate(1deg);
- animation-timing-function: ease-in;
- }
-
- 50% {
- transform: rotate(-1.5deg);
- animation-timing-function: ease-out;
- }
- }
-
- .show-panel,
- .hide-panel {
- display: none;
- --mdc-icon-button-size: 24px;
- }
-
- :host([expanded]) .hide-panel {
- display: block;
- }
-
- :host([expanded]) .show-panel {
- display: block;
- }
-
- ha-md-list-item.hidden-panel,
- ha-md-list-item.hidden-panel span,
- ha-md-list-item.hidden-panel ha-icon[slot="start"] {
- color: var(--secondary-text-color);
- cursor: pointer;
- }
-`;
diff --git a/src/translations/en.json b/src/translations/en.json
index 6f8173b2b6..59e71a558c 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -2021,9 +2021,7 @@
"sidebar": {
"external_app_configuration": "App settings",
"sidebar_toggle": "Sidebar toggle",
- "done": "Done",
- "hide_panel": "Hide panel",
- "show_panel": "Show panel"
+ "edit_sidebar": "Edit sidebar"
},
"panel": {
"my": {