mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-14 04:12:16 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45b43de020 | |||
| 273967fe70 | |||
| 382e07379b | |||
| 01a8b8d3ef | |||
| 3bbce5607e | |||
| 7ce052e2a8 | |||
| e929558a9a | |||
| 9cd4a6937f | |||
| af617695b8 | |||
| 740ad9eb6b | |||
| caeedc41e3 | |||
| fbb76a8ba0 | |||
| 3340637ff3 | |||
| 534bea231c | |||
| 8635951394 |
+4
-4
@@ -34,10 +34,10 @@
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/lint": "6.9.6",
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.0",
|
||||
"@codemirror/view": "6.43.1",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.8",
|
||||
@@ -186,7 +186,7 @@
|
||||
"lodash.template": "4.18.1",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.8.3",
|
||||
"prettier": "3.8.4",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
@@ -194,7 +194,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.60.1",
|
||||
"typescript-eslint": "8.61.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.8",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
import type { LineSeriesOption } from "echarts";
|
||||
|
||||
type Point = NonNullable<LineSeriesOption["data"]>[number];
|
||||
|
||||
interface MeanFrame {
|
||||
sumX: number;
|
||||
sumY: number;
|
||||
count: number;
|
||||
isArray: boolean;
|
||||
}
|
||||
|
||||
interface MinMaxFrame {
|
||||
minPoint: Point;
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxPoint: Point;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
export function downSampleLineData<
|
||||
T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number],
|
||||
>(
|
||||
@@ -19,11 +37,47 @@ export function downSampleLineData<
|
||||
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
|
||||
const step = Math.ceil((max - min) / Math.floor(maxDetails));
|
||||
|
||||
// Group points into frames
|
||||
const frames = new Map<
|
||||
number,
|
||||
{ point: (typeof data)[number]; x: number; y: number }[]
|
||||
>();
|
||||
if (useMean) {
|
||||
// Group points into frames, accumulating sums in insertion order.
|
||||
const frames = new Map<number, MeanFrame>();
|
||||
|
||||
for (const point of data) {
|
||||
const pointData = getPointData(point);
|
||||
if (!Array.isArray(pointData)) continue;
|
||||
const x = Number(pointData[0]);
|
||||
const y = Number(pointData[1]);
|
||||
if (isNaN(x) || isNaN(y)) continue;
|
||||
|
||||
const frameIndex = Math.floor((x - min) / step);
|
||||
const frame = frames.get(frameIndex);
|
||||
if (!frame) {
|
||||
frames.set(frameIndex, {
|
||||
sumX: x,
|
||||
sumY: y,
|
||||
count: 1,
|
||||
isArray: Array.isArray(pointData),
|
||||
});
|
||||
} else {
|
||||
frame.sumX += x;
|
||||
frame.sumY += y;
|
||||
frame.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const result: T[] = [];
|
||||
for (const frame of frames.values()) {
|
||||
const meanX = frame.sumX / frame.count;
|
||||
const meanY = frame.sumY / frame.count;
|
||||
const meanPoint = (
|
||||
frame.isArray ? [meanX, meanY] : { value: [meanX, meanY] }
|
||||
) as T;
|
||||
result.push(meanPoint);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Min/max mode: track the min and max point per frame in insertion order.
|
||||
const frames = new Map<number, MinMaxFrame>();
|
||||
|
||||
for (const point of data) {
|
||||
const pointData = getPointData(point);
|
||||
@@ -35,53 +89,39 @@ export function downSampleLineData<
|
||||
const frameIndex = Math.floor((x - min) / step);
|
||||
const frame = frames.get(frameIndex);
|
||||
if (!frame) {
|
||||
frames.set(frameIndex, [{ point, x, y }]);
|
||||
frames.set(frameIndex, {
|
||||
minPoint: point,
|
||||
minX: x,
|
||||
minY: y,
|
||||
maxPoint: point,
|
||||
maxX: x,
|
||||
maxY: y,
|
||||
});
|
||||
} else {
|
||||
frame.push({ point, x, y });
|
||||
// Match the original strict-less / strict-greater comparisons so the
|
||||
// first occurrence wins on ties.
|
||||
if (y < frame.minY) {
|
||||
frame.minPoint = point;
|
||||
frame.minX = x;
|
||||
frame.minY = y;
|
||||
}
|
||||
if (y > frame.maxY) {
|
||||
frame.maxPoint = point;
|
||||
frame.maxX = x;
|
||||
frame.maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert frames back to points
|
||||
const result: T[] = [];
|
||||
|
||||
if (useMean) {
|
||||
// Use mean values for each frame
|
||||
for (const [_i, framePoints] of frames) {
|
||||
const sumY = framePoints.reduce((acc, p) => acc + p.y, 0);
|
||||
const meanY = sumY / framePoints.length;
|
||||
const sumX = framePoints.reduce((acc, p) => acc + p.x, 0);
|
||||
const meanX = sumX / framePoints.length;
|
||||
|
||||
const firstPoint = framePoints[0].point;
|
||||
const pointData = getPointData(firstPoint);
|
||||
const meanPoint = (
|
||||
Array.isArray(pointData) ? [meanX, meanY] : { value: [meanX, meanY] }
|
||||
) as T;
|
||||
result.push(meanPoint);
|
||||
for (const frame of frames.values()) {
|
||||
// The order of the data must be preserved so max may be before min
|
||||
if (frame.minX > frame.maxX) {
|
||||
result.push(frame.maxPoint as T);
|
||||
}
|
||||
} else {
|
||||
// Use min/max values for each frame
|
||||
for (const [_i, framePoints] of frames) {
|
||||
let minPoint = framePoints[0];
|
||||
let maxPoint = framePoints[0];
|
||||
|
||||
for (const p of framePoints) {
|
||||
if (p.y < minPoint.y) {
|
||||
minPoint = p;
|
||||
}
|
||||
if (p.y > maxPoint.y) {
|
||||
maxPoint = p;
|
||||
}
|
||||
}
|
||||
|
||||
// The order of the data must be preserved so max may be before min
|
||||
if (minPoint.x > maxPoint.x) {
|
||||
result.push(maxPoint.point);
|
||||
}
|
||||
result.push(minPoint.point);
|
||||
if (minPoint.x < maxPoint.x) {
|
||||
result.push(maxPoint.point);
|
||||
}
|
||||
result.push(frame.minPoint as T);
|
||||
if (frame.minX < frame.maxX) {
|
||||
result.push(frame.maxPoint as T);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1520,7 +1520,9 @@ export class HaChartBase extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
/* overflow: hidden clips descenders (e.g. "g", parentheses) with a tight
|
||||
line-height, so give the line box room to contain them */
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.chart-legend .label.clickable:hover {
|
||||
@@ -1558,6 +1560,25 @@ export class HaChartBase extends LitElement {
|
||||
.chart-legend .legend-toggle ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
/* On touch devices, enlarge the toggle tap target via taller rows and
|
||||
leading padding (which also separates it from the previous item), while
|
||||
keeping the icon tight to its own label so the pairing stays clear.
|
||||
Drop the now-pointless row gap and li padding. */
|
||||
@media (pointer: coarse) {
|
||||
.chart-legend ul {
|
||||
row-gap: 0;
|
||||
}
|
||||
/* Only grow the toggle rows, not the expand/collapse chip's row. */
|
||||
.chart-legend li:has(.legend-toggle) {
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
.chart-legend .legend-toggle {
|
||||
padding: 11px;
|
||||
padding-inline-end: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
ha-assist-chip {
|
||||
height: 100%;
|
||||
--_label-text-weight: 500;
|
||||
|
||||
+3
-1
@@ -128,11 +128,13 @@ export const addMatterDevice = (hass: HomeAssistant) => {
|
||||
|
||||
export const commissionMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
code: string
|
||||
code: string,
|
||||
networkOnly: boolean
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "matter/commission",
|
||||
code,
|
||||
network_only: networkOnly,
|
||||
});
|
||||
|
||||
export const acceptSharedMatterDevice = (
|
||||
|
||||
@@ -10,13 +10,21 @@ import "../../components/ha-button";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
import type { ConfigEntryMutableParams } from "../../data/config_entries";
|
||||
import { updateConfigEntry } from "../../data/config_entries";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import type { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
|
||||
|
||||
interface SystemOptionsState {
|
||||
disableNewEntities: boolean;
|
||||
disablePolling: boolean;
|
||||
}
|
||||
|
||||
@customElement("dialog-config-entry-system-options")
|
||||
class DialogConfigEntrySystemOptions extends LitElement {
|
||||
class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptionsState>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _disableNewEntities!: boolean;
|
||||
@@ -38,6 +46,13 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
this._error = undefined;
|
||||
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
||||
this._disablePolling = params.entry.pref_disable_polling;
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
}
|
||||
);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -68,7 +83,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
) || this._params.entry.domain,
|
||||
}
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
@@ -135,7 +150,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${this._submitting}
|
||||
.disabled=${this._submitting || !this.isDirtyState}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.update"
|
||||
@@ -149,11 +164,19 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
private _disableNewEntitiesChanged(ev: Event): void {
|
||||
this._error = undefined;
|
||||
this._disableNewEntities = !(ev.target as HaSwitch).checked;
|
||||
this._updateDirtyState({
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
});
|
||||
}
|
||||
|
||||
private _disablePollingChanged(ev: Event): void {
|
||||
this._error = undefined;
|
||||
this._disablePolling = !(ev.target as HaSwitch).checked;
|
||||
this._updateDirtyState({
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateEntry(): Promise<void> {
|
||||
|
||||
@@ -19,6 +19,64 @@ declare const __WB_MANIFEST__: Parameters<typeof precacheAndRoute>[0];
|
||||
const noFallBackRegEx =
|
||||
/\/(api|static|auth|frontend_latest|frontend_es5|local)\/.*/;
|
||||
|
||||
// Camera / image proxy endpoints that carry credentials in the URL.
|
||||
// We pre-validate the credential in the service worker so obviously invalid
|
||||
// requests (signature expired, token missing) never reach the server and
|
||||
// don't trigger spurious "Login attempt" warnings from http.ban after BFCache
|
||||
// restore, tab resume, network change, or any other browser-initiated replay
|
||||
// of a stale `<img>` URL.
|
||||
const proxyPathRegEx =
|
||||
/^\/api\/(camera_proxy_stream|camera_proxy|image_proxy)\//;
|
||||
|
||||
// Reject signatures this many ms before their nominal expiry to absorb small
|
||||
// client/server clock differences. Erring this direction only ever turns a
|
||||
// would-be valid request into a local 401; we cannot err the other way without
|
||||
// re-introducing the warnings this filter exists to prevent.
|
||||
const JWT_EXPIRY_SKEW_MS = 5000;
|
||||
|
||||
const base64UrlDecode = (input: string): string => {
|
||||
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
||||
return atob(padded);
|
||||
};
|
||||
|
||||
const isJwtExpired = (jwt: string): boolean => {
|
||||
try {
|
||||
const parts = jwt.split(".");
|
||||
if (parts.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
||||
if (typeof payload.exp !== "number") {
|
||||
return false;
|
||||
}
|
||||
return payload.exp * 1000 < Date.now() + JWT_EXPIRY_SKEW_MS;
|
||||
} catch (_err) {
|
||||
// If we can't parse the JWT for any reason, defer to the server.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleProxyRequest: RouteHandler = async ({ request }) => {
|
||||
const req = request as Request;
|
||||
const url = new URL(req.url);
|
||||
|
||||
const token = url.searchParams.get("token");
|
||||
if (token === "undefined" || token === "null" || token === "") {
|
||||
return new Response(null, { status: 401, statusText: "Invalid token" });
|
||||
}
|
||||
|
||||
const authSig = url.searchParams.get("authSig");
|
||||
if (authSig && isJwtExpired(authSig)) {
|
||||
return new Response(null, {
|
||||
status: 401,
|
||||
statusText: "Signature expired",
|
||||
});
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
};
|
||||
|
||||
const initRouting = () => {
|
||||
precacheAndRoute(__WB_MANIFEST__, {
|
||||
// Ignore all URL parameters.
|
||||
@@ -59,6 +117,15 @@ const initRouting = () => {
|
||||
})
|
||||
);
|
||||
|
||||
// Short-circuit camera/image proxy requests with an expired signature or a
|
||||
// missing/undefined token so they don't hit core and get logged as invalid
|
||||
// login attempts. Registered before the generic /api route below so it wins.
|
||||
registerRoute(
|
||||
({ url, request }) =>
|
||||
proxyPathRegEx.test(url.pathname) && request.method === "GET",
|
||||
handleProxyRequest
|
||||
);
|
||||
|
||||
// Get api from network.
|
||||
registerRoute(/\/(api|auth)\/.*/, new NetworkOnly());
|
||||
|
||||
|
||||
@@ -23,18 +23,28 @@ import {
|
||||
} from "../../../data/application_credential";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import type { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential";
|
||||
|
||||
interface CredentialFormState {
|
||||
domain: string;
|
||||
name: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
interface Domain {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@customElement("dialog-add-application-credential")
|
||||
export class DialogAddApplicationCredential extends LitElement {
|
||||
export class DialogAddApplicationCredential extends DirtyStateProviderMixin<CredentialFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _loading = false;
|
||||
@@ -76,6 +86,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
this._error = undefined;
|
||||
this._loading = false;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "shallow" }, this._currentState());
|
||||
this._fetchConfig();
|
||||
}
|
||||
|
||||
@@ -100,10 +111,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
@closed=${this._abortDialog}
|
||||
.preventScrimClose=${!!this._domain ||
|
||||
!!this._name ||
|
||||
!!this._clientId ||
|
||||
!!this._clientSecret}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
.headerTitle=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.caption"
|
||||
)}
|
||||
@@ -284,6 +292,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
ev.stopPropagation();
|
||||
this._domain = ev.detail.value;
|
||||
this._updateDescription();
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private async _updateDescription() {
|
||||
@@ -307,6 +316,16 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
const name = (ev.target as any).name;
|
||||
const value = (ev.target as any).value;
|
||||
this[`_${name}`] = value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _currentState(): CredentialFormState {
|
||||
return {
|
||||
domain: this._domain || "",
|
||||
name: this._name || "",
|
||||
clientId: this._clientId || "",
|
||||
clientSecret: this._clientSecret || "",
|
||||
};
|
||||
}
|
||||
|
||||
private _abortDialog() {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
import type { ObjectSelector, Selector } from "../../../../../data/selector";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { DirtyStateProviderMixin } from "../../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
@@ -56,15 +57,15 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@customElement("supervisor-app-config")
|
||||
class SupervisorAppConfig extends LitElement {
|
||||
class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
Record<string, unknown>
|
||||
>()(LitElement) {
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _configHasChanged = false;
|
||||
|
||||
@state() private _valid = true;
|
||||
|
||||
@state() private _canShowSchema = false;
|
||||
@@ -351,9 +352,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
<div class="card-actions right">
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${this.disabled ||
|
||||
!this._configHasChanged ||
|
||||
!this._valid}
|
||||
.disabled=${this.disabled || !this.isDirtyState || !this._valid}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
@@ -377,6 +376,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("addon")) {
|
||||
this._options = { ...this.addon.options };
|
||||
this._initDirtyTracking({ type: "deep" }, this.addon.options);
|
||||
}
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
@@ -415,11 +415,13 @@ class SupervisorAppConfig extends LitElement {
|
||||
private _configChanged(ev): void {
|
||||
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
|
||||
this._valid = true;
|
||||
this._configHasChanged = true;
|
||||
this._options = ev.detail.value;
|
||||
this._updateDirtyState(ev.detail.value);
|
||||
} else {
|
||||
this._configHasChanged = true;
|
||||
this._valid = ev.detail.isValid;
|
||||
if (ev.detail.isValid) {
|
||||
this._updateDirtyState(ev.detail.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +452,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
this._markDirtyStateClean();
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
@@ -469,7 +471,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
if (this.disabled || !this._configHasChanged || !this._valid) {
|
||||
if (this.disabled || !this.isDirtyState || !this._valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -499,7 +501,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
options,
|
||||
});
|
||||
|
||||
this._configHasChanged = false;
|
||||
this._markDirtyStateClean();
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestSupervisorAppRestart(this, this.hass, this.addon);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,16 @@ import type {
|
||||
} from "../../../../../data/hassio/addon";
|
||||
import { setHassioAddonOption } from "../../../../../data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
import { DirtyStateProviderMixin } from "../../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
|
||||
|
||||
@customElement("supervisor-app-network")
|
||||
class SupervisorAppNetwork extends LitElement {
|
||||
class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
Record<string, number | null>
|
||||
>()(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@@ -30,19 +33,19 @@ class SupervisorAppNetwork extends LitElement {
|
||||
|
||||
@state() private _showOptional = false;
|
||||
|
||||
@state() private _configHasChanged = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _config?: Record<string, any>;
|
||||
@state() private _config?: Record<string, number | null>;
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hasHiddenOptions = Object.keys(this._config).find(
|
||||
(entry) => this._config![entry] === null
|
||||
const config = this._config;
|
||||
|
||||
const hasHiddenOptions = Object.keys(config).find(
|
||||
(entry) => config[entry] === null
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -98,7 +101,7 @@ class SupervisorAppNetwork extends LitElement {
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged || this.disabled}
|
||||
.disabled=${!this.isDirtyState || this.disabled}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
@@ -115,7 +118,10 @@ class SupervisorAppNetwork extends LitElement {
|
||||
}
|
||||
|
||||
private _createSchema = memoizeOne(
|
||||
(config: Record<string, number>, showOptional: boolean): HaFormSchema[] =>
|
||||
(
|
||||
config: Record<string, number | null>,
|
||||
showOptional: boolean
|
||||
): HaFormSchema[] =>
|
||||
(showOptional
|
||||
? Object.keys(config)
|
||||
: Object.keys(config).filter((entry) => config[entry] !== null)
|
||||
@@ -141,12 +147,14 @@ class SupervisorAppNetwork extends LitElement {
|
||||
item.name;
|
||||
|
||||
private _setNetworkConfig(): void {
|
||||
this._config = this.addon.network || {};
|
||||
const config = this.addon.network || {};
|
||||
this._config = config;
|
||||
this._initDirtyTracking({ type: "shallow" }, config);
|
||||
}
|
||||
|
||||
private async _configChanged(ev: CustomEvent): Promise<void> {
|
||||
this._configHasChanged = true;
|
||||
private _configChanged(ev: CustomEvent): void {
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(ev.detail.value);
|
||||
}
|
||||
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
@@ -161,7 +169,7 @@ class SupervisorAppNetwork extends LitElement {
|
||||
|
||||
try {
|
||||
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
this._markDirtyStateClean();
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
@@ -188,14 +196,14 @@ class SupervisorAppNetwork extends LitElement {
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
if (!this._configHasChanged || this.disabled) {
|
||||
if (!this.isDirtyState || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = ev.currentTarget as any;
|
||||
|
||||
this._error = undefined;
|
||||
const networkconfiguration = {};
|
||||
const networkconfiguration: Record<string, number | null> = {};
|
||||
Object.entries(this._config!).forEach(([key, value]) => {
|
||||
networkconfiguration[key] = value ?? null;
|
||||
});
|
||||
@@ -206,7 +214,7 @@ class SupervisorAppNetwork extends LitElement {
|
||||
|
||||
try {
|
||||
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
this._markDirtyStateClean();
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
|
||||
@@ -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
|
||||
@@ -384,7 +412,9 @@ class DialogAreaDetail
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
.disabled=${nameInvalid ||
|
||||
this._submitting ||
|
||||
(!!this._params?.entry && !this.isDirtyState)}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
@@ -418,36 +448,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 +506,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 {
|
||||
|
||||
@@ -12,13 +12,17 @@ import {
|
||||
getPreferredAgentForDownload,
|
||||
} from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { downloadBackupFile } from "../helper/download_backup";
|
||||
import type { DownloadDecryptedBackupDialogParams } from "./show-dialog-download-decrypted-backup";
|
||||
|
||||
@customElement("ha-dialog-download-decrypted-backup")
|
||||
class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
class DialogDownloadDecryptedBackup
|
||||
extends DirtyStateProviderMixin<string>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
@@ -32,6 +36,7 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
public showDialog(params: DownloadDecryptedBackupDialogParams): void {
|
||||
this._open = true;
|
||||
this._params = params;
|
||||
this._initDirtyTracking({ type: "shallow" }, "");
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -60,7 +65,7 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.download.title"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<p>
|
||||
@@ -105,7 +110,11 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
.disabled=${!this.isDirtyState}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.download.download"
|
||||
)}
|
||||
@@ -136,6 +145,7 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
this._agentId,
|
||||
this._encryptionKey
|
||||
);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
if (err?.code === "password_incorrect") {
|
||||
@@ -155,6 +165,7 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
private _keyChanged(ev) {
|
||||
this._encryptionKey = ev.currentTarget.value;
|
||||
this._error = "";
|
||||
this._updateDirtyState(this._encryptionKey);
|
||||
}
|
||||
|
||||
private get _agentId() {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
fetchBackupConfig,
|
||||
} from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import "../components/config/ha-backup-config-data";
|
||||
@@ -59,7 +60,10 @@ const STEPS = ["data", "sync"] as const;
|
||||
const DISALLOWED_AGENTS_NO_HA = [CLOUD_AGENT];
|
||||
|
||||
@customElement("ha-dialog-generate-backup")
|
||||
class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
class DialogGenerateBackup
|
||||
extends DirtyStateProviderMixin<FormData>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _step?: "data" | "sync";
|
||||
@@ -79,6 +83,8 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
this._formData = INITIAL_DATA;
|
||||
this._params = _params;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, INITIAL_DATA);
|
||||
this._updateDirtyState(this._formData);
|
||||
|
||||
this._fetchAgents();
|
||||
this._fetchBackupConfig();
|
||||
@@ -160,6 +166,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
agents_mode: "custom",
|
||||
agent_ids: filteredAgents,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +187,11 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
const selectedAgents = this._formData.agent_ids;
|
||||
|
||||
return html`
|
||||
<ha-dialog .open=${this._open} @closed=${this._dialogClosed}>
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header slot="header">
|
||||
${isFirstStep
|
||||
? html`
|
||||
@@ -276,6 +287,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
...this._formData!,
|
||||
data,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _renderSync() {
|
||||
@@ -370,6 +382,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
...this._formData!,
|
||||
agents_mode: value,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _agentsChanged(ev) {
|
||||
@@ -377,6 +390,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
...this._formData!,
|
||||
agent_ids: ev.detail.value,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -384,6 +398,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
...this._formData!,
|
||||
name: (ev.target as HaInput).value ?? "",
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _disabledAgentIds() {
|
||||
@@ -429,6 +444,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
this._params!.submit?.(params);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
} from "../../../../components/ha-form/types";
|
||||
import { extractApiErrorMessage } from "../../../../data/hassio/common";
|
||||
import { changeMountOptions } from "../../../../data/supervisor/mounts";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LocalBackupLocationDialogParams } from "./show-dialog-local-backup-location";
|
||||
@@ -25,8 +26,14 @@ const SCHEMA = [
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
|
||||
interface LocalBackupLocationFormState {
|
||||
default_backup_mount: string | null | undefined;
|
||||
}
|
||||
|
||||
@customElement("dialog-local-backup-location")
|
||||
class LocalBackupLocationDialog extends LitElement {
|
||||
class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocationFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: LocalBackupLocationDialogParams;
|
||||
@@ -44,6 +51,10 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{ default_backup_mount: undefined }
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -68,7 +79,7 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
header-title=${this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.title`
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error
|
||||
@@ -102,7 +113,7 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this._data}
|
||||
.disabled=${this._waiting || !this.isDirtyState}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
@@ -125,6 +136,9 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
this._data = {
|
||||
default_backup_mount: newLocation === "/backup" ? null : newLocation,
|
||||
};
|
||||
this._updateDirtyState({
|
||||
default_backup_mount: this._data.default_backup_mount,
|
||||
});
|
||||
}
|
||||
|
||||
private async _changeMount() {
|
||||
@@ -140,6 +154,7 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
this._waiting = false;
|
||||
return;
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.show_encryption_key.title"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-icon-button
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type BackupUploadFileFormData,
|
||||
} from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
@@ -28,7 +29,7 @@ import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
||||
|
||||
@customElement("ha-dialog-upload-backup")
|
||||
export class DialogUploadBackup
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<BackupUploadFileFormData>()(LitElement)
|
||||
implements HassDialog<UploadBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -47,6 +48,8 @@ export class DialogUploadBackup
|
||||
this._params = params;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "shallow" }, INITIAL_UPLOAD_FORM_DATA);
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
@@ -64,10 +67,6 @@ export class DialogUploadBackup
|
||||
return true;
|
||||
}
|
||||
|
||||
private _formValid() {
|
||||
return this._formData?.file !== undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._formData) {
|
||||
return nothing;
|
||||
@@ -79,7 +78,7 @@ export class DialogUploadBackup
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.title"
|
||||
)}
|
||||
?prevent-scrim-close=${this._uploading}
|
||||
.preventScrimClose=${this.isDirtyState || this._uploading}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error
|
||||
@@ -112,7 +111,7 @@ export class DialogUploadBackup
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._upload}
|
||||
.disabled=${!this._formValid() || this._uploading}
|
||||
.disabled=${!this.isDirtyState || this._uploading}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.action"
|
||||
@@ -131,11 +130,13 @@ export class DialogUploadBackup
|
||||
...this._formData!,
|
||||
file,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _filesCleared() {
|
||||
this._error = undefined;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private async _upload() {
|
||||
@@ -161,6 +162,7 @@ export class DialogUploadBackup
|
||||
try {
|
||||
await uploadBackup(this.hass, file, agentIds);
|
||||
this._params!.submit?.();
|
||||
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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ class DialogMatterAddDevice extends LitElement {
|
||||
const savedStep = this._step;
|
||||
try {
|
||||
this._step = "commissioning";
|
||||
await commissionMatterDevice(this.hass, code);
|
||||
await commissionMatterDevice(this.hass, code, true);
|
||||
} catch (_err) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
|
||||
@@ -211,7 +211,7 @@ class MatterOptionsPage extends LitElement {
|
||||
this._error = undefined;
|
||||
this._redirectOnNewMatterDevice();
|
||||
try {
|
||||
await commissionMatterDevice(this.hass, code);
|
||||
await commissionMatterDevice(this.hass, code, false);
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
this._stopRedirect();
|
||||
|
||||
+8
-2
@@ -6,13 +6,16 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strategy-element-editor";
|
||||
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-configure-strategy")
|
||||
export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProviderMixin<LovelaceStrategyConfig>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
|
||||
@@ -29,6 +32,7 @@ export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
this._params = params;
|
||||
this._data = params.config.strategy;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -49,7 +53,7 @@ export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
|
||||
)}
|
||||
@@ -80,6 +84,7 @@ export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
|
||||
private _handleConfigChanged(ev: CustomEvent): void {
|
||||
this._data = ev.detail.config;
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -92,6 +97,7 @@ export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
strategy: this._data,
|
||||
});
|
||||
this._submitting = false;
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,16 @@ import type {
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { pickAvailableDashboardUrlPath } from "./pick-available-dashboard-url-path";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
Partial<LovelaceDashboard>
|
||||
>()(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceDashboardDetailsDialogParams;
|
||||
@@ -42,6 +45,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
this._open = true;
|
||||
if (this._params.dashboard) {
|
||||
this._data = this._params.dashboard;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
} else {
|
||||
const suggestions = this._params.suggestions;
|
||||
this._data = {
|
||||
@@ -51,9 +55,12 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
require_admin: false,
|
||||
mode: "storage",
|
||||
};
|
||||
// New dashboards have no saved baseline, so track against an emptyobject to mark them dirty from the outset (keeps Create enabled).
|
||||
this._initDirtyTracking({ type: "deep" }, {});
|
||||
if (suggestions?.title) {
|
||||
this._fillUrlPath(suggestions.title);
|
||||
}
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +79,8 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const yamlMode = this._params.dashboard?.mode === "yaml";
|
||||
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
|
||||
const cancelButton = html`
|
||||
@@ -95,11 +104,11 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._params.dashboard?.mode === "yaml"
|
||||
${yamlMode
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
)
|
||||
@@ -142,10 +151,12 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateDashboard}
|
||||
.disabled=${(this._error && "url_path" in this._error) ||
|
||||
titleInvalid ||
|
||||
this._submitting}
|
||||
?autofocus=${this._params.dashboard?.mode === "yaml"}
|
||||
.disabled=${!yamlMode &&
|
||||
((this._error && "url_path" in this._error) ||
|
||||
titleInvalid ||
|
||||
this._submitting ||
|
||||
!this.isDirtyState)}
|
||||
?autofocus=${yamlMode}
|
||||
>
|
||||
${this._params.urlPath
|
||||
? this._params.dashboard?.mode === "storage"
|
||||
@@ -251,6 +262,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
} else {
|
||||
this._data = value;
|
||||
}
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private _fillUrlPath(title: string) {
|
||||
@@ -270,6 +282,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
? pickAvailableDashboardUrlPath(baseSlug, taken)
|
||||
: baseSlug,
|
||||
};
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
@@ -292,6 +305,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
this._data as LovelaceDashboardCreateParams
|
||||
);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
let localizedErrorMessage: string | undefined;
|
||||
|
||||
@@ -13,6 +13,7 @@ import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { PanelMutableParams } from "../../../../data/panel";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { PanelDetailDialogParams } from "./show-dialog-panel-detail";
|
||||
@@ -25,7 +26,9 @@ interface PanelDetailData {
|
||||
}
|
||||
|
||||
@customElement("dialog-panel-detail")
|
||||
export class DialogPanelDetail extends LitElement {
|
||||
export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: PanelDetailDialogParams;
|
||||
@@ -48,6 +51,7 @@ export class DialogPanelDetail extends LitElement {
|
||||
show_in_sidebar: params.showInSidebar,
|
||||
};
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -70,7 +74,7 @@ export class DialogPanelDetail extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.panel_detail.edit_panel"
|
||||
)}
|
||||
@@ -114,7 +118,7 @@ export class DialogPanelDetail extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updatePanel}
|
||||
.disabled=${titleInvalid || this._submitting}
|
||||
.disabled=${titleInvalid || this._submitting || !this.isDirtyState}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.update"
|
||||
@@ -171,6 +175,7 @@ export class DialogPanelDetail extends LitElement {
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._error = undefined;
|
||||
this._data = ev.detail.value;
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _handleError(err: any) {
|
||||
@@ -228,6 +233,7 @@ export class DialogPanelDetail extends LitElement {
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await this._params!.updatePanel(updates);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._handleError(err);
|
||||
|
||||
@@ -9,6 +9,7 @@ import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail";
|
||||
|
||||
@@ -30,7 +31,9 @@ const detectResourceType = (url?: string) => {
|
||||
};
|
||||
|
||||
@customElement("dialog-lovelace-resource-detail")
|
||||
export class DialogLovelaceResourceDetail extends LitElement {
|
||||
export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
Partial<LovelaceResourcesMutableParams>
|
||||
>()(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceResourceDetailsDialogParams;
|
||||
@@ -57,6 +60,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
};
|
||||
}
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -83,7 +87,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${dialogTitle}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
@@ -119,7 +123,10 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateResource}
|
||||
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
|
||||
.disabled=${urlInvalid ||
|
||||
!this._data?.res_type ||
|
||||
this._submitting ||
|
||||
!this.isDirtyState}
|
||||
>
|
||||
${this._params.resource
|
||||
? this.hass!.localize(
|
||||
@@ -203,6 +210,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
if (!this._data!.res_type) {
|
||||
const type = detectResourceType(this._data!.url);
|
||||
if (!type) {
|
||||
this._updateDirtyState(this._data!);
|
||||
return;
|
||||
}
|
||||
this._data = {
|
||||
@@ -210,6 +218,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
res_type: type,
|
||||
};
|
||||
}
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _updateResource() {
|
||||
@@ -226,7 +235,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
|
||||
this._data! as LovelaceResourcesMutableParams
|
||||
);
|
||||
}
|
||||
this._params = undefined;
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = { base: err?.message || "Unknown error" };
|
||||
} finally {
|
||||
|
||||
@@ -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")}
|
||||
@@ -262,7 +287,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
.disabled=${nameInvalid || this._submitting || !this.isDirtyState}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.common.save")
|
||||
@@ -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";
|
||||
|
||||
@@ -20,12 +20,24 @@ import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
} from "../../../data/user";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
|
||||
interface AddUserFormState {
|
||||
name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
passwordConfirm?: string;
|
||||
isAdmin?: boolean;
|
||||
localOnly?: boolean;
|
||||
}
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends LitElement {
|
||||
export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _loading = false;
|
||||
@@ -70,6 +82,18 @@ export class DialogAddUser extends LitElement {
|
||||
}
|
||||
|
||||
this._open = true;
|
||||
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{
|
||||
name: this._name,
|
||||
username: this._username,
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
isAdmin: false,
|
||||
localOnly: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
@@ -89,7 +113,7 @@ export class DialogAddUser extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.caption"
|
||||
)}
|
||||
@@ -242,6 +266,7 @@ export class DialogAddUser extends LitElement {
|
||||
|
||||
if (parts.length) {
|
||||
this._username = parts[0].toLowerCase();
|
||||
this._publishDirtyState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,16 +274,30 @@ export class DialogAddUser extends LitElement {
|
||||
this._error = undefined;
|
||||
const target = ev.target as HaInput;
|
||||
this[`_${target.name}`] = target.value;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private async _adminChanged(ev: Event): Promise<void> {
|
||||
const target = ev.target as HaSwitch;
|
||||
this._isAdmin = target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev: Event): void {
|
||||
const target = ev.target as HaSwitch;
|
||||
this._localOnly = target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _publishDirtyState(): void {
|
||||
this._updateDirtyState({
|
||||
name: this._name,
|
||||
username: this._username,
|
||||
password: this._password,
|
||||
passwordConfirm: this._passwordConfirm,
|
||||
isAdmin: this._isAdmin,
|
||||
localOnly: this._localOnly,
|
||||
});
|
||||
}
|
||||
|
||||
private async _createUser(ev: Event) {
|
||||
@@ -306,6 +345,7 @@ export class DialogAddUser extends LitElement {
|
||||
},
|
||||
];
|
||||
this._params!.userAddedCallback(user);
|
||||
this._markDirtyStateClean();
|
||||
this._close();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import { adminChangePassword } from "../../../data/auth";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
@@ -43,7 +44,9 @@ interface FormData {
|
||||
}
|
||||
|
||||
@customElement("dialog-admin-change-password")
|
||||
class DialogAdminChangePassword extends LitElement {
|
||||
class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: AdminChangePasswordDialogParams;
|
||||
@@ -65,7 +68,10 @@ class DialogAdminChangePassword extends LitElement {
|
||||
this._userId = params.userId;
|
||||
this._data = undefined;
|
||||
this._error = undefined;
|
||||
this._submitting = false;
|
||||
this._success = false;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "shallow" }, {});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -117,7 +123,7 @@ class DialogAdminChangePassword extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.users.change_password.caption"
|
||||
)}
|
||||
@@ -173,6 +179,7 @@ class DialogAdminChangePassword extends LitElement {
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this._data = ev.detail.value;
|
||||
this._updateDirtyState(this._data ?? {});
|
||||
this._validate();
|
||||
}
|
||||
|
||||
@@ -185,6 +192,7 @@ class DialogAdminChangePassword extends LitElement {
|
||||
this._userId!,
|
||||
this._data.new_password
|
||||
);
|
||||
this._markDirtyStateClean();
|
||||
this._success = true;
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
|
||||
@@ -24,13 +24,23 @@ import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password";
|
||||
import type { UserDetailDialogParams } from "./show-dialog-user-detail";
|
||||
|
||||
interface UserDetailFormState {
|
||||
name: string;
|
||||
isAdmin?: boolean;
|
||||
localOnly?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
@customElement("dialog-user-detail")
|
||||
class DialogUserDetail extends LitElement {
|
||||
class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _name!: string;
|
||||
@@ -57,6 +67,15 @@ class DialogUserDetail extends LitElement {
|
||||
this._localOnly = params.entry.local_only;
|
||||
this._isActive = params.entry.is_active;
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{
|
||||
name: this._name,
|
||||
isAdmin: this._isAdmin,
|
||||
localOnly: this._localOnly,
|
||||
isActive: this._isActive,
|
||||
}
|
||||
);
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -69,177 +88,164 @@ class DialogUserDetail extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${user.name}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${
|
||||
this._error
|
||||
? html`<div class="error">${this._error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${this._error
|
||||
? html`<div class="error">${this._error}</div>`
|
||||
: nothing}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.panel.config.users.editor.id")}:
|
||||
${user.id}<br />
|
||||
</div>
|
||||
${
|
||||
badges.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<div class="badge-container">
|
||||
${badges.map(
|
||||
([icon, label]) => html`
|
||||
<ha-label>
|
||||
<ha-svg-icon slot="icon" .path=${icon}></ha-svg-icon>
|
||||
${label}
|
||||
</ha-label>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${badges.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<div class="badge-container">
|
||||
${badges.map(
|
||||
([icon, label]) => html`
|
||||
<ha-label>
|
||||
<ha-svg-icon slot="icon" .path=${icon}></ha-svg-icon>
|
||||
${label}
|
||||
</ha-label>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
<div class="form">
|
||||
${
|
||||
!user.system_generated
|
||||
? html`
|
||||
<ha-input
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.users.editor.name"
|
||||
)}
|
||||
></ha-input>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.username"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">${user.username}</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changeUsername}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!user.system_generated && this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.password"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">************</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changePassword}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isActive}
|
||||
@change=${this._activeChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated}
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
></ha-switch>
|
||||
</ha-switch>
|
||||
</ha-row-item>
|
||||
${
|
||||
!this._isAdmin && !user.system_generated
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
user.system_generated
|
||||
${!user.system_generated
|
||||
? html`
|
||||
<ha-input
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.users.editor.name"
|
||||
)}
|
||||
></ha-input>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.username"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">${user.username}</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changeUsername}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.password"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">************</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changePassword}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isActive}
|
||||
@change=${this._activeChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated}
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
${!this._isAdmin && !user.system_generated
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_read_only_users"
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
${user.system_generated
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_read_only_users"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
@@ -248,18 +254,19 @@ class DialogUserDetail extends LitElement {
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._deleteEntry}
|
||||
.disabled=${
|
||||
this._submitting || user.system_generated || user.is_owner
|
||||
}
|
||||
.disabled=${this._submitting ||
|
||||
user.system_generated ||
|
||||
user.is_owner}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${
|
||||
!this._name || this._submitting || user.system_generated
|
||||
}
|
||||
.disabled=${!this._name ||
|
||||
this._submitting ||
|
||||
user.system_generated ||
|
||||
!this.isDirtyState}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
@@ -271,18 +278,31 @@ class DialogUserDetail extends LitElement {
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _adminChanged(ev): void {
|
||||
this._isAdmin = ev.target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev): void {
|
||||
this._localOnly = ev.target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _activeChanged(ev): void {
|
||||
this._isActive = ev.target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _publishDirtyState(): void {
|
||||
this._updateDirtyState({
|
||||
name: this._name,
|
||||
isAdmin: this._isAdmin,
|
||||
localOnly: this._localOnly,
|
||||
isActive: this._isActive,
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
@@ -296,6 +316,7 @@ class DialogUserDetail extends LitElement {
|
||||
],
|
||||
local_only: this._localOnly,
|
||||
});
|
||||
this._markDirtyStateClean();
|
||||
this._close();
|
||||
} catch (err: any) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
@@ -308,6 +329,7 @@ class DialogUserDetail extends LitElement {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry()) {
|
||||
this._markDirtyStateClean();
|
||||
this._close();
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -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 ||
|
||||
@@ -234,6 +239,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
slot="primaryAction"
|
||||
@click=${this._updatePipeline}
|
||||
.loading=${this._submitting}
|
||||
.disabled=${!this.isDirtyState}
|
||||
>
|
||||
${isExistingPipeline
|
||||
? this.hass.localize(
|
||||
@@ -266,6 +272,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 +306,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";
|
||||
|
||||
@@ -8,6 +8,7 @@ import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import "../../../components/ha-button";
|
||||
import type { HomeZoneMutableParams } from "../../../data/zone";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeZoneDetailDialogParams } from "./show-dialog-home-zone-detail";
|
||||
@@ -21,7 +22,9 @@ const SCHEMA = [
|
||||
];
|
||||
|
||||
@customElement("dialog-home-zone-detail")
|
||||
class DialogHomeZoneDetail extends LitElement {
|
||||
class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
@@ -43,6 +46,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
longitude: this.hass.config.longitude,
|
||||
radius: this.hass.config.radius,
|
||||
};
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -71,7 +75,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
header-title=${this.hass!.localize("ui.common.edit_item", {
|
||||
name: this._data.name,
|
||||
})}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-form
|
||||
@@ -94,7 +98,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!valid || this._submitting}
|
||||
.disabled=${!valid || this._submitting || !this.isDirtyState}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
@@ -120,6 +124,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
value.radius = value.location.radius;
|
||||
delete value.location;
|
||||
this._data = value;
|
||||
this._updateDirtyState(value);
|
||||
}
|
||||
|
||||
private _computeLabel = (): string => "";
|
||||
|
||||
@@ -11,12 +11,15 @@ import "../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import type { ZoneMutableParams } from "../../../data/zone";
|
||||
import { getZoneEditorInitData } from "../../../data/zone";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
||||
|
||||
@customElement("dialog-zone-detail")
|
||||
class DialogZoneDetail extends LitElement {
|
||||
class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
@@ -53,6 +56,7 @@ class DialogZoneDetail extends LitElement {
|
||||
radius: 100,
|
||||
};
|
||||
}
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -93,7 +97,7 @@ class DialogZoneDetail extends LitElement {
|
||||
name: this._params.entry.name,
|
||||
})
|
||||
: this.hass!.localize("ui.panel.config.zone.detail.new_zone")}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-form
|
||||
@@ -131,7 +135,7 @@ class DialogZoneDetail extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!valid || this._submitting}
|
||||
.disabled=${!valid || this._submitting || !this.isDirtyState}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.common.save")
|
||||
@@ -189,6 +193,7 @@ class DialogZoneDetail extends LitElement {
|
||||
delete value.icon;
|
||||
}
|
||||
this._data = value;
|
||||
this._updateDirtyState(value);
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
|
||||
@@ -32,6 +32,7 @@ import type { EnergyDevicesGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import "../../../../components/ha-card";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { measureTextWidth } from "../../../../util/text";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -189,9 +190,11 @@ export class HuiEnergyDevicesGraphCard
|
||||
)}
|
||||
.height=${`${Math.max(modes.includes("pie") ? 300 : 100, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.extraComponents=${[PieChart]}
|
||||
click-label-for-more-info
|
||||
@chart-click=${this._handleChartClick}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -543,11 +546,20 @@ export class HuiEnergyDevicesGraphCard
|
||||
chartData.splice(this._config.max_devices);
|
||||
}
|
||||
|
||||
this._legendData = chartData.map((d) => ({
|
||||
...d,
|
||||
name: this._getDeviceName(d.name),
|
||||
value: `${formatNumber(d.value[0], this.hass.locale)} kWh`,
|
||||
}));
|
||||
this._legendData = chartData.map((d) => {
|
||||
const id = (d as any).id as string;
|
||||
return {
|
||||
...d,
|
||||
name: this._getDeviceName(d.name),
|
||||
value: `${formatNumber(d.value[0], this.hass.locale)} kWh`,
|
||||
// Untracked is synthetic and external statistics aren't real entities,
|
||||
// so their labels can't open more-info; fall back to toggling visibility.
|
||||
noLabelClick:
|
||||
id === "untracked" ||
|
||||
isExternalStatistic(id) ||
|
||||
!(id in this.hass.states),
|
||||
};
|
||||
});
|
||||
// filter out hidden stats in place
|
||||
for (let i = chartData.length - 1; i >= 0; i--) {
|
||||
if (this._hiddenStats.includes((chartData[i] as any).id)) {
|
||||
@@ -579,7 +591,11 @@ export class HuiEnergyDevicesGraphCard
|
||||
e.detail.event?.target?.type === "tspan" // label
|
||||
) {
|
||||
const id = (e.detail.data as any).id as string;
|
||||
if (id !== "untracked") {
|
||||
if (
|
||||
id !== "untracked" &&
|
||||
!isExternalStatistic(id) &&
|
||||
this.hass.states[id]
|
||||
) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: id,
|
||||
});
|
||||
@@ -587,6 +603,16 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLegendLabelClick(
|
||||
ev: HASSDomEvent<HASSDomEvents["legend-label-click"]>
|
||||
) {
|
||||
const entityId = ev.detail.id;
|
||||
if (isExternalStatistic(entityId) || !this.hass.states[entityId]) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
private _handleChartTypeChange(): void {
|
||||
if (!this._chartType) {
|
||||
return;
|
||||
|
||||
@@ -49,6 +49,9 @@ const addEntityId = (entities: Set<string>, entity) => {
|
||||
};
|
||||
|
||||
const addEntities = (entities: Set<string>, obj) => {
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
if (obj.entity) {
|
||||
addEntityId(entities, obj.entity);
|
||||
}
|
||||
|
||||
+13
-2
@@ -12,6 +12,7 @@ import "../../../../../components/ha-dropdown";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import type { LovelaceStrategyConfig } from "../../../../../data/lovelace/config/strategy";
|
||||
import { DirtyStateProviderMixin } from "../../../../../mixins/dirty-state-provider-mixin";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
@@ -27,7 +28,9 @@ import type { DashboardStrategyEditorDialogParams } from "./show-dialog-dashboar
|
||||
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
|
||||
|
||||
@customElement("dialog-dashboard-strategy-editor")
|
||||
class DialogDashboardStrategyEditor extends LitElement {
|
||||
class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStrategyConfig>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DashboardStrategyEditorDialogParams;
|
||||
@@ -49,6 +52,7 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
this._params = params;
|
||||
this._strategyConfig = params.config.strategy;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._strategyConfig);
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -68,6 +72,7 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
this._strategyConfig = ev.detail.config as LovelaceStrategyConfig;
|
||||
this._updateDirtyState(this._strategyConfig);
|
||||
}
|
||||
|
||||
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
|
||||
@@ -82,6 +87,7 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
strategy: this._strategyConfig!,
|
||||
});
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -137,6 +143,7 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${title || "-"}
|
||||
header-subtitle=${ifDefined(this._params.title)}
|
||||
width="large"
|
||||
@@ -195,7 +202,11 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
?disabled=${!this.isDirtyState}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
@@ -53,7 +54,7 @@ const TABS = ["tab-settings", "tab-visibility"] as const;
|
||||
|
||||
@customElement("hui-dialog-edit-section")
|
||||
export class HuiDialogEditSection
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<LovelaceSectionRawConfig>()(LitElement)
|
||||
implements HassDialog<EditSectionDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -96,6 +97,7 @@ export class HuiDialogEditSection
|
||||
this._viewConfig = findLovelaceContainer(this._params.lovelaceConfig, [
|
||||
this._params.viewIndex,
|
||||
]);
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -159,7 +161,7 @@ export class HuiDialogEditSection
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${classMap({
|
||||
@@ -231,7 +233,11 @@ export class HuiDialogEditSection
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
?disabled=${!this.isDirtyState}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
@@ -242,6 +248,7 @@ export class HuiDialogEditSection
|
||||
private _configChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(this._config!);
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
@@ -399,6 +406,7 @@ export class HuiDialogEditSection
|
||||
return;
|
||||
}
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(this._config!);
|
||||
}
|
||||
|
||||
private _ignoreKeydown(ev: KeyboardEvent) {
|
||||
@@ -423,6 +431,7 @@ export class HuiDialogEditSection
|
||||
);
|
||||
|
||||
this._params.saveConfig(newConfig);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { undoDepth } from "@codemirror/commands";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Lovelace } from "./types";
|
||||
@@ -33,7 +33,9 @@ const strategyStruct = type({
|
||||
});
|
||||
|
||||
@customElement("hui-editor")
|
||||
class LovelaceFullConfigEditor extends LitElement {
|
||||
class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
|
||||
LitElement
|
||||
) {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -44,8 +46,6 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
@state() private _saving?: boolean;
|
||||
|
||||
@state() private _changed?: boolean;
|
||||
|
||||
private _config?: LovelaceRawConfig;
|
||||
|
||||
private _yamlError?: string;
|
||||
@@ -66,10 +66,10 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
slot="actionItems"
|
||||
class="save-button
|
||||
${classMap({
|
||||
saved: this._saving === false || this._changed === true,
|
||||
saved: this._saving === false || this.isDirtyState,
|
||||
})}"
|
||||
>
|
||||
${this._changed
|
||||
${this.isDirtyState
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.unsaved_changes"
|
||||
)
|
||||
@@ -78,7 +78,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
<ha-button
|
||||
slot="actionItems"
|
||||
@click=${this._handleSave}
|
||||
.disabled=${!this._changed}
|
||||
.disabled=${!this.isDirtyState}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.save"
|
||||
)}</ha-button
|
||||
@@ -98,7 +98,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.yamlEditor.setValue(this.lovelace!.rawConfig);
|
||||
this._setValue();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
@@ -110,10 +110,19 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
oldLovelace.rawConfig !== this.lovelace.rawConfig &&
|
||||
!deepEqual(oldLovelace.rawConfig, this.lovelace.rawConfig)
|
||||
) {
|
||||
this.yamlEditor.setValue(this.lovelace!.rawConfig);
|
||||
this._setValue();
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue() {
|
||||
this.yamlEditor.setValue(this.lovelace!.rawConfig);
|
||||
// Baseline the dirty check against the loaded YAML so it resets on save.
|
||||
this._initDirtyTracking(
|
||||
{ type: "custom", compare: (a, b) => a === b },
|
||||
this.yamlEditor.yaml
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -158,17 +167,17 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
this._config = ev.detail.isValid ? ev.detail.value : undefined;
|
||||
this._yamlError = ev.detail.errorMsg;
|
||||
this._changed = undoDepth(this.yamlEditor.codemirror!.state) > 0;
|
||||
if (this._changed && !window.onbeforeunload) {
|
||||
this._updateDirtyState(this.yamlEditor.yaml);
|
||||
if (this.isDirtyState && !window.onbeforeunload) {
|
||||
window.onbeforeunload = () => true;
|
||||
} else if (!this._changed && window.onbeforeunload) {
|
||||
} else if (!this.isDirtyState && window.onbeforeunload) {
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _closeEditor() {
|
||||
if (
|
||||
this._changed &&
|
||||
this.isDirtyState &&
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.confirm_unsaved_changes"
|
||||
@@ -279,7 +288,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
window.onbeforeunload = null;
|
||||
this._changed = false;
|
||||
this._markDirtyStateClean();
|
||||
this._saving = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,16 +60,24 @@ export const createLogMessage = async (
|
||||
// - a possible list of aggregated errors
|
||||
if (error instanceof Error) {
|
||||
lines.push(error.toString() || messageFallback);
|
||||
const stackLines = (await fromError(error))
|
||||
.slice(0, MAX_STACK_FRAMES)
|
||||
.map((frame) => {
|
||||
frame.fileName ??= "";
|
||||
if (URL.canParse(frame.fileName)) {
|
||||
frame.fileName = new URL(frame.fileName).pathname;
|
||||
}
|
||||
frame.fileName = frame.fileName.replace(REMOVAL_PATHS, "");
|
||||
return frame.toString();
|
||||
});
|
||||
let stackLines: (string | undefined)[];
|
||||
try {
|
||||
stackLines = (await fromError(error))
|
||||
.slice(0, MAX_STACK_FRAMES)
|
||||
.map((frame) => {
|
||||
frame.fileName ??= "";
|
||||
if (URL.canParse(frame.fileName)) {
|
||||
frame.fileName = new URL(frame.fileName).pathname;
|
||||
}
|
||||
frame.fileName = frame.fileName.replace(REMOVAL_PATHS, "");
|
||||
return frame.toString();
|
||||
});
|
||||
} catch {
|
||||
// stacktrace-js cannot always parse a stack (for example a DOMException
|
||||
// with no, or an unrecognized, stack), so fall back to the raw stack
|
||||
// instead of letting the error logger itself throw.
|
||||
stackLines = error.stack ? [error.stack] : [];
|
||||
}
|
||||
lines.push(...(stackLines.length > 0 ? stackLines : [stackFallback]));
|
||||
// @ts-expect-error Requires library bump to ES2022
|
||||
if (error.cause) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createLogMessage } from "../../src/resources/log-message";
|
||||
|
||||
const fromError = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("stacktrace-js", () => ({ fromError }));
|
||||
|
||||
describe("createLogMessage", () => {
|
||||
beforeEach(() => {
|
||||
fromError.mockReset();
|
||||
});
|
||||
|
||||
it("includes the error message and parsed stack frames", async () => {
|
||||
fromError.mockResolvedValue([
|
||||
{ fileName: "https://example.com/foo.js", toString: () => "at foo.js" },
|
||||
]);
|
||||
const error = new Error("boom");
|
||||
|
||||
const message = await createLogMessage(error);
|
||||
|
||||
expect(message).toContain("Error: boom");
|
||||
expect(message).toContain("at foo.js");
|
||||
});
|
||||
|
||||
it("does not throw when stacktrace-js cannot parse the stack", async () => {
|
||||
fromError.mockRejectedValue(new Error("Cannot parse given Error object"));
|
||||
const error = new Error("boom");
|
||||
error.stack = "Error: boom\n at <anonymous>";
|
||||
|
||||
const message = await createLogMessage(error);
|
||||
|
||||
expect(message).toContain("Error: boom");
|
||||
// Falls back to the raw stack instead of crashing the logger.
|
||||
expect(message).toContain("at <anonymous>");
|
||||
});
|
||||
|
||||
it("falls back to the provided stack fallback when no stack is available", async () => {
|
||||
fromError.mockRejectedValue(new Error("Cannot parse given Error object"));
|
||||
const error = new Error("boom");
|
||||
error.stack = undefined;
|
||||
|
||||
const message = await createLogMessage(
|
||||
error,
|
||||
undefined,
|
||||
undefined,
|
||||
"@unknown:0:0"
|
||||
);
|
||||
|
||||
expect(message).toContain("@unknown:0:0");
|
||||
});
|
||||
});
|
||||
@@ -1352,14 +1352,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/lint@npm:6.9.6, @codemirror/lint@npm:^6.0.0":
|
||||
version: 6.9.6
|
||||
resolution: "@codemirror/lint@npm:6.9.6"
|
||||
"@codemirror/lint@npm:6.9.7, @codemirror/lint@npm:^6.0.0":
|
||||
version: 6.9.7
|
||||
resolution: "@codemirror/lint@npm:6.9.7"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.0.0"
|
||||
"@codemirror/view": "npm:^6.42.0"
|
||||
crelt: "npm:^1.0.5"
|
||||
checksum: 10/70ed80eaec81038c014a89d8b4ba17396562b7dd541882fa981e2f81f7cdcbd67725a7c055c7772ece3bd052a276976d873f71746fc550b1aede7e18faa32f93
|
||||
checksum: 10/f1af8295e1741a8d0c155cd7552c35d21c859d8848571fa084f5a6f2a50cac51ed25f04e423cab0f094e11b4828a54de4383a97b6b8cf6a242c5941034bffa04
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1383,15 +1383,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/view@npm:6.43.0, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
|
||||
version: 6.43.0
|
||||
resolution: "@codemirror/view@npm:6.43.0"
|
||||
"@codemirror/view@npm:6.43.1, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
|
||||
version: 6.43.1
|
||||
resolution: "@codemirror/view@npm:6.43.1"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.6.0"
|
||||
crelt: "npm:^1.0.6"
|
||||
style-mod: "npm:^4.1.0"
|
||||
w3c-keyname: "npm:^2.2.4"
|
||||
checksum: 10/7cfeebe1507f71a960dfb2d5152400507d28ed5827680bc73e0a093bfba9a796c2e559c960fd2b046379fac31ff0b59663dfc481baadf1d6ececd71eb5b48014
|
||||
checksum: 10/a867a26cb511f161a8ef60b2574afae2561cfe94abd822e0af2a6e4bd94d1ff46f0d5085428c89f6f6b80a52482c8d23f80362173491903f49fb93462a216b95
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4548,105 +4548,105 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.60.1"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.61.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@typescript-eslint/scope-manager": "npm:8.60.1"
|
||||
"@typescript-eslint/type-utils": "npm:8.60.1"
|
||||
"@typescript-eslint/utils": "npm:8.60.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.60.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.61.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.61.0"
|
||||
"@typescript-eslint/utils": "npm:8.61.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.0"
|
||||
ignore: "npm:^7.0.5"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.60.1
|
||||
"@typescript-eslint/parser": ^8.61.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/f3633bb2700bc32299578baeaf6650418656229be256147ba9d1ab09b34ef2b7fed83804ef4d2439e9189dbdcb89399d67bc8fea55262be6caa32114be048538
|
||||
checksum: 10/ca7fbaa2f03ec15bdbf39d2e4d42f1b682085f23830591d1d6c3d9f497fdda497341b2fa67c8d366514a3c22807557e45e7afe1ee70cef527b184250e5422e8f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/parser@npm:8.60.1"
|
||||
"@typescript-eslint/parser@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.61.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.60.1"
|
||||
"@typescript-eslint/types": "npm:8.60.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.60.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.60.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.61.0"
|
||||
"@typescript-eslint/types": "npm:8.61.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/f9c484c4a3897015328f328a1c6ee778d113dd134201f635c0421cb72efe6e63f3a68524aff0df6e19e76ff93daf5cabd946e67f12f10dddcf19bda534aa68dc
|
||||
checksum: 10/82060c36786339867d63337708a08bd4bc65569313998bd086dbe6b901664082c7e40d6b6e085296a459cd4fc1d064479ef570b51e1eb113688bb152a7a6d689
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/project-service@npm:8.60.1"
|
||||
"@typescript-eslint/project-service@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.61.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.60.1"
|
||||
"@typescript-eslint/types": "npm:^8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.61.0"
|
||||
"@typescript-eslint/types": "npm:^8.61.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/fec693dd79c3a1e6a24091127a37af4eb9d9cee8192cf2a434adae48543eadff834bc0623b5b563c8b592b250bc080570f9e7b42807252ea898442c525beeee9
|
||||
checksum: 10/b7d7e973b565f604af43b8afb3ca1c3fbe6fcf16863bde83b42417a196ba9f3a5a3f5d39bf57ed96b8ce577047064d93c353ecb21db5e95dce69f81335c9cd81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.60.1"
|
||||
"@typescript-eslint/scope-manager@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.61.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.60.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.60.1"
|
||||
checksum: 10/7228c110410ff8cfc01e96d8f17c986f8b4dd447fe3a3291baaab8fe946026ccdf0291865f788f18cf538ab49bfc067fe797708b6b8590104a65f7e69f921cc5
|
||||
"@typescript-eslint/types": "npm:8.61.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.0"
|
||||
checksum: 10/295e306665d64f0330fede3fe72febd65c67c3083d747149b66097aa6f7d517f25731dc1dbec900b15768c40f92b082f501296e7524855fe82697f40b8d23ce1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.60.1, @typescript-eslint/tsconfig-utils@npm:^8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.61.0, @typescript-eslint/tsconfig-utils@npm:^8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.61.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/afc78b19b856a71dc4e493f931ae44e1a91dc6441a14cb92e4063db880892f3874768f9d347d4b2f45362f2090e4455407c70f42027d77ddc85d6cba95cdb76c
|
||||
checksum: 10/f678ff5ec887a27d8e590e0c67403b12e372a027ab036dcfc1e3ef614d3bed7a3c455a65fa0a87ff7dae5b0ad1c49cf4aa40639cc368d7eb424efe8349d9cb9f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.60.1"
|
||||
"@typescript-eslint/type-utils@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.61.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.60.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.60.1"
|
||||
"@typescript-eslint/utils": "npm:8.60.1"
|
||||
"@typescript-eslint/types": "npm:8.61.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.0"
|
||||
"@typescript-eslint/utils": "npm:8.61.0"
|
||||
debug: "npm:^4.4.3"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/6f426263be597063831bf308e52328e8d387af5db955a09cb85fde1c72f5b1b36a365133b9c9a74330e5e948e59bf9a9b82605f4c9c4e3bf9b6cb7f4c37e4b18
|
||||
checksum: 10/8290e5fc26241dfd5aeeffad0fb9857a3fa1f9c8107dfb01638970297e0e17be6088f0fd2d6fc7d450e9879afaa7e23f4111182bcf0b625eba74fdf13100b19e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.60.1, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/types@npm:8.60.1"
|
||||
checksum: 10/c603417e621b5b1263c2f60fad9e202d560fd07fce7f40e9a356c0530e5eaf0ff1a9af865237bf93aa18a5a4e2f034ee0cce0fe6c070f08df33e35a099bdea47
|
||||
"@typescript-eslint/types@npm:8.61.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/types@npm:8.61.0"
|
||||
checksum: 10/8e1e1cf5d092beed1a974b3b5d7cc20219ad3e4501b85bbef5bec1c81ab50b09ee70093ad2195c3061c499e804d63aac38dcc20293342b1fa774ba743c0d63bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.60.1"
|
||||
"@typescript-eslint/typescript-estree@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.61.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.60.1"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.60.1"
|
||||
"@typescript-eslint/types": "npm:8.60.1"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.60.1"
|
||||
"@typescript-eslint/project-service": "npm:8.61.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.61.0"
|
||||
"@typescript-eslint/types": "npm:8.61.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.61.0"
|
||||
debug: "npm:^4.4.3"
|
||||
minimatch: "npm:^10.2.2"
|
||||
semver: "npm:^7.7.3"
|
||||
@@ -4654,32 +4654,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/9c3a56266aadf589bc6e770cd04cb3f55b1ee1507dcacda61866408c656ae4462aa7e11baf39eb939bc4d1e3b843cf58e60f3ebdeb3e75f042ff0f6fb39c311b
|
||||
checksum: 10/6d5ab7850226de23ab26d94388f729e792413f5a9e704c8c685b66eb20946efeb290cda91195f062e1065bc20129ec8f5955768316660132087347e66dec0d1a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/utils@npm:8.60.1"
|
||||
"@typescript-eslint/utils@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.61.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.9.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.60.1"
|
||||
"@typescript-eslint/types": "npm:8.60.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.60.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.61.0"
|
||||
"@typescript-eslint/types": "npm:8.61.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/a75f8714995b6280b4c15ca957bbc6634862453461111e4a2a07b8bc72b51a504484a9b957fc5b7a646c4bf09f1e414a0c52cd3b6798c42fb8c4de83b1b5a364
|
||||
checksum: 10/50ff451edb8e5dee92bbab11a75cbd570715623d89094d0541ddfbef208248e82d2f9478d1e09fb9c94496069afd4db9521384b77f7aaa63970f7edfebddfba9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.60.1"
|
||||
"@typescript-eslint/visitor-keys@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.61.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.60.1"
|
||||
"@typescript-eslint/types": "npm:8.61.0"
|
||||
eslint-visitor-keys: "npm:^5.0.0"
|
||||
checksum: 10/6d120b4a790477ae0291e69f6457686c71b929cc40519148f6b6c7fbc09604b15821ae8cf1005aa23acec5105b4016db256a68d68f30eda8d6c24d4fdb0ede86
|
||||
checksum: 10/243018d9d8b1918d2863e50eec6628c792ccda05ad5534f5153fc783b7f54cdb8a58d758eb74260d113274bfab8bb38ad4664f3db9e7d3f844cdffbe6e47e285
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8442,10 +8442,10 @@ __metadata:
|
||||
"@codemirror/lang-jinja": "npm:6.0.1"
|
||||
"@codemirror/lang-yaml": "npm:6.1.3"
|
||||
"@codemirror/language": "npm:6.12.3"
|
||||
"@codemirror/lint": "npm:6.9.6"
|
||||
"@codemirror/lint": "npm:6.9.7"
|
||||
"@codemirror/search": "npm:6.7.0"
|
||||
"@codemirror/state": "npm:6.6.0"
|
||||
"@codemirror/view": "npm:6.43.0"
|
||||
"@codemirror/view": "npm:6.43.1"
|
||||
"@date-fns/tz": "npm:1.5.0"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@eslint/js": "npm:10.0.1"
|
||||
@@ -8570,7 +8570,7 @@ __metadata:
|
||||
node-vibrant: "npm:4.0.4"
|
||||
object-hash: "npm:3.0.0"
|
||||
pinst: "npm:3.0.0"
|
||||
prettier: "npm:3.8.3"
|
||||
prettier: "npm:3.8.4"
|
||||
punycode: "npm:2.3.1"
|
||||
qr-scanner: "npm:1.4.2"
|
||||
qrcode: "npm:1.5.4"
|
||||
@@ -8587,7 +8587,7 @@ __metadata:
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.60.1"
|
||||
typescript-eslint: "npm:8.61.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.8"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
@@ -11350,12 +11350,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prettier@npm:3.8.3":
|
||||
version: 3.8.3
|
||||
resolution: "prettier@npm:3.8.3"
|
||||
"prettier@npm:3.8.4":
|
||||
version: 3.8.4
|
||||
resolution: "prettier@npm:3.8.4"
|
||||
bin:
|
||||
prettier: bin/prettier.cjs
|
||||
checksum: 10/4b3b12cbb29e4c96bed936e5d070167552500c18d37676fb3e0caae6199c42860662608e4dc116230698f6e2bb0267ef2548158224c92d40f188d309d72fdd6f
|
||||
checksum: 10/54684a3cc6689238692b29fab541c01934af7677be94c02293ba49981a1ac121c8bebe2a865f0c3b963e99d208f847c53aed354cc0ce8750e2d45791d64506c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -13461,18 +13461,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.60.1":
|
||||
version: 8.60.1
|
||||
resolution: "typescript-eslint@npm:8.60.1"
|
||||
"typescript-eslint@npm:8.61.0":
|
||||
version: 8.61.0
|
||||
resolution: "typescript-eslint@npm:8.61.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.60.1"
|
||||
"@typescript-eslint/parser": "npm:8.60.1"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.60.1"
|
||||
"@typescript-eslint/utils": "npm:8.60.1"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.61.0"
|
||||
"@typescript-eslint/parser": "npm:8.61.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.61.0"
|
||||
"@typescript-eslint/utils": "npm:8.61.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/e12091ab2540b817c76b0ec6aad92e341f810310bec2b24bc95780aee106049c05363998f6ea52ed066130c8afc41dca1627f56e4c1df1dd519f4d4ca0ce4910
|
||||
checksum: 10/5a21c6ef76400ea30a47629087787834abc1c17e4b406465dfd8c204ef635556f8e3a775d89c46f9eb175ebd6a218284685e935877a2b148c482f0478627bdf9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user