Compare commits

...

1 Commits

Author SHA1 Message Date
Aidan Timson 32ed173deb Migrate 6th set to dirty state provider 2026-06-09 15:03:35 +01:00
6 changed files with 138 additions and 11 deletions
@@ -3,6 +3,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-alert";
@@ -54,9 +55,20 @@ const SENSOR_DOMAINS = ["sensor"];
const TEMPERATURE_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_TEMPERATURE];
const HUMIDITY_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_HUMIDITY];
interface AreaFormState {
name: string;
aliases: string[];
labels: string[];
picture: string | null;
icon: string | null;
floor: string | null;
temperatureEntity: string | null;
humidityEntity: string | null;
}
@customElement("dialog-area-registry-detail")
class DialogAreaDetail
extends LitElement
extends DirtyStateProviderMixin<AreaFormState>()(LitElement)
implements HassDialog<AreaRegistryDetailDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -116,9 +128,23 @@ class DialogAreaDetail
this._humidityEntity = null;
}
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._currentState());
await this.updateComplete;
}
private _currentState(): AreaFormState {
return {
name: this._name,
aliases: this._aliases,
labels: this._labels,
picture: this._picture,
icon: this._icon,
floor: this._floor,
temperatureEntity: this._temperatureEntity,
humidityEntity: this._humidityEntity,
};
}
public closeDialog(): boolean {
this._open = false;
return true;
@@ -326,6 +352,8 @@ class DialogAreaDetail
if (processed.floor) {
this._floor = processed.floor;
}
this._updateDirtyState(this._currentState());
}
protected render() {
@@ -343,7 +371,7 @@ class DialogAreaDetail
header-title=${entry
? this.hass.localize("ui.panel.config.areas.editor.update_area")
: this.hass.localize("ui.panel.config.areas.editor.create_area")}
prevent-scrim-close
.preventScrimClose=${this.isDirtyState}
@closed=${this._dialogClosed}
>
<ha-suggest-with-ai-button
@@ -418,36 +446,43 @@ class DialogAreaDetail
private _nameChanged(ev: InputEvent) {
this._error = undefined;
this._name = (ev.target as HaInput).value ?? "";
this._updateDirtyState(this._currentState());
}
private _floorChanged(ev) {
this._error = undefined;
this._floor = ev.detail.value;
this._updateDirtyState(this._currentState());
}
private _iconChanged(ev) {
this._error = undefined;
this._icon = ev.detail.value;
this._updateDirtyState(this._currentState());
}
private _labelsChanged(ev) {
this._error = undefined;
this._labels = ev.detail.value;
this._updateDirtyState(this._currentState());
}
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value;
this._updateDirtyState(this._currentState());
}
private _aliasesChanged(ev: CustomEvent): void {
this._aliases = ev.detail.value;
this._updateDirtyState(this._currentState());
}
private _sensorChanged(ev: CustomEvent): void {
const deviceClass = (ev.target as HaEntityPicker).includeDeviceClasses![0];
const key = `_${deviceClass}Entity`;
this[key] = ev.detail.value || null;
this._updateDirtyState(this._currentState());
}
private async _updateEntry() {
@@ -469,6 +504,7 @@ class DialogAreaDetail
} else {
await this._params!.updateEntry!(values);
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error =
@@ -15,6 +15,7 @@ import "../../../../components/ha-svg-icon";
import "../../../../components/item/ha-list-item-button";
import "../../../../components/item/ha-row-item";
import "../../../../components/list/ha-list-base";
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
import type {
BackupConfig,
BackupMutableConfig,
@@ -82,7 +83,10 @@ const RECOMMENDED_CONFIG: BackupConfig = {
};
@customElement("ha-dialog-backup-onboarding")
class DialogBackupOnboarding extends LitElement implements HassDialog {
class DialogBackupOnboarding
extends DirtyStateProviderMixin<BackupConfig>()(LitElement)
implements HassDialog
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@@ -115,6 +119,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
}
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._config!);
}
public closeDialog() {
@@ -169,6 +174,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
try {
await this._save(true);
this._params?.submit!(true);
this._markDirtyStateClean();
this.closeDialog();
} catch (err) {
// eslint-disable-next-line no-console
@@ -214,7 +220,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
<ha-dialog
.open=${this._open}
header-title=${this._stepTitle}
prevent-scrim-close
.preventScrimClose=${this.isDirtyState}
@closed=${this._dialogClosed}
>
${isFirstStep
@@ -293,6 +299,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
password: this._config.create_backup.password,
},
};
this._updateDirtyState(this._config);
this._done();
}
@@ -515,6 +522,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
include_addons: data.include_addons || null,
},
};
this._updateDirtyState(this._config);
}
private _scheduleChanged(ev) {
@@ -524,6 +532,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
schedule: value.schedule,
retention: value.retention,
};
this._updateDirtyState(this._config);
}
private _agentsConfigChanged(ev) {
@@ -535,6 +544,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
agent_ids: agents,
},
};
this._updateDirtyState(this._config);
}
static get styles(): CSSResultGroup {
@@ -11,6 +11,7 @@ import "../../../../components/radio/ha-radio-group";
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import "../../../../components/input/ha-input";
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
import type {
GridSourceTypeEnergyPreference,
PowerConfig,
@@ -43,9 +44,17 @@ const energyUnitClasses = ["energy"];
type CostType = "no_cost" | "stat" | "entity" | "number";
interface EnergyGridFormState {
source: GridSourceTypeEnergyPreference | undefined;
powerType: PowerType;
powerConfig: PowerConfig;
importCostType: CostType;
exportCostType: CostType;
}
@customElement("dialog-energy-grid-settings")
export class DialogEnergyGridSettings
extends LitElement
extends DirtyStateProviderMixin<EnergyGridFormState>()(LitElement)
implements HassDialog<EnergySettingsGridDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -144,6 +153,17 @@ export class DialogEnergyGridSettings
);
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._currentState());
}
private _currentState(): EnergyGridFormState {
return {
source: this._source,
powerType: this._powerType,
powerConfig: this._powerConfig,
importCostType: this._importCostType,
exportCostType: this._exportCostType,
};
}
public closeDialog() {
@@ -185,7 +205,7 @@ export class DialogEnergyGridSettings
header-title=${this.hass.localize(
"ui.panel.config.energy.grid.dialog.header"
)}
prevent-scrim-close
.preventScrimClose=${this.isDirtyState}
@closed=${this._dialogClosed}
>
${this._error ? html`<p class="error">${this._error}</p>` : nothing}
@@ -507,6 +527,7 @@ export class DialogEnergyGridSettings
};
}
this._updateMetadata(ev.detail.value);
this._updateDirtyState(this._currentState());
}
private _statisticToChanged(ev: ValueChangedEvent<string>) {
@@ -536,6 +557,7 @@ export class DialogEnergyGridSettings
};
}
this._updateMetadata(ev.detail.value);
this._updateDirtyState(this._currentState());
}
private _nameChanged(ev: InputEvent) {
@@ -546,6 +568,7 @@ export class DialogEnergyGridSettings
if (!this._source.name) {
delete this._source.name;
}
this._updateDirtyState(this._currentState());
}
private _handleImportCostTypeChanged(ev: Event) {
@@ -557,6 +580,7 @@ export class DialogEnergyGridSettings
entity_energy_price: null,
number_energy_price: null,
};
this._updateDirtyState(this._currentState());
}
private _handleExportCostTypeChanged(ev: Event) {
@@ -568,10 +592,12 @@ export class DialogEnergyGridSettings
entity_energy_price_export: null,
number_energy_price_export: null,
};
this._updateDirtyState(this._currentState());
}
private _statCostChanged(ev: ValueChangedEvent<string>) {
this._source = { ...this._source!, stat_cost: ev.detail.value || null };
this._updateDirtyState(this._currentState());
}
private _entityCostChanged(ev: ValueChangedEvent<string>) {
@@ -579,12 +605,14 @@ export class DialogEnergyGridSettings
...this._source!,
entity_energy_price: ev.detail.value || null,
};
this._updateDirtyState(this._currentState());
}
private _numberCostChanged(ev: Event) {
const input = ev.currentTarget as HTMLInputElement;
const value = input.value ? parseFloat(input.value) : null;
this._source = { ...this._source!, number_energy_price: value };
this._updateDirtyState(this._currentState());
}
private _statCompensationChanged(ev: ValueChangedEvent<string>) {
@@ -592,6 +620,7 @@ export class DialogEnergyGridSettings
...this._source!,
stat_compensation: ev.detail.value || null,
};
this._updateDirtyState(this._currentState());
}
private _entityCompensationChanged(ev: ValueChangedEvent<string>) {
@@ -599,12 +628,14 @@ export class DialogEnergyGridSettings
...this._source!,
entity_energy_price_export: ev.detail.value || null,
};
this._updateDirtyState(this._currentState());
}
private _numberCompensationChanged(ev: Event) {
const input = ev.currentTarget as HTMLInputElement;
const value = input.value ? parseFloat(input.value) : null;
this._source = { ...this._source!, number_energy_price_export: value };
this._updateDirtyState(this._currentState());
}
private _handlePowerConfigChanged(
@@ -612,6 +643,7 @@ export class DialogEnergyGridSettings
) {
this._powerType = ev.detail.powerType;
this._powerConfig = ev.detail.powerConfig;
this._updateDirtyState(this._currentState());
}
private async _save() {
@@ -638,6 +670,7 @@ export class DialogEnergyGridSettings
}
await this._params!.saveCallback(source);
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error = err.message;
@@ -18,6 +18,7 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean";
@@ -101,7 +102,9 @@ const HELPERS: HelperCreators = {
};
@customElement("dialog-helper-detail")
export class DialogHelperDetail extends LitElement {
export class DialogHelperDetail extends DirtyStateProviderMixin<
Helper | undefined
>()(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _item?: Helper;
@@ -137,6 +140,7 @@ export class DialogHelperDetail extends LitElement {
this._item = undefined;
if (this._domain && this._domain in HELPERS) {
await HELPERS[this._domain].import();
this._initDirtyTracking({ type: "deep" }, undefined);
}
this._open = true;
await this.updateComplete;
@@ -293,6 +297,7 @@ export class DialogHelperDetail extends LitElement {
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
header-title=${this._domain
? this.hass.localize(
"ui.panel.config.helpers.dialog.create_platform",
@@ -364,6 +369,7 @@ export class DialogHelperDetail extends LitElement {
private _valueChanged(ev: CustomEvent): void {
this._item = ev.detail.value;
this._updateDirtyState(this._item);
}
private async _createItem(): Promise<void> {
@@ -383,6 +389,7 @@ export class DialogHelperDetail extends LitElement {
entityId: `${this._domain}.${createdEntity.id}`,
});
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error = err.message || "Unknown error";
@@ -410,6 +417,7 @@ export class DialogHelperDetail extends LitElement {
try {
await HELPERS[domain].import();
this._domain = domain;
this._initDirtyTracking({ type: "deep" }, undefined);
} finally {
this._loading = false;
}
@@ -13,6 +13,7 @@ import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/input/ha-input";
import "../../../components/item/ha-row-item";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import { adminChangeUsername } from "../../../data/auth";
import type { PersonMutableParams } from "../../../data/person";
import type { User } from "../../../data/user";
@@ -44,8 +45,20 @@ const cropOptions: CropOptions = {
aspectRatio: 1,
};
interface PersonFormState {
name: string;
picture: string | null;
userId: string | undefined;
deviceTrackers: string[];
isAdmin: boolean | undefined;
localOnly: boolean | undefined;
}
@customElement("dialog-person-detail")
class DialogPersonDetail extends LitElement implements HassDialog {
class DialogPersonDetail
extends DirtyStateProviderMixin<PersonFormState>()(LitElement)
implements HassDialog
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _name!: string;
@@ -104,9 +117,21 @@ class DialogPersonDetail extends LitElement implements HassDialog {
this._picture = null;
}
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._currentState());
await this.updateComplete;
}
private _currentState(): PersonFormState {
return {
name: this._name,
picture: this._picture,
userId: this._userId,
deviceTrackers: this._deviceTrackers,
isAdmin: this._isAdmin,
localOnly: this._localOnly,
};
}
public closeDialog() {
this._open = false;
return true;
@@ -134,7 +159,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
return html`
<ha-dialog
.open=${this._open}
prevent-scrim-close
.preventScrimClose=${this.isDirtyState}
header-title=${this._params.entry
? this._params.entry.name
: this.hass!.localize("ui.panel.config.person.detail.new_person")}
@@ -367,14 +392,17 @@ class DialogPersonDetail extends LitElement implements HassDialog {
private _nameChanged(ev: InputEvent) {
this._error = undefined;
this._name = (ev.target as HTMLInputElement).value;
this._updateDirtyState(this._currentState());
}
private _adminChanged(ev): void {
this._isAdmin = ev.target.checked;
this._updateDirtyState(this._currentState());
}
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
this._updateDirtyState(this._currentState());
}
private async _allowLoginChanged(ev): Promise<void> {
@@ -393,6 +421,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only;
this._updateDirtyState(this._currentState());
}
},
name: this._name,
@@ -421,17 +450,20 @@ class DialogPersonDetail extends LitElement implements HassDialog {
this._user = undefined;
this._isAdmin = undefined;
this._localOnly = undefined;
this._updateDirtyState(this._currentState());
}
}
private _deviceTrackersChanged(ev: ValueChangedEvent<string[]>) {
this._error = undefined;
this._deviceTrackers = ev.detail.value;
this._updateDirtyState(this._currentState());
}
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value;
this._updateDirtyState(this._currentState());
}
private async _changePassword() {
@@ -527,6 +559,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
await this._params!.createEntry?.(values);
this._personExists = true;
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error = err ? err.message : "Unknown error";
@@ -11,6 +11,7 @@ import "../../../components/ha-form/ha-form";
import "../../../components/ha-icon-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import type {
AssistPipeline,
AssistPipelineMutableParams,
@@ -28,7 +29,9 @@ import type { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-vo
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("dialog-voice-assistant-pipeline-detail")
export class DialogVoiceAssistantPipelineDetail extends LitElement {
export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
Partial<AssistPipeline>
>()(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: VoiceAssistantPipelineDetailsDialogParams;
@@ -62,6 +65,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
this._hideWakeWord =
this._params.hideWakeWord || !this._data.wake_word_entity;
this._initDirtyTracking({ type: "deep" }, this._data);
return;
}
@@ -98,6 +102,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
stt_engine: this._params.pipeline?.stt_engine || sstDefault,
tts_engine: this._params.pipeline?.tts_engine || ttsDefault,
};
this._initDirtyTracking({ type: "deep" }, this._data);
}
public closeDialog(): void {
@@ -145,7 +150,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
<ha-dialog
.open=${this._open}
header-title=${title}
prevent-scrim-close
.preventScrimClose=${this.isDirtyState}
@closed=${this._dialogClosed}
>
${!this._hideWakeWord ||
@@ -266,6 +271,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
value[key] = ev.detail.value[key];
});
this._data = { ...this._data, ...value };
this._updateDirtyState(this._data);
}
private async _updatePipeline() {
@@ -299,6 +305,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
// eslint-disable-next-line no-console
console.error("No createPipeline function provided");
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error = err?.message || "Unknown error";