mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-20 02:38:53 +00:00
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
This commit is contained in:
committed by
GitHub
parent
c01fbf5f47
commit
fe50c1212a
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
|||||||
import type { Lovelace } from "../types";
|
import type { Lovelace } from "../types";
|
||||||
import { deleteBadge } from "./config-util";
|
import { deleteBadge } from "./config-util";
|
||||||
import type { LovelaceCardPath } from "./lovelace-path";
|
import type { LovelaceCardPath } from "./lovelace-path";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
export interface DeleteBadgeParams {
|
export interface DeleteBadgeParams {
|
||||||
path: LovelaceCardPath;
|
path: LovelaceCardPath;
|
||||||
@@ -23,14 +24,13 @@ export async function performDeleteBadge(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async () => {
|
|
||||||
lovelace.saveConfig(oldConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
lovelace.showToast({
|
lovelace.showToast({
|
||||||
message: hass.localize("ui.common.successfully_deleted"),
|
message: hass.localize("ui.common.successfully_deleted"),
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
action: { action, text: hass.localize("ui.common.undo") },
|
action: {
|
||||||
|
action: () => fireEvent(window, "undo-change"),
|
||||||
|
text: hass.localize("ui.common.undo"),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
|||||||
import type { Lovelace } from "../types";
|
import type { Lovelace } from "../types";
|
||||||
import { deleteCard } from "./config-util";
|
import { deleteCard } from "./config-util";
|
||||||
import type { LovelaceCardPath } from "./lovelace-path";
|
import type { LovelaceCardPath } from "./lovelace-path";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
export interface DeleteCardParams {
|
export interface DeleteCardParams {
|
||||||
path: LovelaceCardPath;
|
path: LovelaceCardPath;
|
||||||
@@ -23,14 +24,13 @@ export async function performDeleteCard(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async () => {
|
|
||||||
lovelace.saveConfig(oldConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
lovelace.showToast({
|
lovelace.showToast({
|
||||||
message: hass.localize("ui.common.successfully_deleted"),
|
message: hass.localize("ui.common.successfully_deleted"),
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
action: { action, text: hass.localize("ui.common.undo") },
|
action: {
|
||||||
|
action: () => fireEvent(window, "undo-change"),
|
||||||
|
text: hass.localize("ui.common.undo"),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import {
|
|||||||
mdiMagnify,
|
mdiMagnify,
|
||||||
mdiPencil,
|
mdiPencil,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
|
mdiRedo,
|
||||||
mdiRefresh,
|
mdiRefresh,
|
||||||
mdiRobot,
|
mdiRobot,
|
||||||
mdiShape,
|
mdiShape,
|
||||||
mdiSofa,
|
mdiSofa,
|
||||||
|
mdiUndo,
|
||||||
mdiViewDashboard,
|
mdiViewDashboard,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
@@ -50,7 +52,10 @@ import "../../components/ha-tab-group-tab";
|
|||||||
import "../../components/ha-tooltip";
|
import "../../components/ha-tooltip";
|
||||||
import { createAreaRegistryEntry } from "../../data/area_registry";
|
import { createAreaRegistryEntry } from "../../data/area_registry";
|
||||||
import type { LovelacePanelConfig } from "../../data/lovelace";
|
import type { LovelacePanelConfig } from "../../data/lovelace";
|
||||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
import type {
|
||||||
|
LovelaceConfig,
|
||||||
|
LovelaceRawConfig,
|
||||||
|
} from "../../data/lovelace/config/types";
|
||||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||||
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||||
import {
|
import {
|
||||||
@@ -92,6 +97,7 @@ import "./views/hui-view";
|
|||||||
import type { HUIView } from "./views/hui-view";
|
import type { HUIView } from "./views/hui-view";
|
||||||
import "./views/hui-view-background";
|
import "./views/hui-view-background";
|
||||||
import "./views/hui-view-container";
|
import "./views/hui-view-container";
|
||||||
|
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||||
|
|
||||||
interface ActionItem {
|
interface ActionItem {
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -113,6 +119,11 @@ interface SubActionItem {
|
|||||||
visible: boolean | undefined;
|
visible: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UndoStackItem {
|
||||||
|
location: string;
|
||||||
|
config: LovelaceRawConfig;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("hui-root")
|
@customElement("hui-root")
|
||||||
class HUIRoot extends LitElement {
|
class HUIRoot extends LitElement {
|
||||||
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
|
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
|
||||||
@@ -130,12 +141,22 @@ class HUIRoot extends LitElement {
|
|||||||
|
|
||||||
@state() private _curView?: number | "hass-unused-entities";
|
@state() private _curView?: number | "hass-unused-entities";
|
||||||
|
|
||||||
|
private _configChangedByUndo = false;
|
||||||
|
|
||||||
private _viewCache?: Record<string, HUIView>;
|
private _viewCache?: Record<string, HUIView>;
|
||||||
|
|
||||||
private _viewScrollPositions: Record<string, number> = {};
|
private _viewScrollPositions: Record<string, number> = {};
|
||||||
|
|
||||||
private _restoreScroll = false;
|
private _restoreScroll = false;
|
||||||
|
|
||||||
|
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
|
||||||
|
apply: (config) => this._applyUndoRedo(config),
|
||||||
|
currentConfig: () => ({
|
||||||
|
location: this.route!.path,
|
||||||
|
config: this.lovelace!.rawConfig,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
private _debouncedConfigChanged: () => void;
|
private _debouncedConfigChanged: () => void;
|
||||||
|
|
||||||
private _conversation = memoizeOne((_components) =>
|
private _conversation = memoizeOne((_components) =>
|
||||||
@@ -157,7 +178,29 @@ class HUIRoot extends LitElement {
|
|||||||
const result: TemplateResult[] = [];
|
const result: TemplateResult[] = [];
|
||||||
if (this._editMode) {
|
if (this._editMode) {
|
||||||
result.push(
|
result.push(
|
||||||
html`<ha-button
|
html`<ha-icon-button
|
||||||
|
slot="toolbar-icon"
|
||||||
|
.path=${mdiUndo}
|
||||||
|
@click=${this._undo}
|
||||||
|
.disabled=${!this._undoRedoController.canUndo}
|
||||||
|
id="button-undo"
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
<ha-tooltip placement="bottom" for="button-undo">
|
||||||
|
${this.hass.localize("ui.common.undo")}
|
||||||
|
</ha-tooltip>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="toolbar-icon"
|
||||||
|
.path=${mdiRedo}
|
||||||
|
@click=${this._redo}
|
||||||
|
.disabled=${!this._undoRedoController.canRedo}
|
||||||
|
id="button-redo"
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
<ha-tooltip placement="bottom" for="button-redo">
|
||||||
|
${this.hass.localize("ui.common.redo")}
|
||||||
|
</ha-tooltip>
|
||||||
|
<ha-button
|
||||||
appearance="filled"
|
appearance="filled"
|
||||||
size="small"
|
size="small"
|
||||||
class="exit-edit-mode"
|
class="exit-edit-mode"
|
||||||
@@ -645,6 +688,27 @@ class HUIRoot extends LitElement {
|
|||||||
window.history.scrollRestoration = "auto";
|
window.history.scrollRestoration = "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (changedProperties.has("lovelace")) {
|
||||||
|
const oldLovelace = changedProperties.get("lovelace") as
|
||||||
|
| Lovelace
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
oldLovelace &&
|
||||||
|
this.lovelace!.rawConfig !== oldLovelace!.rawConfig &&
|
||||||
|
!this._configChangedByUndo
|
||||||
|
) {
|
||||||
|
this._undoRedoController.commit({
|
||||||
|
location: this.route!.path,
|
||||||
|
config: oldLovelace.rawConfig,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._configChangedByUndo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues): void {
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
@@ -1029,6 +1093,7 @@ class HUIRoot extends LitElement {
|
|||||||
|
|
||||||
private _editModeDisable(): void {
|
private _editModeDisable(): void {
|
||||||
this.lovelace!.setEditMode(false);
|
this.lovelace!.setEditMode(false);
|
||||||
|
this._undoRedoController.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _editDashboard() {
|
private async _editDashboard() {
|
||||||
@@ -1207,6 +1272,36 @@ class HUIRoot extends LitElement {
|
|||||||
showShortcutsDialog(this);
|
showShortcutsDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _applyUndoRedo(item: UndoStackItem) {
|
||||||
|
this._configChangedByUndo = true;
|
||||||
|
try {
|
||||||
|
await this.lovelace!.saveConfig(item.config);
|
||||||
|
} catch (err: any) {
|
||||||
|
this._configChangedByUndo = false;
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.undo_redo_failed_to_apply_changes",
|
||||||
|
{
|
||||||
|
error: err.message,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
duration: 4000,
|
||||||
|
dismissable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._navigateToView(item.location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _undo() {
|
||||||
|
this._undoRedoController.undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _redo() {
|
||||||
|
this._undoRedoController.redo();
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
|||||||
@@ -7304,6 +7304,7 @@
|
|||||||
"editor": {
|
"editor": {
|
||||||
"header": "Edit UI",
|
"header": "Edit UI",
|
||||||
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
|
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
|
||||||
|
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
|
||||||
"menu": {
|
"menu": {
|
||||||
"open": "Open dashboard menu",
|
"open": "Open dashboard menu",
|
||||||
"raw_editor": "Raw configuration editor",
|
"raw_editor": "Raw configuration editor",
|
||||||
|
|||||||
Reference in New Issue
Block a user