Use undo notification when deleting a card or badge (#22414)

* Use undo notification instead of confirmation dialog for cards and badges

* Fix notifications

* Improve deletion functions

* Fix errors

* Fix startup notifications

* Add translation and simplify delete method

* Apply suggestions from code review

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Prettier

* Update src/panels/lovelace/editor/delete-badge.ts

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
Paul Bottein 2024-10-22 15:17:01 +02:00 committed by GitHub
parent 11fc5bc755
commit b9922b2f8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 124 additions and 103 deletions

View File

@ -45,6 +45,7 @@ class HcLovelace extends LitElement {
saveConfig: async () => undefined, saveConfig: async () => undefined,
deleteConfig: async () => undefined, deleteConfig: async () => undefined,
setEditMode: () => undefined, setEditMode: () => undefined,
showToast: () => undefined,
}; };
return html` return html`
<hui-view <hui-view

View File

@ -1,13 +1,15 @@
import { html, LitElement, nothing } from "lit";
import { property, state, query } from "lit/decorators";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-button";
import "../components/ha-toast"; import "../components/ha-toast";
import type { HaToast } from "../components/ha-toast"; import type { HaToast } from "../components/ha-toast";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "../components/ha-button";
export interface ShowToastParams { export interface ShowToastParams {
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
id?: string;
message: string; message: string;
action?: ToastActionParams; action?: ToastActionParams;
duration?: number; duration?: number;
@ -27,12 +29,12 @@ class NotificationManager extends LitElement {
@query("ha-toast") private _toast!: HaToast | undefined; @query("ha-toast") private _toast!: HaToast | undefined;
public async showDialog(parameters: ShowToastParams) { public async showDialog(parameters: ShowToastParams) {
if (this._parameters && this._parameters.message !== parameters.message) { if (!parameters.id || this._parameters?.id !== parameters.id) {
this._parameters = undefined; this._toast?.close();
await this.updateComplete;
} }
if (!parameters || parameters.duration === 0) { if (!parameters || parameters.duration === 0) {
this._parameters = undefined;
return; return;
} }
@ -44,10 +46,9 @@ class NotificationManager extends LitElement {
) { ) {
this._parameters.duration = 4000; this._parameters.duration = 4000;
} }
}
public shouldUpdate(changedProperties) { await this.updateComplete;
return !this._toast || changedProperties.has("_parameters"); this._toast?.show();
} }
private _toastClosed() { private _toastClosed() {
@ -61,7 +62,6 @@ class NotificationManager extends LitElement {
return html` return html`
<ha-toast <ha-toast
leading leading
open
dir=${computeRTL(this.hass) ? "rtl" : "ltr"} dir=${computeRTL(this.hass) ? "rtl" : "ltr"}
.labelText=${this._parameters.message} .labelText=${this._parameters.message}
.timeoutMs=${this._parameters.duration!} .timeoutMs=${this._parameters.duration!}
@ -77,12 +77,14 @@ class NotificationManager extends LitElement {
` `
: nothing} : nothing}
${this._parameters?.dismissable ${this._parameters?.dismissable
? html`<ha-icon-button ? html`
<ha-icon-button
.label=${this.hass.localize("ui.common.close")} .label=${this.hass.localize("ui.common.close")}
.path=${mdiClose} .path=${mdiClose}
dialogAction="close" dialogAction="close"
slot="dismiss" slot="dismiss"
></ha-icon-button>` ></ha-icon-button>
`
: nothing} : nothing}
</ha-toast> </ha-toast>
`; `;

View File

@ -131,6 +131,7 @@ class PanelEnergy extends LitElement {
saveConfig: async () => undefined, saveConfig: async () => undefined,
deleteConfig: async () => undefined, deleteConfig: async () => undefined,
setEditMode: () => undefined, setEditMode: () => undefined,
showToast: () => undefined,
}; };
} }

View File

@ -206,7 +206,7 @@ export class HuiBadgeEditMode extends LitElement {
private _cutBadge(): void { private _cutBadge(): void {
this._copyBadge(); this._copyBadge();
this._deleteBadge(); fireEvent(this, "ll-delete-badge", { path: this.path!, silent: true });
} }
private _copyBadge(): void { private _copyBadge(): void {
@ -232,7 +232,7 @@ export class HuiBadgeEditMode extends LitElement {
} }
private _deleteBadge(): void { private _deleteBadge(): void {
fireEvent(this, "ll-delete-badge", { path: this.path! }); fireEvent(this, "ll-delete-badge", { path: this.path!, silent: false });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -199,7 +199,7 @@ export class HuiCardEditMode extends LitElement {
this._cutCard(); this._cutCard();
break; break;
case 4: case 4:
this._deleteCard(true); this._deleteCard();
break; break;
} }
} }
@ -222,7 +222,7 @@ export class HuiCardEditMode extends LitElement {
private _cutCard(): void { private _cutCard(): void {
this._copyCard(); this._copyCard();
this._deleteCard(false); fireEvent(this, "ll-delete-card", { path: this.path!, silent: true });
} }
private _copyCard(): void { private _copyCard(): void {
@ -231,8 +231,8 @@ export class HuiCardEditMode extends LitElement {
this._clipboard = deepClone(cardConfig); this._clipboard = deepClone(cardConfig);
} }
private _deleteCard(confirm: boolean): void { private _deleteCard(): void {
fireEvent(this, "ll-delete-card", { path: this.path!, confirm }); fireEvent(this, "ll-delete-card", { path: this.path!, silent: false });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -274,7 +274,7 @@ export class HuiCardOptions extends LitElement {
this._cutCard(); this._cutCard();
break; break;
case 4: case 4:
this._deleteCard(true); this._deleteCard({ silent: false });
break; break;
} }
} }
@ -297,7 +297,7 @@ export class HuiCardOptions extends LitElement {
private _cutCard(): void { private _cutCard(): void {
this._copyCard(); this._copyCard();
this._deleteCard(false); this._deleteCard({ silent: true });
} }
private _copyCard(): void { private _copyCard(): void {
@ -395,8 +395,8 @@ export class HuiCardOptions extends LitElement {
}); });
} }
private _deleteCard(confirm: boolean): void { private _deleteCard({ silent }: { silent: boolean }): void {
fireEvent(this, "ll-delete-card", { path: this.path!, confirm }); fireEvent(this, "ll-delete-card", { path: this.path!, silent });
} }
} }

