Skip to content

Commit fe50c12

Browse files
authored
Add undo/redo functionality to dashboard editor (#27259)
* Add undo/redo functionality to dashboard editor * Use controller and move toast to undo stack * Store location and navigate to view * Await and catch errors * Process code review
1 parent c01fbf5 commit fe50c12

File tree

4 files changed

+108
-12
lines changed

4 files changed

+108
-12
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
22
import type { Lovelace } from "../types";
33
import { deleteBadge } from "./config-util";
44
import type { LovelaceCardPath } from "./lovelace-path";
5+
import { fireEvent } from "../../../common/dom/fire_event";
56

67
export interface DeleteBadgeParams {
78
path: LovelaceCardPath;
@@ -23,14 +24,13 @@ export async function performDeleteBadge(
2324
return;
2425
}
2526

26-
const action = async () => {
27-
lovelace.saveConfig(oldConfig);
28-
};
29-
3027
lovelace.showToast({
3128
message: hass.localize("ui.common.successfully_deleted"),
3229
duration: 8000,
33-
action: { action, text: hass.localize("ui.common.undo") },
30+
action: {
31+
action: () => fireEvent(window, "undo-change"),
32+
text: hass.localize("ui.common.undo"),
33+
},
3434
});
3535
} catch (err: any) {
3636
// eslint-disable-next-line no-console

src/panels/lovelace/editor/delete-card.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
22
import type { Lovelace } from "../types";
33
import { deleteCard } from "./config-util";
44
import type { LovelaceCardPath } from "./lovelace-path";
5+
import { fireEvent } from "../../../common/dom/fire_event";
56

67
export interface DeleteCardParams {
78
path: LovelaceCardPath;
@@ -23,14 +24,13 @@ export async function performDeleteCard(
2324
return;
2425
}
2526

26-
const action = async () => {
27-
lovelace.saveConfig(oldConfig);
28-
};
29-
3027
lovelace.showToast({
3128
message: hass.localize("ui.common.successfully_deleted"),
3229
duration: 8000,
33-
action: { action, text: hass.localize("ui.common.undo") },
30+
action: {
31+
action: () => fireEvent(window, "undo-change"),
32+
text: hass.localize("ui.common.undo"),
33+
},
3434
});
3535
} catch (err: any) {
3636
// eslint-disable-next-line no-console

src/panels/lovelace/hui-root.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import {
1111
mdiMagnify,
1212
mdiPencil,
1313
mdiPlus,
14+
mdiRedo,
1415
mdiRefresh,
1516
mdiRobot,
1617
mdiShape,
1718
mdiSofa,
19+
mdiUndo,
1820
mdiViewDashboard,
1921
} from "@mdi/js";
2022
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
@@ -50,7 +52,10 @@ import "../../components/ha-tab-group-tab";
5052
import "../../components/ha-tooltip";
5153
import { createAreaRegistryEntry } from "../../data/area_registry";
5254
import type { LovelacePanelConfig } from "../../data/lovelace";
53-
import type { LovelaceConfig } from "../../data/lovelace/config/types";
55+
import type {
56+
LovelaceConfig,
57+
LovelaceRawConfig,
58+
} from "../../data/lovelace/config/types";
5459
import { isStrategyDashboard } from "../../data/lovelace/config/types";
5560
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
5661
import {
@@ -92,6 +97,7 @@ import "./views/hui-view";
9297
import type { HUIView } from "./views/hui-view";
9398
import "./views/hui-view-background";
9499
import "./views/hui-view-container";
100+
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
95101

96102
interface ActionItem {
97103
icon: string;
@@ -113,6 +119,11 @@ interface SubActionItem {
113119
visible: boolean | undefined;
114120
}
115121

122+
interface UndoStackItem {
123+
location: string;
124+
config: LovelaceRawConfig;
125+
}
126+
116127
@customElement("hui-root")
117128
class HUIRoot extends LitElement {
118129
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
@@ -130,12 +141,22 @@ class HUIRoot extends LitElement {
130141

131142
@state() private _curView?: number | "hass-unused-entities";
132143

144+
private _configChangedByUndo = false;
145+
133146
private _viewCache?: Record<string, HUIView>;
134147

135148
private _viewScrollPositions: Record<string, number> = {};
136149

137150
private _restoreScroll = false;
138151

152+
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
153+
apply: (config) => this._applyUndoRedo(config),
154+
currentConfig: () => ({
155+
location: this.route!.path,
156+
config: this.lovelace!.rawConfig,
157+
}),
158+
});
159+
139160
private _debouncedConfigChanged: () => void;
140161

141162
private _conversation = memoizeOne((_components) =>
@@ -157,7 +178,29 @@ class HUIRoot extends LitElement {
157178
const result: TemplateResult[] = [];
158179
if (this._editMode) {
159180
result.push(
160-
html`<ha-button
181+
html`<ha-icon-button
182+
slot="toolbar-icon"
183+
.path=${mdiUndo}
184+
@click=${this._undo}
185+
.disabled=${!this._undoRedoController.canUndo}
186+
id="button-undo"
187+
>
188+
</ha-icon-button>
189+
<ha-tooltip placement="bottom" for="button-undo">
190+
${this.hass.localize("ui.common.undo")}
191+
</ha-tooltip>
192+
<ha-icon-button
193+
slot="toolbar-icon"
194+
.path=${mdiRedo}
195+
@click=${this._redo}
196+
.disabled=${!this._undoRedoController.canRedo}
197+
id="button-redo"
198+
>
199+
</ha-icon-button>
200+
<ha-tooltip placement="bottom" for="button-redo">
201+
${this.hass.localize("ui.common.redo")}
202+
</ha-tooltip>
203+
<ha-button
161204
appearance="filled"
162205
size="small"
163206
class="exit-edit-mode"
@@ -645,6 +688,27 @@ class HUIRoot extends LitElement {
645688
window.history.scrollRestoration = "auto";
646689
}
647690

691+
protected willUpdate(changedProperties: PropertyValues): void {
692+
if (changedProperties.has("lovelace")) {
693+
const oldLovelace = changedProperties.get("lovelace") as
694+
| Lovelace
695+
| undefined;
696+
697+
if (
698+
oldLovelace &&
699+
this.lovelace!.rawConfig !== oldLovelace!.rawConfig &&
700+
!this._configChangedByUndo
701+
) {
702+
this._undoRedoController.commit({
703+
location: this.route!.path,
704+
config: oldLovelace.rawConfig,
705+
});
706+
} else {
707+
this._configChangedByUndo = false;
708+
}
709+
}
710+
}
711+
648712
protected updated(changedProperties: PropertyValues): void {
649713
super.updated(changedProperties);
650714

@@ -1029,6 +1093,7 @@ class HUIRoot extends LitElement {
10291093

10301094
private _editModeDisable(): void {
10311095
this.lovelace!.setEditMode(false);
1096+
this._undoRedoController.reset();
10321097
}
10331098

10341099
private async _editDashboard() {
@@ -1207,6 +1272,36 @@ class HUIRoot extends LitElement {
12071272
showShortcutsDialog(this);
12081273
}
12091274

1275+
private async _applyUndoRedo(item: UndoStackItem) {
1276+
this._configChangedByUndo = true;
1277+
try {
1278+
await this.lovelace!.saveConfig(item.config);
1279+
} catch (err: any) {
1280+
this._configChangedByUndo = false;
1281+
showToast(this, {
1282+
message: this.hass.localize(
1283+
"ui.panel.lovelace.editor.undo_redo_failed_to_apply_changes",
1284+
{
1285+
error: err.message,
1286+
}
1287+
),
1288+
duration: 4000,
1289+
dismissable: true,
1290+
});
1291+
return;
1292+
}
1293+
1294+
this._navigateToView(item.location);
1295+
}
1296+
1297+
private _undo() {
1298+
this._undoRedoController.undo();
1299+
}
1300+
1301+
private _redo() {
1302+
this._undoRedoController.redo();
1303+
}
1304+
12101305
static get styles(): CSSResultGroup {
12111306
return [
12121307
haStyle,

src/translations/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7304,6 +7304,7 @@
73047304
"editor": {
73057305
"header": "Edit UI",
73067306
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
7307+
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
73077308
"menu": {
73087309
"open": "Open dashboard menu",
73097310
"raw_editor": "Raw configuration editor",

0 commit comments

Comments
 (0)