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": {