View File

@ -0,0 +1,39 @@
import type { HomeAssistant } from "../../../types";
import { Lovelace } from "../types";
import { deleteBadge } from "./config-util";
import type { LovelaceCardPath } from "./lovelace-path";
export type DeleteBadgeParams = { path: LovelaceCardPath; silent: boolean };
export async function performDeleteBadge(
hass: HomeAssistant,
lovelace: Lovelace,
params: DeleteBadgeParams
): Promise<void> {
try {
const { path, silent } = params;
const oldConfig = lovelace.config;
const newConfig = deleteBadge(oldConfig, path);
await lovelace.saveConfig(newConfig);
if (silent) {
return;
}
const action = async () => {
lovelace.saveConfig(oldConfig);
};
lovelace.showToast({
message: hass.localize("ui.common.successfully_deleted"),
duration: 8000,
action: { action, text: hass.localize("ui.common.undo") },
});
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
lovelace.showToast({
message: hass.localize("ui.common.deleting_failed"),
});
}
}

View File

@ -1,44 +1,39 @@
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { showDeleteSuccessToast } from "../../../util/toast-deleted-success";
import { Lovelace } from "../types"; import { Lovelace } from "../types";
import { showDeleteCardDialog } from "./card-editor/show-delete-card-dialog"; import { deleteCard } from "./config-util";
import { deleteCard, insertCard } from "./config-util"; import type { LovelaceCardPath } from "./lovelace-path";
import {
LovelaceCardPath,
findLovelaceContainer,
getLovelaceContainerPath,
parseLovelaceCardPath,
} from "./lovelace-path";
export async function confDeleteCard( export type DeleteCardParams = { path: LovelaceCardPath; silent: boolean };
element: HTMLElement,
export async function performDeleteCard(
hass: HomeAssistant, hass: HomeAssistant,
lovelace: Lovelace, lovelace: Lovelace,
path: LovelaceCardPath params: DeleteCardParams
): Promise<void> { ): Promise<void> {
const containerPath = getLovelaceContainerPath(path);
const { cardIndex } = parseLovelaceCardPath(path);
const containerConfig = findLovelaceContainer(lovelace.config, containerPath);
if ("strategy" in containerConfig) {
throw new Error("Deleting cards in a strategy is not supported.");
}
const cardConfig = containerConfig.cards![cardIndex];
showDeleteCardDialog(element, {
cardConfig,
deleteCard: async () => {
try { try {
const newLovelace = deleteCard(lovelace.config, path); const { path, silent } = params;
await lovelace.saveConfig(newLovelace); const oldConfig = lovelace.config;
const newConfig = deleteCard(oldConfig, path);
await lovelace.saveConfig(newConfig);
if (silent) {
return;
}
const action = async () => { const action = async () => {
await lovelace.saveConfig(insertCard(newLovelace, path, cardConfig)); lovelace.saveConfig(oldConfig);
}; };
showDeleteSuccessToast(element, hass!, action);
lovelace.showToast({
message: hass.localize("ui.common.successfully_deleted"),
duration: 8000,
action: { action, text: hass.localize("ui.common.undo") },
});
} catch (err: any) { } catch (err: any) {
showAlertDialog(element, { // eslint-disable-next-line no-console
text: `Deleting failed: ${err.message}`, console.error(err);
lovelace.showToast({
message: hass.localize("ui.common.deleting_failed"),
}); });
} }
},
});
} }

