From bd7fbd4c2cd4d95876c179f05b32d4e0c7252237 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 5 Jan 2025 19:11:09 +0100 Subject: [PATCH 1/2] Improve automation save dialog when leaving editor dirty --- .../dialog-automation-save.ts} | 294 ++++++++++-------- .../show-dialog-automation-save.ts} | 29 +- .../config/automation/ha-automation-editor.ts | 77 +++-- src/panels/config/script/ha-script-editor.ts | 8 +- src/translations/en.json | 9 +- 5 files changed, 246 insertions(+), 171 deletions(-) rename src/panels/config/automation/{automation-rename-dialog/dialog-automation-rename.ts => automation-save-dialog/dialog-automation-save.ts} (57%) rename src/panels/config/automation/{automation-rename-dialog/show-dialog-automation-rename.ts => automation-save-dialog/show-dialog-automation-save.ts} (58%) diff --git a/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts b/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts similarity index 57% rename from src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts rename to src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts index 177c95740251..6c1dc80ce9c4 100644 --- a/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts +++ b/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts @@ -20,13 +20,12 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import type { - AutomationRenameDialogParams, EntityRegistryUpdate, - ScriptRenameDialogParams, -} from "./show-dialog-automation-rename"; + SaveDialogParams, +} from "./show-dialog-automation-save"; -@customElement("ha-dialog-automation-rename") -class DialogAutomationRename extends LitElement implements HassDialog { +@customElement("ha-dialog-automation-save") +class DialogAutomationSave extends LitElement implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _opened = false; @@ -37,7 +36,7 @@ class DialogAutomationRename extends LitElement implements HassDialog { @state() private _entryUpdates!: EntityRegistryUpdate; - private _params!: AutomationRenameDialogParams | ScriptRenameDialogParams; + private _params!: SaveDialogParams; private _newName?: string; @@ -45,9 +44,7 @@ class DialogAutomationRename extends LitElement implements HassDialog { private _newDescription?: string; - public showDialog( - params: AutomationRenameDialogParams | ScriptRenameDialogParams - ): void { + public showDialog(params: SaveDialogParams): void { this._opened = true; this._params = params; this._newIcon = "icon" in params.config ? params.config.icon : undefined; @@ -95,20 +92,153 @@ class DialogAutomationRename extends LitElement implements HassDialog { `; } + protected _renderDiscard() { + if (!this._params.onDiscard) { + return nothing; + } + return html` + + ${this.hass.localize("ui.common.dont_save")} + + `; + } + + protected _renderInputs() { + if (this._params.hideInputs) { + return nothing; + } + + return html` + + + ${this._params.domain === "script" && + this._visibleOptionals.includes("icon") + ? html` + + + + + ` + : nothing} + ${this._visibleOptionals.includes("description") + ? html` ` + : nothing} + ${this._visibleOptionals.includes("category") + ? html` ` + : nothing} + ${this._visibleOptionals.includes("labels") + ? html` ` + : nothing} + ${this._visibleOptionals.includes("area") + ? html` ` + : nothing} + + + ${this._renderOptionalChip( + "description", + this.hass.localize( + "ui.panel.config.automation.editor.dialog.add_description" + ) + )} + ${this._params.domain === "script" + ? this._renderOptionalChip( + "icon", + this.hass.localize( + "ui.panel.config.automation.editor.dialog.add_icon" + ) + ) + : nothing} + ${this._renderOptionalChip( + "area", + this.hass.localize( + "ui.panel.config.automation.editor.dialog.add_area" + ) + )} + ${this._renderOptionalChip( + "category", + this.hass.localize( + "ui.panel.config.automation.editor.dialog.add_category" + ) + )} + ${this._renderOptionalChip( + "labels", + this.hass.localize( + "ui.panel.config.automation.editor.dialog.add_labels" + ) + )} + + `; + } + protected render() { if (!this._opened) { return nothing; } + + const title = this.hass.localize( + this._params.config.alias + ? "ui.panel.config.automation.editor.rename" + : "ui.panel.config.automation.editor.save" + ); + return html` - ${this.hass.localize( - this._params.config.alias - ? "ui.panel.config.automation.editor.rename" - : "ui.panel.config.automation.editor.save" - )} + ${this._params.title || title} ${this._error ? html`` : ""} - - - ${this._params.domain === "script" && - this._visibleOptionals.includes("icon") - ? html` - - - - - ` - : nothing} - ${this._visibleOptionals.includes("description") - ? html` ` - : nothing} - ${this._visibleOptionals.includes("category") - ? html` ` + ${this._params.description + ? html`

${this._params.description}

` : nothing} - ${this._visibleOptionals.includes("labels") - ? html` ` - : nothing} - ${this._visibleOptionals.includes("area") - ? html` ` - : nothing} - - - ${this._renderOptionalChip( - "description", - this.hass.localize( - "ui.panel.config.automation.editor.dialog.add_description" - ) - )} - ${this._params.domain === "script" - ? this._renderOptionalChip( - "icon", - this.hass.localize( - "ui.panel.config.automation.editor.dialog.add_icon" - ) - ) - : nothing} - ${this._renderOptionalChip( - "area", - this.hass.localize( - "ui.panel.config.automation.editor.dialog.add_area" - ) - )} - ${this._renderOptionalChip( - "category", - this.hass.localize( - "ui.panel.config.automation.editor.dialog.add_category" - ) - )} - ${this._renderOptionalChip( - "labels", - this.hass.localize( - "ui.panel.config.automation.editor.dialog.add_labels" - ) - )} - + ${this._renderInputs()} ${this._renderDiscard()}
@@ -247,7 +267,7 @@ class DialogAutomationRename extends LitElement implements HassDialog { ${this.hass.localize( - this._params.config.alias + this._params.config.alias && !this._params.onDiscard ? "ui.panel.config.automation.editor.rename" : "ui.panel.config.automation.editor.save" )} @@ -286,14 +306,19 @@ class DialogAutomationRename extends LitElement implements HassDialog { } } - private _save(): void { + private _handleDiscard() { + this._params.onDiscard?.(); + this.closeDialog(); + } + + private async _save(): Promise { if (!this._newName) { this._error = "Name is required"; return; } if (this._params.domain === "script") { - this._params.updateConfig( + await this._params.updateConfig( { ...this._params.config, alias: this._newName, @@ -303,7 +328,7 @@ class DialogAutomationRename extends LitElement implements HassDialog { this._entryUpdates ); } else { - this._params.updateConfig( + await this._params.updateConfig( { ...this._params.config, alias: this._newName, @@ -351,6 +376,9 @@ class DialogAutomationRename extends LitElement implements HassDialog { display: block; margin-bottom: 16px; } + .destructive { + --mdc-theme-primary: var(--error-color); + } `, ]; } @@ -358,6 +386,6 @@ class DialogAutomationRename extends LitElement implements HassDialog { declare global { interface HTMLElementTagNameMap { - "ha-dialog-automation-rename": DialogAutomationRename; + "ha-dialog-automation-save": DialogAutomationSave; } } diff --git a/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts b/src/panels/config/automation/automation-save-dialog/show-dialog-automation-save.ts similarity index 58% rename from src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts rename to src/panels/config/automation/automation-save-dialog/show-dialog-automation-save.ts index 4573fc85f243..cd816443d1d5 100644 --- a/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts +++ b/src/panels/config/automation/automation-save-dialog/show-dialog-automation-save.ts @@ -3,13 +3,18 @@ import type { AutomationConfig } from "../../../../data/automation"; import type { ScriptConfig } from "../../../../data/script"; import type { EntityRegistryEntry } from "../../../../data/entity_registry"; -export const loadAutomationRenameDialog = () => - import("./dialog-automation-rename"); +export const loadAutomationSaveDialog = () => + import("./dialog-automation-save"); interface BaseRenameDialogParams { entityRegistryUpdate?: EntityRegistryUpdate; entityRegistryEntry?: EntityRegistryEntry; onClose: () => void; + onDiscard?: () => void; + saveText?: string; + description?: string; + title?: string; + hideInputs?: boolean; } export interface EntityRegistryUpdate { @@ -18,31 +23,35 @@ export interface EntityRegistryUpdate { category: string; } -export interface AutomationRenameDialogParams extends BaseRenameDialogParams { +export interface AutomationSaveDialogParams extends BaseRenameDialogParams { config: AutomationConfig; domain: "automation"; updateConfig: ( config: AutomationConfig, entityRegistryUpdate: EntityRegistryUpdate - ) => void; + ) => Promise; } -export interface ScriptRenameDialogParams extends BaseRenameDialogParams { +export interface ScriptSaveDialogParams extends BaseRenameDialogParams { config: ScriptConfig; domain: "script"; updateConfig: ( config: ScriptConfig, entityRegistryUpdate: EntityRegistryUpdate - ) => void; + ) => Promise; } -export const showAutomationRenameDialog = ( +export type SaveDialogParams = + | AutomationSaveDialogParams + | ScriptSaveDialogParams; + +export const showAutomationSaveDialog = ( element: HTMLElement, - dialogParams: AutomationRenameDialogParams | ScriptRenameDialogParams + dialogParams: SaveDialogParams ): void => { fireEvent(element, "show-dialog", { - dialogTag: "ha-dialog-automation-rename", - dialogImport: loadAutomationRenameDialog, + dialogTag: "ha-dialog-automation-save", + dialogImport: loadAutomationSaveDialog, dialogParams, }); }; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 8d06326d6143..3fb42fda0add 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -19,7 +19,7 @@ import { } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { LitElement, css, html, nothing } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { consume } from "@lit-labs/context"; @@ -70,8 +70,8 @@ import "../ha-config-section"; import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode"; import { type EntityRegistryUpdate, - showAutomationRenameDialog, -} from "./automation-rename-dialog/show-dialog-automation-rename"; + showAutomationSaveDialog, +} from "./automation-save-dialog/show-dialog-automation-save"; import "./blueprint-automation-editor"; import "./manual-automation-editor"; import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; @@ -500,7 +500,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( .label=${this.hass.localize("ui.panel.config.automation.editor.save")} .disabled=${this._saving} extended - @click=${this._saveAutomation} + @click=${this._handleSaveAutomation} > @@ -743,20 +743,48 @@ export class HaAutomationEditor extends PreventUnsavedMixin( } private async _confirmUnsavedChanged(): Promise { - if (this._dirty) { - return showConfirmationDialog(this, { - title: this.hass!.localize( - "ui.panel.config.automation.editor.unsaved_confirm_title" + if (!this._dirty) { + return true; + } + + return new Promise((resolve) => { + showAutomationSaveDialog(this, { + config: this._config!, + domain: "automation", + updateConfig: async (config, entityRegistryUpdate) => { + this._config = config; + this._entityRegistryUpdate = entityRegistryUpdate; + this._dirty = true; + this.requestUpdate(); + + const id = this.automationId || String(Date.now()); + try { + await this._saveAutomation(id); + } catch (err: any) { + this.requestUpdate(); + resolve(false); + return; + } + + resolve(true); + }, + onClose: () => resolve(false), + onDiscard: () => resolve(true), + entityRegistryUpdate: this._entityRegistryUpdate, + entityRegistryEntry: this._registryEntry, + title: this.hass.localize( + this.automationId + ? "ui.panel.config.automation.editor.leave.unsaved_confirm_title" + : "ui.panel.config.automation.editor.leave.unsaved_new_title" ), - text: this.hass!.localize( - "ui.panel.config.automation.editor.unsaved_confirm_text" + description: this.hass.localize( + this.automationId + ? "ui.panel.config.automation.editor.leave.unsaved_confirm_text" + : "ui.panel.config.automation.editor.leave.unsaved_new_text" ), - confirmText: this.hass!.localize("ui.common.leave"), - dismissText: this.hass!.localize("ui.common.stay"), - destructive: true, + hideInputs: this.automationId !== null, }); - } - return true; + }); } private _backTapped = async () => { @@ -878,10 +906,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin( private async _promptAutomationAlias(): Promise { return new Promise((resolve) => { - showAutomationRenameDialog(this, { + showAutomationSaveDialog(this, { config: this._config!, domain: "automation", - updateConfig: (config, entityRegistryUpdate) => { + updateConfig: async (config, entityRegistryUpdate) => { this._config = config; this._entityRegistryUpdate = entityRegistryUpdate; this._dirty = true; @@ -910,7 +938,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( }); } - private async _saveAutomation(): Promise { + private async _handleSaveAutomation(): Promise { if (this._yamlErrors) { showToast(this, { message: this._yamlErrors, @@ -926,6 +954,13 @@ export class HaAutomationEditor extends PreventUnsavedMixin( } } + await this._saveAutomation(id); + if (!this.automationId) { + navigate(`/config/automation/edit/${id}`, { replace: true }); + } + } + + private async _saveAutomation(id): Promise { this._saving = true; this._validationErrors = undefined; @@ -990,10 +1025,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin( } this._dirty = false; - - if (!this.automationId) { - navigate(`/config/automation/edit/${id}`, { replace: true }); - } } catch (errors: any) { this._errors = errors.body?.message || errors.error || errors.body; showToast(this, { @@ -1016,7 +1047,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( protected supportedShortcuts(): SupportedShortcuts { return { - s: () => this._saveAutomation(), + s: () => this._handleSaveAutomation(), }; } diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 397d01dc3416..584a6cb14871 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -62,8 +62,8 @@ import { haStyle } from "../../../resources/styles"; import type { Entries, HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode"; -import type { EntityRegistryUpdate } from "../automation/automation-rename-dialog/show-dialog-automation-rename"; -import { showAutomationRenameDialog } from "../automation/automation-rename-dialog/show-dialog-automation-rename"; +import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save"; +import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save"; import "./blueprint-script-editor"; import "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor"; @@ -843,10 +843,10 @@ export class HaScriptEditor extends SubscribeMixin( private async _promptScriptAlias(): Promise { return new Promise((resolve) => { - showAutomationRenameDialog(this, { + showAutomationSaveDialog(this, { config: this._config!, domain: "script", - updateConfig: (config, entityRegistryUpdate) => { + updateConfig: async (config, entityRegistryUpdate) => { this._config = config; this._entityRegistryUpdate = entityRegistryUpdate; this._dirty = true; diff --git a/src/translations/en.json b/src/translations/en.json index 7e530420c276..de0186832d01 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -369,7 +369,8 @@ "copied_clipboard": "Copied to clipboard", "name": "Name", "optional": "optional", - "default": "Default" + "default": "Default", + "dont_save": "Don't save" }, "components": { "selectors": { @@ -3430,6 +3431,12 @@ "placeholder": "Optional description", "add": "Add description" }, + "leave": { + "unsaved_new_title": "Save new automation?", + "unsaved_new_text": "You can save your changes, or delete this automation. You can't undo this action.", + "unsaved_confirm_title": "Save changes?", + "unsaved_confirm_text": "You have made some changes in this automation. You can save these changes, or discard them and leave. You can't undo this action." + }, "icon": "Icon", "blueprint": { "header": "Blueprint", From b3e4953e602d5807dbb113588bc35716ab95ffad Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 16 Jan 2025 08:48:36 +0100 Subject: [PATCH 2/2] Make CI happy --- src/panels/config/automation/ha-automation-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 3fb42fda0add..77ab99ab88e0 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -760,7 +760,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( const id = this.automationId || String(Date.now()); try { await this._saveAutomation(id); - } catch (err: any) { + } catch (_err: any) { this.requestUpdate(); resolve(false); return;