mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-15 07:25:54 +00:00
Refactor undo/redo to be a controller instead (#27279)
This commit is contained in:
committed by
GitHub
parent
e66724ca9e
commit
04bc5fba63
141
src/common/controllers/undo-redo-controller.ts
Normal file
141
src/common/controllers/undo-redo-controller.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type {
|
||||
ReactiveController,
|
||||
ReactiveControllerHost,
|
||||
} from "@lit/reactive-element/reactive-controller";
|
||||
|
||||
const UNDO_REDO_STACK_LIMIT = 75;
|
||||
|
||||
/**
|
||||
* Configuration options for the UndoRedoController.
|
||||
*
|
||||
* @template ConfigType The type of configuration to manage.
|
||||
*/
|
||||
export interface UndoRedoControllerConfig<ConfigType> {
|
||||
stackLimit?: number;
|
||||
currentConfig: () => ConfigType;
|
||||
apply: (config: ConfigType) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A controller to manage undo and redo operations for a given configuration type.
|
||||
*
|
||||
* @template ConfigType The type of configuration to manage.
|
||||
*/
|
||||
export class UndoRedoController<ConfigType> implements ReactiveController {
|
||||
private _host: ReactiveControllerHost;
|
||||
|
||||
private _undoStack: ConfigType[] = [];
|
||||
|
||||
private _redoStack: ConfigType[] = [];
|
||||
|
||||
private readonly _stackLimit: number = UNDO_REDO_STACK_LIMIT;
|
||||
|
||||
private readonly _apply: (config: ConfigType) => void = () => {
|
||||
throw new Error("No apply function provided");
|
||||
};
|
||||
|
||||
private readonly _currentConfig: () => ConfigType = () => {
|
||||
throw new Error("No currentConfig function provided");
|
||||
};
|
||||
|
||||
constructor(
|
||||
host: ReactiveControllerHost,
|
||||
options: UndoRedoControllerConfig<ConfigType>
|
||||
) {
|
||||
if (options.stackLimit !== undefined) {
|
||||
this._stackLimit = options.stackLimit;
|
||||
}
|
||||
|
||||
this._apply = options.apply;
|
||||
this._currentConfig = options.currentConfig;
|
||||
this._host = host;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
window.addEventListener("undo-change", this._onUndoChange);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
window.removeEventListener("undo-change", this._onUndoChange);
|
||||
}
|
||||
|
||||
private _onUndoChange = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this.undo();
|
||||
this._host.requestUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates whether there are actions available to undo.
|
||||
*
|
||||
* @returns `true` if there are actions to undo, `false` otherwise.
|
||||
*/
|
||||
public get canUndo(): boolean {
|
||||
return this._undoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether there are actions available to redo.
|
||||
*
|
||||
* @returns `true` if there are actions to redo, `false` otherwise.
|
||||
*/
|
||||
public get canRedo(): boolean {
|
||||
return this._redoStack.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the current configuration to the undo stack and clears the redo stack.
|
||||
*
|
||||
* @param config The current configuration to commit.
|
||||
*/
|
||||
public commit(config: ConfigType) {
|
||||
if (this._undoStack.length >= this._stackLimit) {
|
||||
this._undoStack.shift();
|
||||
}
|
||||
this._undoStack.push({ ...config });
|
||||
this._redoStack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes the last action and applies the previous configuration
|
||||
* while saving the current configuration to the redo stack.
|
||||
*/
|
||||
public undo() {
|
||||
if (this._undoStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._redoStack.push({ ...this._currentConfig() });
|
||||
const config = this._undoStack.pop()!;
|
||||
this._apply(config);
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the last undone action and reapplies the configuration
|
||||
* while saving the current configuration to the undo stack.
|
||||
*/
|
||||
public redo() {
|
||||
if (this._redoStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._undoStack.push({ ...this._currentConfig() });
|
||||
const config = this._redoStack.pop()!;
|
||||
this._apply(config);
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the undo and redo stacks, clearing all history.
|
||||
*/
|
||||
public reset() {
|
||||
this._undoStack = [];
|
||||
this._redoStack = [];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"undo-change": undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
export const UndoRedoMixin = <T extends Constructor<LitElement>, ConfigType>(
|
||||
superClass: T
|
||||
) => {
|
||||
class UndoRedoClass extends superClass {
|
||||
private _undoStack: ConfigType[] = [];
|
||||
|
||||
private _redoStack: ConfigType[] = [];
|
||||
|
||||
protected _undoStackLimit = 75;
|
||||
|
||||
protected pushToUndo(config: ConfigType) {
|
||||
if (this._undoStack.length >= this._undoStackLimit) {
|
||||
this._undoStack.shift();
|
||||
}
|
||||
this._undoStack.push({ ...config });
|
||||
this._redoStack = [];
|
||||
}
|
||||
|
||||
public undo() {
|
||||
const currentConfig = this.currentConfig;
|
||||
if (this._undoStack.length === 0 || !currentConfig) {
|
||||
return;
|
||||
}
|
||||
this._redoStack.push({ ...currentConfig });
|
||||
const config = this._undoStack.pop()!;
|
||||
this.applyUndoRedo(config);
|
||||
}
|
||||
|
||||
public redo() {
|
||||
const currentConfig = this.currentConfig;
|
||||
if (this._redoStack.length === 0 || !currentConfig) {
|
||||
return;
|
||||
}
|
||||
this._undoStack.push({ ...currentConfig });
|
||||
const config = this._redoStack.pop()!;
|
||||
this.applyUndoRedo(config);
|
||||
}
|
||||
|
||||
public get canUndo(): boolean {
|
||||
return this._undoStack.length > 0;
|
||||
}
|
||||
|
||||
public get canRedo(): boolean {
|
||||
return this._redoStack.length > 0;
|
||||
}
|
||||
|
||||
protected get currentConfig(): ConfigType | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected applyUndoRedo(_: ConfigType) {
|
||||
throw new Error("applyUndoRedo not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
return UndoRedoClass;
|
||||
};
|
||||
@@ -74,7 +74,7 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
|
||||
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
@@ -111,12 +111,9 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const baseEditorMixins = PreventUnsavedMixin(KeyboardShortcutMixin(LitElement));
|
||||
|
||||
export class HaAutomationEditor extends UndoRedoMixin<
|
||||
typeof baseEditorMixins,
|
||||
AutomationConfig
|
||||
>(baseEditorMixins) {
|
||||
export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
KeyboardShortcutMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public automationId: string | null = null;
|
||||
@@ -183,6 +180,11 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => this._config!,
|
||||
});
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -235,8 +237,8 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
.path=${mdiUndo}
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
@click=${this._undo}
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
id="button-undo"
|
||||
>
|
||||
</ha-icon-button>
|
||||
@@ -253,8 +255,8 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.redo")}
|
||||
.path=${mdiRedo}
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
@click=${this._redo}
|
||||
.disabled=${!this._undoRedoController.canRedo}
|
||||
id="button-redo"
|
||||
>
|
||||
</ha-icon-button>
|
||||
@@ -298,16 +300,16 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
${this._mode === "gui" && this.narrow
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
@click=${this._undo}
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
>
|
||||
${this.hass.localize("ui.common.undo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
@click=${this._redo}
|
||||
.disabled=${!this._undoRedoController.canRedo}
|
||||
>
|
||||
${this.hass.localize("ui.common.redo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
|
||||
@@ -518,7 +520,6 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
@undo-paste=${this.undo}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
@@ -791,7 +792,7 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this._config) {
|
||||
this.pushToUndo(this._config);
|
||||
this._undoRedoController.commit(this._config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
@@ -1202,9 +1203,9 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
x: () => this._cutSelectedRow(),
|
||||
Delete: () => this._deleteSelectedRow(),
|
||||
Backspace: () => this._deleteSelectedRow(),
|
||||
z: () => this.undo(),
|
||||
Z: () => this.redo(),
|
||||
y: () => this.redo(),
|
||||
z: () => this._undo(),
|
||||
Z: () => this._redo(),
|
||||
y: () => this._redo(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1238,16 +1239,20 @@ export class HaAutomationEditor extends UndoRedoMixin<
|
||||
this._manualEditor?.deleteSelectedRow();
|
||||
}
|
||||
|
||||
protected get currentConfig() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
protected applyUndoRedo(config: AutomationConfig) {
|
||||
private _applyUndoRedo(config: AutomationConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
this._undoRedoController.undo();
|
||||
}
|
||||
|
||||
private _redo() {
|
||||
this._undoRedoController.redo();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -616,7 +616,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
action: {
|
||||
text: this.hass.localize("ui.common.undo"),
|
||||
action: () => {
|
||||
fireEvent(this, "undo-paste");
|
||||
fireEvent(this, "undo-change");
|
||||
|
||||
this._pastedConfig = undefined;
|
||||
},
|
||||
@@ -742,6 +742,5 @@ declare global {
|
||||
"open-sidebar": SidebarConfig;
|
||||
"request-close-sidebar": undefined;
|
||||
"close-sidebar": undefined;
|
||||
"undo-paste": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
|
||||
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
@@ -78,14 +78,9 @@ import "./blueprint-script-editor";
|
||||
import "./manual-script-editor";
|
||||
import type { HaManualScriptEditor } from "./manual-script-editor";
|
||||
|
||||
const baseEditorMixins = SubscribeMixin(
|
||||
export class HaScriptEditor extends SubscribeMixin(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
);
|
||||
|
||||
export class HaScriptEditor extends UndoRedoMixin<
|
||||
typeof baseEditorMixins,
|
||||
ScriptConfig
|
||||
>(baseEditorMixins) {
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public scriptId: string | null = null;
|
||||
@@ -141,6 +136,11 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => this._config!,
|
||||
});
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -188,8 +188,8 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
.path=${mdiUndo}
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
@click=${this._undo}
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
id="button-undo"
|
||||
>
|
||||
</ha-icon-button>
|
||||
@@ -205,8 +205,8 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.redo")}
|
||||
.path=${mdiRedo}
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
@click=${this._redo}
|
||||
.disabled=${!this._undoRedoController.canRedo}
|
||||
id="button-redo"
|
||||
>
|
||||
</ha-icon-button>
|
||||
@@ -249,16 +249,16 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
${this._mode === "gui" && this.narrow
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.undo}
|
||||
.disabled=${!this.canUndo}
|
||||
@click=${this._undo}
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
>
|
||||
${this.hass.localize("ui.common.undo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this.redo}
|
||||
.disabled=${!this.canRedo}
|
||||
@click=${this._redo}
|
||||
.disabled=${!this._undoRedoController.canRedo}
|
||||
>
|
||||
${this.hass.localize("ui.common.redo")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
|
||||
@@ -463,7 +463,6 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
@value-changed=${this._valueChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@save-script=${this._handleSaveScript}
|
||||
@undo-paste=${this.undo}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
@@ -679,7 +678,7 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
|
||||
private _valueChanged(ev) {
|
||||
if (this._config) {
|
||||
this.pushToUndo(this._config);
|
||||
this._undoRedoController.commit(this._config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
@@ -776,7 +775,7 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
}
|
||||
|
||||
if (this._config) {
|
||||
this.pushToUndo(this._config);
|
||||
this._undoRedoController.commit(this._config);
|
||||
}
|
||||
|
||||
this._manualEditor?.addFields();
|
||||
@@ -1110,9 +1109,9 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
x: () => this._cutSelectedRow(),
|
||||
Delete: () => this._deleteSelectedRow(),
|
||||
Backspace: () => this._deleteSelectedRow(),
|
||||
z: () => this.undo(),
|
||||
Z: () => this.redo(),
|
||||
y: () => this.redo(),
|
||||
z: () => this._undo(),
|
||||
Z: () => this._redo(),
|
||||
y: () => this._redo(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1146,16 +1145,20 @@ export class HaScriptEditor extends UndoRedoMixin<
|
||||
this._manualEditor?.deleteSelectedRow();
|
||||
}
|
||||
|
||||
protected get currentConfig() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
protected applyUndoRedo(config: ScriptConfig) {
|
||||
private _applyUndoRedo(config: ScriptConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
this._undoRedoController.undo();
|
||||
}
|
||||
|
||||
private _redo() {
|
||||
this._undoRedoController.redo();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -491,7 +491,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
action: {
|
||||
text: this.hass.localize("ui.common.undo"),
|
||||
action: () => {
|
||||
fireEvent(this, "undo-paste");
|
||||
fireEvent(this, "undo-change");
|
||||
|
||||
this._pastedConfig = undefined;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user