View File

@ -23,10 +23,11 @@ import { fetchResources } from "../../data/lovelace/resource";
import { WindowWithPreloads } from "../../data/preloads"; import { WindowWithPreloads } from "../../data/preloads";
import "../../layouts/hass-error-screen"; import "../../layouts/hass-error-screen";
import "../../layouts/hass-loading-screen"; import "../../layouts/hass-loading-screen";
import type { ShowToastParams } from "../../managers/notification-manager";
import { HomeAssistant, PanelInfo, Route } from "../../types"; import { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast"; import { showToast } from "../../util/toast";
import { loadLovelaceResources } from "./common/load-resources";
import { checkLovelaceConfig } from "./common/check-lovelace-config"; import { checkLovelaceConfig } from "./common/check-lovelace-config";
import { loadLovelaceResources } from "./common/load-resources";
import { showSaveDialog } from "./editor/show-save-config-dialog"; import { showSaveDialog } from "./editor/show-save-config-dialog";
import "./hui-root"; import "./hui-root";
import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy"; import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
@ -434,6 +435,7 @@ export class LovelacePanel extends LitElement {
throw err; throw err;
} }
}, },
showToast: (params: ShowToastParams) => showToast(this, params),
}; };
} }

View File

@ -20,8 +20,7 @@ import {
import { createSectionElement } from "../create-element/create-section-element"; import { createSectionElement } from "../create-element/create-section-element";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { deleteCard } from "../editor/config-util"; import { performDeleteCard } from "../editor/delete-card";
import { confDeleteCard } from "../editor/delete-card";
import { parseLovelaceCardPath } from "../editor/lovelace-path"; import { parseLovelaceCardPath } from "../editor/lovelace-path";
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy"; import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
import type { Lovelace } from "../types"; import type { Lovelace } from "../types";
@ -257,12 +256,7 @@ export class HuiSection extends ReactiveElement {
this._layoutElement.addEventListener("ll-delete-card", (ev) => { this._layoutElement.addEventListener("ll-delete-card", (ev) => {
ev.stopPropagation(); ev.stopPropagation();
if (!this.lovelace) return; if (!this.lovelace) return;
if (ev.detail.confirm) { performDeleteCard(this.hass, this.lovelace, ev.detail);
confDeleteCard(this, this.hass!, this.lovelace!, ev.detail.path);
} else {
const newLovelace = deleteCard(this.lovelace!.config, ev.detail.path);
this.lovelace.saveConfig(newLovelace);
}
}); });
} }

View File

