Compare commits

...

5 Commits

Author SHA1 Message Date
Petar Petrov
cdc2085f5b Fix location handling for undo/redo 2025-11-25 16:32:31 +02:00
Jan-Philipp Benecke
fe50c1212a 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
2025-11-25 16:09:13 +02:00
Petar Petrov
c01fbf5f47 Revert "Use entity name for activity/logbook renderer" (#28098) 2025-11-25 13:24:51 +01:00
Aidan Timson
5c8da28b61 Standardise scrollable area fade (#28074)
* Extract fade CSS into shared file

* Switch to linear gradient instead of box shadow

* Refactor

* Refactor

* Scrollbar

* Fix

* Create mixin

* Move styles into mixin

* Dont replace mixin styles

* Reuse bar-box-shadow

* Move position
2025-11-25 13:04:08 +02:00
Rahul Harpal
bbb3c0208b Remove defaultHidden property from area configuration in automation, scene, and script pickers (#28096) 2025-11-25 10:55:12 +00:00
12 changed files with 375 additions and 162 deletions

View File

@@ -12,7 +12,7 @@ const UNDO_REDO_STACK_LIMIT = 75;
*/ */
export interface UndoRedoControllerConfig<ConfigType> { export interface UndoRedoControllerConfig<ConfigType> {
stackLimit?: number; stackLimit?: number;
currentConfig: () => ConfigType; currentConfig: (itemBeingApplied?: ConfigType) => ConfigType;
apply: (config: ConfigType) => void; apply: (config: ConfigType) => void;
} }
@@ -34,7 +34,9 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
throw new Error("No apply function provided"); throw new Error("No apply function provided");
}; };
private readonly _currentConfig: () => ConfigType = () => { private readonly _currentConfig: (
itemBeingApplied?: ConfigType
) => ConfigType = () => {
throw new Error("No currentConfig function provided"); throw new Error("No currentConfig function provided");
}; };
@@ -105,8 +107,8 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
if (this._undoStack.length === 0) { if (this._undoStack.length === 0) {
return; return;
} }
this._redoStack.push({ ...this._currentConfig() });
const config = this._undoStack.pop()!; const config = this._undoStack.pop()!;
this._redoStack.push({ ...this._currentConfig(config) });
this._apply(config); this._apply(config);
this._host.requestUpdate(); this._host.requestUpdate();
} }
@@ -119,8 +121,8 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
if (this._redoStack.length === 0) { if (this._redoStack.length === 0) {
return; return;
} }
this._undoStack.push({ ...this._currentConfig() });
const config = this._redoStack.pop()!; const config = this._redoStack.pop()!;
this._undoStack.push({ ...this._currentConfig(config) });
this._apply(config); this._apply(config);
this._host.requestUpdate(); this._host.requestUpdate();
} }

View File

