Compare commits

...

1 Commits

Author SHA1 Message Date
Aidan Timson 4d36d35467 Migrate 5th set to dirty state provider 2026-06-09 15:22:41 +01:00
8 changed files with 92 additions and 67 deletions
+1
View File
@@ -181,6 +181,7 @@ export const DirtyStateProviderMixin =
/**
* Whether any slice's current value differs from its baseline.
* This passes the protected getter to the consuming class.
*/
public get isDirtyState(): boolean {
return this._dirtyStateContext.isDirty;
@@ -1,13 +1,18 @@
import { consume } from "@lit/context";
import { mdiContentSave } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-markdown";
import type { BlueprintAutomationConfig } from "../../../data/automation";
import { fetchBlueprints } from "../../../data/blueprint";
import {
dirtyStateContext,
type DirtyStateContext,
} from "../../../data/context/dirty-state";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
import { saveFabStyles } from "./styles";
@@ -19,7 +24,9 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
@property({ type: Boolean }) public saving = false;
@property({ type: Boolean }) public dirty = false;
@consume({ context: dirtyStateContext, subscribe: true })
@state()
private _dirtyState?: DirtyStateContext;
protected get _config(): BlueprintAutomationConfig {
return this.config;
@@ -58,7 +65,7 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
<ha-button
slot="fab"
size="l"
class=${this.dirty ? "dirty" : ""}
class=${this._dirtyState?.isDirty ? "dirty" : ""}
.disabled=${this.saving}
@click=${this._saveAutomation}
>
@@ -421,7 +421,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
></blueprint-automation-editor>
@@ -434,7 +433,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.stateObj=${stateObj}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@@ -554,7 +552,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
<ha-button
slot="fab"
size="l"
class=${this.dirty ? "dirty" : ""}
class=${this.isDirtyState ? "dirty" : ""}
.disabled=${this.saving}
@click=${this._handleSaveAutomation}
>
@@ -602,7 +600,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
!this.entityId
) {
const initData = getAutomationEditorInitData();
this.dirty = !!initData;
let baseConfig: Partial<AutomationConfig> = { description: "" };
if (!initData || !("use_blueprint" in initData)) {
baseConfig = {
@@ -617,6 +614,8 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
...baseConfig,
...(initData ? normalizeAutomationConfig(initData) : initData),
} as AutomationConfig;
this._initDirtyTracking({ type: "deep" }, baseConfig as AutomationConfig);
this._updateDirtyState(this.config);
this.currentEntityId = undefined;
this.readOnly = false;
}
@@ -624,10 +623,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
if (changedProps.has("entityId") && this.entityId) {
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
this.config = normalizeAutomationConfig(c.config);
this._initDirtyTracking({ type: "deep" }, this.config);
this._checkValidation();
});
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
@@ -690,7 +689,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
if (this.readOnly) {
return;
}
this.dirty = true;
this._updateDirtyState(this.config);
this.errors = undefined;
}
@@ -762,7 +761,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this.dirty = true;
if (!ev.detail.isValid) {
this.yamlErrors = ev.detail.errorMsg;
return;
@@ -772,11 +770,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
id: this.config?.id,
...normalizeAutomationConfig(ev.detail.value),
};
this._updateDirtyState(this.config!);
this.errors = undefined;
}
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
if (!this.isDirtyState) {
return true;
}
@@ -787,7 +786,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._updateDirtyState(this.config);
this.requestUpdate();
const id = this.automationId || String(Date.now());
@@ -901,7 +900,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._updateDirtyState(this.config);
this.requestUpdate();
resolve(true);
},
@@ -918,7 +917,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
config: this.config!,
updateConfig: (config) => {
this.config = config;
this.dirty = true;
this._updateDirtyState(config);
this.requestUpdate();
resolve();
},
@@ -1009,7 +1008,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
}
this.dirty = false;
this._markDirtyStateClean();
} catch (errors: any) {
this.errors = errors.body?.message || errors.error || errors.body;
showEditorToast(this, {
@@ -1068,7 +1067,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
private _applyUndoRedo(config: AutomationConfig) {
this._manualEditor?.triggerCloseSidebar();
this.config = config;
this.dirty = true;
this._updateDirtyState(this.config);
}
private _undo() {
@@ -20,6 +20,7 @@ import {
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import type { Constructor, HomeAssistant, Route } from "../../../types";
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
@@ -87,7 +88,9 @@ export interface EditorDomainHooks<TConfig> {
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
superClass: Constructor<LitElement>
) => {
class AutomationScriptEditorClass extends superClass {
class AutomationScriptEditorClass extends DirtyStateProviderMixin<TConfig>()(
superClass
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@@ -102,8 +105,6 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
@consume({ context: fullEntitiesContext, subscribe: true })
entityRegistry?: EntityRegistryEntry[];
@state() protected dirty = false;
@state() protected errors?: string;
@state() protected yamlErrors?: string;
@@ -217,7 +218,9 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
protected takeControlSave() {
this.readOnly = false;
this.dirty = true;
// Force dirty: set baseline to null so current config always differs
this._initDirtyTracking({ type: "deep" }, null as unknown as TConfig);
this._updateDirtyState(this.config!);
this.blueprintConfig = undefined;
}
@@ -237,10 +240,6 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
}
};
protected get isDirty() {
return this.dirty;
}
protected async promptDiscardChanges() {
return this.confirmUnsavedChanged();
}
@@ -259,9 +258,9 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
const domain = hooks.domain;
try {
const config = await hooks.fetchFileConfig(this.hass, id);
this.dirty = false;
this.readOnly = false;
this.config = hooks.normalizeConfig(config);
this._initDirtyTracking({ type: "deep" }, this.config);
hooks.checkValidation();
} catch (err: any) {
if (err.status_code !== 404) {
@@ -1,3 +1,4 @@
import { consume } from "@lit/context";
import { mdiContentSave } from "@mdi/js";
import {
html,
@@ -19,6 +20,10 @@ import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import type { SidebarConfig } from "../../../data/automation";
import {
dirtyStateContext,
type DirtyStateContext,
} from "../../../data/context/dirty-state";
import type {
Constructor,
HomeAssistant,
@@ -46,7 +51,13 @@ export const ManualEditorMixin = <TConfig>(
@property({ attribute: false }) public config!: TConfig;
@property({ attribute: false }) public dirty = false;
@consume({ context: dirtyStateContext, subscribe: true })
@state()
private _dirtyState?: DirtyStateContext;
protected get dirty(): boolean {
return this._dirtyState?.isDirty ?? false;
}
@state() protected pastedConfig?: TConfig;
+26 -24
View File
@@ -75,6 +75,7 @@ import {
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-subpage";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
@@ -97,8 +98,8 @@ interface DeviceEntities {
type DeviceEntitiesLookup = Record<string, string[]>;
@customElement("ha-scene-editor")
export class HaSceneEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -112,10 +113,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
@property({ attribute: false }) public scenes!: SceneEntity[];
@state() private _dirty = false;
@state() private _errors?: string;
private _sceneRevision = 0;
@state() private _yamlErrors?: string;
@state() private _config?: SceneConfig;
@@ -322,7 +323,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
.disabled=${this._saving}
@click=${this._saveScene}
class=${classMap({
dirty: this._dirty || !this.sceneId,
dirty: this.isDirty || !this.sceneId,
saving: this._saving,
})}
>
@@ -641,7 +642,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
}
if (changedProps.has("sceneId") && !this.sceneId && this.hass) {
this._dirty = false;
this._sceneRevision = 0;
const initData = getSceneEditorInitData();
this._config = {
name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
@@ -656,9 +657,13 @@ export class HaSceneEditor extends PreventUnsavedMixin(
category: "",
};
}
this._dirty =
this._initDirtyTracking({ type: "shallow" }, 0);
if (
initData !== undefined &&
(initData.areaId !== undefined || initData.config !== undefined);
(initData.areaId !== undefined || initData.config !== undefined)
) {
this._updateDirtyState(++this._sceneRevision);
}
}
if (changedProps.has("_entityRegistryEntries")) {
@@ -816,7 +821,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
}
private async _enterLiveMode() {
if (this._dirty) {
if (this.isDirty) {
const result = await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.scene.editor.enter_live_mode_unsaved"
@@ -850,13 +855,13 @@ export class HaSceneEditor extends PreventUnsavedMixin(
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = ev.detail.value;
this._updateDirtyState(++this._sceneRevision);
this._errors = undefined;
}
@@ -911,7 +916,8 @@ export class HaSceneEditor extends PreventUnsavedMixin(
(entity: SceneEntity) => entity.attributes.id === this.sceneId
);
this._dirty = false;
this._sceneRevision = 0;
this._initDirtyTracking({ type: "shallow" }, 0);
this._config = config;
}
@@ -960,7 +966,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
this._entities = [...this._entities, entityId];
this._single_entities.push(entityId);
this._storeState(entityId);
this._dirty = true;
this._updateDirtyState(++this._sceneRevision);
}
private _deleteEntity(ev: Event) {
@@ -978,7 +984,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
if (this._config!.metadata) {
delete this._config!.metadata[deleteEntityId];
}
this._dirty = true;
this._updateDirtyState(++this._sceneRevision);
}
private _pickDevice(device_id: string) {
@@ -994,7 +1000,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
deviceEntities.forEach((entityId) => {
this._storeState(entityId);
});
this._dirty = true;
this._updateDirtyState(++this._sceneRevision);
}
private _devicePicked(ev: CustomEvent) {
@@ -1018,7 +1024,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
delete this._config!.entities[entityId];
});
}
this._dirty = true;
this._updateDirtyState(++this._sceneRevision);
}
private _stateChanged(event: HassEvent) {
@@ -1026,7 +1032,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
event.context.id !== this._activateContextId &&
this._entities.includes(event.data.entity_id)
) {
this._dirty = true;
this._updateDirtyState(++this._sceneRevision);
}
}
@@ -1072,7 +1078,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (this._dirty) {
if (this.isDirty) {
return showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.scene.editor.unsaved_confirm_title"
@@ -1239,7 +1245,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
}
}
this._dirty = false;
this._markDirtyStateClean();
if (isNewScene) {
navigate(`/config/scene/edit/${id}`, { replace: true });
}
@@ -1294,7 +1300,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
updateConfig: async (newConfig, entityRegistryUpdate) => {
this._config = newConfig;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this._updateDirtyState(++this._sceneRevision);
this.requestUpdate();
resolve(true);
},
@@ -1313,7 +1319,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
updateConfig: async (newConfig, entityRegistryUpdate) => {
this._config = newConfig;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this._updateDirtyState(++this._sceneRevision);
this.requestUpdate();
resolve(true);
},
@@ -1322,10 +1328,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
});
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
@@ -1,10 +1,15 @@
import { consume } from "@lit/context";
import { mdiContentSave } from "@mdi/js";
import { css, html, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-markdown";
import { fetchBlueprints } from "../../../data/blueprint";
import {
dirtyStateContext,
type DirtyStateContext,
} from "../../../data/context/dirty-state";
import type { BlueprintScriptConfig } from "../../../data/script";
import { saveFabStyles } from "../automation/styles";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
@@ -15,7 +20,9 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
@property({ type: Boolean }) public saving = false;
@property({ type: Boolean }) public dirty = false;
@consume({ context: dirtyStateContext, subscribe: true })
@state()
private _dirtyState?: DirtyStateContext;
protected get _config(): BlueprintScriptConfig {
return this.config;
@@ -35,7 +42,7 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
<ha-button
slot="fab"
size="l"
class=${this.dirty ? "dirty" : ""}
class=${this._dirtyState?.isDirty ? "dirty" : ""}
.disabled=${this.saving}
@click=${this._saveScript}
>
+13 -14
View File
@@ -377,7 +377,6 @@ export class HaScriptEditor extends SubscribeMixin(
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-script=${this._handleSaveScript}
></blueprint-script-editor>
@@ -389,7 +388,6 @@ export class HaScriptEditor extends SubscribeMixin(
.isWide=${this.isWide}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@@ -470,7 +468,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-button
slot="fab"
size="l"
class=${!this.readOnly && this.dirty ? "dirty" : ""}
class=${!this.readOnly && this.isDirtyState ? "dirty" : ""}
.disabled=${this.saving}
@click=${this._handleSaveScript}
>
@@ -522,7 +520,6 @@ export class HaScriptEditor extends SubscribeMixin(
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this.dirty = !!initData;
const baseConfig: Partial<ScriptConfig> = {};
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [];
@@ -531,12 +528,15 @@ export class HaScriptEditor extends SubscribeMixin(
...baseConfig,
...initData,
} as ScriptConfig;
this._initDirtyTracking({ type: "deep" }, baseConfig as ScriptConfig);
this._updateDirtyState(this.config);
this.readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getScriptStateConfig(this.hass, this.entityId).then((c) => {
this.config = normalizeScriptConfig(c.config);
this._initDirtyTracking({ type: "deep" }, this.config);
this._checkValidation();
});
const regEntry = this.entityRegistry?.find(
@@ -546,7 +546,6 @@ export class HaScriptEditor extends SubscribeMixin(
this.scriptId = regEntry.unique_id;
}
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
}
@@ -582,7 +581,7 @@ export class HaScriptEditor extends SubscribeMixin(
this.config = ev.detail.value;
this.errors = undefined;
this.dirty = true;
this._updateDirtyState(this.config!);
}
private async _runScript() {
@@ -669,7 +668,7 @@ export class HaScriptEditor extends SubscribeMixin(
}
this._manualEditor?.addFields();
this.dirty = true;
this._updateDirtyState(this.config!);
}
private _preprocessYaml() {
@@ -678,18 +677,18 @@ export class HaScriptEditor extends SubscribeMixin(
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this.dirty = true;
if (!ev.detail.isValid) {
this.yamlErrors = ev.detail.errorMsg;
return;
}
this.yamlErrors = undefined;
this.config = ev.detail.value;
this._updateDirtyState(this.config!);
this.errors = undefined;
}
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
if (!this.isDirtyState) {
return true;
}
@@ -700,7 +699,7 @@ export class HaScriptEditor extends SubscribeMixin(
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._updateDirtyState(this.config);
this.requestUpdate();
const id = this.scriptId || String(Date.now());
@@ -815,7 +814,7 @@ export class HaScriptEditor extends SubscribeMixin(
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._updateDirtyState(this.config);
this.requestUpdate();
resolve(true);
},
@@ -834,7 +833,7 @@ export class HaScriptEditor extends SubscribeMixin(
config: this.config!,
updateConfig: (config) => {
this.config = config;
this.dirty = true;
this._updateDirtyState(config);
this.requestUpdate();
resolve();
},
@@ -930,7 +929,7 @@ export class HaScriptEditor extends SubscribeMixin(
}
}
this.dirty = false;
this._markDirtyStateClean();
} catch (errors: any) {
this.errors = errors.body?.message || errors.error || errors.body;
showEditorToast(this, {
@@ -980,7 +979,7 @@ export class HaScriptEditor extends SubscribeMixin(
private _applyUndoRedo(config: ScriptConfig) {
this._manualEditor?.triggerCloseSidebar();
this.config = config;
this.dirty = true;
this._updateDirtyState(this.config);
}
private _undo() {