@ -14,6 +14,7 @@ import { LovelaceHeaderFooterConfig } from "./header-footer/types";
import { LovelaceCardFeatureConfig } from "./card-features/types"; import { LovelaceCardFeatureConfig } from "./card-features/types";
import { LovelaceElement, LovelaceElementConfig } from "./elements/types"; import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
import { LovelaceHeadingBadgeConfig } from "./heading-badges/types"; import { LovelaceHeadingBadgeConfig } from "./heading-badges/types";
import type { ShowToastParams } from "../../managers/notification-manager";
declare global { declare global {
// eslint-disable-next-line // eslint-disable-next-line
@ -35,6 +36,7 @@ export interface Lovelace {
setEditMode: (editMode: boolean) => void; setEditMode: (editMode: boolean) => void;
saveConfig: (newConfig: LovelaceRawConfig) => Promise<void>; saveConfig: (newConfig: LovelaceRawConfig) => Promise<void>;
deleteConfig: () => Promise<void>; deleteConfig: () => Promise<void>;
showToast: (params: ShowToastParams) => void;
} }
export interface LovelaceBadge extends HTMLElement { export interface LovelaceBadge extends HTMLElement {

View File

@ -25,8 +25,14 @@ import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog"; import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { deleteBadge, deleteCard } from "../editor/config-util"; import {
import { confDeleteCard } from "../editor/delete-card"; type DeleteBadgeParams,
performDeleteBadge,
} from "../editor/delete-badge";
import {
type DeleteCardParams,
performDeleteCard,
} from "../editor/delete-card";
import { import {
LovelaceCardPath, LovelaceCardPath,
parseLovelaceCardPath, parseLovelaceCardPath,
@ -43,10 +49,10 @@ declare global {
interface HASSDomEvents { interface HASSDomEvents {
"ll-create-card": { suggested?: string[] } | undefined; "ll-create-card": { suggested?: string[] } | undefined;
"ll-edit-card": { path: LovelaceCardPath }; "ll-edit-card": { path: LovelaceCardPath };
"ll-delete-card": { path: LovelaceCardPath; confirm: boolean }; "ll-delete-card": DeleteCardParams;
"ll-create-badge": undefined; "ll-create-badge": undefined;
"ll-edit-badge": { path: LovelaceCardPath }; "ll-edit-badge": { path: LovelaceCardPath };
"ll-delete-badge": { path: LovelaceCardPath }; "ll-delete-badge": DeleteBadgeParams;
} }
interface HTMLElementEventMap { interface HTMLElementEventMap {
"ll-create-card": HASSDomEvent<HASSDomEvents["ll-create-card"]>; "ll-create-card": HASSDomEvent<HASSDomEvents["ll-create-card"]>;
@ -322,12 +328,8 @@ export class HUIView extends ReactiveElement {
}); });
}); });
this._layoutElement.addEventListener("ll-delete-card", (ev) => { this._layoutElement.addEventListener("ll-delete-card", (ev) => {
if (ev.detail.confirm) { if (!this.lovelace) return;
confDeleteCard(this, this.hass!, this.lovelace!, ev.detail.path); performDeleteCard(this.hass, this.lovelace, ev.detail);
} else {
const newLovelace = deleteCard(this.lovelace!.config, ev.detail.path);
this.lovelace.saveConfig(newLovelace);
}
}); });
this._layoutElement.addEventListener("ll-create-badge", async () => { this._layoutElement.addEventListener("ll-create-badge", async () => {
showCreateBadgeDialog(this, { showCreateBadgeDialog(this, {
@ -345,9 +347,9 @@ export class HUIView extends ReactiveElement {
badgeIndex: cardIndex, badgeIndex: cardIndex,
}); });
}); });
this._layoutElement.addEventListener("ll-delete-badge", (ev) => { this._layoutElement.addEventListener("ll-delete-badge", async (ev) => {
const newLovelace = deleteBadge(this.lovelace!.config, ev.detail.path); if (!this.lovelace) return;
this.lovelace.saveConfig(newLovelace); performDeleteBadge(this.hass, this.lovelace, ev.detail);
}); });
} }

View File

@ -143,6 +143,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
)[0][0]; )[0][0];
showToast(this, { showToast(this, {
id: "integration_starting",
message: message:
this.hass!.localize("ui.notification_toast.integration_starting", { this.hass!.localize("ui.notification_toast.integration_starting", {
integration: domainToName(this.hass!.localize, integration), integration: domainToName(this.hass!.localize, integration),

View File

@ -362,6 +362,7 @@
"help": "Help", "help": "Help",
"successfully_saved": "Successfully saved", "successfully_saved": "Successfully saved",
"successfully_deleted": "Successfully deleted", "successfully_deleted": "Successfully deleted",
"deleting_failed": "Deleting failed",
"error_required": "Required", "error_required": "Required",
"copied": "Copied", "copied": "Copied",
"copied_clipboard": "Copied to clipboard", "copied_clipboard": "Copied to clipboard",

View File

@ -1,19 +0,0 @@
import { ShowToastParams } from "../managers/notification-manager";
import { HomeAssistant } from "../types";
import { showToast } from "./toast";
export const showDeleteSuccessToast = (
el: HTMLElement,
hass: HomeAssistant,
action?: () => void
) => {
const toastParams: ShowToastParams = {
message: hass!.localize("ui.common.successfully_deleted"),
};
if (action) {
toastParams.action = { action, text: hass!.localize("ui.common.undo") };
}
showToast(el, toastParams);
};