Improve automation save dialog when leaving editor dirty (#23589)

* Improve automation save dialog when leaving editor dirty

* Make CI happy
This commit is contained in:
Jan-Philipp Benecke 2025-01-23 08:03:39 +01:00 committed by GitHub
parent 27d683f6e8
commit 09102d34d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 248 additions and 173 deletions

View File

@ -20,13 +20,12 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { import type {
AutomationRenameDialogParams,
EntityRegistryUpdate, EntityRegistryUpdate,
ScriptRenameDialogParams, SaveDialogParams,
} from "./show-dialog-automation-rename"; } from "./show-dialog-automation-save";
@customElement("ha-dialog-automation-rename") @customElement("ha-dialog-automation-save")
class DialogAutomationRename extends LitElement implements HassDialog { class DialogAutomationSave extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false; @state() private _opened = false;
@ -37,7 +36,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
@state() private _entryUpdates!: EntityRegistryUpdate; @state() private _entryUpdates!: EntityRegistryUpdate;
private _params!: AutomationRenameDialogParams | ScriptRenameDialogParams; private _params!: SaveDialogParams;
private _newName?: string; private _newName?: string;
@ -45,9 +44,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
private _newDescription?: string; private _newDescription?: string;
public showDialog( public showDialog(params: SaveDialogParams): void {
params: AutomationRenameDialogParams | ScriptRenameDialogParams
): void {
this._opened = true; this._opened = true;
this._params = params; this._params = params;
this._newIcon = "icon" in params.config ? params.config.icon : undefined; 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`
<ha-button
@click=${this._handleDiscard}
slot="secondaryAction"
class="destructive"
>
${this.hass.localize("ui.common.dont_save")}
</ha-button>
`;
}
protected _renderInputs() {
if (this._params.hideInputs) {
return nothing;
}
return html`
<ha-textfield
dialogInitialFocus
.value=${this._newName}
.placeholder=${this.hass.localize(
`ui.panel.config.${this._params.domain}.editor.default_name`
)}
.label=${this.hass.localize("ui.panel.config.automation.editor.alias")}
required
type="string"
@input=${this._valueChanged}
></ha-textfield>
${this._params.domain === "script" &&
this._visibleOptionals.includes("icon")
? html`
<ha-icon-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.icon"
)}
.value=${this._newIcon}
@value-changed=${this._iconChanged}
>
<ha-domain-icon
slot="fallback"
domain=${this._params.domain}
.hass=${this.hass}
>
</ha-domain-icon>
</ha-icon-picker>
`
: nothing}
${this._visibleOptionals.includes("description")
? html` <ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
autogrow
.value=${this._newDescription}
@input=${this._valueChanged}
></ha-textarea>`
: nothing}
${this._visibleOptionals.includes("category")
? html` <ha-category-picker
id="category"
.hass=${this.hass}
.scope=${this._params.domain}
.value=${this._entryUpdates.category}
@value-changed=${this._registryEntryChanged}
></ha-category-picker>`
: nothing}
${this._visibleOptionals.includes("labels")
? html` <ha-labels-picker
id="labels"
.hass=${this.hass}
.value=${this._entryUpdates.labels}
@value-changed=${this._registryEntryChanged}
></ha-labels-picker>`
: nothing}
${this._visibleOptionals.includes("area")
? html` <ha-area-picker
id="area"
.hass=${this.hass}
.value=${this._entryUpdates.area}
@value-changed=${this._registryEntryChanged}
></ha-area-picker>`
: nothing}
<ha-chip-set>
${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"
)
)}
</ha-chip-set>
`;
}
protected render() { protected render() {
if (!this._opened) { if (!this._opened) {
return nothing; 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` return html`
<ha-dialog <ha-dialog
open open
scrimClickAction scrimClickAction
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${this.hass.localize( .heading=${title}
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)}
> >
<ha-dialog-header slot="heading"> <ha-dialog-header slot="heading">
<ha-icon-button <ha-icon-button
@ -117,13 +247,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
.label=${this.hass.localize("ui.common.close")} .label=${this.hass.localize("ui.common.close")}
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></ha-icon-button>
<span slot="title" <span slot="title">${this._params.title || title}</span>
>${this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)}</span
>
</ha-dialog-header> </ha-dialog-header>
${this._error ${this._error
? html`<ha-alert alert-type="error" ? html`<ha-alert alert-type="error"
@ -132,114 +256,10 @@ class DialogAutomationRename extends LitElement implements HassDialog {
)}</ha-alert )}</ha-alert
>` >`
: ""} : ""}
<ha-textfield ${this._params.description
dialogInitialFocus ? html`<p>${this._params.description}</p>`
.value=${this._newName}
.placeholder=${this.hass.localize(
`ui.panel.config.${this._params.domain}.editor.default_name`
)}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
required
type="string"
@input=${this._valueChanged}
></ha-textfield>
${this._params.domain === "script" &&
this._visibleOptionals.includes("icon")
? html`
<ha-icon-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.icon"
)}
.value=${this._newIcon}
@value-changed=${this._iconChanged}
>
<ha-domain-icon
slot="fallback"
domain=${this._params.domain}
.hass=${this.hass}
>
</ha-domain-icon>
</ha-icon-picker>
`
: nothing} : nothing}
${this._visibleOptionals.includes("description") ${this._renderInputs()} ${this._renderDiscard()}
? html` <ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
autogrow
.value=${this._newDescription}
@input=${this._valueChanged}
></ha-textarea>`
: nothing}
${this._visibleOptionals.includes("category")
? html` <ha-category-picker
id="category"
.hass=${this.hass}
.scope=${this._params.domain}
.value=${this._entryUpdates.category}
@value-changed=${this._registryEntryChanged}
></ha-category-picker>`
: nothing}
${this._visibleOptionals.includes("labels")
? html` <ha-labels-picker
id="labels"
.hass=${this.hass}
.value=${this._entryUpdates.labels}
@value-changed=${this._registryEntryChanged}
></ha-labels-picker>`
: nothing}
${this._visibleOptionals.includes("area")
? html` <ha-area-picker
id="area"
.hass=${this.hass}
.value=${this._entryUpdates.area}
@value-changed=${this._registryEntryChanged}
></ha-area-picker>`
: nothing}
<ha-chip-set>
${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"
)
)}
</ha-chip-set>
<div slot="primaryAction"> <div slot="primaryAction">
<mwc-button @click=${this.closeDialog}> <mwc-button @click=${this.closeDialog}>
@ -247,7 +267,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
</mwc-button> </mwc-button>
<mwc-button @click=${this._save}> <mwc-button @click=${this._save}>
${this.hass.localize( ${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.rename"
: "ui.panel.config.automation.editor.save" : "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<void> {
if (!this._newName) { if (!this._newName) {
this._error = "Name is required"; this._error = "Name is required";
return; return;
} }
if (this._params.domain === "script") { if (this._params.domain === "script") {
this._params.updateConfig( await this._params.updateConfig(
{ {
...this._params.config, ...this._params.config,
alias: this._newName, alias: this._newName,
@ -303,7 +328,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
this._entryUpdates this._entryUpdates
); );
} else { } else {
this._params.updateConfig( await this._params.updateConfig(
{ {
...this._params.config, ...this._params.config,
alias: this._newName, alias: this._newName,
@ -351,6 +376,9 @@ class DialogAutomationRename extends LitElement implements HassDialog {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
.destructive {
--mdc-theme-primary: var(--error-color);
}
`, `,
]; ];
} }
@ -358,6 +386,6 @@ class DialogAutomationRename extends LitElement implements HassDialog {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-dialog-automation-rename": DialogAutomationRename; "ha-dialog-automation-save": DialogAutomationSave;
} }
} }

View File

@ -3,13 +3,18 @@ import type { AutomationConfig } from "../../../../data/automation";
import type { ScriptConfig } from "../../../../data/script"; import type { ScriptConfig } from "../../../../data/script";
import type { EntityRegistryEntry } from "../../../../data/entity_registry"; import type { EntityRegistryEntry } from "../../../../data/entity_registry";
export const loadAutomationRenameDialog = () => export const loadAutomationSaveDialog = () =>
import("./dialog-automation-rename"); import("./dialog-automation-save");
interface BaseRenameDialogParams { interface BaseRenameDialogParams {
entityRegistryUpdate?: EntityRegistryUpdate; entityRegistryUpdate?: EntityRegistryUpdate;
entityRegistryEntry?: EntityRegistryEntry; entityRegistryEntry?: EntityRegistryEntry;
onClose: () => void; onClose: () => void;
onDiscard?: () => void;
saveText?: string;
description?: string;
title?: string;
hideInputs?: boolean;
} }
export interface EntityRegistryUpdate { export interface EntityRegistryUpdate {
@ -18,31 +23,35 @@ export interface EntityRegistryUpdate {
category: string; category: string;
} }
export interface AutomationRenameDialogParams extends BaseRenameDialogParams { export interface AutomationSaveDialogParams extends BaseRenameDialogParams {
config: AutomationConfig; config: AutomationConfig;
domain: "automation"; domain: "automation";
updateConfig: ( updateConfig: (
config: AutomationConfig, config: AutomationConfig,
entityRegistryUpdate: EntityRegistryUpdate entityRegistryUpdate: EntityRegistryUpdate
) => void; ) => Promise<void>;
} }
export interface ScriptRenameDialogParams extends BaseRenameDialogParams { export interface ScriptSaveDialogParams extends BaseRenameDialogParams {
config: ScriptConfig; config: ScriptConfig;
domain: "script"; domain: "script";
updateConfig: ( updateConfig: (
config: ScriptConfig, config: ScriptConfig,
entityRegistryUpdate: EntityRegistryUpdate entityRegistryUpdate: EntityRegistryUpdate
) => void; ) => Promise<void>;
} }
export const showAutomationRenameDialog = ( export type SaveDialogParams =
| AutomationSaveDialogParams
| ScriptSaveDialogParams;
export const showAutomationSaveDialog = (
element: HTMLElement, element: HTMLElement,
dialogParams: AutomationRenameDialogParams | ScriptRenameDialogParams dialogParams: SaveDialogParams
): void => { ): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-automation-rename", dialogTag: "ha-dialog-automation-save",
dialogImport: loadAutomationRenameDialog, dialogImport: loadAutomationSaveDialog,
dialogParams, dialogParams,
}); });
}; };

View File

@ -19,7 +19,7 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; 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 { property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { consume } from "@lit-labs/context"; 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 { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import { import {
type EntityRegistryUpdate, type EntityRegistryUpdate,
showAutomationRenameDialog, showAutomationSaveDialog,
} from "./automation-rename-dialog/show-dialog-automation-rename"; } from "./automation-save-dialog/show-dialog-automation-save";
import "./blueprint-automation-editor"; import "./blueprint-automation-editor";
import "./manual-automation-editor"; import "./manual-automation-editor";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; 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")} .label=${this.hass.localize("ui.panel.config.automation.editor.save")}
.disabled=${this._saving} .disabled=${this._saving}
extended extended
@click=${this._saveAutomation} @click=${this._handleSaveAutomation}
> >
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab> </ha-fab>
@ -743,20 +743,48 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
} }
private async _confirmUnsavedChanged(): Promise<boolean> { private async _confirmUnsavedChanged(): Promise<boolean> {
if (this._dirty) { if (!this._dirty) {
return showConfirmationDialog(this, { return true;
title: this.hass!.localize(
"ui.panel.config.automation.editor.unsaved_confirm_title"
),
text: this.hass!.localize(
"ui.panel.config.automation.editor.unsaved_confirm_text"
),
confirmText: this.hass!.localize("ui.common.leave"),
dismissText: this.hass!.localize("ui.common.stay"),
destructive: true,
});
} }
return true;
return new Promise<boolean>((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"
),
description: this.hass.localize(
this.automationId
? "ui.panel.config.automation.editor.leave.unsaved_confirm_text"
: "ui.panel.config.automation.editor.leave.unsaved_new_text"
),
hideInputs: this.automationId !== null,
});
});
} }
private _backTapped = async () => { private _backTapped = async () => {
@ -878,10 +906,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private async _promptAutomationAlias(): Promise<boolean> { private async _promptAutomationAlias(): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
showAutomationRenameDialog(this, { showAutomationSaveDialog(this, {
config: this._config!, config: this._config!,
domain: "automation", domain: "automation",
updateConfig: (config, entityRegistryUpdate) => { updateConfig: async (config, entityRegistryUpdate) => {
this._config = config; this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate; this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true; this._dirty = true;
@ -910,7 +938,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}); });
} }
private async _saveAutomation(): Promise<void> { private async _handleSaveAutomation(): Promise<void> {
if (this._yamlErrors) { if (this._yamlErrors) {
showToast(this, { showToast(this, {
message: this._yamlErrors, 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<void> {
this._saving = true; this._saving = true;
this._validationErrors = undefined; this._validationErrors = undefined;
@ -990,10 +1025,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
} }
this._dirty = false; this._dirty = false;
if (!this.automationId) {
navigate(`/config/automation/edit/${id}`, { replace: true });
}
} catch (errors: any) { } catch (errors: any) {
this._errors = errors.body?.message || errors.error || errors.body; this._errors = errors.body?.message || errors.error || errors.body;
showToast(this, { showToast(this, {
@ -1016,7 +1047,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
protected supportedShortcuts(): SupportedShortcuts { protected supportedShortcuts(): SupportedShortcuts {
return { return {
s: () => this._saveAutomation(), s: () => this._handleSaveAutomation(),
}; };
} }

View File

@ -62,8 +62,8 @@ import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types"; import type { Entries, HomeAssistant, Route } from "../../../types";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode"; import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
import type { EntityRegistryUpdate } from "../automation/automation-rename-dialog/show-dialog-automation-rename"; import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationRenameDialog } from "../automation/automation-rename-dialog/show-dialog-automation-rename"; import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
import "./blueprint-script-editor"; import "./blueprint-script-editor";
import "./manual-script-editor"; import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor";
@ -843,10 +843,10 @@ export class HaScriptEditor extends SubscribeMixin(
private async _promptScriptAlias(): Promise<boolean> { private async _promptScriptAlias(): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
showAutomationRenameDialog(this, { showAutomationSaveDialog(this, {
config: this._config!, config: this._config!,
domain: "script", domain: "script",
updateConfig: (config, entityRegistryUpdate) => { updateConfig: async (config, entityRegistryUpdate) => {
this._config = config; this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate; this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true; this._dirty = true;

View File

@ -369,7 +369,8 @@
"copied_clipboard": "Copied to clipboard", "copied_clipboard": "Copied to clipboard",
"name": "Name", "name": "Name",
"optional": "optional", "optional": "optional",
"default": "Default" "default": "Default",
"dont_save": "Don't save"
}, },
"components": { "components": {
"selectors": { "selectors": {
@ -3471,6 +3472,12 @@
"placeholder": "Optional description", "placeholder": "Optional description",
"add": "Add 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", "icon": "Icon",
"blueprint": { "blueprint": {
"header": "Blueprint", "header": "Blueprint",