mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-28 12:17:23 +00:00
Compare commits
5 Commits
unassigned
...
fix-undo-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc2085f5b | ||
|
|
fe50c1212a | ||
|
|
c01fbf5f47 | ||
|
|
5c8da28b61 | ||
|
|
bbb3c0208b |
@@ -12,7 +12,7 @@ const UNDO_REDO_STACK_LIMIT = 75;
|
||||
*/
|
||||
export interface UndoRedoControllerConfig<ConfigType> {
|
||||
stackLimit?: number;
|
||||
currentConfig: () => ConfigType;
|
||||
currentConfig: (itemBeingApplied?: ConfigType) => ConfigType;
|
||||
apply: (config: ConfigType) => void;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
|
||||
throw new Error("No apply function provided");
|
||||
};
|
||||
|
||||
private readonly _currentConfig: () => ConfigType = () => {
|
||||
private readonly _currentConfig: (
|
||||
itemBeingApplied?: ConfigType
|
||||
) => ConfigType = () => {
|
||||
throw new Error("No currentConfig function provided");
|
||||
};
|
||||
|
||||
@@ -105,8 +107,8 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
|
||||
if (this._undoStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._redoStack.push({ ...this._currentConfig() });
|
||||
const config = this._undoStack.pop()!;
|
||||
this._redoStack.push({ ...this._currentConfig(config) });
|
||||
this._apply(config);
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
@@ -119,8 +121,8 @@ export class UndoRedoController<ConfigType> implements ReactiveController {
|
||||
if (this._redoStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._undoStack.push({ ...this._currentConfig() });
|
||||
const config = this._redoStack.pop()!;
|
||||
this._undoStack.push({ ...this._currentConfig(config) });
|
||||
this._apply(config);
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
187
src/mixins/scrollable-fade-mixin.ts
Normal file
187
src/mixins/scrollable-fade-mixin.ts
Normal 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;
|
||||
};
|
||||
@@ -299,7 +299,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.automation.picker.headers.area"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -17,8 +9,10 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import { haStyleScrollbar } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { ScrollableFadeMixin } from "../../../../mixins/scrollable-fade-mixin";
|
||||
|
||||
export interface SidebarOverflowMenuEntry {
|
||||
clickAction: () => void;
|
||||
@@ -31,7 +25,9 @@ export interface SidebarOverflowMenuEntry {
|
||||
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
|
||||
|
||||
@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({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
@@ -42,23 +38,10 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _contentScrolled = false;
|
||||
|
||||
@state() private _contentScrollable = false;
|
||||
|
||||
@query(".card-content") private _contentElement!: HTMLDivElement;
|
||||
|
||||
private _contentSize = new ResizeController(this, {
|
||||
target: null,
|
||||
callback: (entries) => {
|
||||
if (entries[0]?.target) {
|
||||
this._canScrollDown(entries[0].target);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
this._contentSize.observe(this._contentElement);
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._contentElement;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -70,9 +53,7 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
yaml: this.yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header
|
||||
class=${classMap({ scrolled: this._contentScrolled })}
|
||||
>
|
||||
<ha-dialog-header>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
@@ -107,34 +88,14 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<div class="card-content" @scroll=${this._onScroll}>
|
||||
<div class="card-content ha-scrollbar">
|
||||
<slot></slot>
|
||||
${this.renderScrollableFades(this.isWide)}
|
||||
</div>
|
||||
<div
|
||||
class=${classMap({ fade: true, scrollable: this._contentScrollable })}
|
||||
></div>
|
||||
</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() {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
@@ -144,86 +105,63 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
box-shadow: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
position: relative;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
position: relative;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
ha-dialog-header.scrolled {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
}
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
||||
}
|
||||
|
||||
.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-top {
|
||||
top: var(--ha-space-17);
|
||||
}
|
||||
|
||||
.fade.scrollable {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.fade {
|
||||
bottom: 0;
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@media all and (max-width: 870px) {
|
||||
.card-content {
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -271,7 +271,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.scene.picker.headers.area"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
||||
@@ -281,7 +281,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.script.picker.headers.area"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
||||
@@ -11,7 +11,6 @@ import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeTimelineColor } from "../../components/chart/timeline-color";
|
||||
import "../../components/entity/state-badge";
|
||||
@@ -464,24 +463,15 @@ class HaLogbookRenderer extends LitElement {
|
||||
entityName: string | undefined,
|
||||
noLink?: boolean
|
||||
) {
|
||||
if (!entityId) {
|
||||
return entityName || "";
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const hasState = Boolean(stateObj);
|
||||
|
||||
const displayName = hasState
|
||||
? this.hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME) ||
|
||||
entityName ||
|
||||
stateObj.attributes.friendly_name ||
|
||||
entityId
|
||||
: entityName || entityId;
|
||||
|
||||
const hasState = entityId && entityId in this.hass.states;
|
||||
const displayName =
|
||||
entityName ||
|
||||
(hasState
|
||||
? this.hass.states[entityId].attributes.friendly_name || entityId
|
||||
: entityId);
|
||||
if (!hasState) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return noLink
|
||||
? displayName
|
||||
: html`<button
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { Lovelace } from "../types";
|
||||
import { deleteBadge } from "./config-util";
|
||||
import type { LovelaceCardPath } from "./lovelace-path";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface DeleteBadgeParams {
|
||||
path: LovelaceCardPath;
|
||||
@@ -23,14 +24,13 @@ export async function performDeleteBadge(
|
||||
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") },
|
||||
action: {
|
||||
action: () => fireEvent(window, "undo-change"),
|
||||
text: hass.localize("ui.common.undo"),
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { Lovelace } from "../types";
|
||||
import { deleteCard } from "./config-util";
|
||||
import type { LovelaceCardPath } from "./lovelace-path";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface DeleteCardParams {
|
||||
path: LovelaceCardPath;
|
||||
@@ -23,14 +24,13 @@ export async function performDeleteCard(
|
||||
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") },
|
||||
action: {
|
||||
action: () => fireEvent(window, "undo-change"),
|
||||
text: hass.localize("ui.common.undo"),
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
mdiMagnify,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRedo,
|
||||
mdiRefresh,
|
||||
mdiRobot,
|
||||
mdiShape,
|
||||
mdiSofa,
|
||||
mdiUndo,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -50,7 +52,10 @@ import "../../components/ha-tab-group-tab";
|
||||
import "../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../data/area_registry";
|
||||
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 type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||
import {
|
||||
@@ -92,6 +97,7 @@ import "./views/hui-view";
|
||||
import type { HUIView } from "./views/hui-view";
|
||||
import "./views/hui-view-background";
|
||||
import "./views/hui-view-container";
|
||||
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||
|
||||
interface ActionItem {
|
||||
icon: string;
|
||||
@@ -113,6 +119,11 @@ interface SubActionItem {
|
||||
visible: boolean | undefined;
|
||||
}
|
||||
|
||||
interface UndoStackItem {
|
||||
location: string;
|
||||
config: LovelaceRawConfig;
|
||||
}
|
||||
|
||||
@customElement("hui-root")
|
||||
class HUIRoot extends LitElement {
|
||||
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
|
||||
@@ -130,12 +141,22 @@ class HUIRoot extends LitElement {
|
||||
|
||||
@state() private _curView?: number | "hass-unused-entities";
|
||||
|
||||
private _configChangedByUndo = false;
|
||||
|
||||
private _viewCache?: Record<string, HUIView>;
|
||||
|
||||
private _viewScrollPositions: Record<string, number> = {};
|
||||
|
||||
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 _conversation = memoizeOne((_components) =>
|
||||
@@ -157,7 +178,29 @@ class HUIRoot extends LitElement {
|
||||
const result: TemplateResult[] = [];
|
||||
if (this._editMode) {
|
||||
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"
|
||||
size="small"
|
||||
class="exit-edit-mode"
|
||||
@@ -645,6 +688,27 @@ class HUIRoot extends LitElement {
|
||||
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 {
|
||||
super.updated(changedProperties);
|
||||
|
||||
@@ -1029,6 +1093,7 @@ class HUIRoot extends LitElement {
|
||||
|
||||
private _editModeDisable(): void {
|
||||
this.lovelace!.setEditMode(false);
|
||||
this._undoRedoController.reset();
|
||||
}
|
||||
|
||||
private async _editDashboard() {
|
||||
@@ -1207,6 +1272,36 @@ class HUIRoot extends LitElement {
|
||||
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 {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -20,6 +20,7 @@ export const colorStyles = css`
|
||||
--divider-color: rgba(0, 0, 0, 0.12);
|
||||
--outline-color: rgba(0, 0, 0, 0.12);
|
||||
--outline-hover-color: rgba(0, 0, 0, 0.24);
|
||||
--shadow-color: rgba(0, 0, 0, 0.16);
|
||||
|
||||
/* rgb */
|
||||
--rgb-primary-color: 0, 154, 199;
|
||||
@@ -224,7 +225,7 @@ export const colorStyles = css`
|
||||
--table-row-alternative-background-color: var(--secondary-background-color);
|
||||
--data-table-background-color: var(--card-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 */
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
@@ -307,6 +308,8 @@ export const darkColorStyles = css`
|
||||
--divider-color: rgba(225, 225, 225, 0.12);
|
||||
--outline-color: rgba(225, 225, 225, 0.12);
|
||||
--outline-hover-color: rgba(225, 225, 225, 0.24);
|
||||
--shadow-color: rgba(0, 0, 0, 0.48);
|
||||
|
||||
--mdc-ripple-color: #aaaaaa;
|
||||
--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-light-color: #6a7081;
|
||||
|
||||
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.48);
|
||||
--bar-box-shadow: 0 2px 12px var(--shadow-color);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -7304,6 +7304,7 @@
|
||||
"editor": {
|
||||
"header": "Edit UI",
|
||||
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
|
||||
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
|
||||
"menu": {
|
||||
"open": "Open dashboard menu",
|
||||
"raw_editor": "Raw configuration editor",
|
||||
|
||||
Reference in New Issue
Block a user