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 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`
<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() {
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`
<ha-dialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)}
.heading=${title}
>
<ha-dialog-header slot="heading">
<ha-icon-button
@ -117,13 +247,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title"
>${this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)}</span
>
<span slot="title">${this._params.title || title}</span>
</ha-dialog-header>
${this._error
? html`<ha-alert alert-type="error"
@ -132,114 +256,10 @@ class DialogAutomationRename extends LitElement implements HassDialog {
)}</ha-alert
>`
: ""}
<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>
`
${this._params.description
? html`<p>${this._params.description}</p>`
: 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>
${this._renderInputs()} ${this._renderDiscard()}
<div slot="primaryAction">
<mwc-button @click=${this.closeDialog}>
@ -247,7 +267,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
</mwc-button>
<mwc-button @click=${this._save}>
${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<void> {
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;
}
}

View File

@ -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<void>;
}
export interface ScriptRenameDialogParams extends BaseRenameDialogParams {
export interface ScriptSaveDialogParams extends BaseRenameDialogParams {
config: ScriptConfig;
domain: "script";
updateConfig: (
config: ScriptConfig,
entityRegistryUpdate: EntityRegistryUpdate
) => void;
) => Promise<void>;
}
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,
});
};

View File

@ -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}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
@ -743,20 +743,48 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (this._dirty) {
return showConfirmationDialog(this, {
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,
});
if (!this._dirty) {
return 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 () => {
@ -878,10 +906,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private async _promptAutomationAlias(): Promise<boolean> {
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<void> {
private async _handleSaveAutomation(): Promise<void> {
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<void> {
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(),
};
}

View File

@ -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<boolean> {
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;

View File

@ -369,7 +369,8 @@
"copied_clipboard": "Copied to clipboard",
"name": "Name",
"optional": "optional",
"default": "Default"
"default": "Default",
"dont_save": "Don't save"
},
"components": {
"selectors": {
@ -3471,6 +3472,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",