mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-14 04:12:16 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 900efcba6c | |||
| c46f286cb8 | |||
| cc6b51d53f | |||
| 6915ca8fdd | |||
| 677e53f685 | |||
| 46b6ae8d7b | |||
| 09fda1ca1e | |||
| 7c1522b975 | |||
| d26ad7b354 | |||
| 66235a4c99 | |||
| 6c02864334 | |||
| 3471cd103a | |||
| 9ae25d96f2 | |||
| 02361f2517 |
@@ -0,0 +1,51 @@
|
||||
name: Blocking labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
- unlabeled
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check for labels which block the Pull Request from being merged
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = [
|
||||
"wait for backend",
|
||||
"Needs UX",
|
||||
"Do Not Review",
|
||||
"Blocked",
|
||||
"has-parent",
|
||||
"Needs Template",
|
||||
];
|
||||
const prLabels = context.payload.pull_request.labels.map(
|
||||
(l) => l.name
|
||||
);
|
||||
const found = blockingLabels.filter((bl) => prLabels.includes(bl));
|
||||
if (found.length > 0) {
|
||||
const message = `This Pull Request is blocked by label${found.length > 1 ? "s" : ""}: ${found.join(", ")}`;
|
||||
await core.summary
|
||||
.addHeading(":no_entry_sign: Pull Request is blocked", 2)
|
||||
.addRaw(message)
|
||||
.write();
|
||||
core.setFailed(message);
|
||||
} else {
|
||||
await core.summary
|
||||
.addHeading(":white_check_mark: Pull Request is clear to merge after review", 2)
|
||||
.addRaw("This Pull Request is not blocked by any labels which prevent it from being merged.")
|
||||
.write();
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { goBack } from "../common/navigate";
|
||||
import "./ha-icon-button-arrow-prev";
|
||||
import "./ha-menu-button";
|
||||
|
||||
const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
|
||||
@@ -135,6 +138,8 @@ export const haTopAppBarFixedStyles = css`
|
||||
export class HaTopAppBarFixed extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "back-button", type: Boolean }) backButton = false;
|
||||
|
||||
@property({ attribute: "center-title", type: Boolean }) centerTitle = false;
|
||||
|
||||
@query(".top-app-bar") protected _barElement!: HTMLElement;
|
||||
@@ -200,16 +205,14 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
<div class="row">
|
||||
${paneHeader
|
||||
? html`<section class="section" id="title">
|
||||
<slot name="navigationIcon"></slot>
|
||||
${title}
|
||||
${this._renderNavigationIcon()} ${title}
|
||||
</section>`
|
||||
: nothing}
|
||||
<section class="section" id="navigation">
|
||||
${paneHeader
|
||||
? nothing
|
||||
: html`<slot name="navigationIcon"></slot> ${this.centerTitle
|
||||
? nothing
|
||||
: title}`}
|
||||
: html`${this._renderNavigationIcon()}
|
||||
${this.centerTitle ? nothing : title}`}
|
||||
</section>
|
||||
${!paneHeader && this.centerTitle
|
||||
? html`<section class="section center">${title}</section>`
|
||||
@@ -225,6 +228,20 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderNavigationIcon() {
|
||||
return html`
|
||||
<slot name="navigationIcon">
|
||||
${this.backButton
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._handleBackClick}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button></ha-menu-button>`}
|
||||
</slot>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderContent() {
|
||||
return html`<div class="top-app-bar-fixed-adjust">
|
||||
<slot></slot>
|
||||
@@ -268,6 +285,11 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
this._barElement?.classList.toggle("scrolled", scrollTop > 0);
|
||||
};
|
||||
|
||||
private _handleBackClick(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected _registerListeners() {
|
||||
this.scrollTarget.addEventListener(
|
||||
"scroll",
|
||||
|
||||
+14
-4
@@ -146,10 +146,20 @@ export const filterUpdateEntitiesParameterized = (
|
||||
return updateCanInstall(entity, showSkipped);
|
||||
});
|
||||
|
||||
export const installUpdates = (hass: HomeAssistant, entityIds: string[]) =>
|
||||
hass.callService("update", "install", {
|
||||
entity_id: entityIds,
|
||||
});
|
||||
export const installUpdates = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[],
|
||||
notifyOnError = true
|
||||
) =>
|
||||
hass.callService(
|
||||
"update",
|
||||
"install",
|
||||
{
|
||||
entity_id: entityIds,
|
||||
},
|
||||
undefined,
|
||||
notifyOnError
|
||||
);
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
|
||||
@@ -40,7 +40,9 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
@state() private _loading = true;
|
||||
|
||||
private async _loadActions() {
|
||||
this._defaultActions = getDefaultAddToActions();
|
||||
this._defaultActions = this._config?.user?.is_admin
|
||||
? getDefaultAddToActions()
|
||||
: [];
|
||||
this._externalActions = [];
|
||||
|
||||
if (this._config?.auth.external?.config.hasEntityAddTo) {
|
||||
|
||||
@@ -266,9 +266,8 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
private _shouldShowAddEntityTo(): boolean {
|
||||
// When new_triggers_conditions labs feature is promoted, this whole check can be removed.
|
||||
return (
|
||||
this._newTriggersAndConditions ||
|
||||
(this._newTriggersAndConditions && !!this.hass.user?.is_admin) ||
|
||||
!!this.hass.auth.external?.config.hasEntityAddTo
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { goBack } from "../common/navigate";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-menu-button";
|
||||
import "../components/ha-top-app-bar-fixed";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "../components/ha-alert";
|
||||
|
||||
@@ -21,18 +20,22 @@ class HassErrorScreen extends LitElement {
|
||||
@property() public error?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.toolbar) {
|
||||
return this._renderContent();
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${!(this.rootnav || history.state?.root)}
|
||||
>
|
||||
${this._renderContent()}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderContent(): TemplateResult {
|
||||
return html`
|
||||
${this.toolbar
|
||||
? html`<div class="toolbar">
|
||||
${this.rootnav || history.state?.root
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._handleBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="content">
|
||||
<ha-alert alert-type="error">${this.error}</ha-alert>
|
||||
<slot>
|
||||
@@ -56,30 +59,9 @@ class HassErrorScreen extends LitElement {
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
height: var(--header-height);
|
||||
padding: 8px 12px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.toolbar {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
ha-icon-button-arrow-prev {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.content {
|
||||
color: var(--primary-text-color);
|
||||
height: calc(100% - var(--header-height));
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { goBack } from "../common/navigate";
|
||||
import "../components/ha-top-app-bar-fixed";
|
||||
import "../components/ha-spinner";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("hass-loading-screen")
|
||||
@@ -22,18 +19,22 @@ class HassLoadingScreen extends LitElement {
|
||||
@property() public message?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this.noToolbar) {
|
||||
return this._renderContent();
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${!(this.rootnav || history.state?.root)}
|
||||
>
|
||||
${this._renderContent()}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderContent(): TemplateResult {
|
||||
return html`
|
||||
${this.noToolbar
|
||||
? ""
|
||||
: html`<div class="toolbar">
|
||||
${this.rootnav || history.state?.root
|
||||
? html`<ha-menu-button></ha-menu-button>`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._handleBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
</div>`}
|
||||
<div class="content">
|
||||
<ha-spinner></ha-spinner>
|
||||
${this.message
|
||||
@@ -43,55 +44,24 @@ class HassLoadingScreen extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleBack() {
|
||||
goBack();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
height: var(--header-height);
|
||||
padding: 8px 12px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.toolbar {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.content {
|
||||
height: calc(100% - var(--header-height));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#loading-text {
|
||||
max-width: 350px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#loading-text {
|
||||
max-width: 350px;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -6,6 +6,9 @@ export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass {
|
||||
/** Provided by `DirtyStateProviderMixin`. */
|
||||
declare isDirtyState: boolean;
|
||||
|
||||
private _handleClick = async (e: MouseEvent) => {
|
||||
// get the right target, otherwise the composedPath would return <home-assistant> in the new event
|
||||
const target = e.composedPath()[0];
|
||||
@@ -33,7 +36,7 @@ export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (this.isDirty) {
|
||||
if (this.isDirtyState) {
|
||||
window.addEventListener("click", this._handleClick, true);
|
||||
window.addEventListener("beforeunload", this._handleUnload);
|
||||
} else {
|
||||
@@ -47,11 +50,6 @@ export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
|
||||
this._removeListeners();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
||||
protected get isDirty(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import type { HaDropdownItem } from "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-state-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
@@ -119,7 +118,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
if (!this._entityRegistry) {
|
||||
return html`
|
||||
<ha-two-pane-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
<ha-menu-button slot="navigationIcon"></ha-menu-button>
|
||||
<div slot="title">
|
||||
${this.hass.localize("ui.components.calendar.my_calendars")}
|
||||
</div>
|
||||
@@ -154,8 +152,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
footer
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<ha-menu-button slot="navigationIcon"></ha-menu-button>
|
||||
|
||||
${!showPane
|
||||
? html`<ha-dropdown slot="title">
|
||||
<ha-button slot="trigger">
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -90,22 +87,12 @@ class PanelClimate extends LitElement {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _back(ev) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button slot="navigationIcon"></ha-menu-button>`}
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParams.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.climate")}</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { BlueprintAutomationConfig } from "../../../data/automation";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
import { saveFabStyles } from "./styles";
|
||||
|
||||
@@ -19,7 +24,9 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ type: Boolean }) public dirty = false;
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext;
|
||||
|
||||
protected get _config(): BlueprintAutomationConfig {
|
||||
return this.config;
|
||||
@@ -58,7 +65,7 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
class=${this._dirtyState?.isDirty ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._saveAutomation}
|
||||
>
|
||||
|
||||
@@ -4,17 +4,25 @@ import { showToast } from "../../../util/toast";
|
||||
|
||||
export const EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET = 60;
|
||||
|
||||
// Editor elements that expose dirty tracking: the top-level automation/script
|
||||
// editors via `isDirtyState`, and the manual editors via `dirty`.
|
||||
interface DirtyStateElement extends HTMLElement {
|
||||
isDirtyState?: boolean;
|
||||
dirty?: boolean;
|
||||
}
|
||||
|
||||
const isDirtyStateElement = (el: HTMLElement | null): el is DirtyStateElement =>
|
||||
el !== null && ("isDirtyState" in el || "dirty" in el);
|
||||
|
||||
function editorSaveFabVisibleFrom(el: HTMLElement): boolean {
|
||||
if (
|
||||
el.localName === "ha-automation-editor" ||
|
||||
el.localName === "ha-script-editor"
|
||||
) {
|
||||
return Boolean((el as { dirty?: boolean }).dirty);
|
||||
return isDirtyStateElement(el) && Boolean(el.isDirtyState);
|
||||
}
|
||||
const holder = closestWithProperty(el, "dirty", false) as
|
||||
| (HTMLElement & { dirty?: boolean })
|
||||
| null;
|
||||
return Boolean(holder?.dirty);
|
||||
const holder = closestWithProperty(el, "dirty", false);
|
||||
return isDirtyStateElement(holder) && Boolean(holder.dirty);
|
||||
}
|
||||
|
||||
export function showEditorToast(
|
||||
|
||||
@@ -146,6 +146,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
currentConfig: () => this.config!,
|
||||
});
|
||||
|
||||
public override get isDirtyState(): boolean {
|
||||
return super.isDirtyState || !!this.yamlErrors;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -421,7 +425,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
></blueprint-automation-editor>
|
||||
@@ -434,7 +437,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
.stateObj=${stateObj}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@@ -554,7 +556,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
class=${this.isDirtyState ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._handleSaveAutomation}
|
||||
>
|
||||
@@ -602,7 +604,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
!this.entityId
|
||||
) {
|
||||
const initData = getAutomationEditorInitData();
|
||||
this.dirty = !!initData;
|
||||
let baseConfig: Partial<AutomationConfig> = { description: "" };
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig = {
|
||||
@@ -617,6 +618,8 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
...baseConfig,
|
||||
...(initData ? normalizeAutomationConfig(initData) : initData),
|
||||
} as AutomationConfig;
|
||||
this._initDirtyTracking({ type: "deep" }, baseConfig as AutomationConfig);
|
||||
this._updateDirtyState(this.config);
|
||||
this.currentEntityId = undefined;
|
||||
this.readOnly = false;
|
||||
}
|
||||
@@ -624,10 +627,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this.config = normalizeAutomationConfig(c.config);
|
||||
this._initDirtyTracking({ type: "deep" }, this.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
|
||||
@@ -690,7 +693,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
@@ -762,7 +765,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
@@ -772,11 +774,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
id: this.config?.id,
|
||||
...normalizeAutomationConfig(ev.detail.value),
|
||||
};
|
||||
this._updateDirtyState(this.config!);
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
if (!this.isDirtyState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -787,7 +790,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.automationId || String(Date.now());
|
||||
@@ -901,7 +904,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -918,7 +921,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(config);
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -1009,7 +1012,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
}
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this._markDirtyStateClean();
|
||||
} catch (errors: any) {
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showEditorToast(this, {
|
||||
@@ -1068,7 +1071,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
private _applyUndoRedo(config: AutomationConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import type { Constructor, HomeAssistant, Route } from "../../../types";
|
||||
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
|
||||
@@ -87,7 +88,9 @@ export interface EditorDomainHooks<TConfig> {
|
||||
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
superClass: Constructor<LitElement>
|
||||
) => {
|
||||
class AutomationScriptEditorClass extends superClass {
|
||||
class AutomationScriptEditorClass extends DirtyStateProviderMixin<TConfig>()(
|
||||
superClass
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
@@ -102,8 +105,6 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
entityRegistry?: EntityRegistryEntry[];
|
||||
|
||||
@state() protected dirty = false;
|
||||
|
||||
@state() protected errors?: string;
|
||||
|
||||
@state() protected yamlErrors?: string;
|
||||
@@ -217,7 +218,9 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
|
||||
protected takeControlSave() {
|
||||
this.readOnly = false;
|
||||
this.dirty = true;
|
||||
// Force dirty: set baseline to null so current config always differs
|
||||
this._initDirtyTracking({ type: "deep" }, null as unknown as TConfig);
|
||||
this._updateDirtyState(this.config!);
|
||||
this.blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
@@ -237,10 +240,6 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
}
|
||||
};
|
||||
|
||||
protected get isDirty() {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this.confirmUnsavedChanged();
|
||||
}
|
||||
@@ -259,9 +258,9 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
const domain = hooks.domain;
|
||||
try {
|
||||
const config = await hooks.fetchFileConfig(this.hass, id);
|
||||
this.dirty = false;
|
||||
this.readOnly = false;
|
||||
this.config = hooks.normalizeConfig(config);
|
||||
this._initDirtyTracking({ type: "deep" }, this.config);
|
||||
hooks.checkValidation();
|
||||
} catch (err: any) {
|
||||
if (err.status_code !== 404) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import {
|
||||
html,
|
||||
@@ -19,6 +20,10 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { SidebarConfig } from "../../../data/automation";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import type {
|
||||
Constructor,
|
||||
HomeAssistant,
|
||||
@@ -46,7 +51,13 @@ export const ManualEditorMixin = <TConfig>(
|
||||
|
||||
@property({ attribute: false }) public config!: TConfig;
|
||||
|
||||
@property({ attribute: false }) public dirty = false;
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext;
|
||||
|
||||
protected get dirty(): boolean {
|
||||
return this._dirtyState?.isDirty ?? false;
|
||||
}
|
||||
|
||||
@state() protected pastedConfig?: TConfig;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../dashboard/ha-config-updates";
|
||||
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
|
||||
|
||||
@@ -347,13 +348,22 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await installUpdates(this.hass, entityIds);
|
||||
await installUpdates(this.hass, entityIds, false);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.updates.update_all_failed"),
|
||||
text: extractApiErrorMessage(err),
|
||||
warning: true,
|
||||
});
|
||||
let message = extractApiErrorMessage(err);
|
||||
// The backend error embeds the raw entity_id; swap in the update's name.
|
||||
for (const entityId of entityIds) {
|
||||
const stateObj = this.hass.states[entityId] as UpdateEntity | undefined;
|
||||
if (stateObj && message.includes(entityId)) {
|
||||
message = message.replaceAll(
|
||||
entityId,
|
||||
stateObj.attributes.title ||
|
||||
stateObj.attributes.friendly_name ||
|
||||
entityId
|
||||
);
|
||||
}
|
||||
}
|
||||
showToast(this, { message, duration: 10000, dismissable: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -236,7 +235,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
<ha-menu-button slot="navigationIcon"></ha-menu-button>
|
||||
<div slot="title">${this.hass.localize("panel.config")}</div>
|
||||
|
||||
<ha-icon-button
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
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 "./ha-energy-power-config";
|
||||
@@ -35,13 +36,19 @@ import {
|
||||
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
|
||||
interface BatteryFormState {
|
||||
source: BatterySourceTypeEnergyPreference;
|
||||
powerType: PowerType;
|
||||
powerConfig: PowerConfig;
|
||||
}
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const socStatisticsUnits = ["%"];
|
||||
const socDeviceClass = "battery";
|
||||
|
||||
@customElement("dialog-energy-battery-settings")
|
||||
export class DialogEnergyBatterySettings
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<BatteryFormState>()(LitElement)
|
||||
implements HassDialog<EnergySettingsBatteryDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -108,6 +115,14 @@ export class DialogEnergyBatterySettings
|
||||
);
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
{
|
||||
source: this._source!,
|
||||
powerType: this._powerType,
|
||||
powerConfig: this._powerConfig,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -137,7 +152,7 @@ export class DialogEnergyBatterySettings
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.header"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : nothing}
|
||||
@@ -241,7 +256,8 @@ export class DialogEnergyBatterySettings
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._isValid()}
|
||||
.disabled=${!this._isValid() ||
|
||||
(!!this._params?.source && !this.isDirtyState)}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
@@ -283,11 +299,13 @@ export class DialogEnergyBatterySettings
|
||||
private _statisticToChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_energy_to: ev.detail.value };
|
||||
this._updateMetadata(ev.detail.value);
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _statisticFromChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
|
||||
this._updateMetadata(ev.detail.value);
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -298,6 +316,7 @@ export class DialogEnergyBatterySettings
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
@@ -305,6 +324,7 @@ export class DialogEnergyBatterySettings
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _statisticSocChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -312,6 +332,15 @@ export class DialogEnergyBatterySettings
|
||||
...this._source!,
|
||||
stat_soc: ev.detail.value || undefined,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _updateFormDirtyState(): void {
|
||||
this._updateDirtyState({
|
||||
source: this._source!,
|
||||
powerType: this._powerType,
|
||||
powerConfig: this._powerConfig,
|
||||
});
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -335,6 +364,7 @@ export class DialogEnergyBatterySettings
|
||||
}
|
||||
|
||||
await this._params!.saveCallback(source);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
saveFrontendSystemData,
|
||||
} from "../../../../data/frontend";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type {
|
||||
@@ -45,7 +46,7 @@ const VIEW_GROUPS: { view: EnergyViewPath; labelKey: LocalizeKeys }[] = [
|
||||
|
||||
@customElement("dialog-energy-customise")
|
||||
export class DialogEnergyCustomise
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<string[]>()(LitElement)
|
||||
implements HassDialog<EnergyCustomiseDialogParams>
|
||||
{
|
||||
@state()
|
||||
@@ -107,6 +108,7 @@ export class DialogEnergyCustomise
|
||||
this._error = err?.message || "Unknown error";
|
||||
this._hidden = new Set();
|
||||
}
|
||||
this._initDirtyTracking({ type: "deep" }, [...this._hidden].sort());
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -120,7 +122,7 @@ export class DialogEnergyCustomise
|
||||
.headerTitle=${this._i18n.localize(
|
||||
"ui.panel.config.energy.customise.title"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${!this._hidden
|
||||
@@ -143,7 +145,10 @@ export class DialogEnergyCustomise
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._submitting || !this._hidden || !!this._error}
|
||||
.disabled=${this._submitting ||
|
||||
!this._hidden ||
|
||||
!!this._error ||
|
||||
!this.isDirtyState}
|
||||
>
|
||||
${this._i18n.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
@@ -216,6 +221,7 @@ export class DialogEnergyCustomise
|
||||
next.add(cardKey);
|
||||
}
|
||||
this._hidden = next;
|
||||
this._updateDirtyState([...this._hidden].sort());
|
||||
};
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
@@ -228,6 +234,7 @@ export class DialogEnergyCustomise
|
||||
await saveFrontendSystemData(this._connection.connection, "energy", {
|
||||
hidden_cards: hidden.length ? hidden : undefined,
|
||||
});
|
||||
this._markDirtyStateClean();
|
||||
this._params?.saveCallback?.();
|
||||
this.closeDialog();
|
||||
} catch (_err) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import type { EnergySettingsDeviceWaterDialogParams } from "./show-dialogs-energy";
|
||||
@@ -29,7 +30,9 @@ const flowRateUnitClasses = ["volume_flow_rate"];
|
||||
|
||||
@customElement("dialog-energy-device-settings-water")
|
||||
export class DialogEnergyDeviceSettingsWater
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<DeviceConsumptionEnergyPreference | null>()(
|
||||
LitElement
|
||||
)
|
||||
implements HassDialog<EnergySettingsDeviceWaterDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -72,6 +75,7 @@ export class DialogEnergyDeviceSettingsWater
|
||||
.filter((id) => id && id !== this._device?.stat_rate) as string[];
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._device ?? null);
|
||||
}
|
||||
|
||||
private _computePossibleParents() {
|
||||
@@ -146,7 +150,7 @@ export class DialogEnergyDeviceSettingsWater
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption_water.dialog.header"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
@@ -226,7 +230,8 @@ export class DialogEnergyDeviceSettingsWater
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._device}
|
||||
.disabled=${!this._device ||
|
||||
(!!this._params?.device && !this.isDirtyState)}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
@@ -239,10 +244,12 @@ export class DialogEnergyDeviceSettingsWater
|
||||
private async _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
if (!ev.detail.value) {
|
||||
this._device = undefined;
|
||||
this._updateDirtyState(this._device ?? null);
|
||||
return;
|
||||
}
|
||||
this._device = { stat_consumption: ev.detail.value };
|
||||
this._computePossibleParents();
|
||||
this._updateDirtyState(this._device);
|
||||
|
||||
if (
|
||||
isExternalStatistic(ev.detail.value) &&
|
||||
@@ -271,6 +278,7 @@ export class DialogEnergyDeviceSettingsWater
|
||||
delete newDevice.stat_rate;
|
||||
}
|
||||
this._device = newDevice;
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -282,6 +290,7 @@ export class DialogEnergyDeviceSettingsWater
|
||||
delete newDevice.name;
|
||||
}
|
||||
this._device = newDevice;
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
|
||||
@@ -293,11 +302,13 @@ export class DialogEnergyDeviceSettingsWater
|
||||
delete newDevice.included_in_stat;
|
||||
}
|
||||
this._device = newDevice;
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
await this._params!.saveCallback(this._device!);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy";
|
||||
@@ -32,7 +33,9 @@ const powerUnitClasses = ["power"];
|
||||
|
||||
@customElement("dialog-energy-device-settings")
|
||||
export class DialogEnergyDeviceSettings
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<DeviceConsumptionEnergyPreference | null>()(
|
||||
LitElement
|
||||
)
|
||||
implements HassDialog<EnergySettingsDeviceDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -75,6 +78,7 @@ export class DialogEnergyDeviceSettings
|
||||
.filter((id) => id && id !== this._device?.stat_rate) as string[];
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._device ?? null);
|
||||
}
|
||||
|
||||
private _computePossibleParents() {
|
||||
@@ -147,7 +151,7 @@ export class DialogEnergyDeviceSettings
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.header"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
@@ -226,7 +230,8 @@ export class DialogEnergyDeviceSettings
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._device}
|
||||
.disabled=${!this._device ||
|
||||
(!!this._params?.device && !this.isDirtyState)}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
@@ -239,10 +244,12 @@ export class DialogEnergyDeviceSettings
|
||||
private async _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
if (!ev.detail.value) {
|
||||
this._device = undefined;
|
||||
this._updateDirtyState(this._device ?? null);
|
||||
return;
|
||||
}
|
||||
this._device = { stat_consumption: ev.detail.value };
|
||||
this._computePossibleParents();
|
||||
this._updateDirtyState(this._device);
|
||||
|
||||
if (
|
||||
isExternalStatistic(ev.detail.value) &&
|
||||
@@ -271,6 +278,7 @@ export class DialogEnergyDeviceSettings
|
||||
delete newDevice.stat_rate;
|
||||
}
|
||||
this._device = newDevice;
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -282,6 +290,7 @@ export class DialogEnergyDeviceSettings
|
||||
delete newDevice.name;
|
||||
}
|
||||
this._device = newDevice;
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
|
||||
@@ -293,11 +302,13 @@ export class DialogEnergyDeviceSettings
|
||||
delete newDevice.included_in_stat;
|
||||
}
|
||||
this._device = newDevice;
|
||||
this._updateDirtyState(this._device);
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
await this._params!.saveCallback(this._device!);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -25,18 +25,26 @@ import {
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
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 type { EnergySettingsGasDialogParams } from "./show-dialogs-energy";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
|
||||
type CostType = "no-costs" | "number" | "entity" | "statistic";
|
||||
|
||||
interface GasFormState {
|
||||
source: GasSourceTypeEnergyPreference;
|
||||
costs: CostType;
|
||||
}
|
||||
|
||||
const gasDeviceClasses = ["gas", "energy"];
|
||||
const gasUnitClasses = ["volume", "energy"];
|
||||
const flowRateUnitClasses = ["volume_flow_rate"];
|
||||
|
||||
@customElement("dialog-energy-gas-settings")
|
||||
export class DialogEnergyGasSettings
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<GasFormState>()(LitElement)
|
||||
implements HassDialog<EnergySettingsGasDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -47,7 +55,7 @@ export class DialogEnergyGasSettings
|
||||
|
||||
@state() private _source?: GasSourceTypeEnergyPreference;
|
||||
|
||||
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
|
||||
@state() private _costs?: CostType;
|
||||
|
||||
@state() private _pickedDisplayUnit?: string | null;
|
||||
|
||||
@@ -101,6 +109,10 @@ export class DialogEnergyGasSettings
|
||||
.filter((id) => id && id !== this._source?.stat_rate) as string[];
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
{ source: this._source!, costs: this._costs! }
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -153,7 +165,7 @@ export class DialogEnergyGasSettings
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.header"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
@@ -318,7 +330,8 @@ export class DialogEnergyGasSettings
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._source.stat_energy_from}
|
||||
.disabled=${!this._source!.stat_energy_from ||
|
||||
(!!this._params?.source && !this.isDirtyState)}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
@@ -329,11 +342,8 @@ export class DialogEnergyGasSettings
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: Event) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as
|
||||
| "no-costs"
|
||||
| "number"
|
||||
| "entity"
|
||||
| "statistic";
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberPriceChanged(ev: InputEvent) {
|
||||
@@ -343,6 +353,7 @@ export class DialogEnergyGasSettings
|
||||
entity_energy_price: null,
|
||||
stat_cost: null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _priceStatChanged(ev: CustomEvent) {
|
||||
@@ -352,6 +363,7 @@ export class DialogEnergyGasSettings
|
||||
number_energy_price: null,
|
||||
stat_cost: ev.detail.value,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _priceEntityChanged(ev: CustomEvent) {
|
||||
@@ -361,6 +373,7 @@ export class DialogEnergyGasSettings
|
||||
number_energy_price: null,
|
||||
stat_cost: null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _flowRateStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -368,6 +381,7 @@ export class DialogEnergyGasSettings
|
||||
...this._source!,
|
||||
stat_rate: ev.detail.value || undefined,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private async _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -399,6 +413,7 @@ export class DialogEnergyGasSettings
|
||||
...this._source!,
|
||||
stat_energy_from: ev.detail.value,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -409,6 +424,11 @@ export class DialogEnergyGasSettings
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _updateFormDirtyState(): void {
|
||||
this._updateDirtyState({ source: this._source!, costs: this._costs! });
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -419,6 +439,7 @@ export class DialogEnergyGasSettings
|
||||
this._source!.stat_cost = null;
|
||||
}
|
||||
await this._params!.saveCallback(this._source!);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
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 "./ha-energy-power-config";
|
||||
@@ -43,9 +44,17 @@ const energyUnitClasses = ["energy"];
|
||||
|
||||
type CostType = "no_cost" | "stat" | "entity" | "number";
|
||||
|
||||
interface GridFormState {
|
||||
source: GridSourceTypeEnergyPreference;
|
||||
powerType: PowerType;
|
||||
powerConfig: PowerConfig;
|
||||
importCostType: CostType;
|
||||
exportCostType: CostType;
|
||||
}
|
||||
|
||||
@customElement("dialog-energy-grid-settings")
|
||||
export class DialogEnergyGridSettings
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<GridFormState>()(LitElement)
|
||||
implements HassDialog<EnergySettingsGridDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -144,6 +153,16 @@ export class DialogEnergyGridSettings
|
||||
);
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
{
|
||||
source: this._source!,
|
||||
powerType: this._powerType,
|
||||
powerConfig: this._powerConfig,
|
||||
importCostType: this._importCostType,
|
||||
exportCostType: this._exportCostType,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -185,7 +204,7 @@ export class DialogEnergyGridSettings
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.header"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : nothing}
|
||||
@@ -446,7 +465,8 @@ export class DialogEnergyGridSettings
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._isValid()}
|
||||
.disabled=${!this._isValid() ||
|
||||
(!!this._params?.source && !this.isDirtyState)}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
@@ -507,6 +527,7 @@ export class DialogEnergyGridSettings
|
||||
};
|
||||
}
|
||||
this._updateMetadata(ev.detail.value);
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _statisticToChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -536,6 +557,7 @@ export class DialogEnergyGridSettings
|
||||
};
|
||||
}
|
||||
this._updateMetadata(ev.detail.value);
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -546,6 +568,7 @@ export class DialogEnergyGridSettings
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handleImportCostTypeChanged(ev: Event) {
|
||||
@@ -557,6 +580,7 @@ export class DialogEnergyGridSettings
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handleExportCostTypeChanged(ev: Event) {
|
||||
@@ -568,10 +592,12 @@ export class DialogEnergyGridSettings
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _statCostChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_cost: ev.detail.value || null };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _entityCostChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -579,12 +605,14 @@ export class DialogEnergyGridSettings
|
||||
...this._source!,
|
||||
entity_energy_price: ev.detail.value || null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberCostChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
this._source = { ...this._source!, number_energy_price: value };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _statCompensationChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -592,6 +620,7 @@ export class DialogEnergyGridSettings
|
||||
...this._source!,
|
||||
stat_compensation: ev.detail.value || null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _entityCompensationChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -599,12 +628,14 @@ export class DialogEnergyGridSettings
|
||||
...this._source!,
|
||||
entity_energy_price_export: ev.detail.value || null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberCompensationChanged(ev: Event) {
|
||||
const input = ev.currentTarget as HTMLInputElement;
|
||||
const value = input.value ? parseFloat(input.value) : null;
|
||||
this._source = { ...this._source!, number_energy_price_export: value };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _handlePowerConfigChanged(
|
||||
@@ -612,6 +643,17 @@ export class DialogEnergyGridSettings
|
||||
) {
|
||||
this._powerType = ev.detail.powerType;
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _updateFormDirtyState(): void {
|
||||
this._updateDirtyState({
|
||||
source: this._source!,
|
||||
powerType: this._powerType,
|
||||
powerConfig: this._powerConfig,
|
||||
importCostType: this._importCostType,
|
||||
exportCostType: this._exportCostType,
|
||||
});
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -638,6 +680,7 @@ export class DialogEnergyGridSettings
|
||||
}
|
||||
|
||||
await this._params!.saveCallback(source);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
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 { brandsUrl } from "../../../../util/brands-url";
|
||||
@@ -35,12 +36,17 @@ import {
|
||||
} from "../../../../data/recorder";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
|
||||
interface SolarFormState {
|
||||
source: SolarSourceTypeEnergyPreference;
|
||||
forecast: boolean;
|
||||
}
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const powerUnitClasses = ["power"];
|
||||
|
||||
@customElement("dialog-energy-solar-settings")
|
||||
export class DialogEnergySolarSettings
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<SolarFormState>()(LitElement)
|
||||
implements HassDialog<EnergySettingsSolarDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -88,6 +94,10 @@ export class DialogEnergySolarSettings
|
||||
.filter((id) => id && id !== this._source?.stat_rate) as string[];
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
{ source: this._source!, forecast: this._forecast! }
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -114,7 +124,7 @@ export class DialogEnergySolarSettings
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.header"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
@@ -248,7 +258,8 @@ export class DialogEnergySolarSettings
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._source.stat_energy_from}
|
||||
.disabled=${!this._source!.stat_energy_from ||
|
||||
(!!this._params?.source && !this.isDirtyState)}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
@@ -275,23 +286,23 @@ export class DialogEnergySolarSettings
|
||||
|
||||
private _handleForecastChanged(ev: Event) {
|
||||
this._forecast = (ev.currentTarget as HaRadioGroup).value === "true";
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _forecastCheckChanged(ev) {
|
||||
const input = ev.currentTarget as HaCheckbox;
|
||||
const entry = (input as any).entry as ConfigEntry;
|
||||
const checked = input.checked;
|
||||
const list = this._source!.config_entry_solar_forecast
|
||||
? [...this._source!.config_entry_solar_forecast]
|
||||
: [];
|
||||
if (checked) {
|
||||
if (this._source!.config_entry_solar_forecast === null) {
|
||||
this._source!.config_entry_solar_forecast = [];
|
||||
}
|
||||
this._source!.config_entry_solar_forecast.push(entry.entry_id);
|
||||
list.push(entry.entry_id);
|
||||
} else {
|
||||
this._source!.config_entry_solar_forecast!.splice(
|
||||
this._source!.config_entry_solar_forecast!.indexOf(entry.entry_id),
|
||||
1
|
||||
);
|
||||
list.splice(list.indexOf(entry.entry_id), 1);
|
||||
}
|
||||
this._source = { ...this._source!, config_entry_solar_forecast: list };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _addForecast() {
|
||||
@@ -299,11 +310,16 @@ export class DialogEnergySolarSettings
|
||||
startFlowHandler: "forecast_solar",
|
||||
dialogClosedCallback: (params) => {
|
||||
if (params.entryId) {
|
||||
if (this._source!.config_entry_solar_forecast === null) {
|
||||
this._source!.config_entry_solar_forecast = [];
|
||||
}
|
||||
this._source!.config_entry_solar_forecast.push(params.entryId);
|
||||
const list = this._source!.config_entry_solar_forecast
|
||||
? [...this._source!.config_entry_solar_forecast]
|
||||
: [];
|
||||
list.push(params.entryId);
|
||||
this._source = {
|
||||
...this._source!,
|
||||
config_entry_solar_forecast: list,
|
||||
};
|
||||
this._fetchSolarForecastConfigEntries();
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -325,10 +341,12 @@ export class DialogEnergySolarSettings
|
||||
this.requestUpdate("_params");
|
||||
}
|
||||
}
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _powerStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = { ...this._source!, stat_rate: ev.detail.value };
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -339,6 +357,14 @@ export class DialogEnergySolarSettings
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _updateFormDirtyState(): void {
|
||||
this._updateDirtyState({
|
||||
source: this._source!,
|
||||
forecast: this._forecast!,
|
||||
});
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -347,6 +373,7 @@ export class DialogEnergySolarSettings
|
||||
this._source!.config_entry_solar_forecast = null;
|
||||
}
|
||||
await this._params!.saveCallback(this._source!);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -24,16 +24,24 @@ import {
|
||||
} from "../../../../data/recorder";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
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 type { EnergySettingsWaterDialogParams } from "./show-dialogs-energy";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
|
||||
type CostType = "no-costs" | "number" | "entity" | "statistic";
|
||||
|
||||
interface WaterFormState {
|
||||
source: WaterSourceTypeEnergyPreference;
|
||||
costs: CostType;
|
||||
}
|
||||
|
||||
const flowRateUnitClasses = ["volume_flow_rate"];
|
||||
|
||||
@customElement("dialog-energy-water-settings")
|
||||
export class DialogEnergyWaterSettings
|
||||
extends LitElement
|
||||
extends DirtyStateProviderMixin<WaterFormState>()(LitElement)
|
||||
implements HassDialog<EnergySettingsWaterDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -44,7 +52,7 @@ export class DialogEnergyWaterSettings
|
||||
|
||||
@state() private _source?: WaterSourceTypeEnergyPreference;
|
||||
|
||||
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
|
||||
@state() private _costs?: CostType;
|
||||
|
||||
@state() private _water_units?: string[];
|
||||
|
||||
@@ -84,6 +92,10 @@ export class DialogEnergyWaterSettings
|
||||
.filter((id) => id && id !== this._source?.stat_rate) as string[];
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
{ source: this._source!, costs: this._costs! }
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -121,7 +133,7 @@ export class DialogEnergyWaterSettings
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.header"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||
@@ -259,7 +271,8 @@ export class DialogEnergyWaterSettings
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._save}
|
||||
.disabled=${!this._source.stat_energy_from}
|
||||
.disabled=${!this._source!.stat_energy_from ||
|
||||
(!!this._params?.source && !this.isDirtyState)}
|
||||
slot="primaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
@@ -270,11 +283,8 @@ export class DialogEnergyWaterSettings
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: Event) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as
|
||||
| "no-costs"
|
||||
| "number"
|
||||
| "entity"
|
||||
| "statistic";
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _numberPriceChanged(ev: InputEvent) {
|
||||
@@ -284,6 +294,7 @@ export class DialogEnergyWaterSettings
|
||||
entity_energy_price: null,
|
||||
stat_cost: null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _priceStatChanged(ev: CustomEvent) {
|
||||
@@ -293,6 +304,7 @@ export class DialogEnergyWaterSettings
|
||||
number_energy_price: null,
|
||||
stat_cost: ev.detail.value,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _priceEntityChanged(ev: CustomEvent) {
|
||||
@@ -302,6 +314,7 @@ export class DialogEnergyWaterSettings
|
||||
number_energy_price: null,
|
||||
stat_cost: null,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _flowRateStatisticChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -309,6 +322,7 @@ export class DialogEnergyWaterSettings
|
||||
...this._source!,
|
||||
stat_rate: ev.detail.value || undefined,
|
||||
};
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private async _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -338,6 +352,7 @@ export class DialogEnergyWaterSettings
|
||||
this.requestUpdate("_params");
|
||||
}
|
||||
}
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -348,6 +363,11 @@ export class DialogEnergyWaterSettings
|
||||
if (!this._source.name) {
|
||||
delete this._source.name;
|
||||
}
|
||||
this._updateFormDirtyState();
|
||||
}
|
||||
|
||||
private _updateFormDirtyState(): void {
|
||||
this._updateDirtyState({ source: this._source!, costs: this._costs! });
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -358,6 +378,7 @@ export class DialogEnergyWaterSettings
|
||||
this._source!.stat_cost = null;
|
||||
}
|
||||
await this._params!.saveCallback(this._source!);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -75,6 +75,7 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -97,8 +98,8 @@ interface DeviceEntities {
|
||||
type DeviceEntitiesLookup = Record<string, string[]>;
|
||||
|
||||
@customElement("ha-scene-editor")
|
||||
export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
KeyboardShortcutMixin(LitElement)
|
||||
export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -112,10 +113,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
|
||||
@property({ attribute: false }) public scenes!: SceneEntity[];
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
|
||||
private _sceneRevision = 0;
|
||||
|
||||
@state() private _yamlErrors?: string;
|
||||
|
||||
@state() private _config?: SceneConfig;
|
||||
@@ -322,7 +323,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
.disabled=${this._saving}
|
||||
@click=${this._saveScene}
|
||||
class=${classMap({
|
||||
dirty: this._dirty || !this.sceneId,
|
||||
dirty: this.isDirtyState || !this.sceneId,
|
||||
saving: this._saving,
|
||||
})}
|
||||
>
|
||||
@@ -641,7 +642,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
if (changedProps.has("sceneId") && !this.sceneId && this.hass) {
|
||||
this._dirty = false;
|
||||
this._sceneRevision = 0;
|
||||
const initData = getSceneEditorInitData();
|
||||
this._config = {
|
||||
name: this.hass.localize("ui.panel.config.scene.editor.default_name"),
|
||||
@@ -656,9 +657,13 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
category: "",
|
||||
};
|
||||
}
|
||||
this._dirty =
|
||||
this._initDirtyTracking({ type: "shallow" }, 0);
|
||||
if (
|
||||
initData !== undefined &&
|
||||
(initData.areaId !== undefined || initData.config !== undefined);
|
||||
(initData.areaId !== undefined || initData.config !== undefined)
|
||||
) {
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityRegistryEntries")) {
|
||||
@@ -816,7 +821,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _enterLiveMode() {
|
||||
if (this._dirty) {
|
||||
if (this.isDirtyState) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.scene.editor.enter_live_mode_unsaved"
|
||||
@@ -850,13 +855,14 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlErrors = ev.detail.errorMsg;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
return;
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
this._errors = undefined;
|
||||
}
|
||||
|
||||
@@ -911,7 +917,8 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
(entity: SceneEntity) => entity.attributes.id === this.sceneId
|
||||
);
|
||||
|
||||
this._dirty = false;
|
||||
this._sceneRevision = 0;
|
||||
this._initDirtyTracking({ type: "shallow" }, 0);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@@ -960,7 +967,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._entities = [...this._entities, entityId];
|
||||
this._single_entities.push(entityId);
|
||||
this._storeState(entityId);
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _deleteEntity(ev: Event) {
|
||||
@@ -978,7 +985,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
if (this._config!.metadata) {
|
||||
delete this._config!.metadata[deleteEntityId];
|
||||
}
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _pickDevice(device_id: string) {
|
||||
@@ -994,7 +1001,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
deviceEntities.forEach((entityId) => {
|
||||
this._storeState(entityId);
|
||||
});
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _devicePicked(ev: CustomEvent) {
|
||||
@@ -1018,7 +1025,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
delete this._config!.entities[entityId];
|
||||
});
|
||||
}
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
|
||||
private _stateChanged(event: HassEvent) {
|
||||
@@ -1026,7 +1033,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
event.context.id !== this._activateContextId &&
|
||||
this._entities.includes(event.data.entity_id)
|
||||
) {
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1072,7 +1079,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (this._dirty) {
|
||||
if (this.isDirtyState) {
|
||||
return showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.scene.editor.unsaved_confirm_title"
|
||||
@@ -1239,7 +1246,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
this._markDirtyStateClean();
|
||||
if (isNewScene) {
|
||||
navigate(`/config/scene/edit/${id}`, { replace: true });
|
||||
}
|
||||
@@ -1294,7 +1301,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
updateConfig: async (newConfig, entityRegistryUpdate) => {
|
||||
this._config = newConfig;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -1313,7 +1320,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
updateConfig: async (newConfig, entityRegistryUpdate) => {
|
||||
this._config = newConfig;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this._updateDirtyState(++this._sceneRevision);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -1322,10 +1329,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
});
|
||||
}
|
||||
|
||||
protected get isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this._confirmUnsavedChanged();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import { css, html, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
} from "../../../data/context/dirty-state";
|
||||
import type { BlueprintScriptConfig } from "../../../data/script";
|
||||
import { saveFabStyles } from "../automation/styles";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
@@ -15,7 +20,9 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
|
||||
@property({ type: Boolean }) public saving = false;
|
||||
|
||||
@property({ type: Boolean }) public dirty = false;
|
||||
@consume({ context: dirtyStateContext, subscribe: true })
|
||||
@state()
|
||||
private _dirtyState?: DirtyStateContext;
|
||||
|
||||
protected get _config(): BlueprintScriptConfig {
|
||||
return this.config;
|
||||
@@ -35,7 +42,7 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
class=${this._dirtyState?.isDirty ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._saveScript}
|
||||
>
|
||||
|
||||
@@ -107,6 +107,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
currentConfig: () => this.config!,
|
||||
});
|
||||
|
||||
public override get isDirtyState(): boolean {
|
||||
return super.isDirtyState || !!this.yamlErrors;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -377,7 +381,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-script=${this._handleSaveScript}
|
||||
></blueprint-script-editor>
|
||||
@@ -389,7 +392,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.isWide=${this.isWide}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@@ -470,7 +472,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
<ha-button
|
||||
slot="fab"
|
||||
size="l"
|
||||
class=${!this.readOnly && this.dirty ? "dirty" : ""}
|
||||
class=${!this.readOnly && this.isDirtyState ? "dirty" : ""}
|
||||
.disabled=${this.saving}
|
||||
@click=${this._handleSaveScript}
|
||||
>
|
||||
@@ -522,7 +524,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
|
||||
const initData = getScriptEditorInitData();
|
||||
this.dirty = !!initData;
|
||||
const baseConfig: Partial<ScriptConfig> = {};
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig.sequence = [];
|
||||
@@ -531,12 +532,15 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
...baseConfig,
|
||||
...initData,
|
||||
} as ScriptConfig;
|
||||
this._initDirtyTracking({ type: "deep" }, baseConfig as ScriptConfig);
|
||||
this._updateDirtyState(this.config);
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getScriptStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this.config = normalizeScriptConfig(c.config);
|
||||
this._initDirtyTracking({ type: "deep" }, this.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
const regEntry = this.entityRegistry?.find(
|
||||
@@ -546,7 +550,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this.scriptId = regEntry.unique_id;
|
||||
}
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
}
|
||||
@@ -582,7 +585,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
this.config = ev.detail.value;
|
||||
this.errors = undefined;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config!);
|
||||
}
|
||||
|
||||
private async _runScript() {
|
||||
@@ -669,7 +672,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
this._manualEditor?.addFields();
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config!);
|
||||
}
|
||||
|
||||
private _preprocessYaml() {
|
||||
@@ -678,18 +681,18 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this.yamlErrors = undefined;
|
||||
this.config = ev.detail.value;
|
||||
this._updateDirtyState(this.config!);
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
if (!this.isDirtyState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -700,7 +703,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.scriptId || String(Date.now());
|
||||
@@ -815,7 +818,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
@@ -834,7 +837,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(config);
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -930,7 +933,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this._markDirtyStateClean();
|
||||
} catch (errors: any) {
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showEditorToast(this, {
|
||||
@@ -980,7 +983,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
private _applyUndoRedo(config: ScriptConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this._updateDirtyState(this.config);
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
createHistoryLogbookUrl,
|
||||
@@ -34,8 +34,6 @@ import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-target-picker";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
@@ -117,22 +115,13 @@ class HaPanelHistory extends LitElement {
|
||||
this._unsubscribeHistory();
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const entitiesSelected = this._getEntityIds().length > 0;
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._showBack
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
slot="navigationIcon"
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button slot="navigationIcon"></ha-menu-button>`}
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${!!this._showBack}
|
||||
>
|
||||
<h1 class="page-title" slot="title">
|
||||
${this.hass.localize("panel.history")}
|
||||
</h1>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -90,22 +87,12 @@ class PanelLight extends LitElement {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _back(ev) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button slot="navigationIcon"></ha-menu-button>`}
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParms.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.light")}</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
|
||||
@@ -5,7 +5,7 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
createHistoryLogbookUrl,
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
} from "../../common/url/search-params";
|
||||
import "../../components/date-picker/ha-date-range-picker";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-target-picker";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
@@ -65,21 +63,12 @@ export class HaPanelLogbook extends LitElement {
|
||||
this._time = { range: [start, end] };
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._showBack
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
slot="navigationIcon"
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button slot="navigationIcon"></ha-menu-button>`}
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${!!this._showBack}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.logbook")}</div>
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
|
||||
@@ -80,12 +80,7 @@ class HuiTimestampDisplay extends LitElement {
|
||||
if (!changedProperties.has("format") || !this._connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (INTERVAL_FORMAT.includes("relative")) {
|
||||
this._startInterval();
|
||||
} else {
|
||||
this._clearInterval();
|
||||
}
|
||||
this._startInterval();
|
||||
}
|
||||
|
||||
private get _format(): string {
|
||||
|
||||
@@ -486,15 +486,10 @@ export class HaCardConditionEditor extends LitElement {
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.icon-badge-wrapper {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 870px) {
|
||||
.icon-badge-wrapper {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -90,41 +87,27 @@ class PanelMaintenance extends LitElement {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _back(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button slot="navigationIcon"></ha-menu-button>`}
|
||||
<div class="main-title">
|
||||
${this.hass.localize("panel.maintenance")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParams.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.maintenance")}</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -165,77 +148,10 @@ class PanelMaintenance extends LitElement {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
.header {
|
||||
background-color: var(--app-header-background-color);
|
||||
color: var(--app-header-text-color, white);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .header {
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-left,
|
||||
0px
|
||||
) - var(--safe-area-inset-right, 0px)
|
||||
);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
:host([scrolled]) .header {
|
||||
box-shadow: var(
|
||||
--bar-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding: 0px 12px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.main-title {
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.narrow .main-title {
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
padding-inline-end: var(--safe-area-inset-right);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
:host([narrow]) hui-view-container {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-inline-start: var(--safe-area-inset-left);
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { getEntityLocation } from "../../common/entity/get_entity_location";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import "../../components/map/ha-map";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@@ -23,7 +22,6 @@ class HaPanelMap extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
<ha-menu-button slot="navigationIcon"></ha-menu-button>
|
||||
<div slot="title">${this.hass.localize("panel.map")}</div>
|
||||
${!__DEMO__ && this.hass.user?.is_admin
|
||||
? html`<ha-icon-button
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
mdiListBoxOutline,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
@@ -15,7 +15,6 @@ import "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import "../../components/media-player/ha-media-manage-button";
|
||||
import "../../components/media-player/ha-media-player-browse";
|
||||
@@ -100,7 +99,7 @@ class PanelMediaBrowser extends LitElement {
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button slot="navigationIcon"></ha-menu-button>`}
|
||||
: nothing}
|
||||
<h1 class="page-title" slot="title">
|
||||
${!this._currentItem
|
||||
? this.hass.localize(
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@@ -90,22 +87,12 @@ class PanelSecurity extends LitElement {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _back(ev) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`<ha-menu-button slot="navigationIcon"></ha-menu-button>`}
|
||||
<ha-top-app-bar-fixed
|
||||
.narrow=${this.narrow}
|
||||
.backButton=${this._searchParms.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.security")}</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
|
||||
@@ -32,7 +32,6 @@ import type { HaDropdownItem } from "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-state-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
@@ -190,7 +189,6 @@ class PanelTodo extends LitElement {
|
||||
footer
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
<ha-menu-button slot="navigationIcon"></ha-menu-button>
|
||||
<div slot="title">
|
||||
${!showPane
|
||||
? html`<ha-dropdown class="lists">
|
||||
|
||||
@@ -2750,7 +2750,6 @@
|
||||
"group_integrations": "Integrations",
|
||||
"group_apps": "Apps",
|
||||
"update_all": "Update all",
|
||||
"update_all_failed": "Failed to start updates",
|
||||
"no_updates": "No updates available",
|
||||
"no_update_entities": {
|
||||
"title": "Unable to check for updates",
|
||||
|
||||
+29
-15
@@ -77,15 +77,19 @@ export const clearBrandsTokenRefresh = (): void => {
|
||||
};
|
||||
|
||||
export const brandsUrl = (options: BrandsOptions, hassUrl?: string): string => {
|
||||
// The brands API requires a token; without one the request 401s. Return an
|
||||
// empty src so no request fires until the token is available. Components
|
||||
// re-render once the token arrives (see connection-mixin) and recompute this.
|
||||
if (!_brandsAccessToken) {
|
||||
return "";
|
||||
}
|
||||
hassUrl = hassUrl ?? location.origin;
|
||||
const base = `/api/brands/integration/${options.domain}/${
|
||||
options.darkOptimized ? "dark_" : ""
|
||||
}${options.type}.png`;
|
||||
|
||||
const url = new URL(base, hassUrl);
|
||||
if (_brandsAccessToken) {
|
||||
url.searchParams.set("token", _brandsAccessToken);
|
||||
}
|
||||
url.searchParams.set("token", _brandsAccessToken);
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
@@ -93,34 +97,44 @@ export const hardwareBrandsUrl = (
|
||||
options: HardwareBrandsOptions,
|
||||
hassUrl?: string
|
||||
): string => {
|
||||
// See brandsUrl: wait for the token before producing a loadable URL.
|
||||
if (!_brandsAccessToken) {
|
||||
return "";
|
||||
}
|
||||
hassUrl = hassUrl ?? location.origin;
|
||||
const base = `/api/brands/hardware/${options.category}/${
|
||||
options.darkOptimized ? "dark_" : ""
|
||||
}${options.manufacturer}${options.model ? `_${options.model}` : ""}.png`;
|
||||
|
||||
const url = new URL(base, hassUrl);
|
||||
if (_brandsAccessToken) {
|
||||
url.searchParams.set("token", _brandsAccessToken);
|
||||
}
|
||||
url.searchParams.set("token", _brandsAccessToken);
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const addBrandsAuth = (url: string, hassUrl?: string): string => {
|
||||
hassUrl = hassUrl ?? location.origin;
|
||||
if (!_brandsAccessToken) {
|
||||
return url;
|
||||
}
|
||||
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
const parsedUrl = new URL(url, hassUrl);
|
||||
if (!parsedUrl.pathname.startsWith("/api/brands/")) {
|
||||
return url;
|
||||
}
|
||||
parsedUrl.searchParams.set("token", _brandsAccessToken);
|
||||
return parsedUrl.toString();
|
||||
parsedUrl = new URL(url, hassUrl);
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Non-brands URLs (e.g. CDN brands.home-assistant.io or camera proxies) are
|
||||
// returned unchanged; they don't use the brands token.
|
||||
if (!parsedUrl.pathname.startsWith("/api/brands/")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Brands API request without a token would 401; return an empty src so it
|
||||
// doesn't fire until the token is available.
|
||||
if (!_brandsAccessToken) {
|
||||
return "";
|
||||
}
|
||||
|
||||
parsedUrl.searchParams.set("token", _brandsAccessToken);
|
||||
return parsedUrl.toString();
|
||||
};
|
||||
|
||||
export const extractDomainFromBrandUrl = (url: string): string => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assert, describe, it, vi, afterEach } from "vitest";
|
||||
import { assert, describe, it, vi, afterEach, beforeEach } from "vitest";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import {
|
||||
addBrandsAuth,
|
||||
@@ -6,17 +6,74 @@ import {
|
||||
clearBrandsTokenRefresh,
|
||||
fetchAndScheduleBrandsAccessToken,
|
||||
fetchBrandsAccessToken,
|
||||
hardwareBrandsUrl,
|
||||
scheduleBrandsTokenRefresh,
|
||||
} from "../../src/util/brands-url";
|
||||
|
||||
// NOTE: the cached brands token is module-level state that persists across
|
||||
// tests. The "without a token" assertions below must run before any test
|
||||
// fetches a token, so this block is intentionally declared first.
|
||||
describe("Brands URLs without a token", () => {
|
||||
// The brands API requires a token; until one is fetched the URL builders
|
||||
// return an empty src so no token-less request (which 401s) fires. Components
|
||||
// re-render once the token arrives and recompute the URL.
|
||||
it("brandsUrl returns an empty src", () => {
|
||||
assert.strictEqual(
|
||||
brandsUrl(
|
||||
{ domain: "cloud", type: "logo" },
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
""
|
||||
);
|
||||
});
|
||||
|
||||
it("hardwareBrandsUrl returns an empty src", () => {
|
||||
assert.strictEqual(
|
||||
hardwareBrandsUrl(
|
||||
{ category: "boards", manufacturer: "raspberry_pi" },
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
""
|
||||
);
|
||||
});
|
||||
|
||||
it("addBrandsAuth returns an empty src for brands URLs", () => {
|
||||
assert.strictEqual(
|
||||
addBrandsAuth(
|
||||
"/api/brands/integration/demo/icon.png",
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
""
|
||||
);
|
||||
});
|
||||
|
||||
it("addBrandsAuth returns non-brands URLs unchanged", () => {
|
||||
assert.strictEqual(
|
||||
addBrandsAuth(
|
||||
"/api/camera_proxy/camera.foo?token=abc",
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
"/api/camera_proxy/camera.foo?token=abc"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Generate brands Url", () => {
|
||||
// Fetch a token before these run so the URL builders produce loadable URLs.
|
||||
beforeEach(async () => {
|
||||
const mockHass = {
|
||||
callWS: async () => ({ token: "test-token-123" }),
|
||||
} as unknown as HomeAssistant;
|
||||
await fetchBrandsAccessToken(mockHass);
|
||||
});
|
||||
|
||||
it("Generate logo brands url for cloud component", () => {
|
||||
assert.strictEqual(
|
||||
brandsUrl(
|
||||
{ domain: "cloud", type: "logo" },
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
"http://homeassistant.local:8123/api/brands/integration/cloud/logo.png"
|
||||
"http://homeassistant.local:8123/api/brands/integration/cloud/logo.png?token=test-token-123"
|
||||
);
|
||||
});
|
||||
it("Generate icon brands url for cloud component", () => {
|
||||
@@ -25,7 +82,7 @@ describe("Generate brands Url", () => {
|
||||
{ domain: "cloud", type: "icon" },
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
"http://homeassistant.local:8123/api/brands/integration/cloud/icon.png"
|
||||
"http://homeassistant.local:8123/api/brands/integration/cloud/icon.png?token=test-token-123"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -35,7 +92,7 @@ describe("Generate brands Url", () => {
|
||||
{ domain: "cloud", type: "logo", darkOptimized: true },
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
"http://homeassistant.local:8123/api/brands/integration/cloud/dark_logo.png"
|
||||
"http://homeassistant.local:8123/api/brands/integration/cloud/dark_logo.png?token=test-token-123"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -51,16 +108,6 @@ describe("addBrandsAuth", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns brands URL unchanged when no token is available", () => {
|
||||
assert.strictEqual(
|
||||
addBrandsAuth(
|
||||
"/api/brands/integration/demo/icon.png",
|
||||
"http://homeassistant.local:8123"
|
||||
),
|
||||
"/api/brands/integration/demo/icon.png"
|
||||
);
|
||||
});
|
||||
|
||||
it("Appends token to brands URL when token is available", async () => {
|
||||
const mockHass = {
|
||||
callWS: async () => ({ token: "test-token-123" }),
|
||||
|
||||
@@ -12265,9 +12265,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"shell-quote@npm:^1.8.3":
|
||||
version: 1.8.3
|
||||
resolution: "shell-quote@npm:1.8.3"
|
||||
checksum: 10/5473e354637c2bd698911224129c9a8961697486cff1fb221f234d71c153fc377674029b0223d1d3c953a68d451d79366abfe53d1a0b46ee1f28eb9ade928f4c
|
||||
version: 1.8.4
|
||||
resolution: "shell-quote@npm:1.8.4"
|
||||
checksum: 10/a3e3796385f2cd5cf0b78207a4439f0c7395c0833fc75b2473084b5d298c109c5c0fa687fcd1c04e4b4484866e5bb8eaae7efae443b80fff71ea7e29baf11f0c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user