Compare commits

..

1 Commits

Author SHA1 Message Date
Aidan Timson 900efcba6c Add a pull request standards workflow 2026-06-11 12:46:43 +01:00
40 changed files with 612 additions and 981 deletions
+1
View File
@@ -30,6 +30,7 @@ jobs:
"Do Not Review",
"Blocked",
"has-parent",
"Needs Template",
];
const prLabels = context.payload.pull_request.labels.map(
(l) => l.name
@@ -0,0 +1,185 @@
name: Pull request standards
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, no PR code checkout
types:
- opened
- edited
- reopened
- ready_for_review
branches:
- dev
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
check:
name: Check pull request follows contribution standards
runs-on: ubuntu-latest
permissions:
pull-requests: write # To label and comment on pull requests
steps:
- name: Check pull request standards
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = context.payload.pull_request;
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
if (pr.user.type === "Bot") {
core.info(`Skipping bot author: ${pr.user.login}`);
return;
}
if (pr.draft) {
core.info("Skipping draft pull request");
return;
}
try {
await github.rest.orgs.checkMembershipForUser({
org: "home-assistant",
username: pr.user.login,
});
core.info(`Skipping organization member: ${pr.user.login}`);
return;
} catch (error) {
core.info(`${pr.user.login} is not an organization member, checking standards`);
}
const label = "Needs Template";
const marker = "<!-- pr-standards-check -->";
const { owner, repo } = context.repo;
const issue_number = pr.number;
const body = (pr.body || "").replace(/<!--[\s\S]*?-->/g, "");
const normalized = body.toLowerCase();
// Ignore 404s from mutations that race manual edits or cancelled runs.
const ignoreMissing = async (fn) => {
try {
await fn();
} catch (error) {
if (error.status === 404) {
core.info("Target already removed, nothing to do");
return;
}
throw error;
}
};
// Hide/restore our comment via GraphQL (REST cannot minimize).
const setMinimized = async (subjectId, minimized) => {
const mutation = minimized
? `mutation($id: ID!) {
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
clientMutationId
}
}`
: `mutation($id: ID!) {
unminimizeComment(input: { subjectId: $id }) {
clientMutationId
}
}`;
try {
await github.graphql(mutation, { id: subjectId });
} catch (error) {
core.info(
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
);
}
};
// Content of a "## <name>" section, or null when the heading is absent.
const section = (name) => {
const match = body.match(
new RegExp(`##\\s*${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
);
return match ? match[1] : null;
};
const problems = [];
const requiredHeadings = [
"## proposed change",
"## type of change",
"## checklist",
];
if (requiredHeadings.some((h) => !normalized.includes(h))) {
problems.push(
"Use the pull request template without removing its sections."
);
}
const typeOfChange = section("type of change");
if (typeOfChange !== null) {
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
if (ticked !== 1) {
problems.push(
'Select exactly one option under "Type of change".'
);
}
}
const proposedChange = section("proposed change");
if (proposedChange !== null && proposedChange.trim().length === 0) {
problems.push('Describe your changes under "Proposed change".');
}
const isValid = problems.length === 0;
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number, per_page: 100 }
);
const existing = comments.find((c) => c.body.includes(marker));
const hasLabel = pr.labels.some((l) => l.name === label);
if (isValid) {
core.info("Pull request standards met");
if (hasLabel) {
await ignoreMissing(() =>
github.rest.issues.removeLabel({
owner, repo, issue_number, name: label,
})
);
}
if (existing) {
await setMinimized(existing.node_id, true);
}
return;
}
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
if (!hasLabel) {
await github.rest.issues.addLabels({
owner, repo, issue_number, labels: [label],
});
}
const message =
`${marker}\n` +
`Hey @${pr.user.login}!\n\n` +
`Thank you for your contribution! To help reviewers, please update ` +
`this pull request to follow our pull request standards:\n\n` +
problems.map((p) => `- ${p}`).join("\n") +
`\n\n` +
`Please complete the ` +
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
`for more on creating a great pull request (see point 6).`;
if (existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id, body: message,
});
await setMinimized(existing.node_id, false);
} else {
await github.rest.issues.createComment({
owner, repo, issue_number, body: message,
});
}
+4 -4
View File
@@ -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.7",
"@codemirror/lint": "6.9.6",
"@codemirror/search": "6.7.0",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.43.1",
"@codemirror/view": "6.43.0",
"@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.4",
"prettier": "3.8.3",
"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.61.0",
"typescript-eslint": "8.60.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.8",
"webpack-stats-plugin": "1.1.3",
+46 -86
View File
@@ -1,23 +1,5 @@
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],
>(
@@ -37,47 +19,11 @@ export function downSampleLineData<
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.ceil((max - min) / Math.floor(maxDetails));
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>();
// Group points into frames
const frames = new Map<
number,
{ point: (typeof data)[number]; x: number; y: number }[]
>();
for (const point of data) {
const pointData = getPointData(point);
@@ -89,39 +35,53 @@ export function downSampleLineData<
const frameIndex = Math.floor((x - min) / step);
const frame = frames.get(frameIndex);
if (!frame) {
frames.set(frameIndex, {
minPoint: point,
minX: x,
minY: y,
maxPoint: point,
maxX: x,
maxY: y,
});
frames.set(frameIndex, [{ point, x, y }]);
} else {
// 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;
}
frame.push({ point, x, y });
}
}
// Convert frames back to points
const result: T[] = [];
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);
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);
}
result.push(frame.minPoint as T);
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);
}
}
}
+1 -22
View File
@@ -1520,9 +1520,7 @@ export class HaChartBase extends LitElement {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
/* 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);
line-height: 1;
}
@media (hover: hover) {
.chart-legend .label.clickable:hover {
@@ -1560,25 +1558,6 @@ 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;
+1 -3
View File
@@ -128,13 +128,11 @@ export const addMatterDevice = (hass: HomeAssistant) => {
export const commissionMatterDevice = (
hass: HomeAssistant,
code: string,
networkOnly: boolean
code: string
): Promise<void> =>
hass.callWS({
type: "matter/commission",
code,
network_only: networkOnly,
});
export const acceptSharedMatterDevice = (
@@ -10,21 +10,13 @@ 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 DirtyStateProviderMixin<SystemOptionsState>()(
LitElement
) {
class DialogConfigEntrySystemOptions extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _disableNewEntities!: boolean;
@@ -46,13 +38,6 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
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;
}
@@ -83,7 +68,7 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
) || this._params.entry.domain,
}
)}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
@@ -150,7 +135,7 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting || !this.isDirtyState}
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.update"
@@ -164,19 +149,11 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
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> {
-67
View File
@@ -19,64 +19,6 @@ 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.
@@ -117,15 +59,6 @@ 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,28 +23,18 @@ 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 DirtyStateProviderMixin<CredentialFormState>()(
LitElement
) {
export class DialogAddApplicationCredential extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _loading = false;
@@ -86,7 +76,6 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
this._error = undefined;
this._loading = false;
this._open = true;
this._initDirtyTracking({ type: "shallow" }, this._currentState());
this._fetchConfig();
}
@@ -111,7 +100,10 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
<ha-dialog
.open=${this._open}
@closed=${this._abortDialog}
.preventScrimClose=${this.isDirtyState}
.preventScrimClose=${!!this._domain ||
!!this._name ||
!!this._clientId ||
!!this._clientSecret}
.headerTitle=${this.hass.localize(
"ui.panel.config.application_credentials.editor.caption"
)}
@@ -292,7 +284,6 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
ev.stopPropagation();
this._domain = ev.detail.value;
this._updateDescription();
this._updateDirtyState(this._currentState());
}
private async _updateDescription() {
@@ -316,16 +307,6 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
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,7 +32,6 @@ 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";
@@ -57,15 +56,15 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
const MASKED_FIELDS = ["password", "secret", "token"];
@customElement("supervisor-app-config")
class SupervisorAppConfig extends DirtyStateProviderMixin<
Record<string, unknown>
>()(LitElement) {
class SupervisorAppConfig extends 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;
@@ -352,7 +351,9 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
<div class="card-actions right">
<ha-progress-button
@click=${this._saveTapped}
.disabled=${this.disabled || !this.isDirtyState || !this._valid}
.disabled=${this.disabled ||
!this._configHasChanged ||
!this._valid}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
@@ -376,7 +377,6 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
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,13 +415,11 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
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);
}
}
}
@@ -452,7 +450,7 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
};
try {
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
this._markDirtyStateClean();
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
@@ -471,7 +469,7 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
if (this.disabled || !this.isDirtyState || !this._valid) {
if (this.disabled || !this._configHasChanged || !this._valid) {
return;
}
@@ -501,7 +499,7 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
options,
});
this._markDirtyStateClean();
this._configHasChanged = false;
if (this.addon?.state === "started") {
await suggestSupervisorAppRestart(this, this.hass, this.addon);
}
@@ -15,16 +15,13 @@ 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 DirtyStateProviderMixin<
Record<string, number | null>
>()(LitElement) {
class SupervisorAppNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -33,19 +30,19 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
@state() private _showOptional = false;
@state() private _configHasChanged = false;
@state() private _error?: string;
@state() private _config?: Record<string, number | null>;
@state() private _config?: Record<string, any>;
protected render() {
if (!this._config) {
return nothing;
}
const config = this._config;
const hasHiddenOptions = Object.keys(config).find(
(entry) => config[entry] === null
const hasHiddenOptions = Object.keys(this._config).find(
(entry) => this._config![entry] === null
);
return html`
@@ -101,7 +98,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
</ha-progress-button>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this.isDirtyState || this.disabled}
.disabled=${!this._configHasChanged || this.disabled}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
@@ -118,10 +115,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
}
private _createSchema = memoizeOne(
(
config: Record<string, number | null>,
showOptional: boolean
): HaFormSchema[] =>
(config: Record<string, number>, showOptional: boolean): HaFormSchema[] =>
(showOptional
? Object.keys(config)
: Object.keys(config).filter((entry) => config[entry] !== null)
@@ -147,14 +141,12 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
item.name;
private _setNetworkConfig(): void {
const config = this.addon.network || {};
this._config = config;
this._initDirtyTracking({ type: "shallow" }, config);
this._config = this.addon.network || {};
}
private _configChanged(ev: CustomEvent): void {
private async _configChanged(ev: CustomEvent): Promise<void> {
this._configHasChanged = true;
this._config = ev.detail.value;
this._updateDirtyState(ev.detail.value);
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
@@ -169,7 +161,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
try {
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
this._markDirtyStateClean();
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
@@ -196,14 +188,14 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
if (!this.isDirtyState || this.disabled) {
if (!this._configHasChanged || this.disabled) {
return;
}
const button = ev.currentTarget as any;
this._error = undefined;
const networkconfiguration: Record<string, number | null> = {};
const networkconfiguration = {};
Object.entries(this._config!).forEach(([key, value]) => {
networkconfiguration[key] = value ?? null;
});
@@ -214,7 +206,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
try {
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
this._markDirtyStateClean();
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
@@ -3,7 +3,6 @@ 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";
@@ -55,20 +54,9 @@ 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 DirtyStateProviderMixin<AreaFormState>()(LitElement)
extends LitElement
implements HassDialog<AreaRegistryDetailDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -128,23 +116,9 @@ 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;
@@ -352,8 +326,6 @@ class DialogAreaDetail
if (processed.floor) {
this._floor = processed.floor;
}
this._updateDirtyState(this._currentState());
}
protected render() {
@@ -371,7 +343,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")}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<ha-suggest-with-ai-button
@@ -412,9 +384,7 @@ class DialogAreaDetail
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid ||
this._submitting ||
(!!this._params?.entry && !this.isDirtyState)}
.disabled=${nameInvalid || this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
@@ -448,43 +418,36 @@ 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() {
@@ -506,7 +469,6 @@ class DialogAreaDetail
} else {
await this._params!.updateEntry!(values);
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error =
@@ -15,7 +15,6 @@ 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,
@@ -83,10 +82,7 @@ const RECOMMENDED_CONFIG: BackupConfig = {
};
@customElement("ha-dialog-backup-onboarding")
class DialogBackupOnboarding
extends DirtyStateProviderMixin<BackupConfig>()(LitElement)
implements HassDialog
{
class DialogBackupOnboarding extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@@ -119,7 +115,6 @@ class DialogBackupOnboarding
}
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._config!);
}
public closeDialog() {
@@ -174,7 +169,6 @@ class DialogBackupOnboarding
try {
await this._save(true);
this._params?.submit!(true);
this._markDirtyStateClean();
this.closeDialog();
} catch (err) {
// eslint-disable-next-line no-console
@@ -220,7 +214,7 @@ class DialogBackupOnboarding
<ha-dialog
.open=${this._open}
header-title=${this._stepTitle}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
${isFirstStep
@@ -299,7 +293,6 @@ class DialogBackupOnboarding
password: this._config.create_backup.password,
},
};
this._updateDirtyState(this._config);
this._done();
}
@@ -522,7 +515,6 @@ class DialogBackupOnboarding
include_addons: data.include_addons || null,
},
};
this._updateDirtyState(this._config);
}
private _scheduleChanged(ev) {
@@ -532,7 +524,6 @@ class DialogBackupOnboarding
schedule: value.schedule,
retention: value.retention,
};
this._updateDirtyState(this._config);
}
private _agentsConfigChanged(ev) {
@@ -544,7 +535,6 @@ class DialogBackupOnboarding
agent_ids: agents,
},
};
this._updateDirtyState(this._config);
}
static get styles(): CSSResultGroup {
@@ -12,17 +12,13 @@ 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 DirtyStateProviderMixin<string>()(LitElement)
implements HassDialog
{
class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@@ -36,7 +32,6 @@ class DialogDownloadDecryptedBackup
public showDialog(params: DownloadDecryptedBackupDialogParams): void {
this._open = true;
this._params = params;
this._initDirtyTracking({ type: "shallow" }, "");
}
public closeDialog() {
@@ -65,7 +60,7 @@ class DialogDownloadDecryptedBackup
header-title=${this.hass.localize(
"ui.panel.config.backup.dialogs.download.title"
)}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<p>
@@ -110,11 +105,7 @@ class DialogDownloadDecryptedBackup
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._submit}
.disabled=${!this.isDirtyState}
>
<ha-button slot="primaryAction" @click=${this._submit}>
${this.hass.localize(
"ui.panel.config.backup.dialogs.download.download"
)}
@@ -145,7 +136,6 @@ class DialogDownloadDecryptedBackup
this._agentId,
this._encryptionKey
);
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
if (err?.code === "password_incorrect") {
@@ -165,7 +155,6 @@ class DialogDownloadDecryptedBackup
private _keyChanged(ev) {
this._encryptionKey = ev.currentTarget.value;
this._error = "";
this._updateDirtyState(this._encryptionKey);
}
private get _agentId() {
@@ -28,7 +28,6 @@ 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";
@@ -60,10 +59,7 @@ const STEPS = ["data", "sync"] as const;
const DISALLOWED_AGENTS_NO_HA = [CLOUD_AGENT];
@customElement("ha-dialog-generate-backup")
class DialogGenerateBackup
extends DirtyStateProviderMixin<FormData>()(LitElement)
implements HassDialog
{
class DialogGenerateBackup extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _step?: "data" | "sync";
@@ -83,8 +79,6 @@ class DialogGenerateBackup
this._formData = INITIAL_DATA;
this._params = _params;
this._open = true;
this._initDirtyTracking({ type: "deep" }, INITIAL_DATA);
this._updateDirtyState(this._formData);
this._fetchAgents();
this._fetchBackupConfig();
@@ -166,7 +160,6 @@ class DialogGenerateBackup
agents_mode: "custom",
agent_ids: filteredAgents,
};
this._updateDirtyState(this._formData);
}
}
}
@@ -187,11 +180,7 @@ class DialogGenerateBackup
const selectedAgents = this._formData.agent_ids;
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
@closed=${this._dialogClosed}
>
<ha-dialog .open=${this._open} @closed=${this._dialogClosed}>
<ha-dialog-header slot="header">
${isFirstStep
? html`
@@ -287,7 +276,6 @@ class DialogGenerateBackup
...this._formData!,
data,
};
this._updateDirtyState(this._formData);
}
private _renderSync() {
@@ -382,7 +370,6 @@ class DialogGenerateBackup
...this._formData!,
agents_mode: value,
};
this._updateDirtyState(this._formData);
}
private _agentsChanged(ev) {
@@ -390,7 +377,6 @@ class DialogGenerateBackup
...this._formData!,
agent_ids: ev.detail.value,
};
this._updateDirtyState(this._formData);
}
private _nameChanged(ev: InputEvent) {
@@ -398,7 +384,6 @@ class DialogGenerateBackup
...this._formData!,
name: (ev.target as HaInput).value ?? "",
};
this._updateDirtyState(this._formData);
}
private _disabledAgentIds() {
@@ -444,7 +429,6 @@ class DialogGenerateBackup
}
this._params!.submit?.(params);
this._markDirtyStateClean();
this.closeDialog();
}
@@ -13,7 +13,6 @@ 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";
@@ -26,14 +25,8 @@ const SCHEMA = [
},
] as const satisfies HaFormSchema[];
interface LocalBackupLocationFormState {
default_backup_mount: string | null | undefined;
}
@customElement("dialog-local-backup-location")
class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocationFormState>()(
LitElement
) {
class LocalBackupLocationDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: LocalBackupLocationDialogParams;
@@ -51,10 +44,6 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
): Promise<void> {
this._dialogParams = dialogParams;
this._open = true;
this._initDirtyTracking(
{ type: "shallow" },
{ default_backup_mount: undefined }
);
}
public closeDialog(): void {
@@ -79,7 +68,7 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
header-title=${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
${this._error
@@ -113,7 +102,7 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this.isDirtyState}
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
@@ -136,9 +125,6 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
this._data = {
default_backup_mount: newLocation === "/backup" ? null : newLocation,
};
this._updateDirtyState({
default_backup_mount: this._data.default_backup_mount,
});
}
private async _changeMount() {
@@ -154,7 +140,6 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
this._waiting = false;
return;
}
this._markDirtyStateClean();
this.closeDialog();
}
@@ -52,6 +52,7 @@ 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,7 +21,6 @@ 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";
@@ -29,7 +28,7 @@ import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
@customElement("ha-dialog-upload-backup")
export class DialogUploadBackup
extends DirtyStateProviderMixin<BackupUploadFileFormData>()(LitElement)
extends LitElement
implements HassDialog<UploadBackupDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -48,8 +47,6 @@ 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() {
@@ -67,6 +64,10 @@ export class DialogUploadBackup
return true;
}
private _formValid() {
return this._formData?.file !== undefined;
}
protected render() {
if (!this._params || !this._formData) {
return nothing;
@@ -78,7 +79,7 @@ export class DialogUploadBackup
header-title=${this.hass.localize(
"ui.panel.config.backup.dialogs.upload.title"
)}
.preventScrimClose=${this.isDirtyState || this._uploading}
?prevent-scrim-close=${this._uploading}
@closed=${this._dialogClosed}
>
${this._error
@@ -111,7 +112,7 @@ export class DialogUploadBackup
<ha-button
slot="primaryAction"
@click=${this._upload}
.disabled=${!this.isDirtyState || this._uploading}
.disabled=${!this._formValid() || this._uploading}
>
${this.hass.localize(
"ui.panel.config.backup.dialogs.upload.action"
@@ -130,13 +131,11 @@ 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() {
@@ -162,7 +161,6 @@ 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,7 +18,6 @@ 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";
@@ -102,9 +101,7 @@ const HELPERS: HelperCreators = {
};
@customElement("dialog-helper-detail")
export class DialogHelperDetail extends DirtyStateProviderMixin<
Helper | undefined
>()(LitElement) {
export class DialogHelperDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _item?: Helper;
@@ -140,7 +137,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
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;
@@ -297,7 +293,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
header-title=${this._domain
? this.hass.localize(
"ui.panel.config.helpers.dialog.create_platform",
@@ -369,7 +364,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
private _valueChanged(ev: CustomEvent): void {
this._item = ev.detail.value;
this._updateDirtyState(this._item);
}
private async _createItem(): Promise<void> {
@@ -389,7 +383,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
entityId: `${this._domain}.${createdEntity.id}`,
});
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error = err.message || "Unknown error";
@@ -417,7 +410,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
try {
await HELPERS[domain].import();
this._domain = domain;
this._initDirtyTracking({ type: "deep" }, undefined);
} finally {
this._loading = false;
}
@@ -283,7 +283,7 @@ class DialogMatterAddDevice extends LitElement {
const savedStep = this._step;
try {
this._step = "commissioning";
await commissionMatterDevice(this.hass, code, true);
await commissionMatterDevice(this.hass, code);
} 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, false);
await commissionMatterDevice(this.hass, code);
} catch (err: any) {
this._error = err.message;
this._stopRedirect();
@@ -6,16 +6,13 @@ 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 DirtyStateProviderMixin<LovelaceStrategyConfig>()(
LitElement
) {
export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
@@ -32,7 +29,6 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
this._params = params;
this._data = params.config.strategy;
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._data);
}
public closeDialog(): void {
@@ -53,7 +49,7 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
header-title=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
)}
@@ -84,7 +80,6 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
private _handleConfigChanged(ev: CustomEvent): void {
this._data = ev.detail.config;
this._updateDirtyState(this._data!);
}
private async _save() {
@@ -97,7 +92,6 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
strategy: this._data,
});
this._submitting = false;
this._markDirtyStateClean();
this.closeDialog();
}
@@ -14,16 +14,13 @@ 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 DirtyStateProviderMixin<
Partial<LovelaceDashboard>
>()(LitElement) {
export class DialogLovelaceDashboardDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: LovelaceDashboardDetailsDialogParams;
@@ -45,7 +42,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
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 = {
@@ -55,12 +51,9 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
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!);
}
}
@@ -79,8 +72,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
return nothing;
}
const yamlMode = this._params.dashboard?.mode === "yaml";
const titleInvalid = !this._data.title || !this._data.title.trim();
const cancelButton = html`
@@ -104,11 +95,11 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
: this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
)}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<div>
${yamlMode
${this._params.dashboard?.mode === "yaml"
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
)
@@ -151,12 +142,10 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
<ha-button
slot="primaryAction"
@click=${this._updateDashboard}
.disabled=${!yamlMode &&
((this._error && "url_path" in this._error) ||
titleInvalid ||
this._submitting ||
!this.isDirtyState)}
?autofocus=${yamlMode}
.disabled=${(this._error && "url_path" in this._error) ||
titleInvalid ||
this._submitting}
?autofocus=${this._params.dashboard?.mode === "yaml"}
>
${this._params.urlPath
? this._params.dashboard?.mode === "storage"
@@ -262,7 +251,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
} else {
this._data = value;
}
this._updateDirtyState(this._data!);
}
private _fillUrlPath(title: string) {
@@ -282,7 +270,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
? pickAvailableDashboardUrlPath(baseSlug, taken)
: baseSlug,
};
this._updateDirtyState(this._data!);
}
private async _updateDashboard() {
@@ -305,7 +292,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
this._data as LovelaceDashboardCreateParams
);
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
let localizedErrorMessage: string | undefined;
@@ -13,7 +13,6 @@ 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";
@@ -26,9 +25,7 @@ interface PanelDetailData {
}
@customElement("dialog-panel-detail")
export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>()(
LitElement
) {
export class DialogPanelDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: PanelDetailDialogParams;
@@ -51,7 +48,6 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
show_in_sidebar: params.showInSidebar,
};
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._data);
}
public closeDialog(): void {
@@ -74,7 +70,7 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
header-title=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.panel_detail.edit_panel"
)}
@@ -118,7 +114,7 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
<ha-button
slot="primaryAction"
@click=${this._updatePanel}
.disabled=${titleInvalid || this._submitting || !this.isDirtyState}
.disabled=${titleInvalid || this._submitting}
>
${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.update"
@@ -175,7 +171,6 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
private _valueChanged(ev: CustomEvent) {
this._error = undefined;
this._data = ev.detail.value;
this._updateDirtyState(this._data!);
}
private async _handleError(err: any) {
@@ -233,7 +228,6 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
if (Object.keys(updates).length > 0) {
await this._params!.updatePanel(updates);
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._handleError(err);
@@ -9,7 +9,6 @@ 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";
@@ -31,9 +30,7 @@ const detectResourceType = (url?: string) => {
};
@customElement("dialog-lovelace-resource-detail")
export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
Partial<LovelaceResourcesMutableParams>
>()(LitElement) {
export class DialogLovelaceResourceDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: LovelaceResourceDetailsDialogParams;
@@ -60,7 +57,6 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
};
}
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._data);
}
public closeDialog(): void {
@@ -87,7 +83,7 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
header-title=${dialogTitle}
@closed=${this._dialogClosed}
>
@@ -123,10 +119,7 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
<ha-button
slot="primaryAction"
@click=${this._updateResource}
.disabled=${urlInvalid ||
!this._data?.res_type ||
this._submitting ||
!this.isDirtyState}
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
>
${this._params.resource
? this.hass!.localize(
@@ -210,7 +203,6 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
if (!this._data!.res_type) {
const type = detectResourceType(this._data!.url);
if (!type) {
this._updateDirtyState(this._data!);
return;
}
this._data = {
@@ -218,7 +210,6 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
res_type: type,
};
}
this._updateDirtyState(this._data!);
}
private async _updateResource() {
@@ -235,8 +226,7 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
this._data! as LovelaceResourcesMutableParams
);
}
this._markDirtyStateClean();
this.closeDialog();
this._params = undefined;
} catch (err: any) {
this._error = { base: err?.message || "Unknown error" };
} finally {
@@ -13,7 +13,6 @@ 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";
@@ -45,20 +44,8 @@ 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 DirtyStateProviderMixin<PersonFormState>()(LitElement)
implements HassDialog
{
class DialogPersonDetail extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _name!: string;
@@ -117,21 +104,9 @@ class DialogPersonDetail
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;
@@ -159,7 +134,7 @@ class DialogPersonDetail
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
header-title=${this._params.entry
? this._params.entry.name
: this.hass!.localize("ui.panel.config.person.detail.new_person")}
@@ -287,7 +262,7 @@ class DialogPersonDetail
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || this._submitting || !this.isDirtyState}
.disabled=${nameInvalid || this._submitting}
>
${this._params.entry
? this.hass!.localize("ui.common.save")
@@ -392,17 +367,14 @@ class DialogPersonDetail
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> {
@@ -421,7 +393,6 @@ class DialogPersonDetail
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,
@@ -450,20 +421,17 @@ class DialogPersonDetail
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() {
@@ -559,7 +527,6 @@ class DialogPersonDetail
await this._params!.createEntry?.(values);
this._personExists = true;
}
this._markDirtyStateClean();
this.closeDialog();
} catch (err: any) {
this._error = err ? err.message : "Unknown error";
+2 -42
View File
@@ -20,24 +20,12 @@ 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 DirtyStateProviderMixin<AddUserFormState>()(
LitElement
) {
export class DialogAddUser extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _loading = false;
@@ -82,18 +70,6 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
}
this._open = true;
this._initDirtyTracking(
{ type: "shallow" },
{
name: this._name,
username: this._username,
password: "",
passwordConfirm: "",
isAdmin: false,
localOnly: false,
}
);
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
@@ -113,7 +89,7 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
header-title=${this.hass.localize(
"ui.panel.config.users.add_user.caption"
)}
@@ -266,7 +242,6 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
if (parts.length) {
this._username = parts[0].toLowerCase();
this._publishDirtyState();
}
}
@@ -274,30 +249,16 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
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) {
@@ -345,7 +306,6 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
},
];
this._params!.userAddedCallback(user);
this._markDirtyStateClean();
this._close();
}
@@ -9,7 +9,6 @@ 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";
@@ -44,9 +43,7 @@ interface FormData {
}
@customElement("dialog-admin-change-password")
class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
LitElement
) {
class DialogAdminChangePassword extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: AdminChangePasswordDialogParams;
@@ -68,10 +65,7 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
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 {
@@ -123,7 +117,7 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
header-title=${this.hass.localize(
"ui.panel.config.users.change_password.caption"
)}
@@ -179,7 +173,6 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
private _valueChanged(ev) {
this._data = ev.detail.value;
this._updateDirtyState(this._data ?? {});
this._validate();
}
@@ -192,7 +185,6 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
this._userId!,
this._data.new_password
);
this._markDirtyStateClean();
this._success = true;
} catch (err: any) {
showToast(this, {
+162 -184
View File
@@ -24,23 +24,13 @@ 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 DirtyStateProviderMixin<UserDetailFormState>()(
LitElement
) {
class DialogUserDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _name!: string;
@@ -67,15 +57,6 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
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;
}
@@ -88,164 +69,177 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
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>
`}
<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"
${
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>
`
)}
></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>
</div>
`
: 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
}
<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
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.users.users_privileges_note"
"ui.panel.config.users.editor.system_generated_read_only_users"
)}
</ha-alert>
`
: 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}
: nothing
}
</div>
<ha-dialog-footer slot="footer">
@@ -254,19 +248,18 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
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 ||
!this.isDirtyState}
.disabled=${
!this._name || this._submitting || user.system_generated
}
>
${this.hass!.localize("ui.common.save")}
</ha-button>
@@ -278,31 +271,18 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
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() {
@@ -316,7 +296,6 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
],
local_only: this._localOnly,
});
this._markDirtyStateClean();
this._close();
} catch (err: any) {
this._error = err?.message || "Unknown error";
@@ -329,7 +308,6 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
this._submitting = true;
try {
if (await this._params!.removeEntry()) {
this._markDirtyStateClean();
this._close();
}
} finally {
@@ -11,7 +11,6 @@ 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,
@@ -29,9 +28,7 @@ import type { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-vo
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("dialog-voice-assistant-pipeline-detail")
export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
Partial<AssistPipeline>
>()(LitElement) {
export class DialogVoiceAssistantPipelineDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: VoiceAssistantPipelineDetailsDialogParams;
@@ -65,7 +62,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
this._hideWakeWord =
this._params.hideWakeWord || !this._data.wake_word_entity;
this._initDirtyTracking({ type: "deep" }, this._data);
return;
}
@@ -102,7 +98,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
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 {
@@ -150,7 +145,7 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
<ha-dialog
.open=${this._open}
header-title=${title}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
${!this._hideWakeWord ||
@@ -239,7 +234,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
slot="primaryAction"
@click=${this._updatePipeline}
.loading=${this._submitting}
.disabled=${!this.isDirtyState}
>
${isExistingPipeline
? this.hass.localize(
@@ -272,7 +266,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
value[key] = ev.detail.value[key];
});
this._data = { ...this._data, ...value };
this._updateDirtyState(this._data);
}
private async _updatePipeline() {
@@ -306,7 +299,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
// 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,7 +8,6 @@ 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";
@@ -22,9 +21,7 @@ const SCHEMA = [
];
@customElement("dialog-home-zone-detail")
class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams>()(
LitElement
) {
class DialogHomeZoneDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: Record<string, string>;
@@ -46,7 +43,6 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
longitude: this.hass.config.longitude,
radius: this.hass.config.radius,
};
this._initDirtyTracking({ type: "deep" }, this._data);
this._open = true;
}
@@ -75,7 +71,7 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
header-title=${this.hass!.localize("ui.common.edit_item", {
name: this._data.name,
})}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<ha-form
@@ -98,7 +94,7 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!valid || this._submitting || !this.isDirtyState}
.disabled=${!valid || this._submitting}
>
${this.hass!.localize("ui.common.save")}
</ha-button>
@@ -124,7 +120,6 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
value.radius = value.location.radius;
delete value.location;
this._data = value;
this._updateDirtyState(value);
}
private _computeLabel = (): string => "";
+3 -8
View File
@@ -11,15 +11,12 @@ 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 DirtyStateProviderMixin<ZoneMutableParams>()(
LitElement
) {
class DialogZoneDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: Record<string, string>;
@@ -56,7 +53,6 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
radius: 100,
};
}
this._initDirtyTracking({ type: "deep" }, this._data);
this._open = true;
}
@@ -97,7 +93,7 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
name: this._params.entry.name,
})
: this.hass!.localize("ui.panel.config.zone.detail.new_zone")}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<ha-form
@@ -135,7 +131,7 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!valid || this._submitting || !this.isDirtyState}
.disabled=${!valid || this._submitting}
>
${this._params.entry
? this.hass!.localize("ui.common.save")
@@ -193,7 +189,6 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
delete value.icon;
}
this._data = value;
this._updateDirtyState(value);
}
private _computeLabel = (
@@ -32,7 +32,6 @@ 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";
@@ -190,11 +189,9 @@ 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>
@@ -546,20 +543,11 @@ export class HuiEnergyDevicesGraphCard
chartData.splice(this._config.max_devices);
}
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),
};
});
this._legendData = chartData.map((d) => ({
...d,
name: this._getDeviceName(d.name),
value: `${formatNumber(d.value[0], this.hass.locale)} kWh`,
}));
// filter out hidden stats in place
for (let i = chartData.length - 1; i >= 0; i--) {
if (this._hiddenStats.includes((chartData[i] as any).id)) {
@@ -591,11 +579,7 @@ export class HuiEnergyDevicesGraphCard
e.detail.event?.target?.type === "tspan" // label
) {
const id = (e.detail.data as any).id as string;
if (
id !== "untracked" &&
!isExternalStatistic(id) &&
this.hass.states[id]
) {
if (id !== "untracked") {
fireEvent(this, "hass-more-info", {
entityId: id,
});
@@ -603,16 +587,6 @@ 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,9 +49,6 @@ const addEntityId = (entities: Set<string>, entity) => {
};
const addEntities = (entities: Set<string>, obj) => {
if (!obj) {
return;
}
if (obj.entity) {
addEntityId(entities, obj.entity);
}
@@ -12,7 +12,6 @@ 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,
@@ -28,9 +27,7 @@ import type { DashboardStrategyEditorDialogParams } from "./show-dialog-dashboar
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
@customElement("dialog-dashboard-strategy-editor")
class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStrategyConfig>()(
LitElement
) {
class DialogDashboardStrategyEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DashboardStrategyEditorDialogParams;
@@ -52,7 +49,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
this._params = params;
this._strategyConfig = params.config.strategy;
this._open = true;
this._initDirtyTracking({ type: "deep" }, this._strategyConfig);
await this.updateComplete;
}
@@ -72,7 +68,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
ev.stopPropagation();
this._guiModeAvailable = ev.detail.guiModeAvailable;
this._strategyConfig = ev.detail.config as LovelaceStrategyConfig;
this._updateDirtyState(this._strategyConfig);
}
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
@@ -87,7 +82,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
strategy: this._strategyConfig!,
});
showSaveSuccessToast(this, this.hass);
this._markDirtyStateClean();
this.closeDialog();
}
@@ -143,7 +137,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
header-title=${title || "-"}
header-subtitle=${ifDefined(this._params.title)}
width="large"
@@ -202,11 +195,7 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
>
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
?disabled=${!this.isDirtyState}
>
<ha-button slot="primaryAction" @click=${this._save}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
@@ -30,7 +30,6 @@ 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,
@@ -54,7 +53,7 @@ const TABS = ["tab-settings", "tab-visibility"] as const;
@customElement("hui-dialog-edit-section")
export class HuiDialogEditSection
extends DirtyStateProviderMixin<LovelaceSectionRawConfig>()(LitElement)
extends LitElement
implements HassDialog<EditSectionDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -97,7 +96,6 @@ export class HuiDialogEditSection
this._viewConfig = findLovelaceContainer(this._params.lovelaceConfig, [
this._params.viewIndex,
]);
this._initDirtyTracking({ type: "deep" }, this._config);
}
public closeDialog() {
@@ -161,7 +159,7 @@ export class HuiDialogEditSection
return html`
<ha-dialog
.open=${this._open}
.preventScrimClose=${this.isDirtyState}
prevent-scrim-close
@keydown=${this._ignoreKeydown}
@closed=${this._dialogClosed}
class=${classMap({
@@ -233,11 +231,7 @@ export class HuiDialogEditSection
${this.hass!.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
?disabled=${!this.isDirtyState}
>
<ha-button slot="primaryAction" @click=${this._save}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
@@ -248,7 +242,6 @@ export class HuiDialogEditSection
private _configChanged(ev: CustomEvent): void {
ev.stopPropagation();
this._config = ev.detail.value;
this._updateDirtyState(this._config!);
}
private _handleTabChanged(ev: CustomEvent): void {
@@ -406,7 +399,6 @@ export class HuiDialogEditSection
return;
}
this._config = ev.detail.value;
this._updateDirtyState(this._config!);
}
private _ignoreKeydown(ev: KeyboardEvent) {
@@ -431,7 +423,6 @@ export class HuiDialogEditSection
);
this._params.saveConfig(newConfig);
this._markDirtyStateClean();
this.closeDialog();
}
+14 -23
View File
@@ -1,3 +1,4 @@
import { undoDepth } from "@codemirror/commands";
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
@@ -16,7 +17,6 @@ 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,9 +33,7 @@ const strategyStruct = type({
});
@customElement("hui-editor")
class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
LitElement
) {
class LovelaceFullConfigEditor extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -46,6 +44,8 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
@state() private _saving?: boolean;
@state() private _changed?: boolean;
private _config?: LovelaceRawConfig;
private _yamlError?: string;
@@ -66,10 +66,10 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
slot="actionItems"
class="save-button
${classMap({
saved: this._saving === false || this.isDirtyState,
saved: this._saving === false || this._changed === true,
})}"
>
${this.isDirtyState
${this._changed
? this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.unsaved_changes"
)
@@ -78,7 +78,7 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
<ha-button
slot="actionItems"
@click=${this._handleSave}
.disabled=${!this.isDirtyState}
.disabled=${!this._changed}
>${this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.save"
)}</ha-button
@@ -98,7 +98,7 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._setValue();
this.yamlEditor.setValue(this.lovelace!.rawConfig);
}
protected updated(changedProps: PropertyValues<this>) {
@@ -110,19 +110,10 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
oldLovelace.rawConfig !== this.lovelace.rawConfig &&
!deepEqual(oldLovelace.rawConfig, this.lovelace.rawConfig)
) {
this._setValue();
this.yamlEditor.setValue(this.lovelace!.rawConfig);
}
}
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,
@@ -167,17 +158,17 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
private _yamlChanged(ev: CustomEvent) {
this._config = ev.detail.isValid ? ev.detail.value : undefined;
this._yamlError = ev.detail.errorMsg;
this._updateDirtyState(this.yamlEditor.yaml);
if (this.isDirtyState && !window.onbeforeunload) {
this._changed = undoDepth(this.yamlEditor.codemirror!.state) > 0;
if (this._changed && !window.onbeforeunload) {
window.onbeforeunload = () => true;
} else if (!this.isDirtyState && window.onbeforeunload) {
} else if (!this._changed && window.onbeforeunload) {
window.onbeforeunload = null;
}
}
private async _closeEditor() {
if (
this.isDirtyState &&
this._changed &&
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_unsaved_changes"
@@ -288,7 +279,7 @@ class LovelaceFullConfigEditor extends DirtyStateProviderMixin<string>()(
});
}
window.onbeforeunload = null;
this._markDirtyStateClean();
this._changed = false;
this._saving = false;
}
+10 -18
View File
@@ -60,24 +60,16 @@ export const createLogMessage = async (
// - a possible list of aggregated errors
if (error instanceof Error) {
lines.push(error.toString() || messageFallback);
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] : [];
}
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();
});
lines.push(...(stackLines.length > 0 ? stackLines : [stackFallback]));
// @ts-expect-error Requires library bump to ES2022
if (error.cause) {
-51
View File
@@ -1,51 +0,0 @@
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");
});
});
+88 -88
View File
@@ -1352,14 +1352,14 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/lint@npm:6.9.7, @codemirror/lint@npm:^6.0.0":
version: 6.9.7
resolution: "@codemirror/lint@npm:6.9.7"
"@codemirror/lint@npm:6.9.6, @codemirror/lint@npm:^6.0.0":
version: 6.9.6
resolution: "@codemirror/lint@npm:6.9.6"
dependencies:
"@codemirror/state": "npm:^6.0.0"
"@codemirror/view": "npm:^6.42.0"
crelt: "npm:^1.0.5"
checksum: 10/f1af8295e1741a8d0c155cd7552c35d21c859d8848571fa084f5a6f2a50cac51ed25f04e423cab0f094e11b4828a54de4383a97b6b8cf6a242c5941034bffa04
checksum: 10/70ed80eaec81038c014a89d8b4ba17396562b7dd541882fa981e2f81f7cdcbd67725a7c055c7772ece3bd052a276976d873f71746fc550b1aede7e18faa32f93
languageName: node
linkType: hard
@@ -1383,15 +1383,15 @@ __metadata:
languageName: node
linkType: hard
"@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"
"@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"
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/a867a26cb511f161a8ef60b2574afae2561cfe94abd822e0af2a6e4bd94d1ff46f0d5085428c89f6f6b80a52482c8d23f80362173491903f49fb93462a216b95
checksum: 10/7cfeebe1507f71a960dfb2d5152400507d28ed5827680bc73e0a093bfba9a796c2e559c960fd2b046379fac31ff0b59663dfc481baadf1d6ececd71eb5b48014
languageName: node
linkType: hard
@@ -4548,105 +4548,105 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.61.0"
"@typescript-eslint/eslint-plugin@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.60.1"
dependencies:
"@eslint-community/regexpp": "npm:^4.12.2"
"@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"
"@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"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
"@typescript-eslint/parser": ^8.61.0
"@typescript-eslint/parser": ^8.60.1
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/ca7fbaa2f03ec15bdbf39d2e4d42f1b682085f23830591d1d6c3d9f497fdda497341b2fa67c8d366514a3c22807557e45e7afe1ee70cef527b184250e5422e8f
checksum: 10/f3633bb2700bc32299578baeaf6650418656229be256147ba9d1ab09b34ef2b7fed83804ef4d2439e9189dbdcb89399d67bc8fea55262be6caa32114be048538
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/parser@npm:8.61.0"
"@typescript-eslint/parser@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/parser@npm:8.60.1"
dependencies:
"@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"
"@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"
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/82060c36786339867d63337708a08bd4bc65569313998bd086dbe6b901664082c7e40d6b6e085296a459cd4fc1d064479ef570b51e1eb113688bb152a7a6d689
checksum: 10/f9c484c4a3897015328f328a1c6ee778d113dd134201f635c0421cb72efe6e63f3a68524aff0df6e19e76ff93daf5cabd946e67f12f10dddcf19bda534aa68dc
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/project-service@npm:8.61.0"
"@typescript-eslint/project-service@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/project-service@npm:8.60.1"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.61.0"
"@typescript-eslint/types": "npm:^8.61.0"
"@typescript-eslint/tsconfig-utils": "npm:^8.60.1"
"@typescript-eslint/types": "npm:^8.60.1"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/b7d7e973b565f604af43b8afb3ca1c3fbe6fcf16863bde83b42417a196ba9f3a5a3f5d39bf57ed96b8ce577047064d93c353ecb21db5e95dce69f81335c9cd81
checksum: 10/fec693dd79c3a1e6a24091127a37af4eb9d9cee8192cf2a434adae48543eadff834bc0623b5b563c8b592b250bc080570f9e7b42807252ea898442c525beeee9
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/scope-manager@npm:8.61.0"
"@typescript-eslint/scope-manager@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/scope-manager@npm:8.60.1"
dependencies:
"@typescript-eslint/types": "npm:8.61.0"
"@typescript-eslint/visitor-keys": "npm:8.61.0"
checksum: 10/295e306665d64f0330fede3fe72febd65c67c3083d747149b66097aa6f7d517f25731dc1dbec900b15768c40f92b082f501296e7524855fe82697f40b8d23ce1
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/visitor-keys": "npm:8.60.1"
checksum: 10/7228c110410ff8cfc01e96d8f17c986f8b4dd447fe3a3291baaab8fe946026ccdf0291865f788f18cf538ab49bfc067fe797708b6b8590104a65f7e69f921cc5
languageName: node
linkType: hard
"@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"
"@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"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/f678ff5ec887a27d8e590e0c67403b12e372a027ab036dcfc1e3ef614d3bed7a3c455a65fa0a87ff7dae5b0ad1c49cf4aa40639cc368d7eb424efe8349d9cb9f
checksum: 10/afc78b19b856a71dc4e493f931ae44e1a91dc6441a14cb92e4063db880892f3874768f9d347d4b2f45362f2090e4455407c70f42027d77ddc85d6cba95cdb76c
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/type-utils@npm:8.61.0"
"@typescript-eslint/type-utils@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/type-utils@npm:8.60.1"
dependencies:
"@typescript-eslint/types": "npm:8.61.0"
"@typescript-eslint/typescript-estree": "npm:8.61.0"
"@typescript-eslint/utils": "npm:8.61.0"
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/typescript-estree": "npm:8.60.1"
"@typescript-eslint/utils": "npm:8.60.1"
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/8290e5fc26241dfd5aeeffad0fb9857a3fa1f9c8107dfb01638970297e0e17be6088f0fd2d6fc7d450e9879afaa7e23f4111182bcf0b625eba74fdf13100b19e
checksum: 10/6f426263be597063831bf308e52328e8d387af5db955a09cb85fde1c72f5b1b36a365133b9c9a74330e5e948e59bf9a9b82605f4c9c4e3bf9b6cb7f4c37e4b18
languageName: node
linkType: hard
"@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
"@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
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/typescript-estree@npm:8.61.0"
"@typescript-eslint/typescript-estree@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/typescript-estree@npm:8.60.1"
dependencies:
"@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"
"@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"
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/6d5ab7850226de23ab26d94388f729e792413f5a9e704c8c685b66eb20946efeb290cda91195f062e1065bc20129ec8f5955768316660132087347e66dec0d1a
checksum: 10/9c3a56266aadf589bc6e770cd04cb3f55b1ee1507dcacda61866408c656ae4462aa7e11baf39eb939bc4d1e3b843cf58e60f3ebdeb3e75f042ff0f6fb39c311b
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/utils@npm:8.61.0"
"@typescript-eslint/utils@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/utils@npm:8.60.1"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.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/scope-manager": "npm:8.60.1"
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/typescript-estree": "npm:8.60.1"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/50ff451edb8e5dee92bbab11a75cbd570715623d89094d0541ddfbef208248e82d2f9478d1e09fb9c94496069afd4db9521384b77f7aaa63970f7edfebddfba9
checksum: 10/a75f8714995b6280b4c15ca957bbc6634862453461111e4a2a07b8bc72b51a504484a9b957fc5b7a646c4bf09f1e414a0c52cd3b6798c42fb8c4de83b1b5a364
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.61.0":
version: 8.61.0
resolution: "@typescript-eslint/visitor-keys@npm:8.61.0"
"@typescript-eslint/visitor-keys@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/visitor-keys@npm:8.60.1"
dependencies:
"@typescript-eslint/types": "npm:8.61.0"
"@typescript-eslint/types": "npm:8.60.1"
eslint-visitor-keys: "npm:^5.0.0"
checksum: 10/243018d9d8b1918d2863e50eec6628c792ccda05ad5534f5153fc783b7f54cdb8a58d758eb74260d113274bfab8bb38ad4664f3db9e7d3f844cdffbe6e47e285
checksum: 10/6d120b4a790477ae0291e69f6457686c71b929cc40519148f6b6c7fbc09604b15821ae8cf1005aa23acec5105b4016db256a68d68f30eda8d6c24d4fdb0ede86
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.7"
"@codemirror/lint": "npm:6.9.6"
"@codemirror/search": "npm:6.7.0"
"@codemirror/state": "npm:6.6.0"
"@codemirror/view": "npm:6.43.1"
"@codemirror/view": "npm:6.43.0"
"@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.4"
prettier: "npm:3.8.3"
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.61.0"
typescript-eslint: "npm:8.60.1"
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.4":
version: 3.8.4
resolution: "prettier@npm:3.8.4"
"prettier@npm:3.8.3":
version: 3.8.3
resolution: "prettier@npm:3.8.3"
bin:
prettier: bin/prettier.cjs
checksum: 10/54684a3cc6689238692b29fab541c01934af7677be94c02293ba49981a1ac121c8bebe2a865f0c3b963e99d208f847c53aed354cc0ce8750e2d45791d64506c5
checksum: 10/4b3b12cbb29e4c96bed936e5d070167552500c18d37676fb3e0caae6199c42860662608e4dc116230698f6e2bb0267ef2548158224c92d40f188d309d72fdd6f
languageName: node
linkType: hard
@@ -13461,18 +13461,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.61.0":
version: 8.61.0
resolution: "typescript-eslint@npm:8.61.0"
"typescript-eslint@npm:8.60.1":
version: 8.60.1
resolution: "typescript-eslint@npm:8.60.1"
dependencies:
"@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"
"@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"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/5a21c6ef76400ea30a47629087787834abc1c17e4b406465dfd8c204ef635556f8e3a775d89c46f9eb175ebd6a218284685e935877a2b148c482f0478627bdf9
checksum: 10/e12091ab2540b817c76b0ec6aad92e341f810310bec2b24bc95780aee106049c05363998f6ea52ed066130c8afc41dca1627f56e4c1df1dd519f4d4ca0ce4910
languageName: node
linkType: hard