mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-02 22:41:47 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14617aaf3c | |||
| 42e1051d9c | |||
| cfe30114f0 | |||
| 288c03c248 | |||
| 8cd9a5adf6 | |||
| 4d3437b491 | |||
| ceb51714be |
+1
-1
@@ -180,7 +180,7 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.0.5",
|
||||
"lint-staged": "17.0.6",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -13,7 +12,6 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@@ -21,8 +19,6 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -31,39 +27,38 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
inset-inline-end: -6px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: 3px solid;
|
||||
border: var(--ha-border-width-md) solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 0 0 2px var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
background-color: var(--ha-color-green-60);
|
||||
border-color: var(--ha-color-green-60);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
border-color: var(--ha-color-orange-60);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
border-color: var(--ha-color-red-60);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
border-color: var(--ha-color-neutral-60);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export class HaAutomationRow extends LitElement {
|
||||
::slotted([slot="leading-icon"]) {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) ::slotted([slot="leading-icon"]) {
|
||||
:host([building-block]) ::slotted(#condition-icon) {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
color: var(--white-color);
|
||||
transform: rotate(-45deg);
|
||||
|
||||
@@ -101,18 +101,22 @@ export class HaSankeyChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
// Keep numbers and units left-to-right, even in RTL locales.
|
||||
const formattedValue = html`<div style="direction:ltr; display: inline;">
|
||||
${value}
|
||||
</div>`;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${value}`;
|
||||
${node?.label ?? data.id}<br />${formattedValue}`;
|
||||
}
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return html`${source?.label ?? data.source} →
|
||||
${target?.label ?? data.target}<br />${value}`;
|
||||
${target?.label ?? data.target}<br />${formattedValue}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -485,6 +485,17 @@ export const migrateAutomationTrigger = (
|
||||
}
|
||||
delete trigger.platform;
|
||||
}
|
||||
|
||||
if ("options" in trigger) {
|
||||
if (trigger.options && "behavior" in trigger.options) {
|
||||
if (trigger.options.behavior === "any") {
|
||||
trigger.options.behavior = "each";
|
||||
} else if (trigger.options.behavior === "last") {
|
||||
trigger.options.behavior = "all";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trigger;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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,7 +63,6 @@ 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 {
|
||||
@@ -122,8 +121,8 @@ declare global {
|
||||
const DEFAULT_VIEW: MoreInfoView = "info";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends DirtyStateProviderMixin()(
|
||||
SubscribeMixin(ScrollableFadeMixin(LitElement))
|
||||
export class MoreInfoDialog extends SubscribeMixin(
|
||||
ScrollableFadeMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -641,8 +640,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin()(
|
||||
@closed=${this._dialogClosed}
|
||||
@opened=${this._handleOpened}
|
||||
@show-child-view=${this._showChildView}
|
||||
.preventScrimClose=${(this._currView === "settings" &&
|
||||
this.isDirtyState) ||
|
||||
.preventScrimClose=${this._currView === "settings" ||
|
||||
!this._isEscapeEnabled}
|
||||
flexcontent
|
||||
>
|
||||
@@ -959,12 +957,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin()(
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -139,6 +139,8 @@ export class HassTabsSubpage extends LitElement {
|
||||
);
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues<this>) {
|
||||
this.toggleAttribute("narrow", this._narrow);
|
||||
|
||||
if (changedProperties.has("route")) {
|
||||
const currentPath = `${this.route.prefix}${this.route.path}`;
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/tag/tag";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -25,7 +25,9 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
@property() public stage: AddonStage = "stable";
|
||||
|
||||
@property() public state: AddonState = null;
|
||||
@property() public state?: AddonState;
|
||||
|
||||
@property({ type: Boolean }) public installed = false;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@@ -77,13 +79,23 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.tags?.length || this.state
|
||||
${this.tags?.length || this.state !== undefined || this.installed
|
||||
? html`
|
||||
<div class="footer">
|
||||
<supervisor-apps-state
|
||||
.state=${this.state || "unknown"}
|
||||
></supervisor-apps-state>
|
||||
|
||||
${this.state !== undefined
|
||||
? html`<supervisor-apps-state
|
||||
.state=${this.state || "unknown"}
|
||||
></supervisor-apps-state>`
|
||||
: this.installed
|
||||
? html`<div class="installed">
|
||||
<ha-svg-icon .path=${mdiCheckCircle}></ha-svg-icon>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.apps.state.installed"
|
||||
)}</span
|
||||
>
|
||||
</div>`
|
||||
: html`<span></span>`}
|
||||
${this.tags?.length
|
||||
? html`<div class="tags">
|
||||
${this.tags.map(
|
||||
@@ -159,6 +171,17 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.installed {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.installed ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
|
||||
import {
|
||||
mdiAlertDecagramOutline,
|
||||
mdiArrowUpBoldCircle,
|
||||
mdiArrowUpBoldCircleOutline,
|
||||
mdiFlask,
|
||||
mdiPuzzle,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
@@ -10,6 +17,7 @@ import type { HassioAddonRepository } from "../../../data/hassio/addon";
|
||||
import type { StoreAddon } from "../../../data/supervisor/store";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./components/supervisor-apps-card-content";
|
||||
import type { AppTag } from "./components/supervisor-apps-card-content";
|
||||
import { filterAndSort } from "./components/supervisor-apps-filter";
|
||||
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
|
||||
|
||||
@@ -54,21 +62,29 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
<div class="content">
|
||||
<h1>${repo.name}</h1>
|
||||
<div class="card-group">
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
${addons.map((addon) => {
|
||||
const tags = this._getAppTags(addon);
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
"has-footer": tags.length > 0 || addon.installed,
|
||||
})}
|
||||
>
|
||||
<supervisor-apps-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.stage=${addon.stage}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.installed=${addon.installed}
|
||||
.tags=${tags}
|
||||
.icon=${addon.installed && addon.update_available
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
@@ -108,8 +124,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
></supervisor-apps-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -119,6 +135,32 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
|
||||
}
|
||||
|
||||
private _getAppTags(addon: StoreAddon): AppTag[] {
|
||||
const labels: AppTag[] = [];
|
||||
|
||||
if (addon.installed && addon.update_available) {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.state.update_available`
|
||||
),
|
||||
variant: "brand",
|
||||
iconPath: mdiArrowUpBoldCircleOutline,
|
||||
});
|
||||
}
|
||||
if (addon.stage !== "stable") {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
|
||||
),
|
||||
variant: addon.stage === "experimental" ? "warning" : "danger",
|
||||
iconPath:
|
||||
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
supervisorAppsStyle,
|
||||
@@ -127,6 +169,9 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-content.has-footer {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
.not_available {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
@@ -211,11 +212,27 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
<div id="condition-icon" class="icon-badge-wrapper" slot="leading-icon">
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
${this.optionsInSidebar && this.condition.condition !== "trigger"
|
||||
? html`<ha-automation-row-live-test
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
></ha-automation-row-live-test>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.optionsInSidebar &&
|
||||
this.condition.condition !== "trigger" &&
|
||||
this._liveTestResult.message
|
||||
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
@@ -531,17 +548,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
>${this._renderRow()}
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this.condition.condition !== "trigger"
|
||||
? this._liveTestResult.state
|
||||
: "unknown"}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult.state : "unknown"}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test
|
||||
></ha-automation-row>`
|
||||
</ha-automation-row>`
|
||||
: html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
|
||||
@@ -53,6 +53,11 @@ export const rowStyles = css`
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon-badge-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.note-indicator {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
@@ -15,19 +15,13 @@ 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 DirtyStateProviderMixin<CategoryFormState>()(
|
||||
DialogMixin<CategoryRegistryDetailDialogParams>(LitElement)
|
||||
class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@@ -50,10 +44,6 @@ class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
|
||||
this._name = this.params?.suggestedName || "";
|
||||
this._icon = null;
|
||||
}
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{ name: this._name, icon: this._icon }
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -62,14 +52,13 @@ class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
|
||||
}
|
||||
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")}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -107,9 +96,7 @@ class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid ||
|
||||
!!this._submitting ||
|
||||
(!isCreate && !this.isDirtyState)}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this._i18n.localize("ui.common.save")
|
||||
@@ -127,17 +114,15 @@ class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
|
||||
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 {
|
||||
@@ -146,11 +131,10 @@ class DialogCategoryDetail extends DirtyStateProviderMixin<CategoryFormState>()(
|
||||
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,13 +1,11 @@
|
||||
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";
|
||||
@@ -35,16 +33,14 @@ 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")
|
||||
@@ -64,9 +60,13 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
this._error = undefined;
|
||||
if (this.entry.unique_id !== changedProperties.get("entry")?.unique_id) {
|
||||
if (
|
||||
this.entry.unique_id !==
|
||||
(changedProperties.get("entry") as ExtEntityRegistryEntry)?.unique_id
|
||||
) {
|
||||
this._item = undefined;
|
||||
}
|
||||
|
||||
this._getItem();
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,7 @@ 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>
|
||||
@@ -123,7 +124,7 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateItem}
|
||||
.disabled=${!(this._dirtyState?.isDirty || this._isHelperDirty) ||
|
||||
.disabled=${!this._dirty ||
|
||||
!!this._submitting ||
|
||||
!!(this._item && !this._item.name)}
|
||||
>
|
||||
@@ -138,12 +139,22 @@ 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() {
|
||||
@@ -156,20 +167,15 @@ 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,7 +45,6 @@ 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 {
|
||||
@@ -145,28 +144,6 @@ 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;
|
||||
@@ -181,50 +158,41 @@ 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!: EntitySettingsState["entityId"];
|
||||
@state() private _entityId!: string;
|
||||
|
||||
@state() private _deviceClass?: EntitySettingsState["deviceClass"];
|
||||
@state() private _deviceClass?: string;
|
||||
|
||||
@state() private _switchAsDomain: EntitySettingsState["switchAsDomain"] =
|
||||
"switch";
|
||||
@state() private _switchAsDomain = "switch";
|
||||
|
||||
@state() private _switchAsInvert: EntitySettingsState["switchAsInvert"] =
|
||||
false;
|
||||
@state() private _switchAsInvert = false;
|
||||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _labels?: string[] | null;
|
||||
|
||||
@state() private _disabledBy!: EntitySettingsState["disabledBy"];
|
||||
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
|
||||
|
||||
@state() private _hiddenBy!: EntitySettingsState["hiddenBy"];
|
||||
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state()
|
||||
private _unit_of_measurement?: EntitySettingsState["unitOfMeasurement"];
|
||||
@state() private _unit_of_measurement?: string | null;
|
||||
|
||||
@state() private _precision?: EntitySettingsState["precision"];
|
||||
@state() private _precision?: number | null;
|
||||
|
||||
@state()
|
||||
private _precipitation_unit?: EntitySettingsState["precipitationUnit"];
|
||||
@state() private _precipitation_unit?: string | null;
|
||||
|
||||
@state() private _pressure_unit?: EntitySettingsState["pressureUnit"];
|
||||
@state() private _pressure_unit?: string | null;
|
||||
|
||||
@state()
|
||||
private _temperature_unit?: EntitySettingsState["temperatureUnit"];
|
||||
@state() private _temperature_unit?: string | null;
|
||||
|
||||
@state() private _visibility_unit?: EntitySettingsState["visibilityUnit"];
|
||||
@state() private _visibility_unit?: string | null;
|
||||
|
||||
@state() private _wind_speed_unit?: EntitySettingsState["windSpeedUnit"];
|
||||
@state() private _wind_speed_unit?: string | null;
|
||||
|
||||
@state() private _cameraPrefs?: CameraPreferences;
|
||||
|
||||
@@ -236,9 +204,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
@state() private _weatherConvertibleUnits?: WeatherUnits;
|
||||
|
||||
@state() private _defaultCode?: EntitySettingsState["defaultCode"];
|
||||
@state() private _defaultCode?: string | null;
|
||||
|
||||
@state() private _calendarColor?: EntitySettingsState["calendarColor"];
|
||||
@state() private _calendarColor?: string | null;
|
||||
|
||||
@state() private _associatedZone?: string;
|
||||
|
||||
@@ -248,7 +216,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
private _deviceClassOptions?: string[][];
|
||||
|
||||
private _currentState(): EntitySettingsState {
|
||||
private _initialStateJson!: string;
|
||||
|
||||
private _lastDirty = false;
|
||||
|
||||
private _currentState() {
|
||||
return {
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
@@ -343,6 +315,9 @@ 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)) {
|
||||
@@ -441,9 +416,17 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._switchAsDomain = "switch";
|
||||
this._switchAsInvert = false;
|
||||
}
|
||||
this._initialStateJson = JSON.stringify(this._currentState());
|
||||
this._lastDirty = false;
|
||||
}
|
||||
|
||||
this._dirtyState?.setState(this._currentState());
|
||||
if (this._initialStateJson) {
|
||||
const dirty = this.dirty;
|
||||
if (dirty !== this._lastDirty) {
|
||||
this._lastDirty = dirty;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -1163,6 +1146,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public get dirty(): boolean {
|
||||
return JSON.stringify(this._currentState()) !== this._initialStateJson;
|
||||
}
|
||||
|
||||
public async updateEntry(): Promise<{
|
||||
close: boolean;
|
||||
entry: ExtEntityRegistryEntry;
|
||||
@@ -1448,10 +1435,12 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1470,18 +1459,22 @@ 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
|
||||
@@ -1489,35 +1482,43 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1552,6 +1553,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _areaPicked(ev: CustomEvent) {
|
||||
fireEvent(this, "change");
|
||||
this._areaId = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -1624,6 +1626,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
private _resetNameAndOpenDeviceSettings() {
|
||||
this._name = this.entry.name || "";
|
||||
fireEvent(this, "change");
|
||||
|
||||
this._openDeviceSettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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";
|
||||
@@ -14,7 +13,6 @@ 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 {
|
||||
@@ -40,16 +38,14 @@ 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;
|
||||
|
||||
@@ -137,6 +133,7 @@ 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">
|
||||
@@ -151,7 +148,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._dirtyState?.isDirty || !!this._submitting}
|
||||
.disabled=${!this._dirty || !!this._submitting}
|
||||
.loading=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
|
||||
@@ -160,6 +157,11 @@ 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!];
|
||||
|
||||
@@ -205,10 +207,8 @@ 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");
|
||||
}
|
||||
|
||||
+12
-9
@@ -25,6 +25,7 @@ import {
|
||||
type ExtEntityRegistryEntry,
|
||||
} from "../../../../../data/entity/entity_registry";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { OVERRIDE_DEVICE_CLASSES } from "../../../entities/entity-registry-settings-editor";
|
||||
import "./matter-add-device/matter-add-device-apple-home";
|
||||
import "./matter-add-device/matter-add-device-existing";
|
||||
import "./matter-add-device/matter-add-device-generic";
|
||||
@@ -139,15 +140,17 @@ class DialogMatterAddDevice extends LitElement {
|
||||
entityIds
|
||||
);
|
||||
|
||||
const mainEntry = Object.values(entries).find(
|
||||
(e) => e.original_name === null
|
||||
);
|
||||
if (!mainEntry) return;
|
||||
|
||||
const domain = computeDomain(mainEntry.entity_id);
|
||||
if (domain === "cover" || domain === "binary_sensor") {
|
||||
this._mainEntity = mainEntry;
|
||||
}
|
||||
this._mainEntity = Object.values(entries).find((entry) => {
|
||||
if (entry.entity_category) return false;
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
if (!deviceClasses) return false;
|
||||
const deviceClass = entry.device_class ?? entry.original_device_class;
|
||||
if (!deviceClass) return false;
|
||||
return deviceClasses.some(
|
||||
(classes) => classes.length > 1 && classes.includes(deviceClass)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
|
||||
@@ -438,9 +438,7 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kWh</div>`;
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -580,9 +580,7 @@ class HuiPowerSankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatPowerShort(this.hass, value)}
|
||||
</div>`;
|
||||
formatPowerShort(this.hass, value);
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -511,9 +511,11 @@ class HuiWaterFlowSankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatFlowRateShort(this.hass.locale, this.hass.config.unit_system.length, value)}
|
||||
</div>`;
|
||||
formatFlowRateShort(
|
||||
this.hass.locale,
|
||||
this.hass.config.unit_system.length,
|
||||
value
|
||||
);
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -30,6 +30,7 @@ import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -230,11 +231,28 @@ export class HaCardConditionEditor extends LitElement {
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-svg-icon
|
||||
<div
|
||||
id="condition-icon"
|
||||
class="icon-badge-wrapper"
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
.path=${ICON_CONDITION[condition.condition]}
|
||||
></ha-svg-icon>
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${ICON_CONDITION[condition.condition]}
|
||||
></ha-svg-icon>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`<ha-automation-row-live-test
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
></ha-automation-row-live-test>`}
|
||||
</div>
|
||||
${!hideLiveTest && this._liveTestResult.message
|
||||
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
@@ -255,18 +273,6 @@ export class HaCardConditionEditor extends LitElement {
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_error"
|
||||
)}
|
||||
</ha-automation-row-event-chip>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test>
|
||||
`}
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
@@ -479,17 +485,15 @@ export class HaCardConditionEditor extends LitElement {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.condition-icon {
|
||||
.icon-badge-wrapper {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 870px) {
|
||||
.condition-icon {
|
||||
display: inline-block;
|
||||
.icon-badge-wrapper {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
|
||||
@@ -8557,7 +8557,7 @@ __metadata:
|
||||
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
leaflet.markercluster: "npm:1.5.3"
|
||||
license-checker-rseidelsohn: "npm:5.0.1"
|
||||
lint-staged: "npm:17.0.5"
|
||||
lint-staged: "npm:17.0.6"
|
||||
lit: "npm:3.3.3"
|
||||
lit-analyzer: "npm:2.0.3"
|
||||
lit-html: "npm:3.3.3"
|
||||
@@ -9936,21 +9936,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lint-staged@npm:17.0.5":
|
||||
version: 17.0.5
|
||||
resolution: "lint-staged@npm:17.0.5"
|
||||
"lint-staged@npm:17.0.6":
|
||||
version: 17.0.6
|
||||
resolution: "lint-staged@npm:17.0.6"
|
||||
dependencies:
|
||||
listr2: "npm:^10.2.1"
|
||||
picomatch: "npm:^4.0.4"
|
||||
string-argv: "npm:^0.3.2"
|
||||
tinyexec: "npm:^1.1.2"
|
||||
yaml: "npm:^2.8.4"
|
||||
tinyexec: "npm:1.2.2"
|
||||
yaml: "npm:^2.9.0"
|
||||
dependenciesMeta:
|
||||
yaml:
|
||||
optional: true
|
||||
bin:
|
||||
lint-staged: bin/lint-staged.js
|
||||
checksum: 10/a0bea43689d68ec0bf6a56943884dbdb96b6b49e2677bf80654d802678b2edf9fc65338ca8ef3fc310f245933ea2a809db1ac94431dc445c57a4d49620d9d4da
|
||||
checksum: 10/371918cfb293ed0ca5d16fc2a1de304b5a95d21b87dc1ea7f3751567c8f8a07971a40349fac8edb5fce3c6ea6713f70922ea90184b142fd432a5bb4db6c316b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -13172,10 +13172,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyexec@npm:^1.0.2, tinyexec@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "tinyexec@npm:1.1.2"
|
||||
checksum: 10/2bbe37f9001c6f5723ab39eb8dc1e88f77e830d7cf2e8f34bb75019eb505fcfe3b061b4799c502ff31fa63aa1a9adc649add5ff1e17b7fbd8c16e1afb75d0b9e
|
||||
"tinyexec@npm:1.2.2, tinyexec@npm:^1.0.2":
|
||||
version: 1.2.2
|
||||
resolution: "tinyexec@npm:1.2.2"
|
||||
checksum: 10/e6f3cabafc33a46063868b4e9c0ab76722e21cb46f0177f7bef5a9656e09ea6fa37115d3e47f776aff11aab9ab696b0c840c8e0099fab574b1e37767c4371aec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14755,7 +14755,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.8.4":
|
||||
"yaml@npm:^2.9.0":
|
||||
version: 2.9.0
|
||||
resolution: "yaml@npm:2.9.0"
|
||||
bin:
|
||||
|
||||
Reference in New Issue
Block a user