mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-03 15:02:01 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ce62ca5ec | |||
| 8a8b4bf138 | |||
| 6f6c860338 | |||
| a79051caf1 | |||
| 2e459e8029 | |||
| 373855ddb2 | |||
| af8f250b60 |
@@ -0,0 +1,21 @@
|
||||
import { createContext } from "@lit/context";
|
||||
|
||||
export interface DirtyStateContext<State = unknown> {
|
||||
/** Whether current state differs from the initial snapshot */
|
||||
isDirty: boolean;
|
||||
/** Current tracked state */
|
||||
state: State;
|
||||
/** Update the tracked state — triggers dirty comparison */
|
||||
setState: (state: State) => void;
|
||||
/** Reset initial snapshot to current state (marks clean) */
|
||||
markClean: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton context key for dirty-state tracking.
|
||||
*
|
||||
* Because Lit context keys are singletons, the value type is
|
||||
* `DirtyStateContext<unknown>`. The provider mixin and consumer controller
|
||||
* supply type-safe APIs on top of this boundary.
|
||||
*/
|
||||
export const dirtyStateContext = createContext<DirtyStateContext>("dirtyState");
|
||||
@@ -63,6 +63,7 @@ import { subscribeLabFeature } from "../../data/labs";
|
||||
import type { ItemType } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import {
|
||||
@@ -121,8 +122,8 @@ declare global {
|
||||
const DEFAULT_VIEW: MoreInfoView = "info";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends SubscribeMixin(
|
||||
ScrollableFadeMixin(LitElement)
|
||||
export class MoreInfoDialog extends DirtyStateProviderMixin()(
|
||||
SubscribeMixin(ScrollableFadeMixin(LitElement))
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -640,7 +641,8 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
@closed=${this._dialogClosed}
|
||||
@opened=${this._handleOpened}
|
||||
@show-child-view=${this._showChildView}
|
||||
.preventScrimClose=${this._currView === "settings" ||
|
||||
.preventScrimClose=${(this._currView === "settings" &&
|
||||
this.isDirtyState) ||
|
||||
!this._isEscapeEnabled}
|
||||
flexcontent
|
||||
>
|
||||
@@ -957,6 +959,12 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_currView") || changedProps.has("_entry")) {
|
||||
if (this._currView === "settings" && this._entry) {
|
||||
this._initDirtyTracking({ type: "deep" });
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_currView")) {
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { provide } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import { shallowEqual } from "../common/util/shallow-equal";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../data/context/dirty-state";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
export type CompareStrategy<State> =
|
||||
| { type: "deep" }
|
||||
| { type: "shallow" }
|
||||
| { type: "custom"; compare: (a: State, b: State) => boolean };
|
||||
|
||||
function resolveCompare<State>(
|
||||
strategy: CompareStrategy<State>
|
||||
): (a: State, b: State) => boolean {
|
||||
switch (strategy.type) {
|
||||
case "deep":
|
||||
return (a, b) => deepEqual(a, b);
|
||||
case "shallow":
|
||||
return (a, b) => shallowEqual(a, b);
|
||||
default:
|
||||
return strategy.compare;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin that provides dirty-state tracking via Lit context.
|
||||
*
|
||||
* Uses the `@provide` decorator so any descendant component can consume
|
||||
* dirty-state with `@consume({ context: dirtyStateContext, subscribe: true })`.
|
||||
*
|
||||
* Curried generic pattern: `State` is explicitly provided while `Base` is
|
||||
* inferred from the superclass argument.
|
||||
*
|
||||
* @example Eager init (state known upfront, e.g. dialog open):
|
||||
* ```ts
|
||||
* interface MyDialogState { name: string; icon: string }
|
||||
*
|
||||
* class MyDialog extends DirtyStateProviderMixin<MyDialogState>()(LitElement) {
|
||||
* open() {
|
||||
* this._initDirtyTracking({ type: "shallow" }, { name: "", icon: "" });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Deferred init (child consumer reports initial state):
|
||||
* ```ts
|
||||
* class MyPage extends DirtyStateProviderMixin<FormState>()(LitElement) {
|
||||
* connectedCallback() {
|
||||
* super.connectedCallback();
|
||||
* this._initDirtyTracking({ type: "deep" });
|
||||
* // First setState from a child consumer sets the baseline
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Child consumers:
|
||||
* ```ts
|
||||
* @consume({ context: dirtyStateContext, subscribe: true })
|
||||
* @state()
|
||||
* private _dirtyState?: DirtyStateContext;
|
||||
*
|
||||
* // Read: this._dirtyState?.isDirty
|
||||
* // Write: this._dirtyState?.setState(newState)
|
||||
* ```
|
||||
*/
|
||||
export const DirtyStateProviderMixin =
|
||||
<State = unknown>() =>
|
||||
<Base extends Constructor<LitElement>>(superClass: Base) => {
|
||||
class DirtyStateProviderMixinClass extends superClass {
|
||||
private _dirtyInitialState: State | undefined;
|
||||
|
||||
private _dirtyCurrentState: State | undefined;
|
||||
|
||||
private _dirtyCompareFn: (a: State, b: State) => boolean = deepEqual;
|
||||
|
||||
@provide({ context: dirtyStateContext })
|
||||
@state()
|
||||
private _dirtyStateContext: DirtyStateContext = this._buildContextValue(
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
|
||||
/**
|
||||
* Build the context value object for the provider.
|
||||
*
|
||||
* The returned type is `DirtyStateContext` (i.e. `DirtyStateContext<unknown>`)
|
||||
* because the singleton context key is typed at `unknown`. The single
|
||||
* `unknown → State` narrowing cast in `setState` is the only unsafe boundary
|
||||
* and is confined here.
|
||||
*/
|
||||
private _buildContextValue(
|
||||
currentState: State | undefined,
|
||||
isDirty: boolean
|
||||
): DirtyStateContext {
|
||||
return {
|
||||
isDirty,
|
||||
state: currentState,
|
||||
setState: (incoming: unknown) => {
|
||||
this._updateDirtyState(incoming as State);
|
||||
},
|
||||
markClean: () => {
|
||||
this._markDirtyStateClean();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize dirty state tracking.
|
||||
*
|
||||
* When `initialState` is provided, tracking starts immediately.
|
||||
* When omitted (deferred mode), the first `_updateDirtyState` /
|
||||
* `setState` call from a consumer becomes the baseline snapshot.
|
||||
*
|
||||
* Call again to reset (e.g. when the underlying entity changes).
|
||||
*/
|
||||
protected _initDirtyTracking(
|
||||
strategy: CompareStrategy<State>,
|
||||
initialState?: State
|
||||
): void {
|
||||
this._dirtyCompareFn = resolveCompare(strategy);
|
||||
if (initialState !== undefined) {
|
||||
this._dirtyInitialState = initialState;
|
||||
this._dirtyCurrentState = initialState;
|
||||
this._dirtyStateContext = this._buildContextValue(
|
||||
initialState,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
this._dirtyInitialState = undefined;
|
||||
this._dirtyCurrentState = undefined;
|
||||
this._dirtyStateContext = this._buildContextValue(undefined, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tracked state. Triggers dirty comparison against initial snapshot.
|
||||
*
|
||||
* If called before `_initDirtyTracking` provided an initial state (deferred
|
||||
* mode), the first call sets the baseline and reports clean.
|
||||
*
|
||||
* Guarded: no-ops if the computed dirty status and state reference are
|
||||
* unchanged, preventing render loops when called from `updated()`.
|
||||
*/
|
||||
protected _updateDirtyState(newState: State): void {
|
||||
// Deferred init: first state becomes the baseline
|
||||
if (this._dirtyInitialState === undefined) {
|
||||
this._dirtyInitialState = newState;
|
||||
this._dirtyCurrentState = newState;
|
||||
this._dirtyStateContext = this._buildContextValue(newState, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDirty = !this._dirtyCompareFn(
|
||||
this._dirtyInitialState,
|
||||
newState
|
||||
);
|
||||
if (
|
||||
this._dirtyCurrentState !== undefined &&
|
||||
this._dirtyCompareFn(this._dirtyCurrentState, newState) &&
|
||||
this._dirtyStateContext.isDirty === isDirty
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirtyCurrentState = newState;
|
||||
this._dirtyStateContext = this._buildContextValue(newState, isDirty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the initial snapshot to the current state, marking the state as clean.
|
||||
* Call this after a successful save.
|
||||
*/
|
||||
protected _markDirtyStateClean(): void {
|
||||
this._dirtyInitialState = this._dirtyCurrentState;
|
||||
this._dirtyStateContext = this._buildContextValue(
|
||||
this._dirtyCurrentState,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current state differs from the initial snapshot.
|
||||
*/
|
||||
public get isDirtyState(): boolean {
|
||||
return this._dirtyStateContext.isDirty;
|
||||
}
|
||||
}
|
||||
return DirtyStateProviderMixinClass;
|
||||
};
|
||||
@@ -15,13 +15,19 @@ import type {
|
||||
} from "../../../data/category_registry";
|
||||
import { internationalizationContext } from "../../../data/context";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { ValueChangedEvent } from "../../../types";
|
||||
import type { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail";
|
||||
|
||||
interface CategoryFormState {
|
||||
name: string;
|
||||
icon: string | null;
|
||||
}
|
||||
|
||||
@customElement("dialog-category-registry-detail")
|
||||
class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParams>(
|
||||
LitElement
|
||||
class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
|
||||
DialogMixin<CategoryRegistryDetailDialogParams>(LitElement)
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@@ -44,6 +50,10 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
this._name = this.params?.suggestedName || "";
|
||||
this._icon = null;
|
||||
}
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{ name: this._name, icon: this._icon }
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -52,13 +62,14 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
}
|
||||
const entry = this.params.entry;
|
||||
const nameInvalid = !this._isNameValid();
|
||||
const isCreate = !entry;
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
header-title=${entry
|
||||
? this._i18n.localize("ui.panel.config.category.editor.edit")
|
||||
: this._i18n.localize("ui.panel.config.category.editor.create")}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -96,7 +107,9 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
.disabled=${nameInvalid ||
|
||||
!!this._submitting ||
|
||||
(!isCreate && !this.isDirtyState)}
|
||||
>
|
||||
${entry
|
||||
? this._i18n.localize("ui.common.save")
|
||||
@@ -114,15 +127,17 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._updateDirtyState({ name: this._name, icon: this._icon });
|
||||
}
|
||||
|
||||
private _iconChanged(ev: ValueChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._icon = ev.detail.value;
|
||||
this._updateDirtyState({ name: this._name, icon: this._icon });
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
const create = !this.params!.entry;
|
||||
const create = !this.params?.entry;
|
||||
this._submitting = true;
|
||||
let newValue: CategoryRegistryEntry | undefined;
|
||||
try {
|
||||
@@ -131,10 +146,11 @@ class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParam
|
||||
icon: this._icon || (create ? undefined : null),
|
||||
};
|
||||
if (create) {
|
||||
newValue = await this.params!.createEntry!(values);
|
||||
newValue = await this.params?.createEntry?.(values);
|
||||
} else {
|
||||
newValue = await this.params!.updateEntry!(values);
|
||||
newValue = await this.params?.updateEntry?.(values);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeEntityEntryName } from "../../../../../common/entity/compute_entity_name";
|
||||
import "../../../../../components/ha-button";
|
||||
import { dirtyStateContext } from "../../../../../data/context/dirty-state";
|
||||
import type { ExtEntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import { removeEntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
|
||||
@@ -33,14 +35,16 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
|
||||
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: ContextType<typeof dirtyStateContext>;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _item?: Helper | null;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _componentLoaded?: boolean;
|
||||
|
||||
@query("entity-registry-settings-editor")
|
||||
@@ -60,13 +64,9 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
this._error = undefined;
|
||||
if (
|
||||
this.entry.unique_id !==
|
||||
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
|
||||
) {
|
||||
if (this.entry.unique_id !== changedProperties.get("entry")?.unique_id) {
|
||||
this._item = undefined;
|
||||
}
|
||||
|
||||
this._getItem();
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,6 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.entry=${this.entry}
|
||||
.disabled=${!!this._submitting}
|
||||
@change=${this._entityRegistryChanged}
|
||||
hide-name
|
||||
hide-icon
|
||||
></entity-registry-settings-editor>
|
||||
@@ -124,7 +123,7 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateItem}
|
||||
.disabled=${!this._dirty ||
|
||||
.disabled=${!(this._dirtyState?.isDirty || this._isHelperDirty) ||
|
||||
!!this._submitting ||
|
||||
!!(this._item && !this._item.name)}
|
||||
>
|
||||
@@ -139,22 +138,12 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
return JSON.stringify(this._item) !== this._originalItemJson;
|
||||
}
|
||||
|
||||
private _updateDirty() {
|
||||
this._dirty = (this._registryEditor?.dirty ?? false) || this._isHelperDirty;
|
||||
}
|
||||
|
||||
private _entityRegistryChanged() {
|
||||
this._error = undefined;
|
||||
this._updateDirty();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
if (this._item === null) {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._item = ev.detail.value;
|
||||
this._updateDirty();
|
||||
}
|
||||
|
||||
private async _getItem() {
|
||||
@@ -167,15 +156,20 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
|
||||
private async _updateItem(): Promise<void> {
|
||||
this._submitting = true;
|
||||
this._error = undefined;
|
||||
try {
|
||||
if (this._componentLoaded && this._item) {
|
||||
await HELPERS_CRUD[this.entry.platform].update(
|
||||
this.hass!,
|
||||
this.hass,
|
||||
this._item.id,
|
||||
this._item
|
||||
);
|
||||
}
|
||||
const result = await this._registryEditor!.updateEntry();
|
||||
this._dirtyState?.markClean();
|
||||
this._originalItemJson = this._item
|
||||
? JSON.stringify(this._item)
|
||||
: undefined;
|
||||
if (result.close) {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
STREAM_TYPE_HLS,
|
||||
updateCameraPrefs,
|
||||
} from "../../../data/camera";
|
||||
import { dirtyStateContext } from "../../../data/context/dirty-state";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { deleteConfigEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
@@ -144,6 +145,28 @@ const SCANNER_SOURCE_TYPES = ["router", "bluetooth", "bluetooth_le"];
|
||||
|
||||
const ZONE_DOMAINS = ["zone"];
|
||||
|
||||
interface EntitySettingsState {
|
||||
name: string | null;
|
||||
icon: string | null;
|
||||
entityId: string;
|
||||
areaId: string | null;
|
||||
labels: string[];
|
||||
deviceClass: string | undefined;
|
||||
disabledBy: EntityRegistryEntry["disabled_by"];
|
||||
hiddenBy: EntityRegistryEntry["hidden_by"];
|
||||
unitOfMeasurement: string | null | undefined;
|
||||
precision: number | null | undefined;
|
||||
defaultCode: string | null | undefined;
|
||||
calendarColor: string | null;
|
||||
precipitationUnit: string | null | undefined;
|
||||
pressureUnit: string | null | undefined;
|
||||
temperatureUnit: string | null | undefined;
|
||||
visibilityUnit: string | null | undefined;
|
||||
windSpeedUnit: string | null | undefined;
|
||||
switchAsDomain: string;
|
||||
switchAsInvert: boolean;
|
||||
}
|
||||
|
||||
@customElement("entity-registry-settings-editor")
|
||||
export class EntityRegistrySettingsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -158,41 +181,50 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: ContextType<typeof dirtyStateContext>;
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@state() private _icon!: string;
|
||||
|
||||
@state() private _entityId!: string;
|
||||
@state() private _entityId!: EntitySettingsState["entityId"];
|
||||
|
||||
@state() private _deviceClass?: string;
|
||||
@state() private _deviceClass?: EntitySettingsState["deviceClass"];
|
||||
|
||||
@state() private _switchAsDomain = "switch";
|
||||
@state() private _switchAsDomain: EntitySettingsState["switchAsDomain"] =
|
||||
"switch";
|
||||
|
||||
@state() private _switchAsInvert = false;
|
||||
@state() private _switchAsInvert: EntitySettingsState["switchAsInvert"] =
|
||||
false;
|
||||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _labels?: string[] | null;
|
||||
|
||||
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
|
||||
@state() private _disabledBy!: EntitySettingsState["disabledBy"];
|
||||
|
||||
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
|
||||
@state() private _hiddenBy!: EntitySettingsState["hiddenBy"];
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _unit_of_measurement?: string | null;
|
||||
@state()
|
||||
private _unit_of_measurement?: EntitySettingsState["unitOfMeasurement"];
|
||||
|
||||
@state() private _precision?: number | null;
|
||||
@state() private _precision?: EntitySettingsState["precision"];
|
||||
|
||||
@state() private _precipitation_unit?: string | null;
|
||||
@state()
|
||||
private _precipitation_unit?: EntitySettingsState["precipitationUnit"];
|
||||
|
||||
@state() private _pressure_unit?: string | null;
|
||||
@state() private _pressure_unit?: EntitySettingsState["pressureUnit"];
|
||||
|
||||
@state() private _temperature_unit?: string | null;
|
||||
@state()
|
||||
private _temperature_unit?: EntitySettingsState["temperatureUnit"];
|
||||
|
||||
@state() private _visibility_unit?: string | null;
|
||||
@state() private _visibility_unit?: EntitySettingsState["visibilityUnit"];
|
||||
|
||||
@state() private _wind_speed_unit?: string | null;
|
||||
@state() private _wind_speed_unit?: EntitySettingsState["windSpeedUnit"];
|
||||
|
||||
@state() private _cameraPrefs?: CameraPreferences;
|
||||
|
||||
@@ -204,9 +236,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@state() private _weatherConvertibleUnits?: WeatherUnits;
|
||||
|
||||
@state() private _defaultCode?: string | null;
|
||||
@state() private _defaultCode?: EntitySettingsState["defaultCode"];
|
||||
|
||||
@state() private _calendarColor?: string | null;
|
||||
@state() private _calendarColor?: EntitySettingsState["calendarColor"];
|
||||
|
||||
@state() private _associatedZone?: string;
|
||||
|
||||
@@ -216,11 +248,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
private _deviceClassOptions?: string[][];
|
||||
|
||||
private _initialStateJson!: string;
|
||||
|
||||
private _lastDirty = false;
|
||||
|
||||
private _currentState() {
|
||||
private _currentState(): EntitySettingsState {
|
||||
return {
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
@@ -315,9 +343,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit;
|
||||
}
|
||||
|
||||
this._initialStateJson = JSON.stringify(this._currentState());
|
||||
this._lastDirty = false;
|
||||
|
||||
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
|
||||
if (!deviceClasses || this._hideDeviceClassOverride(domain)) {
|
||||
@@ -416,17 +441,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._switchAsDomain = "switch";
|
||||
this._switchAsInvert = false;
|
||||
}
|
||||
this._initialStateJson = JSON.stringify(this._currentState());
|
||||
this._lastDirty = false;
|
||||
}
|
||||
|
||||
if (this._initialStateJson) {
|
||||
const dirty = this.dirty;
|
||||
if (dirty !== this._lastDirty) {
|
||||
this._lastDirty = dirty;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
}
|
||||
this._dirtyState?.setState(this._currentState());
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -1146,10 +1163,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public get dirty(): boolean {
|
||||
return JSON.stringify(this._currentState()) !== this._initialStateJson;
|
||||
}
|
||||
|
||||
public async updateEntry(): Promise<{
|
||||
close: boolean;
|
||||
entry: ExtEntityRegistryEntry;
|
||||
@@ -1435,12 +1448,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._name = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _iconChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._icon = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1459,22 +1470,18 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _entityIdChanged(ev: InputEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._entityId = `${computeDomain(this._origEntityId)}.${(ev.target as HTMLInputElement).value}`;
|
||||
}
|
||||
|
||||
private _deviceClassChanged(ev: HaSelectSelectEvent<string, true>): void {
|
||||
fireEvent(this, "change");
|
||||
this._deviceClass = ev.detail.value;
|
||||
}
|
||||
|
||||
private _unitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._unit_of_measurement = ev.detail.value;
|
||||
}
|
||||
|
||||
private _defaultcodeChanged(ev: InputEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._defaultCode =
|
||||
(ev.target as HTMLInputElement).value === ""
|
||||
? null
|
||||
@@ -1482,43 +1489,35 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _calendarColorChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._calendarColor = ev.detail.value || null;
|
||||
}
|
||||
|
||||
private _associatedZoneChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._associatedZone = ev.detail.value || "zone.home";
|
||||
}
|
||||
|
||||
private _precipitationUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._precipitation_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _precisionChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._precision =
|
||||
ev.detail.value === "default" ? null : Number(ev.detail.value);
|
||||
}
|
||||
|
||||
private _pressureUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._pressure_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _temperatureUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._temperature_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _visibilityUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._visibility_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
private _windSpeedUnitChanged(ev: HaSelectSelectEvent): void {
|
||||
fireEvent(this, "change");
|
||||
this._wind_speed_unit = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1553,7 +1552,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _areaPicked(ev: CustomEvent) {
|
||||
fireEvent(this, "change");
|
||||
this._areaId = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1626,8 +1624,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
private _resetNameAndOpenDeviceSettings() {
|
||||
this._name = this.entry.name || "";
|
||||
fireEvent(this, "change");
|
||||
|
||||
this._openDeviceSettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceName } from "../../../common/entity/compute_device_name";
|
||||
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
deleteConfigEntry,
|
||||
getConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import { dirtyStateContext } from "../../../data/context/dirty-state";
|
||||
import { updateDeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
@@ -38,14 +40,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Object }) public entry!: ExtEntityRegistryEntry;
|
||||
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: ContextType<typeof dirtyStateContext>;
|
||||
|
||||
@state() private _helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@query("entity-registry-settings-editor")
|
||||
private _registryEditor?: EntityRegistrySettingsEditor;
|
||||
|
||||
@@ -133,7 +137,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.entry=${this.entry}
|
||||
.helperConfigEntry=${this._helperConfigEntry}
|
||||
.disabled=${!!this._submitting}
|
||||
@change=${this._entityRegistryChanged}
|
||||
></entity-registry-settings-editor>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
@@ -148,7 +151,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._dirty || !!this._submitting}
|
||||
.disabled=${!this._dirtyState?.isDirty || !!this._submitting}
|
||||
.loading=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
|
||||
@@ -157,11 +160,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityRegistryChanged() {
|
||||
this._error = undefined;
|
||||
this._dirty = this._registryEditor?.dirty ?? false;
|
||||
}
|
||||
|
||||
private _openDeviceSettings() {
|
||||
const device = this.hass.devices[this.entry.device_id!];
|
||||
|
||||
@@ -207,8 +205,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
private async _updateEntry(): Promise<void> {
|
||||
this._submitting = true;
|
||||
this._error = undefined;
|
||||
try {
|
||||
const result = await this._registryEditor!.updateEntry();
|
||||
this._dirtyState?.markClean();
|
||||
if (result.close) {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user