diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 64476ae81a..d2f77a9770 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -14,7 +14,6 @@ import { mdiTooltipAccount, mdiViewDashboard, } from "@mdi/js"; -import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { @@ -30,6 +29,7 @@ import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; import { stringCompare } from "../common/string/compare"; import { throttle } from "../common/util/throttle"; +import { subscribeFrontendUserData } from "../data/frontend"; import type { ActionHandlerDetail } from "../data/lovelace/action_handler"; import type { PersistentNotification } from "../data/persistent_notification"; import { subscribeNotifications } from "../data/persistent_notification"; @@ -41,11 +41,13 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; +import "./ha-fade-in"; import "./ha-icon"; import "./ha-icon-button"; import "./ha-md-list"; import "./ha-md-list-item"; import type { HaMdListItem } from "./ha-md-list-item"; +import "./ha-spinner"; import "./ha-svg-icon"; import "./user/ha-user-badge"; @@ -187,38 +189,57 @@ class HaSidebar extends SubscribeMixin(LitElement) { @property({ attribute: "always-expand", type: Boolean }) public alwaysExpand = false; - @property({ attribute: false }) - public panelOrder!: string[]; - - @property({ attribute: false }) - public hiddenPanels!: string[]; - @state() private _notifications?: PersistentNotification[]; @state() private _updatesCount = 0; @state() private _issuesCount = 0; + @state() private _panelOrder?: string[]; + + @state() private _hiddenPanels?: string[]; + private _mouseLeaveTimeout?: number; private _tooltipHideTimeout?: number; private _recentKeydownActiveUntil = 0; - private _unsubPersistentNotifications: UnsubscribeFunc | undefined; - @query(".tooltip") private _tooltip!: HTMLDivElement; - public hassSubscribe(): UnsubscribeFunc[] { - return this.hass.user?.is_admin - ? [ - subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { - this._issuesCount = repairs.issues.filter( - (issue) => !issue.ignored - ).length; - }), - ] - : []; + public hassSubscribe() { + return [ + subscribeFrontendUserData( + this.hass.connection, + "sidebar", + ({ value }) => { + this._panelOrder = value?.panelOrder; + this._hiddenPanels = value?.hiddenPanels; + + // fallback to old localStorage values + if (!this._panelOrder) { + const storedOrder = localStorage.getItem("sidebarPanelOrder"); + this._panelOrder = storedOrder ? JSON.parse(storedOrder) : []; + } + if (!this._hiddenPanels) { + const storedHidden = localStorage.getItem("sidebarHiddenPanels"); + this._hiddenPanels = storedHidden ? JSON.parse(storedHidden) : []; + } + } + ), + subscribeNotifications(this.hass.connection, (notifications) => { + this._notifications = notifications; + }), + ...(this.hass.user?.is_admin + ? [ + subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { + this._issuesCount = repairs.issues.filter( + (issue) => !issue.ignored + ).length; + }), + ] + : []), + ]; } protected render() { @@ -254,8 +275,8 @@ class HaSidebar extends SubscribeMixin(LitElement) { changedProps.has("_updatesCount") || changedProps.has("_issuesCount") || changedProps.has("_notifications") || - changedProps.has("hiddenPanels") || - changedProps.has("panelOrder") + changedProps.has("_hiddenPanels") || + changedProps.has("_panelOrder") ) { return true; } @@ -279,23 +300,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { ); } - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this._subscribePersistentNotifications(); - } - - private _subscribePersistentNotifications(): void { - if (this._unsubPersistentNotifications) { - this._unsubPersistentNotifications(); - } - this._unsubPersistentNotifications = subscribeNotifications( - this.hass.connection, - (notifications) => { - this._notifications = notifications; - } - ); - } - protected updated(changedProps) { super.updated(changedProps); if (changedProps.has("alwaysExpand")) { @@ -307,14 +311,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - oldHass?.connected === false && - this.hass.connected === true - ) { - this._subscribePersistentNotifications(); - } - this._calculateCounts(); if (!SUPPORT_SCROLL_IF_NEEDED) { @@ -369,11 +365,19 @@ class HaSidebar extends SubscribeMixin(LitElement) { } private _renderAllPanels(selectedPanel: string) { + if (!this._panelOrder || !this._hiddenPanels) { + return html` + + `; + } + const [beforeSpacer, afterSpacer] = computePanels( this.hass.panels, this.hass.defaultPanel, - this.panelOrder, - this.hiddenPanels, + this._panelOrder, + this._hiddenPanels, this.hass.locale ); @@ -559,18 +563,9 @@ class HaSidebar extends SubscribeMixin(LitElement) { return; } - showEditSidebarDialog(this, { - saveCallback: this._saveSidebar, - }); + showEditSidebarDialog(this); } - 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 // for 100ms so that we ignore it when pressing down arrow scrolls the @@ -730,13 +725,22 @@ class HaSidebar extends SubscribeMixin(LitElement) { display: none; } + ha-fade-in, ha-md-list { - padding: 4px 0; - box-sizing: border-box; - height: calc(100% - var(--header-height) - 132px); height: calc( 100% - var(--header-height) - 132px - var(--safe-area-inset-bottom) ); + } + + ha-fade-in { + display: flex; + justify-content: center; + align-items: center; + } + + ha-md-list { + padding: 4px 0; + box-sizing: border-box; overflow-x: hidden; background: none; margin-left: var(--safe-area-inset-left); diff --git a/src/data/frontend.ts b/src/data/frontend.ts index 7e46217cca..8f0c6749cc 100644 --- a/src/data/frontend.ts +++ b/src/data/frontend.ts @@ -5,9 +5,15 @@ export interface CoreFrontendUserData { showEntityIdPicker?: boolean; } +export interface SidebarFrontendUserData { + panelOrder: string[]; + hiddenPanels: string[]; +} + declare global { interface FrontendUserData { core: CoreFrontendUserData; + sidebar: SidebarFrontendUserData; } } diff --git a/src/data/translation.ts b/src/data/translation.ts index 260294e506..88ffff8ee8 100644 --- a/src/data/translation.ts +++ b/src/data/translation.ts @@ -1,9 +1,5 @@ import type { HomeAssistant } from "../types"; -import { - fetchFrontendUserData, - saveFrontendUserData, - subscribeFrontendUserData, -} from "./frontend"; +import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend"; export enum NumberFormat { language = "language", @@ -78,9 +74,6 @@ export type TranslationCategory = | "selector" | "services"; -export const fetchTranslationPreferences = (hass: HomeAssistant) => - fetchFrontendUserData(hass.connection, "language"); - export const subscribeTranslationPreferences = ( hass: HomeAssistant, callback: (data: { value: FrontendLocaleData | null }) => void diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts index efc42284cc..fed10ad593 100644 --- a/src/dialogs/sidebar/dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts @@ -1,18 +1,25 @@ import "@material/mwc-linear-progress/mwc-linear-progress"; import { mdiClose } from "@mdi/js"; -import { css, html, LitElement, nothing } from "lit"; +import { css, html, LitElement, nothing, type TemplateResult } 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-alert"; 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 "../../components/ha-md-dialog"; import type { HaMdDialog } from "../../components/ha-md-dialog"; import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar"; +import "../../components/ha-spinner"; +import { + fetchFrontendUserData, + saveFrontendUserData, +} from "../../data/frontend"; import type { HomeAssistant } from "../../types"; -import type { EditSidebarDialogParams } from "./show-dialog-edit-sidebar"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; @customElement("dialog-edit-sidebar") class DialogEditSidebar extends LitElement { @@ -22,21 +29,43 @@ class DialogEditSidebar extends LitElement { @query("ha-md-dialog") private _dialog?: HaMdDialog; - @state() private _order: string[] = []; + @state() private _order?: string[]; - @state() private _hidden: string[] = []; + @state() private _hidden?: string[]; - private _saveCallback?: (order: string[], hidden: string[]) => void; + @state() private _error?: string; - public async showDialog(params: EditSidebarDialogParams): Promise { + /** + * If user has old localStorage values, show a confirmation dialog + */ + @state() private _migrateToUserData = false; + + public async showDialog(): Promise { this._open = true; - const storedOrder = localStorage.getItem("sidebarPanelOrder"); - const storedHidden = localStorage.getItem("sidebarHiddenPanels"); + this._getData(); + } - this._order = storedOrder ? JSON.parse(storedOrder) : this._order; - this._hidden = storedHidden ? JSON.parse(storedHidden) : this._hidden; - this._saveCallback = params.saveCallback; + private async _getData() { + try { + const data = await fetchFrontendUserData(this.hass.connection, "sidebar"); + this._order = data?.panelOrder; + this._hidden = data?.hiddenPanels; + + // fallback to old localStorage values + if (!this._order) { + const storedOrder = localStorage.getItem("sidebarPanelOrder"); + this._migrateToUserData = !!storedOrder; + this._order = storedOrder ? JSON.parse(storedOrder) : []; + } + if (!this._hidden) { + const storedHidden = localStorage.getItem("sidebarHiddenPanels"); + this._migrateToUserData = this._migrateToUserData || !!storedHidden; + this._hidden = storedHidden ? JSON.parse(storedHidden) : []; + } + } catch (err: any) { + this._error = err.message || err; + } } private _dialogClosed(): void { @@ -52,12 +81,16 @@ class DialogEditSidebar extends LitElement { panels ? Object.values(panels) : [] ); - protected render() { - if (!this._open) { - return nothing; + private _renderContent(): TemplateResult { + if (!this._order || !this._hidden) { + return html``; } - const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar"); + if (this._error) { + return html`${this._error}`; + } const panels = this._panels(this.hass.panels); @@ -71,7 +104,7 @@ class DialogEditSidebar extends LitElement { const items = [ ...beforeSpacer, - ...panels.filter((panel) => this._hidden.includes(panel.url_path)), + ...panels.filter((panel) => this._hidden!.includes(panel.url_path)), ...afterSpacer.filter((panel) => panel.url_path !== "config"), ].map((panel) => ({ value: panel.url_path, @@ -89,6 +122,26 @@ class DialogEditSidebar extends LitElement { disableSorting: panel.url_path === "developer-tools", })); + return html` + `; + } + + protected render() { + if (!this._open) { + return nothing; + } + + const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar"); + return html` @@ -98,26 +151,22 @@ class DialogEditSidebar extends LitElement { .path=${mdiClose} @click=${this.closeDialog} > - ${dialogTitle} + ${dialogTitle} + ${!this._migrateToUserData + ? html`${this.hass.localize("ui.sidebar.edit_subtitle")}` + : nothing} -
- - -
+
${this._renderContent()}
${this.hass.localize("ui.common.cancel")} - + ${this.hass.localize("ui.common.save")}
@@ -131,8 +180,27 @@ class DialogEditSidebar extends LitElement { this._hidden = [...hidden]; } - private _save(): void { - this._saveCallback?.(this._order ?? [], this._hidden ?? []); + private async _save() { + if (this._migrateToUserData) { + const confirmation = await showConfirmationDialog(this, { + destructive: true, + text: this.hass.localize("ui.sidebar.migrate_to_user_data"), + }); + if (!confirmation) { + return; + } + } + + try { + await saveFrontendUserData(this.hass.connection, "sidebar", { + panelOrder: this._order!, + hiddenPanels: this._hidden!, + }); + } catch (err: any) { + this._error = err.message || err; + return; + } + this.closeDialog(); } @@ -149,6 +217,12 @@ class DialogEditSidebar extends LitElement { min-height: 100%; } } + + ha-fade-in { + display: flex; + justify-content: center; + align-items: center; + } `; } diff --git a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts index 4a88bafd6b..dc8ce5b1ea 100644 --- a/src/dialogs/sidebar/show-dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/show-dialog-edit-sidebar.ts @@ -1,18 +1,11 @@ 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 => { +export const showEditSidebarDialog = (element: HTMLElement): void => { fireEvent(element, "show-dialog", { dialogTag: "dialog-edit-sidebar", dialogImport: loadEditSidebarDialog, - dialogParams, + dialogParams: {}, }); }; diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index 75d5a54765..16656921c8 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -5,31 +5,23 @@ import type { HASSDomEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event"; import { listenMediaQuery } from "../common/dom/media_query"; import { toggleAttribute } from "../common/dom/toggle_attribute"; +import { computeRTLDirection } from "../common/util/compute_rtl"; import "../components/ha-drawer"; import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer"; 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 interface HASSDomEvents { "hass-toggle-menu": undefined | { open?: boolean }; - "hass-edit-sidebar": EditSideBarEvent; "hass-show-notifications": undefined; } interface HTMLElementEventMap { - "hass-edit-sidebar": HASSDomEvent; "hass-toggle-menu": HASSDomEvent; } } -interface EditSideBarEvent { - order: string[]; - hidden: string[]; -} - @customElement("home-assistant-main") export class HomeAssistantMain extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -44,22 +36,6 @@ 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) => { @@ -81,8 +57,6 @@ export class HomeAssistantMain extends LitElement { .hass=${this.hass} .narrow=${sidebarNarrow} .route=${this.route} - .panelOrder=${this._panelOrder} - .hiddenPanels=${this._hiddenPanels} .alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"} > ) => { - this._panelOrder = ev.detail.order; - this._hiddenPanels = ev.detail.hidden; - } - ); - this.addEventListener("hass-toggle-menu", (ev) => { if (this._sidebarEditMode) { return; diff --git a/src/panels/profile/ha-profile-section-general.ts b/src/panels/profile/ha-profile-section-general.ts index a14043f625..46cbc1c268 100644 --- a/src/panels/profile/ha-profile-section-general.ts +++ b/src/panels/profile/ha-profile-section-general.ts @@ -150,6 +150,23 @@ class HaProfileSectionGeneral extends LitElement { .narrow=${this.narrow} .hass=${this.hass} > + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.header" + )} + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.description" + )} + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.button" + )} + + ${this.hass.user!.is_admin ? html` - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.header" - )} - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.description" - )} - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.button" - )} - - ${this.hass.dockedSidebar !== "auto" || !this.narrow ? html` { - fireEvent(this, "hass-edit-sidebar", { - order, - hidden, - }); - }; - private _handleLogOut() { showConfirmationDialog(this, { title: this.hass.localize("ui.panel.profile.logout_title"), diff --git a/src/translations/en.json b/src/translations/en.json index 725074c0ca..ff6bd50f71 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2019,7 +2019,9 @@ "sidebar": { "external_app_configuration": "App settings", "sidebar_toggle": "Sidebar toggle", - "edit_sidebar": "Edit sidebar" + "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 into. To create a sidebar per device, you should use a different user for that device." }, "panel": { "my": {