@@ -0,0 +1,187 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { css, html } from "lit";
import type {
CSSResultGroup,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { classMap } from "lit/directives/class-map";
import { state } from "lit/decorators";
import type { Constructor } from "../types";
const stylesArray = (styles?: CSSResultGroup | CSSResultGroup[]) =>
styles === undefined ? [] : Array.isArray(styles) ? styles : [styles];
export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class ScrollableFadeClass extends superClass {
@state() protected _contentScrolled = false;
@state() protected _contentScrollable = false;
private _scrollTarget?: HTMLElement | null;
private _onScroll = (ev: Event) => {
const target = ev.currentTarget as HTMLElement;
this._contentScrolled = (target.scrollTop ?? 0) > 0;
this._updateScrollableState(target);
};
private _resize = new ResizeController(this, {
target: null,
callback: (entries) => {
const target = entries[0]?.target as HTMLElement | undefined;
if (target) {
this._updateScrollableState(target);
}
},
});
private static readonly DEFAULT_SAFE_AREA_PADDING = 16;
private static readonly DEFAULT_SCROLLABLE_ELEMENT: HTMLElement | null =
null;
protected get scrollFadeSafeAreaPadding() {
return ScrollableFadeClass.DEFAULT_SAFE_AREA_PADDING;
}
protected get scrollableElement(): HTMLElement | null {
return ScrollableFadeClass.DEFAULT_SCROLLABLE_ELEMENT;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated?.(changedProperties);
this._attachScrollableElement();
}
protected updated(changedProperties: PropertyValues) {
super.updated?.(changedProperties);
this._attachScrollableElement();
}
disconnectedCallback() {
this._detachScrollableElement();
super.disconnectedCallback();
}
protected renderScrollableFades(rounded = false): TemplateResult {
return html`
<div
class=${classMap({
"fade-top": true,
rounded,
visible: this._contentScrolled,
})}
></div>
<div
class=${classMap({
"fade-bottom": true,
rounded,
visible: this._contentScrollable,
})}
></div>
`;
}
static get styles() {
const superCtor = Object.getPrototypeOf(this) as
| typeof LitElement
| undefined;
const inheritedStyles = stylesArray(
(superCtor?.styles ?? []) as CSSResultGroup | CSSResultGroup[]
);
return [
...inheritedStyles,
css`
.fade-top,
.fade-bottom {
position: absolute;
left: var(--ha-space-0);
right: var(--ha-space-0);
height: var(--ha-space-4);
pointer-events: none;
transition: opacity 180ms ease-in-out;
background: linear-gradient(
to bottom,
var(--shadow-color),
transparent
);
border-radius: var(--ha-border-radius-square);
z-index: 100;
opacity: 0;
}
.fade-top {
top: var(--ha-space-0);
}
.fade-bottom {
bottom: var(--ha-space-0);
transform: rotate(180deg);
}
.fade-top.visible,
.fade-bottom.visible {
opacity: 1;
}
.fade-top.rounded,
.fade-bottom.rounded {
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-bottom-left-radius: var(--ha-border-radius-square);
border-bottom-right-radius: var(--ha-border-radius-square);
}
.fade-top.rounded {
border-top-left-radius: var(--ha-border-radius-square);
border-top-right-radius: var(--ha-border-radius-square);
}
.fade-bottom.rounded {
border-bottom-left-radius: var(--ha-border-radius-square);
border-bottom-right-radius: var(--ha-border-radius-square);
}
`,
];
}
private _attachScrollableElement() {
const element = this.scrollableElement;
if (element === this._scrollTarget) {
return;
}
this._detachScrollableElement();
if (!element) {
return;
}
this._scrollTarget = element;
element.addEventListener("scroll", this._onScroll, { passive: true });
this._resize.observe(element);
this._updateScrollableState(element);
}
private _detachScrollableElement() {
if (!this._scrollTarget) {
return;
}
this._scrollTarget.removeEventListener("scroll", this._onScroll);
this._resize.unobserve?.(this._scrollTarget);
this._scrollTarget = undefined;
}
private _updateScrollableState(element: HTMLElement) {
const safeAreaInsetBottom =
parseFloat(
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
) || 0;
const { scrollHeight = 0, clientHeight = 0, scrollTop = 0 } = element;
this._contentScrollable =
scrollHeight - clientHeight >
scrollTop + safeAreaInsetBottom + this.scrollFadeSafeAreaPadding;
}
}
return ScrollableFadeClass;
};

View File

