From 3ade1fbf439fba08826ecfcc984e2a6bf1eef15e Mon Sep 17 00:00:00 2001 From: karwosts Date: Fri, 22 Mar 2024 08:43:00 -0700 Subject: [PATCH] Trap sidebar links when page has a dirty editor --- src/components/ha-sidebar.ts | 43 ++++++++++++++++++- .../config/automation/ha-automation-editor.ts | 13 +++++- src/translations/en.json | 3 +- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 6110c8546818..60273daa0e79 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -35,6 +35,7 @@ import { customElement, eventOptions, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { storage } from "../common/decorators/storage"; +import { navigate } from "../common/navigate"; import { fireEvent } from "../common/dom/fire_event"; import { toggleAttribute } from "../common/dom/toggle_attribute"; import { stringCompare } from "../common/string/compare"; @@ -49,6 +50,7 @@ import { UpdateEntity, updateCanInstall } from "../data/update"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { haStyleScrollbar } from "../resources/styles"; +import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box"; import type { HomeAssistant, PanelInfo, Route } from "../types"; import "./ha-icon"; import "./ha-icon-button"; @@ -224,6 +226,13 @@ class HaSidebar extends SubscribeMixin(LitElement) { }) private _hiddenPanels: string[] = []; + @storage({ + key: "pageDirty", + state: false, + subscribe: true, + }) + private _pageDirty = false; + public hassSubscribe(): UnsubscribeFunc[] { return this.hass.user?.is_admin ? [ @@ -289,6 +298,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); + this._pageDirty = false; subscribeNotifications(this.hass.connection, (notifications) => { this._notifications = notifications; }); @@ -430,6 +440,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { href=${`/${urlPath}`} data-panel=${urlPath} tabindex="-1" + @click=${this._confirmNavigate} @mouseenter=${this._itemMouseEnter} @mouseleave=${this._itemMouseLeave} > @@ -545,6 +556,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { href="/config" data-panel="config" tabindex="-1" + @click=${this._confirmNavigate} @mouseenter=${this._itemMouseEnter} @mouseleave=${this._itemMouseLeave} > @@ -615,6 +627,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { tabindex="-1" role="option" aria-label=${this.hass.localize("panel.profile")} + @click=${this._confirmNavigate} @mouseenter=${this._itemMouseEnter} @mouseleave=${this._itemMouseLeave} > @@ -661,13 +674,41 @@ class HaSidebar extends SubscribeMixin(LitElement) { : ""}`; } - private _handleExternalAppConfiguration(ev: Event) { + private async _handleExternalAppConfiguration(ev: Event) { ev.preventDefault(); + if (this._pageDirty && !(await this._confirmDirty())) { + return; + } this.hass.auth.external!.fireMessage({ type: "config_screen/show", }); } + private async _confirmDirty(): Promise { + const confirmed = await showConfirmationDialog(this, { + text: this.hass!.localize("ui.sidebar.confirm_unsaved"), + confirmText: this.hass!.localize("ui.common.leave"), + dismissText: this.hass!.localize("ui.common.stay"), + destructive: true, + }); + if (confirmed) { + this._pageDirty = false; + } + return confirmed; + } + + private async _confirmNavigate(ev: CustomEvent) { + if (!this._pageDirty) { + return; + } + const url = (ev.currentTarget as any).href; + ev.preventDefault(); + const leave = await this._confirmDirty(); + if (leave) { + navigate(url); + } + } + private get _tooltip() { return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 11571368c4ff..afa263c49e1a 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -28,6 +28,7 @@ import { property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; +import { storage } from "../../../common/decorators/storage"; import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button-menu"; import "../../../components/ha-fab"; @@ -98,7 +99,12 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { @state() private _config?: AutomationConfig; - @state() private _dirty = false; + @storage({ + key: "pageDirty", + state: true, + subscribe: false, + }) + private _dirty = false; @state() private _errors?: string; @@ -117,6 +123,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private _configSubscriptionsId = 1; + disconnectedCallback() { + super.disconnectedCallback(); + this._dirty = false; + } + protected render(): TemplateResult { const stateObj = this._entityId ? this.hass.states[this._entityId] diff --git a/src/translations/en.json b/src/translations/en.json index 03bf3a621e22..1c81e42ed056 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1634,7 +1634,8 @@ "sidebar_toggle": "Sidebar toggle", "done": "Done", "hide_panel": "Hide panel", - "show_panel": "Show panel" + "show_panel": "Show panel", + "confirm_unsaved": "[%key:ui::panel::config::common::editor::confirm_unsaved%]" }, "panel": { "my": {