@@ -299,7 +299,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}, },
area: { area: {
title: localize("ui.panel.config.automation.picker.headers.area"), title: localize("ui.panel.config.automation.picker.headers.area"),
defaultHidden: true,
groupable: true, groupable: true,
filterable: true, filterable: true,
sortable: true, sortable: true,

View File

@@ -1,14 +1,6 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiClose, mdiDotsVertical } from "@mdi/js"; import { mdiClose, mdiDotsVertical } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { import { customElement, property, query } from "lit/decorators";
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -17,8 +9,10 @@ import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu"; import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider"; import "../../../../components/ha-md-divider";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning"; import "../ha-automation-editor-warning";
import { ScrollableFadeMixin } from "../../../../mixins/scrollable-fade-mixin";
export interface SidebarOverflowMenuEntry { export interface SidebarOverflowMenuEntry {
clickAction: () => void; clickAction: () => void;
@@ -31,7 +25,9 @@ export interface SidebarOverflowMenuEntry {
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[]; export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
@customElement("ha-automation-sidebar-card") @customElement("ha-automation-sidebar-card")
export default class HaAutomationSidebarCard extends LitElement { export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
LitElement
) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "wide" }) public isWide = false; @property({ type: Boolean, attribute: "wide" }) public isWide = false;
@@ -42,23 +38,10 @@ export default class HaAutomationSidebarCard extends LitElement {
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@state() private _contentScrolled = false;
@state() private _contentScrollable = false;
@query(".card-content") private _contentElement!: HTMLDivElement; @query(".card-content") private _contentElement!: HTMLDivElement;
private _contentSize = new ResizeController(this, { protected get scrollableElement(): HTMLElement | null {
target: null, return this._contentElement;
callback: (entries) => {
if (entries[0]?.target) {
this._canScrollDown(entries[0].target);
}
},
});
protected firstUpdated(_changedProperties: PropertyValues): void {
this._contentSize.observe(this._contentElement);
} }
protected render() { protected render() {
@@ -70,9 +53,7 @@ export default class HaAutomationSidebarCard extends LitElement {
yaml: this.yamlMode, yaml: this.yamlMode,
})} })}
> >
<ha-dialog-header <ha-dialog-header>
class=${classMap({ scrolled: this._contentScrolled })}
>
<ha-icon-button <ha-icon-button
slot="navigationIcon" slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")} .label=${this.hass.localize("ui.common.close")}
@@ -107,34 +88,14 @@ export default class HaAutomationSidebarCard extends LitElement {
> >
</ha-automation-editor-warning>` </ha-automation-editor-warning>`
: nothing} : nothing}
<div class="card-content" @scroll=${this._onScroll}> <div class="card-content ha-scrollbar">
<slot></slot> <slot></slot>
${this.renderScrollableFades(this.isWide)}
</div> </div>
<div
class=${classMap({ fade: true, scrollable: this._contentScrollable })}
></div>
</ha-card> </ha-card>
`; `;
} }
@eventOptions({ passive: true })
private _onScroll(ev) {
const top = ev.target.scrollTop ?? 0;
this._contentScrolled = top > 0;
this._canScrollDown(ev.target);
}
private _canScrollDown(element: HTMLElement) {
const safeAreaInsetBottom =
parseFloat(
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
) || 0;
this._contentScrollable =
(element.scrollHeight ?? 0) - (element.clientHeight ?? 0) >
(element.scrollTop ?? 0) + safeAreaInsetBottom + 16;
}
private _closeSidebar() { private _closeSidebar() {
fireEvent(this, "close-sidebar"); fireEvent(this, "close-sidebar");
} }
@@ -144,7 +105,11 @@ export default class HaAutomationSidebarCard extends LitElement {
ev.preventDefault(); ev.preventDefault();
} }
static styles = css` static get styles() {
return [
...super.styles,
haStyleScrollbar,
css`
ha-card { ha-card {
position: relative; position: relative;
height: 100%; height: 100%;
@@ -168,8 +133,6 @@ export default class HaAutomationSidebarCard extends LitElement {
ha-dialog-header { ha-dialog-header {
border-radius: var(--ha-card-border-radius); border-radius: var(--ha-card-border-radius);
box-shadow: none;
transition: box-shadow 180ms ease-in-out;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
position: relative; position: relative;
@@ -179,32 +142,6 @@ export default class HaAutomationSidebarCard extends LitElement {
); );
} }
ha-dialog-header.scrolled {
box-shadow: var(--bar-box-shadow);
}
.fade {
position: absolute;
bottom: 1px;
left: 1px;
right: 1px;
height: 16px;
pointer-events: none;
transition: box-shadow 180ms ease-in-out;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
transform: rotate(180deg);
border-radius: var(--ha-card-border-radius);
border-bottom-left-radius: var(--ha-border-radius-square);
border-bottom-right-radius: var(--ha-border-radius-square);
}
.fade.scrollable {
box-shadow: var(--bar-box-shadow);
}
.card-content { .card-content {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
@@ -213,17 +150,18 @@ export default class HaAutomationSidebarCard extends LitElement {
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px); padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
} }
@media all and (max-width: 870px) { .fade-top {
.fade { top: var(--ha-space-17);
bottom: 0;
border-radius: var(--ha-border-radius-square);
} }
@media all and (max-width: 870px) {
.card-content { .card-content {
padding-bottom: 42px; padding-bottom: 42px;
} }
} }
`; `,
];
}
} }
declare global { declare global {

View File

@@ -271,7 +271,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
}, },
area: { area: {
title: localize("ui.panel.config.scene.picker.headers.area"), title: localize("ui.panel.config.scene.picker.headers.area"),
defaultHidden: true,
groupable: true, groupable: true,
filterable: true, filterable: true,
sortable: true, sortable: true,

View File

@@ -281,7 +281,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}, },
area: { area: {
title: localize("ui.panel.config.script.picker.headers.area"), title: localize("ui.panel.config.script.picker.headers.area"),
defaultHidden: true,
groupable: true, groupable: true,
filterable: true, filterable: true,
sortable: true, sortable: true,

View File

@@ -11,7 +11,6 @@ import { formatTimeWithSeconds } from "../../common/datetime/format_time";
import { restoreScroll } from "../../common/decorators/restore-scroll"; import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import { computeTimelineColor } from "../../components/chart/timeline-color"; import { computeTimelineColor } from "../../components/chart/timeline-color";
import "../../components/entity/state-badge"; import "../../components/entity/state-badge";
@@ -464,24 +463,15 @@ class HaLogbookRenderer extends LitElement {
entityName: string | undefined, entityName: string | undefined,
noLink?: boolean noLink?: boolean
) { ) {
if (!entityId) { const hasState = entityId && entityId in this.hass.states;
return entityName || ""; const displayName =
}
const stateObj = this.hass.states[entityId];
const hasState = Boolean(stateObj);
const displayName = hasState
? this.hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME) ||
entityName || entityName ||
stateObj.attributes.friendly_name || (hasState
entityId ? this.hass.states[entityId].attributes.friendly_name || entityId
: entityName || entityId; : entityId);
if (!hasState) { if (!hasState) {
return displayName; return displayName;
} }
return noLink return noLink
? displayName ? displayName
: html`<button : html`<button

View File

@@ -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

View File

@@ -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

View File

@@ -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: (itemBeingApplied) => ({
location: itemBeingApplied?.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,

View File

@@ -20,6 +20,7 @@ export const colorStyles = css`
--divider-color: rgba(0, 0, 0, 0.12); --divider-color: rgba(0, 0, 0, 0.12);
--outline-color: rgba(0, 0, 0, 0.12); --outline-color: rgba(0, 0, 0, 0.12);
--outline-hover-color: rgba(0, 0, 0, 0.24); --outline-hover-color: rgba(0, 0, 0, 0.24);
--shadow-color: rgba(0, 0, 0, 0.16);
/* rgb */ /* rgb */
--rgb-primary-color: 0, 154, 199; --rgb-primary-color: 0, 154, 199;
@@ -224,7 +225,7 @@ export const colorStyles = css`
--table-row-alternative-background-color: var(--secondary-background-color); --table-row-alternative-background-color: var(--secondary-background-color);
--data-table-background-color: var(--card-background-color); --data-table-background-color: var(--card-background-color);
--markdown-code-background-color: var(--primary-background-color); --markdown-code-background-color: var(--primary-background-color);
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.16); --bar-box-shadow: 0 2px 12px var(--shadow-color);
/* https://github.com/material-components/material-web/blob/master/docs/theming.md */ /* https://github.com/material-components/material-web/blob/master/docs/theming.md */
--mdc-theme-primary: var(--primary-color); --mdc-theme-primary: var(--primary-color);
@@ -307,6 +308,8 @@ export const darkColorStyles = css`
--divider-color: rgba(225, 225, 225, 0.12); --divider-color: rgba(225, 225, 225, 0.12);
--outline-color: rgba(225, 225, 225, 0.12); --outline-color: rgba(225, 225, 225, 0.12);
--outline-hover-color: rgba(225, 225, 225, 0.24); --outline-hover-color: rgba(225, 225, 225, 0.24);
--shadow-color: rgba(0, 0, 0, 0.48);
--mdc-ripple-color: #aaaaaa; --mdc-ripple-color: #aaaaaa;
--mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1); --mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1);
@@ -350,7 +353,7 @@ export const darkColorStyles = css`
--ha-button-neutral-color: #d9dae0; --ha-button-neutral-color: #d9dae0;
--ha-button-neutral-light-color: #6a7081; --ha-button-neutral-light-color: #6a7081;
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.48); --bar-box-shadow: 0 2px 12px var(--shadow-color);
} }
`; `;

View File

@@ -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",