Compare commits

..

73 Commits

Author SHA1 Message Date
Aidan Timson ba22a12a20 Fix 2026-03-03 14:46:42 +00:00
Aidan Timson 098b54f749 Fix 2026-03-03 14:46:15 +00:00
Aidan Timson 4c6a7091a6 Filtering 2026-03-03 14:46:15 +00:00
Aidan Timson 322cb35526 More types 2026-03-03 14:46:15 +00:00
Aidan Timson c34f6bea2b Always show 2026-03-03 14:46:15 +00:00
Aidan Timson 41bf0652b0 Setup log classification 2026-03-03 14:46:15 +00:00
renovate[bot] 23af40743b Update dependency lint-staged to v16.3.0 (#29954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 14:36:48 +00:00
Paul Bottein c4326b4f3a Use max width for dashboard footer (#29947) 2026-03-03 14:57:57 +01:00
Paul Bottein d248f5614f Add label for toggle button in area strategy (#29949) 2026-03-03 13:34:05 +01:00
Aidan Timson a4da7b26ea Fix copy to clipboard for wa dialogs (#29951)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-03-03 13:09:30 +01:00
Wendelin 3c49cdf3c0 ha-bottom-sheet reduce motion support (#29950) 2026-03-03 11:06:23 +00:00
Petar Petrov 26af81d1a4 Use net battery power in power sankey card (#29940) 2026-03-03 11:53:54 +01:00
Aidan Timson 2a08f2d79b Add tooltip for config dashboard action button in toolbar (#29948) 2026-03-03 10:33:37 +01:00
Marcin Bauer a5be02b743 Add tooltip for Lovelace dropdown action button in top app bar (#29933) 2026-03-03 09:15:37 +00:00
sevorl 4228871f00 Fix missing slot attribute on wa-divider in automation sidebar action (#29942) 2026-03-03 08:56:33 +00:00
Wendelin 9a7a8fd377 Add reportValidity in ha-form (#29884)
* Add validation for required fields in ha-auth-flow before submission

* Add reportValidity methods to form components for improved validation handling

* Remove async reportValidity funcs

* Review
2026-03-03 09:05:49 +02:00
Wendelin 8b82882e15 ha-authorize fix rtl check (#29937)
Add RTL direction handling in updated lifecycle method
2026-03-02 18:22:06 +01:00
Matthias Alphart 2701015eda Fix data-table content bottom margin (#29805)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 17:02:31 +01:00
Aidan Timson 1991a9e493 Code editor fullscreen in dialogs (#29882)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-03-02 17:01:13 +01:00
Petar Petrov 2b72c54194 Migrate Energy date selector to new footer (#29867) 2026-03-02 17:00:34 +01:00
Paul Bottein a7cb2fe7a7 Fix updates, discovered devices and repairs cards flickering (#29935) 2026-03-02 13:50:42 +00:00
Paul Bottein 51ea0c8201 Fix sidebar not closing when reduced motion is enabled (#29934) 2026-03-02 13:19:26 +00:00
Wendelin ead7081bc6 Dialog: Add show event target check (#29927)
Add event phase check in _handleShow and _handleAfterShow methods
2026-03-02 11:41:52 +00:00
Wendelin ee982b1899 Add error translation for loading energy preferences (#29924) 2026-03-02 11:49:46 +02:00
Aidan Timson e8b100a39e Remove cache to fix re-add repo issue (#29926)
Remove cache to fix readd repo issue
2026-03-02 11:49:19 +02:00
Copilot 50c361db62 Add mixin to remove code duplication in automation/script editors (#29842)
* Initial plan

* Changes before error encountered

Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>

* Fix mixin: use function-body syntax for decorators, curried generics for type safety

Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>

* Simplify automation/script editor mixin signature

* Add shared styles and loading animation to automation/script editor mixin

Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>

* Remove underscore prefix from protected members per style guide

Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
2026-03-02 11:44:04 +02:00
sevorl e7a8d15a13 Use ha-duration-input for wait_template timeout (#29862) 2026-03-02 09:07:51 +01:00
karwosts fbd0409837 Init ha-form expansion elements to undefined instead of null (#29900)
* Init ha-form expansion elements to undefined instead of null

* revert change to error/warning
2026-03-02 09:29:06 +02:00
karwosts a0d100611f Fix distribution card stub error (#29915)
* Fix distribution card stub error

* unit check not required
2026-03-02 09:06:10 +02:00
dependabot[bot] a969bf1065 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#29922)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 06:10:19 +00:00
renovate[bot] a153330610 Update dependency gulp-zopfli-green to v7 (#29919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 07:06:41 +01:00
renovate[bot] bd2f1ca3a8 Update dependency @html-eslint/eslint-plugin to v0.57.1 (#29905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 20:08:37 +01:00
renovate[bot] 3263034416 Update dependency @codemirror/language to v6.12.2 (#29904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 20:07:43 +01:00
Paul Bottein 82b28b547a Fix control select menu color in ios (#29892) 2026-02-27 17:26:04 +01:00
Bram Kragten 61c2c750b4 Fix overflow for icon buttons (#29891) 2026-02-27 15:44:21 +00:00
Petar Petrov 117690ee70 Fix sensor card graph not updating when value is unchanged (#29889) 2026-02-27 15:41:54 +00:00
Petar Petrov e753de85eb Make hui-sections-view always fill the screen so footer is at the bottom (#29890) 2026-02-27 15:39:21 +00:00
Paul Bottein a240019968 Add render icon property to ha-control-select-menu (#29881) 2026-02-27 16:23:58 +01:00
Petar Petrov 0bdf4b8777 Fix monetary device class state display with non-ISO 4217 currency symbols (#29887) 2026-02-27 14:59:14 +01:00
renovate[bot] 6337828ed8 Update dependency barcode-detector to v3.1.0 (#29886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 15:46:29 +02:00
Aidan Timson b8e5af652b Add audits and yaml mode to more info details (#29854)
* Add audits and yaml mode to more info details

* Reset yaml mode on back

* Use mapped array for state entries

* Typo

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Memoize

* Rename

* Fix

* Format audits in normal mode

* Refactor, dont pass hass

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-02-27 14:45:55 +01:00
Petar Petrov e4ae29e8b5 Fix energy compare tooltip showing wrong year (#29885) 2026-02-27 14:37:52 +01:00
Aidan Timson 08231dbbb0 Use large width on system log dialogs (#29879) 2026-02-27 12:46:10 +01:00
Paul Bottein 0ca656933d Revert "Add render icon property to ha-control-select-menu"
This reverts commit b23cf8eba4.
2026-02-27 12:21:23 +01:00
Paul Bottein b23cf8eba4 Add render icon property to ha-control-select-menu 2026-02-27 12:20:52 +01:00
Robert Resch 61b546415d Revert "Add vacuum mapping not configured issue" (#29876) 2026-02-27 11:18:49 +01:00
Brandon Chen 4e1b709303 Fix YAML content invisible in dark mode for conversation debug result… (#29874) 2026-02-27 09:11:28 +01:00
renovate[bot] 34e65b302d Update dependency typescript-eslint to v8.56.1 (#29868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 18:08:04 +00:00
renovate[bot] 336d0e1b9d Update dependency @html-eslint/eslint-plugin to v0.57.0 (#29863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 17:25:10 +01:00
Paul Bottein 58d4cf8d84 Fix scrollbar in 2026.3 (#29865) 2026-02-26 16:44:12 +01:00
Aidan Timson d3453aff37 Add missing theming variable support to dialog and bottom sheet (#29857) 2026-02-26 16:43:20 +01:00
Aidan Timson 64ff2e414c Add thread configuration my link (#29861) 2026-02-26 15:06:46 +00:00
Wendelin 2ca25c980f Fix quick search icon size (#29858) 2026-02-26 15:59:27 +01:00
Aidan Timson 73d93bc601 Add matter configuration my link (#29859) 2026-02-26 14:41:43 +00:00
Wendelin 5ca6a8aced Fix ha-icon-button-toggle selected style (#29856) 2026-02-26 13:02:12 +00:00
Aidan Timson 7ff4993e0b Fix esc closing dialogs with prevent scrim close (#29851) 2026-02-26 13:20:05 +02:00
Norbert Rittel 4e6fbacccc Remove trailing periods from "Learn more" etc. links / tooltips (#29835) 2026-02-26 10:38:54 +00:00
Petar Petrov 2958d49e36 Convert Energy Now tiles to badges (#29845) 2026-02-26 10:38:01 +00:00
Norbert Rittel 92289dc7ea Improve "Create a new … helper" option in entity picker (#29853) 2026-02-26 10:34:42 +00:00
Petar Petrov f6c1a890e4 Dynamically calculate the date range picker's vertical opening direction (#29850) 2026-02-26 09:33:34 +00:00
Wendelin d06321ed43 Fix protocols dashboards fab padding (#29847) 2026-02-26 10:31:50 +02:00
dependabot[bot] 3c3d8d9974 Bump rollup from 2.79.2 to 2.80.0 (#29841)
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.2 to 2.80.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/v2.80.0/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.2...v2.80.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 2.80.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 08:18:15 +02:00
Paul Bottein 4f39fa482d Only ask to refresh dashboard in edit mode or yaml mode (#29826) 2026-02-26 08:16:21 +02:00
renovate[bot] 5d0fe3236c Update dependency @swc/helpers to v0.5.19 (#29836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 07:07:37 +01:00
renovate[bot] b86142ae50 Update Node.js to v24.14.0 (#29831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 19:25:42 +00:00
renovate[bot] 5d2f3ee5e8 Update dependency tar to v7.5.9 (#29832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 19:24:58 +00:00
AlCalzone e3f7c631a7 Rename "Z-Wave JS" to "Z-Wave" when not referring to the project/org (#29830) 2026-02-25 19:15:16 +00:00
renovate[bot] 49f9d95853 Update dependency vite-tsconfig-paths to v6.1.1 (#29829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:53:12 +01:00
renovate[bot] db3d7701b5 Update dependency typescript-eslint to v8.56.0 (#29828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 17:35:36 +00:00
renovate[bot] 3e55acf531 Update dependency @home-assistant/webawesome to v3.2.1-ha.3 (#29810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:26:47 +01:00
renovate[bot] f102618d9d Update dependency eslint-plugin-wc to v3.1.0 (#29824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:25:06 +01:00
renovate[bot] a3c02b511d Update dependency jsdom to v28.1.0 (#29825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:24:38 +01:00
Bram Kragten 74111d248e Fix css minifying (#29827) 2026-02-25 17:53:50 +01:00
113 changed files with 2741 additions and 1901 deletions
+2 -2
View File
@@ -89,13 +89,13 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
- name: Upload frontend build
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: frontend-build
path: hass_frontend/
+2 -2
View File
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz
+1 -1
View File
@@ -1 +1 @@
24.13.1
24.14.0
+14 -14
View File
@@ -30,7 +30,7 @@
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.2",
"@codemirror/language": "6.12.1",
"@codemirror/language": "6.12.2",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.5.4",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.2.1-ha.2",
"@home-assistant/webawesome": "3.2.1-ha.3",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
@@ -83,7 +83,7 @@
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.18",
"@swc/helpers": "0.5.19",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
@@ -92,7 +92,7 @@
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.0.8",
"barcode-detector": "3.1.0",
"color-name": "2.1.0",
"comlink": "4.4.2",
"core-js": "3.48.0",
@@ -106,7 +106,7 @@
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
@@ -149,7 +149,7 @@
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@bundle-stats/plugin-webpack-filter": "4.21.10",
"@html-eslint/eslint-plugin": "0.56.0",
"@html-eslint/eslint-plugin": "0.57.1",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
@@ -172,7 +172,7 @@
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "6.1.13",
"@types/tar": "7.0.87",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.18",
@@ -188,7 +188,7 @@
"eslint-plugin-lit": "2.2.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.4.1",
"eslint-plugin-wc": "3.0.2",
"eslint-plugin-wc": "3.1.0",
"fancy-log": "2.0.0",
"fs-extra": "11.3.3",
"glob": "13.0.6",
@@ -198,9 +198,9 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "28.0.0",
"jsdom": "28.1.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lint-staged": "16.3.0",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -210,12 +210,12 @@
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.5",
"sinon": "21.0.1",
"tar": "7.5.8",
"tar": "7.5.9",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0",
"vite-tsconfig-paths": "6.0.5",
"typescript-eslint": "8.56.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.0.18",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
@@ -235,6 +235,6 @@
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.13.1"
"node": "24.14.0"
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20260226.0"
version = "20260128.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
+10 -2
View File
@@ -2,7 +2,7 @@
import { genClientId } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
@@ -23,6 +23,7 @@ import type {
DataEntryFlowStepForm,
} from "../data/data_entry_flow";
import "./ha-auth-form";
import type { HaAuthForm } from "./ha-auth-form";
type State = "loading" | "error" | "step";
@@ -52,6 +53,8 @@ export class HaAuthFlow extends LitElement {
@state() private _submitting = false;
@query("ha-auth-form") private _form?: HaAuthForm;
createRenderRoot() {
return this;
}
@@ -179,7 +182,7 @@ export class HaAuthFlow extends LitElement {
<div class="action">
<ha-button
@click=${this._handleSubmit}
.disabled=${this._submitting}
.loading=${this._submitting}
>
${this.step.type === "form"
? this.localize("ui.panel.page-authorize.form.next")
@@ -370,6 +373,11 @@ export class HaAuthFlow extends LitElement {
this._providerChanged(this.authProvider);
return;
}
if (!this._form?.reportValidity()) {
return;
}
this._submitting = true;
const postData = { ...this._stepData, client_id: this.clientId };
+4
View File
@@ -12,6 +12,10 @@ export class HaAuthFormString extends HaFormString {
return this;
}
public reportValidity(): boolean {
return this.querySelector("ha-auth-textfield")?.reportValidity() ?? true;
}
protected render(): TemplateResult {
return html`
<style>
+22 -21
View File
@@ -133,33 +133,34 @@ const computeStateToPartsFromEntityAttributes = (
),
});
} catch (_err) {
// fallback to default
// fallback to default numeric formatting below
}
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
if (parts.length) {
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
const valueParts: ValuePart[] = [];
const valueParts: ValuePart[] = [];
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
}
}
return valueParts;
}
return valueParts;
}
// default processing of numeric values
+27 -1
View File
@@ -1,3 +1,24 @@
import { deepActiveElement } from "../dom/deep-active-element";
const getClipboardFallbackRoot = (): HTMLElement => {
const activeElement = deepActiveElement();
if (activeElement instanceof HTMLElement) {
let root: Node = activeElement.getRootNode();
let host: HTMLElement | null = null;
while (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
host = root.host;
root = root.host.getRootNode();
}
if (host) {
return host;
}
}
return document.body;
};
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
if (navigator.clipboard) {
try {
@@ -8,10 +29,15 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
}
}
const root = rootEl ?? document.body;
const root = rootEl || getClipboardFallbackRoot();
const el = document.createElement("textarea");
el.value = str;
el.setAttribute("readonly", "");
el.style.position = "fixed";
el.style.top = "0";
el.style.left = "0";
el.style.opacity = "0";
root.appendChild(el);
el.select();
document.execCommand("copy");
+5 -15
View File
@@ -118,8 +118,6 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public clickable = false;
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
/**
* Add an extra row at the bottom of the data table
* @type {TemplateResult}
@@ -519,7 +517,6 @@ export class HaDataTable extends LitElement {
this._filteredData,
localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups,
@@ -716,14 +713,13 @@ export class HaDataTable extends LitElement {
data: DataTableRowData[],
localize: LocalizeFunc,
appendRow,
hasFab: boolean,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
collapsedGroups: string[],
sortColumn: string | undefined,
sortDirection: SortingDirection
) => {
if (appendRow || hasFab || groupColumn) {
if (appendRow || groupColumn) {
let items = [...data];
if (groupColumn) {
@@ -813,13 +809,11 @@ export class HaDataTable extends LitElement {
items.push({ append: true, selectable: false, content: appendRow });
}
if (hasFab) {
items.push({ empty: true });
}
items.push({ empty: true });
return items;
}
return data;
return [...data, { empty: true }];
}
);
@@ -871,7 +865,6 @@ export class HaDataTable extends LitElement {
this._filteredData,
this.localizeFunc || this.hass.localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups,
@@ -1089,11 +1082,8 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__row.empty-row {
height: max(
var(
--data-table-empty-row-height,
var(--data-table-row-height, 52px)
),
height: var(
--data-table-empty-row-height,
var(--safe-area-inset-bottom, 0px)
);
}
+5 -5
View File
@@ -672,11 +672,11 @@ export class HaAssistChat extends LitElement {
--markdown-code-background-color: var(--primary-background-color);
--markdown-code-text-color: var(--primary-text-color);
--markdown-list-indent: 1.15em;
&:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;
flex-shrink: 0;
}
}
ha-markdown:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;
flex-shrink: 0;
}
.bouncer {
width: 48px;
+12 -1
View File
@@ -1,7 +1,7 @@
import { mdiClose } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, queryAll } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
@@ -133,6 +133,17 @@ export class HaBaseTimeInput extends LitElement {
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
@queryAll("ha-textfield") private _inputs?: HaTextField[];
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._inputs?.every((input) => input.reportValidity()) ?? true;
}
protected render(): TemplateResult {
return html`
${this.label
+12
View File
@@ -406,6 +406,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
@media (prefers-reduced-motion: reduce) {
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: 1ms;
--hide-duration: 1ms;
}
wa-drawer::part(dialog) {
transition: 1ms;
}
}
wa-drawer::part(dialog)::backdrop {
-webkit-backdrop-filter: var(
--ha-bottom-sheet-scrim-backdrop-filter,
+1 -1
View File
@@ -245,7 +245,7 @@ export class HaButton extends Button {
}
.label {
overflow: hidden;
overflow: var(--ha-button-label-overflow, hidden);
text-overflow: ellipsis;
padding: var(--ha-space-1) 0;
}
+32 -4
View File
@@ -84,6 +84,9 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "disable-fullscreen" })
public disableFullscreen = false;
@property({ type: Boolean, attribute: "in-dialog" })
public inDialog = false;
@property({ type: Boolean, attribute: "has-toolbar" })
public hasToolbar = true;
@@ -132,6 +135,7 @@ export class HaCodeEditor extends ReactiveElement {
public connectedCallback() {
super.connectedCallback();
this.classList.toggle("in-dialog", this.inDialog);
// Force update on reconnection so editor is recreated
if (this.hasUpdated) {
this.requestUpdate();
@@ -150,6 +154,7 @@ export class HaCodeEditor extends ReactiveElement {
}
public disconnectedCallback() {
fireEvent(this, "dialog-set-fullscreen", false);
super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation);
this.removeEventListener("keydown", this._handleKeyDown);
@@ -216,6 +221,9 @@ export class HaCodeEditor extends ReactiveElement {
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
if (changedProps.has("inDialog")) {
this.classList.toggle("in-dialog", this.inDialog);
}
if (changedProps.has("_isFullscreen")) {
this.classList.toggle("fullscreen", this._isFullscreen);
this._updateToolbarButtons();
@@ -434,10 +442,19 @@ export class HaCodeEditor extends ReactiveElement {
private _updateFullscreenState(
fullscreen: boolean = this._isFullscreen
): boolean {
const previousFullscreen = this._isFullscreen;
this.classList.toggle("in-dialog", this.inDialog);
// Update the current fullscreen state based on selected value. If fullscreen
// is disabled, or we have no toolbar, ensure we are not in fullscreen mode.
this._isFullscreen =
fullscreen && !this.disableFullscreen && this.hasToolbar;
if (previousFullscreen !== this._isFullscreen) {
fireEvent(this, "dialog-set-fullscreen", this._isFullscreen);
}
// Return whether successfully in requested state
return this._isFullscreen === fullscreen;
}
@@ -846,10 +863,10 @@ export class HaCodeEditor extends ReactiveElement {
:host(.fullscreen) {
position: fixed !important;
top: calc(var(--header-height, 56px) + 8px) !important;
left: 8px !important;
right: 8px !important;
bottom: 8px !important;
top: calc(var(--header-height, 56px) + var(--ha-space-2)) !important;
left: var(--ha-space-2) !important;
right: var(--ha-space-2) !important;
bottom: var(--ha-space-2) !important;
z-index: 6;
border-radius: var(--ha-border-radius-lg) !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
@@ -867,6 +884,17 @@ export class HaCodeEditor extends ReactiveElement {
display: block !important;
}
:host(.in-dialog.fullscreen) {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
padding: 0 !important;
}
:host(.hasToolbar) .cm-editor {
padding-top: var(--code-editor-toolbar-height);
}
+2 -1
View File
@@ -33,6 +33,7 @@ export class HaControlButton extends LitElement {
--control-button-background-color: var(--disabled-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: var(--ha-border-radius-md);
--control-button-font-weight: var(--ha-font-weight-medium);
--control-button-padding: 8px;
--mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
@@ -59,7 +60,7 @@ export class HaControlButton extends LitElement {
box-sizing: border-box;
line-height: inherit;
font-family: var(--ha-font-family-body);
font-weight: var(--ha-font-weight-medium);
font-weight: var(--control-button-font-weight);
outline: none;
overflow: hidden;
background: none;
+17 -33
View File
@@ -1,11 +1,9 @@
import { mdiMenuDown } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../types";
import "./ha-attribute-icon";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon";
@@ -16,17 +14,10 @@ export interface SelectOption {
value: string;
iconPath?: string;
icon?: string;
attributeIcon?: {
stateObj: HassEntity;
attribute: string;
attributeValue?: string;
};
}
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow = false;
@@ -47,6 +38,9 @@ export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public options: SelectOption[] = [];
@property({ attribute: false })
public renderIcon?: (value: string) => TemplateResult<1> | typeof nothing;
@query("button") private _triggerButton!: HTMLButtonElement;
public override render() {
@@ -94,14 +88,8 @@ export class HaControlSelectMenu extends LitElement {
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
: option.attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${option.attributeIcon.stateObj}
.attribute=${option.attributeIcon.attribute}
.attributeValue=${option.attributeIcon.attributeValue}
></ha-attribute-icon>`
: this.renderIcon
? html`<span slot="icon">${this.renderIcon(option.value)}</span>`
: nothing}
${option.label}</ha-dropdown-item
>`;
@@ -119,24 +107,20 @@ export class HaControlSelectMenu extends LitElement {
}
private _renderIcon() {
const { iconPath, icon, attributeIcon } =
this.getValueObject(this.options, this.value) ?? {};
const value = this.getValueObject(this.options, this.value);
const defaultIcon = this.querySelector("[slot='icon']");
return html`
<div class="icon">
${iconPath
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
: icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
: attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${attributeIcon.stateObj}
.attribute=${attributeIcon.attribute}
.attributeValue=${attributeIcon.attributeValue}
></ha-attribute-icon>`
${value?.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${value.iconPath}
></ha-svg-icon>`
: value?.icon
? html`<ha-icon slot="icon" .icon=${value.icon}></ha-icon>`
: this.renderIcon && this.value
? this.renderIcon(this.value)
: defaultIcon
? html`<slot name="icon"></slot>`
: nothing}
@@ -172,12 +156,12 @@ export class HaControlSelectMenu extends LitElement {
font-size: var(--ha-font-size-m);
line-height: 1.4;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
border: none;
text-align: left;
color: var(--primary-text-color);
height: var(--control-select-menu-height);
padding: var(--control-select-menu-padding);
overflow: hidden;
+8 -1
View File
@@ -1,7 +1,7 @@
import { mdiCalendar } from "@mdi/js";
import type { HassConfig } from "home-assistant-js-websocket";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event";
@@ -9,6 +9,7 @@ import { TimeZone } from "../data/translation";
import type { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
@@ -52,6 +53,12 @@ export class HaDateInput extends LitElement {
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
@query("ha-textfield", true) private _input?: HaTextField;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
render() {
return html`<ha-textfield
.label=${this.label}
+79 -24
View File
@@ -10,7 +10,9 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event";
import { withViewTransition } from "../common/util/view-transition";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -127,6 +129,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
private _escapePressed = false;
public connectedCallback(): void {
super.connectedCallback();
this.addEventListener(
"dialog-set-fullscreen",
this._handleFullscreenChanged as EventListener
);
}
protected get scrollableElement(): HTMLElement | null {
return this.bodyContainer;
}
@@ -194,7 +204,10 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
`;
}
private _handleShow = async () => {
private _handleShow = async (ev: Event) => {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
this._open = true;
fireEvent(this, "opened");
@@ -220,22 +233,46 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
});
};
private _handleAfterShow = () => {
private _handleAfterShow = (ev: Event) => {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
fireEvent(this, "after-show");
};
private _handleAfterHide = (ev: DialogHideEvent) => {
if (ev.eventPhase === Event.AT_TARGET) {
this._open = false;
this._setFullscreen(false);
fireEvent(this, "closed");
}
};
public disconnectedCallback(): void {
this.removeEventListener(
"dialog-set-fullscreen",
this._handleFullscreenChanged as EventListener
);
this._setFullscreen(false);
super.disconnectedCallback();
this._open = false;
}
private _handleFullscreenChanged(ev: HASSDomEvent<boolean>): void {
if (!this._open) {
this._setFullscreen(ev.detail);
return;
}
withViewTransition(() => {
this._setFullscreen(ev.detail);
});
}
private _setFullscreen(fullscreen: boolean): void {
this.toggleAttribute("fullscreen", fullscreen);
}
@eventOptions({ passive: true })
private _handleBodyScroll(ev: Event) {
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
@@ -301,10 +338,27 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
}
:host([width="full"]) wa-dialog {
:host([width="full"]) wa-dialog,
:host([fullscreen]) wa-dialog {
--width: var(--full-width);
}
:host([fullscreen]) wa-dialog::part(dialog) {
min-height: var(--safe-height);
max-height: var(--safe-height);
margin-top: 0;
transform: none;
}
:host([fullscreen]) .content-wrapper {
overflow: hidden;
}
:host([fullscreen]) .body {
overflow: hidden;
padding: 0;
}
wa-dialog::part(dialog) {
-webkit-backdrop-filter: var(
--ha-dialog-surface-backdrop-filter,
@@ -356,29 +410,29 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
@media all and (max-width: 450px), all and (max-height: 500px) {
:host([type="standard"]) {
--ha-dialog-border-radius: 0;
}
wa-dialog {
/* Make the container fill the whole screen width and not the safe width */
--full-width: var(--ha-dialog-width-full, 100vw);
--width: var(--full-width);
}
:host([type="standard"]) wa-dialog {
/* Make the container fill the whole screen width and not the safe width */
--full-width: var(--ha-dialog-width-full, 100vw);
--width: var(--full-width);
}
wa-dialog::part(dialog) {
/* Make the dialog fill the whole screen height and not the safe height */
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100dvh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100dvh);
margin-top: 0;
margin-bottom: 0;
/* Use safe area as padding instead of the container size */
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Reset the transform to center the dialog */
transform: none;
}
:host([type="standard"]) wa-dialog::part(dialog) {
/* Make the dialog fill the whole screen height and not the safe height */
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100dvh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100dvh);
margin-top: 0;
margin-bottom: 0;
/* Use safe area as padding instead of the container size */
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Reset the transform to center the dialog */
transform: none;
}
}
@@ -465,6 +519,7 @@ declare global {
}
interface HASSDomEvents {
"dialog-set-fullscreen": boolean;
opened: undefined;
"after-show": undefined;
closed: undefined;
+3 -1
View File
@@ -186,9 +186,11 @@ export class HaDrawer extends DrawerBase {
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
/* Use 1ms instead of "none" so the transitionend event still fires.
The MDC drawer foundation relies on it to complete the close cycle. */
.mdc-drawer,
.mdc-drawer-app-content {
transition: none;
transition: 1ms;
}
}
`,
+16 -5
View File
@@ -1,12 +1,12 @@
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
import type { ValueChangedEvent } from "../types";
import "./ha-base-time-input";
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
export interface HaDurationData {
days?: number;
@@ -19,7 +19,7 @@ export interface HaDurationData {
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
@customElement("ha-duration-input")
class HaDurationInput extends LitElement {
export class HaDurationInput extends LitElement {
@property({ attribute: false }) public data?: HaDurationData;
@property() public label?: string;
@@ -42,8 +42,19 @@ class HaDurationInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-base-time-input", true) private _input?: HaBaseTimeInput;
private _toggleNegative = false;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
return html`
<div class="row">
+9 -2
View File
@@ -1,8 +1,9 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import "./ha-form";
import "../ha-expansion-panel";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormElement,
@@ -35,6 +36,12 @@ export class HaFormExpandable extends LitElement implements HaFormElement {
key: string
) => string;
@query("ha-form", true) private _form?: HaForm;
public reportValidity(): boolean {
return this._form?.reportValidity() ?? true;
}
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;
+11 -8
View File
@@ -1,15 +1,15 @@
import type { TemplateResult, PropertyValues } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaTextField } from "../ha-textfield";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import type {
HaFormElement,
HaFormFloatData,
HaFormFloatSchema,
} from "./types";
import type { LocalizeFunc } from "../../common/translations/localize";
@customElement("ha-form-float")
export class HaFormFloat extends LitElement implements HaFormElement {
@@ -25,12 +25,15 @@ export class HaFormFloat extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield") private _input?: HaTextField;
@query("ha-textfield", true) private _input?: HaTextField;
public focus() {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
+21 -7
View File
@@ -1,14 +1,15 @@
import "./ha-form";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, queryAll } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormGridSchema,
HaFormDataContainer,
HaFormElement,
HaFormGridSchema,
HaFormSchema,
} from "./types";
import type { HomeAssistant } from "../../types";
@customElement("ha-form-grid")
export class HaFormGrid extends LitElement implements HaFormElement {
@@ -33,9 +34,22 @@ export class HaFormGrid extends LitElement implements HaFormElement {
key: string
) => string;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
@queryAll("ha-form", true) private _forms?: HaForm[];
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
const forms = this._forms ?? [];
let valid = true;
for (const form of forms) {
if (!form.reportValidity()) {
valid = false;
}
}
return valid;
}
protected updated(changedProps: PropertyValues): void {
+25 -10
View File
@@ -2,10 +2,11 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-slider";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import type {
@@ -13,7 +14,6 @@ import type {
HaFormIntegerData,
HaFormIntegerSchema,
} from "./types";
import type { LocalizeFunc } from "../../common/translations/localize";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@@ -29,24 +29,39 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield ha-slider") private _input?:
@query("ha-textfield, ha-slider", true) private _input?:
| HaTextField
| HTMLInputElement;
private _lastValue?: HaFormIntegerData;
public focus() {
if (this._input) {
this._input.focus();
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
const showSlider = this._showSlider();
if (showSlider && this.schema.required && isNaN(Number(this.data))) {
return false;
}
if (!showSlider) {
return this._input?.reportValidity() ?? true;
}
return true;
}
protected render(): TemplateResult {
if (
private _showSlider(): boolean {
return (
this.schema.valueMin !== undefined &&
this.schema.valueMax !== undefined &&
this.schema.valueMax - this.schema.valueMin < 256
) {
);
}
protected render(): TemplateResult {
if (this._showSlider()) {
return html`
<div>
${this.label}
@@ -44,6 +44,13 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
this._dropdown?.focus();
}
public reportValidity(): boolean {
if (!this.schema.required || (this.data && this.data.length > 0)) {
return true;
}
return false;
}
protected render(): TemplateResult {
const options = Array.isArray(this.schema.options)
? this.schema.options
@@ -8,16 +8,17 @@ import type { LocalizeFunc } from "../../common/translations/localize";
import type { HomeAssistant } from "../../types";
import "../ha-button";
import "../ha-dropdown";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
import "../ha-dropdown-item";
import "../ha-svg-icon";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormElement,
HaFormOptionalActionsSchema,
HaFormSchema,
} from "./types";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
const NO_ACTIONS = [];
@@ -53,6 +54,11 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
this.renderRoot.querySelector("ha-form")?.focus();
}
public reportValidity(): boolean {
const form = this.renderRoot.querySelector<HaForm>("ha-form");
return form ? form.reportValidity() : true;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("data")) {
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../ha-duration-input";
import type { HaDurationInput } from "../ha-duration-input";
import type { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
@customElement("ha-form-positive_time_period_dict")
@@ -14,12 +15,15 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-time-input", true) private _input?: HTMLElement;
@query("ha-duration-input", true) private _input?: HaDurationInput;
public focus() {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
+10 -3
View File
@@ -1,16 +1,16 @@
import memoizeOne from "memoize-one";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-selector/ha-selector-select";
import type {
HaFormElement,
HaFormSelectData,
HaFormSelectSchema,
} from "./types";
import type { SelectSelector } from "../../data/selector";
import "../ha-selector/ha-selector-select";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@@ -41,6 +41,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
})
);
public reportValidity(): boolean {
if (!this.schema.required || this.data) {
return true;
}
return false;
}
protected render(): TemplateResult {
return html`
<ha-selector-select
+8 -5
View File
@@ -37,12 +37,15 @@ export class HaFormString extends LitElement implements HaFormElement {
@state() protected unmaskedPassword = false;
@query("ha-textfield") private _input?: HaTextField;
@query("ha-textfield", true) private _input?: HaTextField;
public focus(): void {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
+55 -18
View File
@@ -1,5 +1,5 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, ReactiveElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
@@ -24,7 +24,7 @@ const LOAD_ELEMENTS = {
};
const getValue = (obj, item) =>
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : undefined;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
@@ -76,22 +76,64 @@ export class HaForm extends LitElement implements HaFormElement {
return {};
}
public async focus() {
await this.updateComplete;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
public reportValidity(): boolean {
const root = this.renderRoot.querySelector(".root");
if (!root) {
return;
return true;
}
for (const child of root.children) {
if (child.tagName !== "HA-ALERT") {
if (child instanceof ReactiveElement) {
// eslint-disable-next-line no-await-in-loop
await child.updateComplete;
}
(child as HTMLElement).focus();
break;
const elements = [...root.children].filter(
(child) => child.localName !== "ha-alert"
) as (HTMLElement & { reportValidity?: () => boolean })[];
let isValid = true;
let firstInvalidElement: HTMLElement | undefined;
this.schema.forEach((item, index) => {
const element = elements[index];
if (!element) {
return;
}
let elementValid = true;
if (
"reportValidity" in element &&
typeof element.reportValidity === "function"
) {
elementValid = element.reportValidity();
} else if (
item.required &&
!(
"type" in item && ["boolean", "constant"].includes(item.type ?? "")
) &&
!(
"selector" in item &&
("boolean" in item.selector || "constant" in item.selector)
)
) {
const value = getValue(this.data, item);
elementValid = value !== undefined && value !== null && value !== "";
}
if (!elementValid) {
isValid = false;
if (!firstInvalidElement) {
firstInvalidElement = element;
}
}
});
if (firstInvalidElement) {
firstInvalidElement.focus?.();
}
return isValid;
}
protected willUpdate(changedProps: PropertyValues) {
@@ -105,11 +147,6 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
protected render(): TemplateResult {
return html`
<div class="root" part="root">
+1 -1
View File
@@ -38,7 +38,7 @@ export class HaBadge extends LitElement {
font-weight: var(--ha-heading-badge-font-weight, 400);
line-height: var(--ha-heading-badge-line-height, 20px);
letter-spacing: 0.1px;
--mdc-icon-size: 14px;
--mdc-icon-size: 16px;
}
::slotted([slot="icon"]) {
--ha-icon-display: block;
+1
View File
@@ -74,6 +74,7 @@ export class HaIconButton extends LitElement {
);
--wa-color-on-normal: currentColor;
--wa-color-fill-quiet: transparent;
--ha-button-label-overflow: visible;
}
ha-button::after {
content: "";
+9 -11
View File
@@ -84,13 +84,11 @@ export class HaMarkdown extends LitElement {
ha-markdown-element > :is(ol, ul) {
padding-inline-start: var(--markdown-list-indent, revert);
}
li {
&:has(input[type="checkbox"]) {
list-style: none;
& > input[type="checkbox"] {
margin-left: 0;
}
}
li:has(input[type="checkbox"]) {
list-style: none;
}
li:has(input[type="checkbox"]) > input[type="checkbox"] {
margin-left: 0;
}
svg {
background-color: var(--markdown-svg-background-color, none);
@@ -137,10 +135,10 @@ export class HaMarkdown extends LitElement {
--markdown-table-border-width: 0;
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th,
td {
vertical-align: middle;
}
}
table[role="presentation"] th,
table[role="presentation"] td {
vertical-align: middle;
}
table[role="presentation"] td[valign="top"],
table[role="presentation"] th[valign="top"] {
@@ -1,8 +1,9 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { DateSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
@customElement("ha-selector-date")
export class HaDateSelector extends LitElement {
@@ -20,6 +21,12 @@ export class HaDateSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-date-input", true) private _input?: HaDateInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
return html`
<ha-date-input
@@ -29,6 +29,10 @@ export class HaDateTimeSelector extends LitElement {
@query("ha-time-input") private _timeInput!: HaTimeInput;
public reportValidity(): boolean {
return this._dateInput.reportValidity() && this._timeInput.reportValidity();
}
protected render() {
const values =
typeof this.value === "string" ? this.value.split(" ") : undefined;
@@ -1,10 +1,10 @@
import memoizeOne from "memoize-one";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { DurationSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import type { HaDurationData } from "../ha-duration-input";
import "../ha-duration-input";
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
@customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement {
@@ -25,6 +25,12 @@ export class HaTimeDuration extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-duration-input", true) private _input?: HaDurationInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
private _data = memoizeOne(
(value?: HaDurationData | string | number): HaDurationData | undefined => {
if (typeof value === "number") {
@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import type { NumberSelector } from "../../data/selector";
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@@ -30,8 +31,14 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield", true) private _input?: HaTextField | HTMLInputElement;
private _valueStr = "";
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("value")) {
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {
+11 -4
View File
@@ -1,6 +1,6 @@
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { StringSelector } from "../../data/selector";
@@ -32,11 +32,18 @@ export class HaTextSelector extends LitElement {
@state() private _unmaskedPassword = false;
@query("ha-textfield, ha-textarea") private _input?: HTMLInputElement;
public async focus() {
await this.updateComplete;
(
this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement
)?.focus();
this._input?.focus();
}
public reportValidity(): boolean {
if (this.selector.text?.multiple) {
return true;
}
return this._input?.reportValidity() ?? true;
}
protected render() {
@@ -1,8 +1,9 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { TimeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@@ -20,6 +21,12 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public required = false;
@query("ha-time-input") private _input?: HaTimeInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
return html`
<ha-time-input
+20 -2
View File
@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { Selector } from "../../data/selector";
@@ -94,9 +94,27 @@ export class HaSelector extends LitElement {
@property({ attribute: false }) public context?: Record<string, any>;
@query("#selector", true) private _selectorElement?: HTMLElement;
public reportValidity(): boolean {
if (
this._selectorElement &&
"reportValidity" in this._selectorElement &&
typeof this._selectorElement.reportValidity === "function"
) {
return this._selectorElement?.reportValidity() ?? true;
}
if (this.required) {
return (
this.value !== undefined && this.value !== null && this.value !== ""
);
}
return true;
}
public async focus() {
await this.updateComplete;
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
this._selectorElement?.focus();
}
private get _type() {
+2 -2
View File
@@ -37,8 +37,8 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
import type { UpdateEntity } from "../data/update";
import { updateCanInstall } from "../data/update";
import { showEditSidebarDialog } from "../dialogs/sidebar/show-dialog-edit-sidebar";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -981,7 +981,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
ha-md-list-item,
ha-md-list-item .item-text,
.title {
transition: none;
transition: 1ms;
}
}
`,
+9 -3
View File
@@ -1,11 +1,11 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import type { FrontendLocaleData } from "../data/translation";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import type { ValueChangedEvent } from "../types";
import "./ha-base-time-input";
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
@customElement("ha-time-input")
export class HaTimeInput extends LitElement {
@@ -26,6 +26,12 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
@query("ha-base-time-input") private _input?: HaBaseTimeInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
const useAMPM = useAmPm(this.locale);
+1 -1
View File
@@ -46,7 +46,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
transition: 1ms;
}
}
.mdc-top-app-bar__title {
@@ -298,7 +298,7 @@ export class TopAppBarBaseBase extends BaseElement {
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
transition: 1ms;
}
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
+11
View File
@@ -47,6 +47,9 @@ export class HaYamlEditor extends LitElement {
@property({ type: Boolean, attribute: "disable-fullscreen" })
public disableFullscreen = false;
@property({ type: Boolean, attribute: "in-dialog" })
public inDialog = false;
@property({ type: Boolean }) public required = false;
@property({ attribute: "copy-clipboard", type: Boolean })
@@ -101,6 +104,13 @@ export class HaYamlEditor extends LitElement {
}
}
public disableCodeEditorFullscreen(): void {
this.disableFullscreen = true;
if (this._codeEditor) {
this._codeEditor.disableFullscreen = true;
}
}
protected render() {
if (this._yaml === undefined) {
return nothing;
@@ -114,6 +124,7 @@ export class HaYamlEditor extends LitElement {
.value=${this._yaml}
.readOnly=${this.readOnly}
.disableFullscreen=${this.disableFullscreen}
.inDialog=${this.inDialog}
mode="yaml"
autocomplete-entities
autocomplete-icons
+3 -1
View File
@@ -37,9 +37,11 @@ export interface LovelaceViewHeaderConfig {
badges_wrap?: "wrap" | "scroll";
}
export const DEFAULT_FOOTER_MAX_WIDTH_PX = 600;
export interface LovelaceViewFooterConfig {
card?: LovelaceCardConfig;
column_span?: number;
max_width?: number;
}
export interface LovelaceViewSidebarConfig {
+13
View File
@@ -7,10 +7,23 @@ export type SystemLogLevel =
| "info"
| "debug";
export type SystemLogErrorType =
| "auth"
| "connection"
| "invalid_response"
| "rate_limit"
| "server"
| "slow_setup"
| "timeout"
| "ssl"
| "statistics"
| "dns";
export interface LoggedError {
name: string;
message: [string];
level: SystemLogLevel;
error_type?: SystemLogErrorType;
source: [string, number];
exception: string;
count: number;
@@ -9,6 +9,7 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
@@ -39,6 +40,38 @@ class MoreInfoClimate extends LitElement {
@state() private _mainControl: MainControl = "temperature";
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="fan_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected willUpdate(changedProps: PropertyValues): void {
if (
changedProps.has("stateObj") &&
@@ -205,12 +238,8 @@ class MoreInfoClimate extends LitElement {
"preset_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "preset_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -234,12 +263,8 @@ class MoreInfoClimate extends LitElement {
"fan_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "fan_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderFanModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
</ha-control-select-menu>
@@ -263,12 +288,8 @@ class MoreInfoClimate extends LitElement {
"swing_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderSwingModeIcon}
>
<ha-svg-icon
slot="icon"
@@ -297,13 +318,9 @@ class MoreInfoClimate extends LitElement {
"swing_horizontal_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_horizontal_mode",
attributeValue: mode,
},
})
)}
.renderIcon=${this._renderSwingHorizontalModeIcon}
>
<ha-svg-icon
slot="icon"
+18 -14
View File
@@ -40,6 +40,22 @@ class MoreInfoFan extends LitElement {
@state() public _presetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderDirectionIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${value}
></ha-attribute-icon>`;
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic(this, "light");
@@ -192,15 +208,9 @@ class MoreInfoFan extends LitElement {
"preset_mode",
mode
),
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "preset_mode",
attributeValue: mode,
}
: undefined,
})
)}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -226,14 +236,8 @@ class MoreInfoFan extends LitElement {
direction
)
: direction,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "direction",
attributeValue: direction,
}
: undefined,
}))}
.renderIcon=${this._renderDirectionIcon}
>
<ha-attribute-icon
slot="icon"
@@ -23,6 +23,14 @@ class MoreInfoHumidifier extends LitElement {
@state() public _mode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj")) {
@@ -106,14 +114,8 @@ class MoreInfoHumidifier extends LitElement {
mode
)
: mode,
attributeIcon: stateObj
? {
stateObj,
attribute: "mode",
attributeValue: mode,
}
: undefined,
})) || []}
.renderIcon=${this._renderModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -55,6 +55,14 @@ class MoreInfoLight extends LitElement {
@state() private _mainControl: MainControl = "brightness";
private _renderEffectIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${value}
></ha-attribute-icon>`;
protected updated(changedProps: PropertyValues<typeof this>): void {
if (changedProps.has("stateObj")) {
this._effect = this.stateObj?.attributes.effect;
@@ -271,15 +279,9 @@ class MoreInfoLight extends LitElement {
effect
)
: effect,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "effect",
attributeValue: effect,
}
: undefined,
})
)}
.renderIcon=${this._renderEffectIcon}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
</ha-control-select-menu>
@@ -24,6 +24,14 @@ class MoreInfoWaterHeater extends LitElement {
@property({ attribute: false }) public stateObj?: WaterHeaterEntity;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="operation_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -85,12 +93,8 @@ class MoreInfoWaterHeater extends LitElement {
.map((mode) => ({
value: mode,
label: this.hass.formatEntityState(stateObj, mode),
attributeIcon: {
stateObj,
attribute: "operation_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderOperationModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
</ha-control-select-menu>
+125 -45
View File
@@ -2,17 +2,27 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import checkValidDate from "../../common/datetime/check_valid_date";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import "../../components/ha-attribute-value";
import "../../components/ha-card";
import type { LocalizeKeys } from "../../common/translations/localize";
import { computeShownAttributes } from "../../data/entity/entity_attributes";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../../components/ha-yaml-editor";
interface DetailsViewParams {
entityId: string;
}
interface DetailEntry {
translationKey: LocalizeKeys;
value: string;
}
@customElement("ha-more-info-details")
class HaMoreInfoDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -21,6 +31,8 @@ class HaMoreInfoDetails extends LitElement {
@property({ attribute: false }) public params?: DetailsViewParams;
@property({ attribute: false }) public yamlMode = false;
@state() private _stateObj?: HassEntity;
protected willUpdate(changedProps: PropertyValues): void {
@@ -37,60 +49,128 @@ class HaMoreInfoDetails extends LitElement {
return nothing;
}
const translatedState = this.hass.formatEntityState(this._stateObj);
const detailsAttributes = computeShownAttributes(this._stateObj);
const detailsAttributeSet = new Set(detailsAttributes);
const builtInAttributes = Object.keys(this._stateObj.attributes).filter(
(attribute) => !detailsAttributeSet.has(attribute)
const { stateEntries, attributes, yamlData } = this._getDetailData(
this._stateObj
);
const allAttributes = [...detailsAttributes, ...builtInAttributes];
return html`
<div class="content">
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="attribute-group">
<div class="data-entry">
<div class="key">
${this.hass.localize(
"ui.dialogs.more_info_control.translated"
)}
${this.yamlMode
? html`<ha-yaml-editor
.hass=${this.hass}
.value=${yamlData}
read-only
auto-update
in-dialog
></ha-yaml-editor>`
: html`
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="data-group">
${stateEntries.map(
(entry) =>
html`<div class="data-entry">
<div class="key">
${this.hass.localize(entry.translationKey)}
</div>
<div class="value">${entry.value}</div>
</div>`
)}
</div>
</div>
<div class="value">${translatedState}</div>
</div>
<div class="data-entry">
<div class="key">
${this.hass.localize("ui.dialogs.more_info_control.raw")}
</div>
<div class="value">${this._stateObj.state}</div>
</div>
</div>
</div>
</ha-card>
</section>
</ha-card>
</section>
<section class="section">
<h2 class="section-title">
${this.hass.localize("ui.dialogs.more_info_control.attributes")}
</h2>
<ha-card>
<div class="card-content">
<div class="attribute-group">
${this._renderAttributes(allAttributes)}
</div>
</div>
</ha-card>
</section>
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.dialogs.more_info_control.attributes"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="data-group">
${this._renderAttributes(attributes)}
</div>
</div>
</ha-card>
</section>
`}
</div>
`;
}
private _getDetailData = memoizeOne(
(
stateObj: HassEntity
): {
stateEntries: DetailEntry[];
attributes: string[];
yamlData: {
state: {
translated: string;
raw: string;
last_changed: string;
last_updated: string;
};
attributes: Record<string, string>;
};
} => {
const translatedState = this.hass.formatEntityState(stateObj);
const detailsAttributes = computeShownAttributes(stateObj);
const detailsAttributeSet = new Set(detailsAttributes);
const builtInAttributes = Object.keys(stateObj.attributes).filter(
(attribute) => !detailsAttributeSet.has(attribute)
);
return {
stateEntries: [
{
translationKey: "ui.dialogs.more_info_control.translated",
value: translatedState,
},
{
translationKey: "ui.dialogs.more_info_control.raw",
value: stateObj.state,
},
{
translationKey: "ui.dialogs.more_info_control.last_changed",
value: this._formatTimestamp(stateObj.last_changed),
},
{
translationKey: "ui.dialogs.more_info_control.last_updated",
value: this._formatTimestamp(stateObj.last_updated),
},
],
attributes: [...detailsAttributes, ...builtInAttributes],
yamlData: {
state: {
translated: translatedState,
raw: stateObj.state,
last_changed: stateObj.last_changed,
last_updated: stateObj.last_updated,
},
attributes: stateObj.attributes,
},
};
}
);
private _formatTimestamp(value: string): string {
const date = new Date(value);
return checkValidDate(date)
? formatDateTimeWithSeconds(date, this.hass.locale, this.hass.config)
: value;
}
private _renderAttributes(attributes: string[]) {
if (attributes.length === 0) {
return html`<div class="empty">
@@ -159,7 +239,7 @@ class HaMoreInfoDetails extends LitElement {
border-bottom: 1px solid var(--divider-color);
}
.attribute-group .data-entry:last-of-type {
.data-group .data-entry:last-of-type {
border-bottom: none;
}
+53 -1
View File
@@ -1,6 +1,7 @@
import {
mdiChartBoxOutline,
mdiClose,
mdiCodeBraces,
mdiCogOutline,
mdiDevices,
mdiDotsVertical,
@@ -118,6 +119,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@query(".content") private _contentElement?: HTMLDivElement;
@query("ha-adaptive-dialog") private _dialogElement?: HTMLElement;
@state() private _entityId?: string | null;
@state() private _data?: Record<string, any>;
@@ -132,6 +135,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@state() private _infoEditMode = false;
@state() private _detailsYamlMode = false;
@state() private _isEscapeEnabled = true;
@state() private _sensorNumericDeviceClasses?: string[] = [];
@@ -174,6 +179,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
}
public closeDialog() {
const dialog = this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
if (dialog) {
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
}
this._open = false;
}
@@ -182,6 +191,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._parentEntityIds = [];
this._entry = undefined;
this._infoEditMode = false;
this._detailsYamlMode = false;
this._initialView = DEFAULT_VIEW;
this._currView = DEFAULT_VIEW;
this._childView = undefined;
@@ -250,7 +260,13 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
private _goBack() {
if (this._childView) {
const dialog =
this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
if (dialog) {
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
}
this._childView = undefined;
this._detailsYamlMode = false;
return;
}
if (this._initialView !== this._currView) {
@@ -314,6 +330,14 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._infoEditMode = !this._infoEditMode;
}
private _toggleDetailsYamlMode() {
const dialog = this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
if (dialog) {
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
}
this._detailsYamlMode = !this._detailsYamlMode;
}
private _handleToggleInfoEditModeEvent(ev) {
this._infoEditMode = ev.detail;
}
@@ -637,7 +661,18 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
</ha-dropdown-item>
</ha-dropdown>
`
: nothing}
: this._childView?.viewTag === "ha-more-info-details"
? html`
<ha-icon-button
slot="headerActionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.toggle_yaml_mode"
)}
.path=${mdiCodeBraces}
@click=${this._toggleDetailsYamlMode}
></ha-icon-button>
`
: nothing}
<div
class=${classMap({
"content-wrapper": true,
@@ -663,6 +698,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
yamlMode: this._detailsYamlMode,
})}
</div>
`
@@ -728,9 +764,25 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const previousChildView = changedProps.get("_childView") as
| ChildView
| undefined;
if (
previousChildView?.viewTag === "ha-more-info-details" &&
this._childView?.viewTag !== "ha-more-info-details"
) {
const dialog =
this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
if (dialog) {
fireEvent(dialog as HTMLElement, "dialog-set-fullscreen", false);
}
}
if (changedProps.has("_currView")) {
this._childView = undefined;
this._infoEditMode = false;
this._detailsYamlMode = false;
}
}
+42 -4
View File
@@ -12,10 +12,11 @@ import {
mdiUnfoldLessHorizontal,
mdiUnfoldMoreHorizontal,
} from "@mdi/js";
import type { TemplateResult } from "lit";
import type { TemplateResult, 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 { canShowPage } from "../common/config/can_show_page";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/chips/ha-assist-chip";
@@ -83,7 +84,15 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
* Do we need to add padding for a fab.
* @type {Boolean}
*/
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
@property({ attribute: "has-fab", type: Boolean, reflect: true })
public hasFab = false;
/**
* Show tabs on top or at bottom (narrow) of the page.
* @type {Boolean}
*/
@property({ attribute: "show-tabs", type: Boolean, reflect: true })
public showTabs = false;
/**
* Add an extra row at the bottom of the data table
@@ -200,7 +209,19 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
this._dataTable.clearSelection();
}
protected willUpdate() {
protected willUpdate(changedProperties: PropertyValues) {
if (
changedProperties.has("tabs") ||
(changedProperties.has("hass") &&
(this.hass?.config.components !==
changedProperties.get("hass")?.config.components ||
this.hass?.userData?.showAdvanced !==
changedProperties.get("hass")?.userData?.showAdvanced))
) {
this.showTabs =
this.tabs.filter((page) => canShowPage(this.hass, page)).length > 1;
}
if (this.hasUpdated) {
return;
}
@@ -491,7 +512,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
.noDataText=${this.noDataText}
.filter=${this.filter}
.selectable=${this._selectMode}
.hasFab=${this.hasFab}
.id=${this.id}
.clickable=${this.clickable}
.appendRow=${this.appendRow}
@@ -713,6 +733,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
width: 100%;
height: 100%;
--data-table-border-width: 0;
--data-table-empty-row-height: var(--safe-area-inset-bottom, 0px);
}
:host(:not([narrow])) ha-data-table,
.pane {
@@ -725,6 +746,23 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
);
display: block;
}
/* Last content row should keep the same padding above the fab as the fab
has to the bottom (16px standard fab bottom padding) + the safe-area inset. */
:host([has-fab]) ha-data-table {
--data-table-empty-row-height: calc(
48px + 16px * 2 + var(--safe-area-inset-bottom, 0px)
);
}
/* In narrow view with tabs shown at the bottom, the tab bar already
accounts for safe-area-inset-bottom. No extra empty-row height is needed. */
:host([narrow][show-tabs]:not([has-fab])) ha-data-table {
--data-table-empty-row-height: 0px;
}
/* Reserve space for fab + doubled narrow-mode bottom padding (28px * 2)
when using narrow layout with bottom tabs. */
:host([narrow][show-tabs][has-fab]) ha-data-table {
--data-table-empty-row-height: calc(48px + 28px * 2);
}
.pane-content {
height: calc(
+21 -17
View File
@@ -37,7 +37,7 @@ export interface PageNavigation {
}
@customElement("hass-tabs-subpage")
class HassTabsSubpage extends LitElement {
export class HassTabsSubpage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
@@ -65,6 +65,14 @@ class HassTabsSubpage extends LitElement {
*/
@property({ type: Boolean, attribute: "has-fab" }) public hasFab = false;
/**
* Whether tabs are shown (2 or more tabs visible).
* When both, show-tabs and narrow are true, tabs are shown as bottom bar.
* @type {Boolean}
*/
@property({ type: Boolean, attribute: "show-tabs", reflect: true })
public showTabs = false;
@state() private _activeTab?: PageNavigation;
// @ts-ignore
@@ -83,6 +91,7 @@ class HassTabsSubpage extends LitElement {
const shownTabs = tabs.filter((page) => canShowPage(this.hass, page));
if (shownTabs.length < 2) {
this.showTabs = false;
if (shownTabs.length === 1) {
const page = shownTabs[0];
return [
@@ -92,6 +101,7 @@ class HassTabsSubpage extends LitElement {
return [""];
}
this.showTabs = true;
return shownTabs.map(
(page) => html`
<a href=${page.path} @click=${this._tabClicked}>
@@ -135,7 +145,6 @@ class HassTabsSubpage extends LitElement {
this.narrow,
this.localizeFunc || this.hass.localize
);
const showTabs = tabs.length > 1;
return html`
<div class="toolbar ${classMap({ narrow: this.narrow })}">
<slot name="toolbar">
@@ -160,12 +169,12 @@ class HassTabsSubpage extends LitElement {
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
${this.narrow || !showTabs
${this.narrow || !this.showTabs
? html`<div class="main-title">
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
<slot name="header">${!this.showTabs ? tabs[0] : ""}</slot>
</div>`
: ""}
${showTabs && !this.narrow
${this.showTabs && !this.narrow
? html`<div id="tabbar">${tabs}</div>`
: ""}
<div id="toolbar-icon">
@@ -173,13 +182,11 @@ class HassTabsSubpage extends LitElement {
</div>
</div>
</slot>
${showTabs && this.narrow
${this.showTabs && this.narrow
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
: ""}
</div>
<div
class=${classMap({ container: true, tabs: showTabs && this.narrow })}
>
<div class="container">
${this.pane
? html`<div class="pane">
<div class="shadow-container"></div>
@@ -188,15 +195,12 @@ class HassTabsSubpage extends LitElement {
</div>
</div>`
: nothing}
<div
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
@scroll=${this._saveScrollPos}
>
<div class="content ha-scrollbar" @scroll=${this._saveScrollPos}>
<slot></slot>
${this.hasFab ? html`<div class="fab-bottom-space"></div>` : nothing}
</div>
</div>
<div id="fab" class=${classMap({ tabs: showTabs })}>
<div id="fab">
<slot name="fab"></slot>
</div>
`;
@@ -373,7 +377,7 @@ class HassTabsSubpage extends LitElement {
margin-left: var(--safe-area-inset-left);
margin-inline-start: var(--safe-area-inset-left);
}
:host([narrow]) .content.tabs {
:host([narrow][show-tabs]) .content {
/* Bottom bar reuses header height */
margin-bottom: calc(
var(--header-height, 0px) + var(--safe-area-inset-bottom, 0px)
@@ -384,7 +388,7 @@ class HassTabsSubpage extends LitElement {
height: calc(64px + var(--safe-area-inset-bottom, 0px));
}
:host([narrow]) .content.tabs .fab-bottom-space {
:host([narrow][show-tabs]) .content .fab-bottom-space {
height: calc(80px + var(--safe-area-inset-bottom, 0px));
}
@@ -400,7 +404,7 @@ class HassTabsSubpage extends LitElement {
justify-content: flex-end;
gap: var(--ha-space-2);
}
:host([narrow]) #fab.tabs {
:host([narrow][show-tabs]) #fab {
bottom: calc(84px + var(--safe-area-inset-bottom, 0px));
}
#fab[is-wide] {
+10 -10
View File
@@ -2,10 +2,10 @@ import type { LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize";
import { computeLocalize } from "../common/translations/localize";
import { computeDirectionStyles } from "../common/util/compute_rtl";
import { translationMetadata } from "../resources/translations-metadata";
import type { Constructor, Resources } from "../types";
import { getLocalLanguage, getTranslation } from "../util/common-translation";
import { translationMetadata } from "../resources/translations-metadata";
import { computeDirectionStyles } from "../common/util/compute_rtl";
const empty = () => "";
@@ -28,16 +28,16 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
this._initializeLocalizeLite();
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
computeDirectionStyles(
translationMetadata.translations[this.language!].isRTL,
this
);
}
protected willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (!this.updated || changedProperties.has("language")) {
computeDirectionStyles(
translationMetadata.translations[this.language!].isRTL,
this
);
}
if (changedProperties.get("language")) {
this._resources = undefined;
this._initializeLocalizeLite();
@@ -35,7 +35,7 @@ import type { RepositoryDialogParams } from "./show-dialog-repositories";
class AppsRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@query("#repository_input", true) private _optionInput?: HaTextField;
@query("#repository_input") private _optionInput?: HaTextField;
@state() private _repositories?: HassioAddonRepository[];
@@ -1,30 +1,17 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isTemplate } from "../../../../../common/string/has-template";
import type { WaitAction } from "../../../../../data/script";
import type { HomeAssistant } from "../../../../../types";
import type { ActionElement } from "../ha-automation-action-row";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../../components/ha-form/types";
const SCHEMA = [
{
name: "wait_template",
selector: {
template: {},
},
},
{
name: "timeout",
required: false,
selector: {
text: {},
},
},
{
name: "continue_on_timeout",
selector: { boolean: {} },
},
] as const;
type TimeoutType = "string_template" | "object_template" | "duration";
@customElement("ha-automation-action-wait_template")
export class HaWaitAction extends LitElement implements ActionElement {
@@ -38,12 +25,47 @@ export class HaWaitAction extends LitElement implements ActionElement {
return { wait_template: "", continue_on_timeout: true };
}
private _schema = memoizeOne(
(timeoutType: TimeoutType) =>
[
{
name: "wait_template",
selector: { template: {} },
},
{
name: "timeout",
required: false,
selector:
timeoutType === "string_template"
? { template: {} }
: timeoutType === "object_template"
? { object: {} }
: { duration: { enable_millisecond: true } },
},
{
name: "continue_on_timeout",
selector: { boolean: {} },
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const timeout = this.action.timeout;
const timeoutType: TimeoutType =
typeof timeout === "string" && isTemplate(timeout)
? "string_template"
: typeof timeout === "object" &&
timeout !== null &&
Object.values(timeout).some(
(v) => typeof v === "string" && isTemplate(v)
)
? "object_template"
: "duration";
return html`
<ha-form
.hass=${this.hass}
.data=${this.action}
.schema=${SCHEMA}
.schema=${this._schema(timeoutType)}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
></ha-form>
@@ -51,7 +73,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
}
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.actions.type.wait_template.${
@@ -27,19 +27,15 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type {
@@ -72,27 +68,22 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type {
Entries,
HomeAssistant,
Route,
ValueChangedEvent,
} from "../../../types";
import type { Entries, ValueChangedEvent } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import {
type EntityRegistryUpdate,
showAutomationSaveDialog,
} from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import "./blueprint-automation-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "./ha-automation-script-editor-mixin";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -119,53 +110,13 @@ declare global {
}
@customElement("ha-automation-editor")
export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public automationId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public automations!: AutomationEntity[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: AutomationConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintAutomationConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaAutomationEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@state() private _saving = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityRegistry!: EntityRegistryEntry[];
@@ -180,24 +131,18 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _configSubscriptionsId = 1;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newAutomationId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
currentConfig: () => this.config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this.entityRegCreated &&
this._newAutomationId &&
changedProps.has("_entityRegistry")
) {
@@ -207,26 +152,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
entity.unique_id === this._newAutomationId
);
if (automation) {
this._entityRegCreated(automation);
this._entityRegCreated = undefined;
this.entityRegCreated(automation);
this.entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
if (!this.config) {
return this.renderLoading();
}
const stateObj = this._entityId
? this.hass.states[this._entityId]
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
: undefined;
const useBlueprint = "use_blueprint" in this._config;
const useBlueprint = "use_blueprint" in this.config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -236,11 +177,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.header=${this._config.alias ||
.backCallback=${this.backTapped}
.header=${this.config.alias ||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
${this.mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -284,7 +225,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</span>
</ha-tooltip>`
: nothing}
${this._config?.id && !this.narrow
${this.config?.id && !this.narrow
? html`
<ha-button
appearance="plain"
@@ -308,7 +249,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
${this.mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -342,7 +283,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -366,9 +307,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item
value="rename"
.disabled=${this._readOnly ||
.disabled=${this.readOnly ||
!this.automationId ||
this._mode === "yaml"}
this.mode === "yaml"}
>
${this.hass.localize("ui.panel.config.automation.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -377,7 +318,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
? html`
<ha-dropdown-item
@click=${this._promptAutomationMode}
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.change_mode"
@@ -391,12 +332,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
: nothing}
<ha-dropdown-item
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.automationId)}
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.automationId)}
value="duplicate"
>
${this.hass.localize(
this._readOnly
this.readOnly
? "ui.panel.config.automation.editor.migrate"
: "ui.panel.config.automation.editor.duplicate"
)}
@@ -410,7 +351,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this._readOnly}
.disabled=${this.readOnly}
>
${this.hass.localize(
"ui.panel.config.automation.editor.take_control"
@@ -422,7 +363,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -456,10 +397,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</ha-dropdown-item>
</ha-dropdown>
<div
class=${this._mode === "yaml" ? "yaml-mode" : ""}
class=${this.mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this._mode === "gui"
${this.mode === "gui"
? html`
<div>
${useBlueprint
@@ -469,10 +410,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
></blueprint-automation-editor>
@@ -483,16 +424,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
${this.errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -501,7 +442,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
)
: undefined}
>
${this._errors || this._validationErrors}
${this.errors || this.validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -510,7 +451,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
: nothing}
</ha-alert>`
: nothing}
${this._blueprintConfig
${this.blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
@@ -518,21 +459,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
@click=${this.takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
@click=${this.revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
: this.readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -575,7 +516,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
`}
</div>
`
: this._mode === "yaml"
: this.mode === "yaml"
? html`${stateObj?.state === "off"
? html`
<ha-alert alert-type="info">
@@ -598,7 +539,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
.readOnly=${this.readOnly}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveAutomation}
.showErrors=${false}
@@ -606,9 +547,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${this._dirty ? "dirty" : ""}
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this._saving}
.disabled=${this.saving}
extended
@click=${this._handleSaveAutomation}
>
@@ -645,7 +586,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this.hass
) {
const initData = getAutomationEditorInitData();
this._dirty = !!initData;
this.dirty = !!initData;
let baseConfig: Partial<AutomationConfig> = { description: "" };
if (!initData || !("use_blueprint" in initData)) {
baseConfig = {
@@ -656,35 +597,35 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
actions: [],
};
}
this._config = {
this.config = {
...baseConfig,
...(initData ? normalizeAutomationConfig(initData) : initData),
} as AutomationConfig;
this._entityId = undefined;
this._readOnly = false;
this.currentEntityId = undefined;
this.readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
this._config = normalizeAutomationConfig(c.config);
this.config = normalizeAutomationConfig(c.config);
this._checkValidation();
});
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
if (
changedProps.has("automations") &&
this.automationId &&
!this._entityId
!this.currentEntityId
) {
this._setEntityId();
}
if (changedProps.has("_config")) {
if (changedProps.has("config")) {
Object.values(this._configSubscriptions).forEach((sub) =>
sub(this._config)
sub(this.config)
);
}
}
@@ -693,24 +634,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
const automation = this.automations.find(
(entity: AutomationEntity) => entity.attributes.id === this.automationId
);
this._entityId = automation?.entity_id;
this.currentEntityId = automation?.entity_id;
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
triggers: this._config.triggers,
conditions: this._config.conditions,
actions: this._config.actions,
triggers: this.config.triggers,
conditions: this.config.conditions,
actions: this.config.actions,
});
this._validationErrors = (
this.validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -728,9 +669,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this.hass,
this.automationId as string
);
this._dirty = false;
this._readOnly = false;
this._config = normalizeAutomationConfig(config);
this.dirty = false;
this.readOnly = false;
this.config = normalizeAutomationConfig(config);
this._checkValidation();
} catch (err: any) {
const entity = this._entityRegistry.find(
@@ -761,34 +702,27 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
ev.stopPropagation();
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._config = ev.detail.value;
if (this._readOnly) {
this.config = ev.detail.value;
if (this.readOnly) {
return;
}
this._dirty = true;
this._errors = undefined;
this.dirty = true;
this.errors = undefined;
}
private _showInfo() {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this._entityId });
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
fireEvent(this, "hass-more-info", { entityId: this.currentEntityId });
}
private _editCategory() {
if (!this._registryEntry) {
if (!this.registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -801,36 +735,36 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
showAssignCategoryDialog(this, {
scope: "automation",
entityReg: this._registryEntry,
entityReg: this.registryEntry,
});
}
private async _showTrace() {
if (this._config?.id) {
const result = await this._confirmUnsavedChanged();
if (this.config?.id) {
const result = await this.confirmUnsavedChanged();
if (result) {
navigate(
`/config/automation/trace/${encodeURIComponent(this._config.id)}`
`/config/automation/trace/${encodeURIComponent(this.config.id)}`
);
}
}
}
private _runActions() {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
triggerAutomationActions(
this.hass,
this.hass.states[this._entityId].entity_id
this.hass.states[this.currentEntityId].entity_id
);
}
private async _toggle(): Promise<void> {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
const service = stateObj.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: stateObj.entity_id,
@@ -838,42 +772,42 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private _preprocessYaml() {
if (!this._config) {
if (!this.config) {
return {};
}
const cleanConfig: AutomationConfig = { ...this._config };
const cleanConfig: AutomationConfig = { ...this.config };
delete cleanConfig.id;
return cleanConfig;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
this.dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
this.yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = {
id: this._config?.id,
this.yamlErrors = undefined;
this.config = {
id: this.config?.id,
...normalizeAutomationConfig(ev.detail.value),
};
this._errors = undefined;
this.errors = undefined;
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
const id = this.automationId || String(Date.now());
@@ -889,8 +823,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
title: this.hass.localize(
this.automationId
? "ui.panel.config.automation.editor.leave.unsaved_confirm_title"
@@ -906,15 +840,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this._config as BlueprintAutomationConfig;
const config = this.config as BlueprintAutomationConfig;
try {
const result = await substituteBlueprint(
@@ -931,35 +858,20 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
description: config.description,
};
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this._readOnly = true;
this._errors = undefined;
this.readOnly = true;
this.errors = undefined;
} catch (err: any) {
this._errors = err.message;
this.errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this._readOnly
const result = this.readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.migrate_automation"
@@ -968,12 +880,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
"ui.panel.config.automation.picker.migrate_automation_description"
),
})
: await this._confirmUnsavedChanged();
: await this.confirmUnsavedChanged();
if (result) {
showAutomationEditor({
...this._config,
...this.config,
id: undefined,
alias: this._readOnly ? this._config?.alias : undefined,
alias: this.readOnly ? this.config?.alias : undefined,
});
}
}
@@ -985,7 +897,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
),
text: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_text",
{ name: this._config?.alias }
{ name: this.config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -1001,43 +913,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptAutomationAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
});
});
}
@@ -1045,10 +935,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private async _promptAutomationMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this._config!,
config: this.config!,
updateConfig: (config) => {
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
this.requestUpdate();
resolve();
},
@@ -1058,9 +948,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private async _handleSaveAutomation(): Promise<void> {
if (this._yamlErrors) {
if (this.yamlErrors) {
showToast(this, {
message: this._yamlErrors,
message: this.yamlErrors,
});
return;
}
@@ -1082,22 +972,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private async _saveAutomation(id): Promise<void> {
this._saving = true;
this._validationErrors = undefined;
this.saving = true;
this.validationErrors = undefined;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
if (this.entityRegistryUpdate !== undefined && !this.currentEntityId) {
this._newAutomationId = id;
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
this.entityRegCreated = resolve;
});
}
try {
await saveAutomationConfig(this.hass, id, this._config!);
await saveAutomationConfig(this.hass, id, this.config!);
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
// wait for automation to appear in entity registry when creating a new automation
if (entityRegPromise) {
@@ -1131,23 +1021,23 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
automation: this._entityRegistryUpdate.category || null,
automation: this.entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
});
}
}
this._dirty = false;
this.dirty = false;
} catch (errors: any) {
this._errors = errors.body?.message || errors.error || errors.body;
this.errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this._saving = false;
this.saving = false;
}
}
@@ -1157,7 +1047,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
ev.detail.unsub = () => {
delete this._configSubscriptions[id];
};
ev.detail.callback(this._config);
ev.detail.callback(this.config);
}
protected supportedShortcuts(): SupportedShortcuts {
@@ -1173,14 +1063,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1205,8 +1087,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _applyUndoRedo(config: AutomationConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
}
private _undo() {
@@ -1235,7 +1117,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._showInfo();
break;
case "settings":
this._showSettings();
this.showSettings();
break;
case "category":
this._editCategory();
@@ -1256,11 +1138,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._takeControl();
break;
case "toggle_yaml_mode":
if (this._mode === "gui") {
this._switchYamlMode();
if (this.mode === "gui") {
this.switchYamlMode();
break;
}
this._switchUiMode();
this.switchUiMode();
break;
case "disable":
this._toggle();
@@ -1277,25 +1159,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-automation-editor,
blueprint-automation-editor {
margin: 0 auto;
@@ -1309,17 +1174,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-entity-toggle {
margin-right: 8px;
margin-inline-end: 8px;
@@ -1335,24 +1189,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
max-width: 1040px;
padding: 28px 20px 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}
@@ -0,0 +1,199 @@
import { consume } from "@lit/context";
import type { CSSResult, TemplateResult, LitElement } from "lit";
import { css, html } from "lit";
import { property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import { goBack } from "../../../common/navigate";
import { afterNextRender } from "../../../common/util/render-status";
import { fullEntitiesContext } from "../../../data/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import type { Constructor, HomeAssistant, Route } from "../../../types";
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
import "../../../components/ha-fade-in";
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
/** Minimum config shape shared by both AutomationConfig and ScriptConfig. */
interface BaseEditorConfig {
alias?: string;
}
/** Shared CSS styles for both automation and script editors. */
export const automationScriptEditorStyles: CSSResult = css`
:host {
--ha-automation-editor-max-width: var(--ha-automation-editor-width, 1540px);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`;
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
superClass: Constructor<LitElement>
) => {
class AutomationScriptEditorClass extends superClass {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public entityId: string | null = null;
@state() protected dirty = false;
@state() protected errors?: string;
@state() protected yamlErrors?: string;
@state() protected currentEntityId?: string;
@state() protected mode: "gui" | "yaml" = "gui";
@state() protected readOnly = false;
@state() protected saving = false;
@state() protected validationErrors?: (string | TemplateResult)[];
@state() protected config?: TConfig;
@state() protected blueprintConfig?: TConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: { currentEntityId?: string }, value) {
return value.find(
({ entity_id }) => entity_id === this.currentEntityId
);
},
watch: ["currentEntityId"],
})
protected registryEntry?: EntityRegistryEntry;
protected entityRegistryUpdate?: EntityRegistryUpdate;
protected entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
protected renderLoading(): TemplateResult {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
}
protected showSettings() {
showMoreInfoDialog(this, {
entityId: this.currentEntityId!,
view: "settings",
});
}
protected async switchUiMode() {
if (this.yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this.yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this.yamlErrors = undefined;
this.mode = "gui";
}
protected switchYamlMode() {
this.mode = "yaml";
}
protected takeControlSave() {
this.readOnly = false;
this.dirty = true;
this.blueprintConfig = undefined;
}
protected revertBlueprint() {
this.config = this.blueprintConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this.blueprintConfig = undefined;
this.readOnly = false;
}
protected backTapped = async () => {
const result = await this.confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
protected get isDirty() {
return this.dirty;
}
protected async promptDiscardChanges() {
return this.confirmUnsavedChanged();
}
/**
* Asks whether unsaved changes should be discarded.
* Subclasses must override this to show a confirmation dialog.
* @returns true to proceed (discard/save changes), false to cancel.
*/
protected confirmUnsavedChanged(): Promise<boolean> {
return Promise.resolve(true);
}
}
return AutomationScriptEditorClass;
};
@@ -279,7 +279,7 @@ export default class HaAutomationSidebarAction extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<wa-divider></wa-divider>`
<wa-divider slot="menu-items"></wa-divider>`
: nothing}
<ha-dropdown-item
slot="menu-items"
@@ -21,6 +21,7 @@ import "../../../components/ha-menu-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tip";
import "../../../components/ha-top-app-bar-fixed";
import "../../../components/ha-tooltip";
import type { CloudStatus } from "../../../data/cloud";
import type { RepairsIssue } from "../../../data/repairs";
import {
@@ -211,6 +212,17 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
}
protected render(): TemplateResult {
const quickBarLabel = [
this.hass.localize("ui.dialogs.quick-bar.title"),
this.hass.enableShortcuts && !isMobileClient
? isMac
? "(⌘ + K)"
: "(Ctrl + K)"
: undefined,
]
.filter(Boolean)
.join(" ");
const { updates: canInstallUpdates, total: totalUpdates } =
this._filterUpdateEntitiesParameterized(
this.hass.states,
@@ -231,10 +243,15 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
<ha-icon-button
slot="actionItems"
.label=${this.hass.localize("ui.dialogs.quick-bar.title")}
id="button-quick-bar"
.label=${quickBarLabel}
.path=${mdiMagnify}
hide-title
@click=${this._showQuickBar}
></ha-icon-button>
<ha-tooltip placement="bottom" for="button-quick-bar"
>${quickBarLabel}</ha-tooltip
>
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
@@ -86,6 +86,7 @@ class DialogSystemLogDetail extends LitElement {
<ha-dialog
.hass=${this.hass}
.open=${this._open}
width="large"
@closed=${this._dialogClosed}
>
<span slot="headerTitle">${title}</span>
@@ -109,6 +110,13 @@ class DialogSystemLogDetail extends LitElement {
${item.name}<br />
${this.hass.localize("ui.panel.config.logs.detail.source")}:
${item.source.join(":")}
<br />
${this.hass.localize("ui.panel.config.logs.classification")}:
${item.error_type
? this.hass.localize(
`ui.panel.config.logs.error_type.${item.error_type}`
)
: this.hass.localize("ui.panel.config.logs.other")}
${integration
? html`
<br />
+267 -46
View File
@@ -1,10 +1,16 @@
import {
mdiDotsVertical,
mdiChevronDown,
mdiChip,
mdiDns,
mdiDownload,
mdiFilterVariant,
mdiFilterVariantRemove,
mdiPackageVariant,
mdiPuzzle,
mdiRadar,
mdiRefresh,
mdiText,
mdiVolumeHigh,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
@@ -17,10 +23,14 @@ import { navigate } from "../../../common/navigate";
import { stringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-button";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-generic-picker";
import "../../../components/ha-icon-button";
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/search-input";
import "../../../components/search-input-outlined";
import type { LogProvider } from "../../../data/error_log";
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
@@ -28,6 +38,7 @@ import "../../../layouts/hass-subpage";
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route, ValueChangedEvent } from "../../../types";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "./error-log-card";
import "./system-log-card";
import type { SystemLogCard } from "./system-log-card";
@@ -81,13 +92,9 @@ export class HaConfigLogs extends LitElement {
@state() private _logProviders = logProviders;
public connectedCallback() {
super.connectedCallback();
const systemLog = this.systemLog;
if (systemLog && systemLog.loaded) {
systemLog.fetchData();
}
}
@state() private _showSystemLogFilters = false;
@state() private _systemLogFiltersCount = 0;
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
@@ -98,37 +105,140 @@ export class HaConfigLogs extends LitElement {
this._filter = ev.detail.value;
}
protected render(): TemplateResult {
const search = this.narrow
? html`
<div slot="header">
<search-input
class="header"
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
`
: html`
<div class="search">
<search-input
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
`;
private _toggleSystemLogFilters = () => {
this._showSystemLogFilters = !this._showSystemLogFilters;
};
private _handleSystemLogFiltersChanged(ev: CustomEvent) {
this._showSystemLogFilters = ev.detail.open;
this._systemLogFiltersCount = ev.detail.count;
}
private _downloadSystemLog = () => {
this.systemLog?.downloadLogs();
};
private _refreshSystemLog = () => {
this.systemLog?.fetchData();
};
private _clearSystemLog = () => {
this.systemLog?.clearLogs();
};
private _clearSystemLogFilters = () => {
this.systemLog?.clearFilters();
};
private _handleSystemLogOverflowAction(ev: HaDropdownSelectEvent): void {
if (ev.detail.item.value === "show-full-logs") {
this._showDetail();
}
}
protected render(): TemplateResult {
const showSystemLog = this._selectedLogProvider === "core" && !this._detail;
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
const header =
selectedProvider?.primary ||
this.hass.localize("ui.panel.config.logs.caption");
const searchRow = html`
<div
class="search-row ${showSystemLog
? "with-filters"
: ""} ${showSystemLog && this._showSystemLogFilters && !this.narrow
? "with-pane"
: ""}"
>
${showSystemLog
? this._showSystemLogFilters && !this.narrow
? html`
<div class="filter-controls">
<div class="relative filter-button">
<ha-assist-chip
.label=${this.hass.localize(
"ui.components.subpage-data-table.filters"
)}
active
@click=${this._toggleSystemLogFilters}
>
<ha-svg-icon
slot="icon"
.path=${mdiFilterVariant}
></ha-svg-icon>
</ha-assist-chip>
${this._systemLogFiltersCount
? html`<div class="badge">
${this._systemLogFiltersCount}
</div>`
: nothing}
</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
.label=${this.hass.localize(
"ui.components.subpage-data-table.clear_filter"
)}
.disabled=${!this._systemLogFiltersCount}
@click=${this._clearSystemLogFilters}
></ha-icon-button>
</div>
`
: html`
<div class="relative filter-button">
<ha-assist-chip
.label=${this.hass.localize(
"ui.components.subpage-data-table.filters"
)}
.active=${this._showSystemLogFilters ||
Boolean(this._systemLogFiltersCount)}
@click=${this._toggleSystemLogFilters}
>
<ha-svg-icon
slot="icon"
.path=${mdiFilterVariant}
></ha-svg-icon>
</ha-assist-chip>
${this._systemLogFiltersCount
? html`<div class="badge">
${this._systemLogFiltersCount}
</div>`
: nothing}
</div>
`
: nothing}
<search-input-outlined
class="search-input"
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
@value-changed=${this._filterChanged}
></search-input-outlined>
${showSystemLog
? html`
<ha-assist-chip
class="clear-chip"
.label=${this.hass.localize("ui.panel.config.logs.clear")}
.disabled=${!this.systemLog?.hasItems}
@click=${this._clearSystemLog}
></ha-assist-chip>
`
: nothing}
</div>
`;
const search = this.narrow
? html`<div slot="header">${searchRow}</div>`
: searchRow;
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.logs.caption")}
.header=${header}
back-path="/config/system"
>
${isComponentLoaded(this.hass, "hassio") && this._logProviders
@@ -164,17 +274,48 @@ export class HaConfigLogs extends LitElement {
</ha-generic-picker>
`
: nothing}
${showSystemLog
? html`
<ha-icon-button
slot="toolbar-icon"
.path=${mdiDownload}
@click=${this._downloadSystemLog}
.label=${this.hass.localize(
"ui.panel.config.logs.download_logs"
)}
></ha-icon-button>
<ha-icon-button
slot="toolbar-icon"
.path=${mdiRefresh}
@click=${this._refreshSystemLog}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>
<ha-dropdown
slot="toolbar-icon"
@wa-select=${this._handleSystemLogOverflowAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
.label=${this.hass.localize("ui.common.menu")}
></ha-icon-button>
<ha-dropdown-item value="show-full-logs">
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
${this.hass.localize("ui.panel.config.logs.show_full_logs")}
</ha-dropdown-item>
</ha-dropdown>
`
: nothing}
${search}
<div class="content">
${this._selectedLogProvider === "core" && !this._detail
${showSystemLog
? html`
<system-log-card
.hass=${this.hass}
.header=${this._logProviders.find(
(p) => p.key === this._selectedLogProvider
)!.name}
.filter=${this._filter}
@switch-log-view=${this._showDetail}
.showFilters=${this._showSystemLogFilters}
@system-log-filters-changed=${this
._handleSystemLogFiltersChanged}
></system-log-card>
`
: html`<error-log-card
@@ -194,6 +335,7 @@ export class HaConfigLogs extends LitElement {
private _showDetail() {
this._detail = !this._detail;
this._showSystemLogFilters = false;
}
private _openPicker(ev: Event) {
@@ -208,6 +350,8 @@ export class HaConfigLogs extends LitElement {
}
this._selectedLogProvider = provider;
this._filter = "";
this._showSystemLogFilters = false;
this._systemLogFiltersCount = 0;
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
}
@@ -342,24 +486,101 @@ export class HaConfigLogs extends LitElement {
-webkit-user-select: initial;
-moz-user-select: initial;
}
.search {
.search-row {
position: sticky;
top: 0;
z-index: 2;
display: flex;
align-items: center;
height: 56px;
width: 100%;
gap: var(--ha-space-4);
padding: 0 var(--ha-space-4);
background: var(--primary-background-color);
border-bottom: 1px solid var(--divider-color);
box-sizing: border-box;
}
search-input {
.search-row.with-pane {
display: grid;
grid-template-columns:
var(--sidepane-width, 250px) minmax(0, 1fr)
auto;
align-items: center;
gap: 0;
padding: 0;
}
.search-row.with-pane .filter-controls {
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0;
width: 100%;
height: 100%;
padding: 0 var(--ha-space-4);
border-inline-end: 1px solid var(--divider-color);
box-sizing: border-box;
}
.search-row.with-pane .search-input {
width: 100%;
min-width: 0;
margin-inline-start: var(--ha-space-4);
}
.search-row.with-pane .clear-chip {
justify-self: end;
margin-inline-start: var(--ha-space-4);
margin-inline-end: var(--ha-space-4);
}
search-input-outlined {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
flex: 1;
}
search-input.header {
--mdc-ripple-color: transparant;
margin-left: -16px;
margin-inline-start: -16px;
margin-inline-end: initial;
.relative {
position: relative;
}
.badge {
position: absolute;
top: -4px;
right: -4px;
inset-inline-end: -4px;
inset-inline-start: initial;
min-width: 16px;
box-sizing: border-box;
border-radius: var(--ha-border-radius-circle);
font-size: var(--ha-font-size-xs);
font-weight: var(--ha-font-weight-normal);
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
text-align: center;
padding: 0 2px;
color: var(--text-primary-color);
}
.content {
direction: ltr;
height: calc(
100vh -
1px - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px) -
56px
);
overflow: hidden;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--ha-assist-chip-container-color: var(--card-background-color);
}
.clear-chip {
white-space: nowrap;
}
ha-generic-picker {
--md-list-item-leading-icon-color: var(--ha-color-primary-50);
+286 -182
View File
@@ -1,21 +1,15 @@
import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-filter-states";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-spinner";
import { getSignedPath } from "../../../data/auth";
import { getErrorLogDownloadUrl } from "../../../data/error_log";
import { domainToName } from "../../../data/integration";
import type { LoggedError } from "../../../data/system_log";
import type { LoggedError, SystemLogErrorType } from "../../../data/system_log";
import {
fetchSystemLog,
getLoggedErrorIntegration,
@@ -25,7 +19,6 @@ import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
import { formatSystemLogTime } from "./util";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("system-log-card")
export class SystemLogCard extends LitElement {
@@ -33,15 +26,45 @@ export class SystemLogCard extends LitElement {
@property() public filter = "";
@property() public header?: string;
public loaded = false;
@property({ type: Boolean, attribute: "show-filters" })
public showFilters = false;
@state() private _items?: LoggedError[];
@state() private _levelFilter: string[] = [];
@state() private _errorTypeFilter: (SystemLogErrorType | "unknown")[] = [];
public async fetchData(): Promise<void> {
this._items = undefined;
this._items = await fetchSystemLog(this.hass!);
this._items = await fetchSystemLog(this.hass);
}
public async clearLogs(): Promise<void> {
await this.hass.callService("system_log", "clear");
this._items = [];
}
public async downloadLogs(): Promise<void> {
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = getErrorLogDownloadUrl(this.hass);
const logFileName = `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);
}
public get activeFiltersCount(): number {
return this._levelFilter.length + this._errorTypeFilter.length;
}
public get hasItems(): boolean {
return (this._items?.length || 0) > 0;
}
public clearFilters(): void {
this._levelFilter = [];
this._errorTypeFilter = [];
this._notifyFiltersState();
}
private _timestamp(item: LoggedError): string {
@@ -64,8 +87,30 @@ export class SystemLogCard extends LitElement {
}
private _getFilteredItems = memoizeOne(
(localize: LocalizeFunc, items: LoggedError[], filter: string) =>
(
localize: LocalizeFunc,
items: LoggedError[],
filter: string,
levelFilter: string[],
errorTypeFilter: (SystemLogErrorType | "unknown")[]
) =>
items.filter((item: LoggedError) => {
if (levelFilter.length && !levelFilter.includes(item.level)) {
return false;
}
if (errorTypeFilter.length) {
const matchesKnown =
item.error_type !== undefined &&
errorTypeFilter.includes(item.error_type);
const matchesUnknown =
item.error_type === undefined &&
errorTypeFilter.includes("unknown");
if (!matchesKnown && !matchesUnknown) {
return false;
}
}
if (filter) {
const integration = getLoggedErrorIntegration(item);
return (
@@ -74,6 +119,14 @@ export class SystemLogCard extends LitElement {
) ||
item.source[0].toLowerCase().includes(filter) ||
item.name.toLowerCase().includes(filter) ||
(item.error_type &&
(item.error_type.includes(filter) ||
this.hass
.localize(
`ui.panel.config.logs.error_type.${item.error_type}`
)
.toLowerCase()
.includes(filter))) ||
(integration &&
domainToName(localize, integration)
.toLowerCase()
@@ -82,203 +135,203 @@ export class SystemLogCard extends LitElement {
this._multipleMessages(item).toLowerCase().includes(filter)
);
}
return item;
})
);
protected render() {
const filteredItems = this._items
? this._getFilteredItems(
this.hass.localize,
this._items,
this.filter.toLowerCase()
)
: [];
if (this._items === undefined) {
return html`
<div class="loading-container">
<ha-spinner></ha-spinner>
</div>
`;
}
const filteredItems = this._getFilteredItems(
this.hass.localize,
this._items,
this.filter.toLowerCase(),
this._levelFilter,
this._errorTypeFilter
);
const levels = [...new Set(this._items.map((item) => item.level))];
const errorTypes = [
...new Set(
this._items
.map((item) => item.error_type)
.filter((type): type is SystemLogErrorType => Boolean(type))
),
];
const integrations = filteredItems.length
? filteredItems.map((item) => getLoggedErrorIntegration(item))
: [];
return html`
<div class="system-log-intro">
<ha-card outlined>
${this._items === undefined
? html`
<div class="loading-container">
<ha-spinner></ha-spinner>
</div>
`
: html`
<div class="header">
<h1 class="card-header">${this.header || "Logs"}</h1>
<div class="header-buttons">
<ha-icon-button
.path=${mdiDownload}
@click=${this._downloadLogs}
.label=${this.hass.localize(
"ui.panel.config.logs.download_logs"
)}
></ha-icon-button>
<ha-icon-button
.path=${mdiRefresh}
@click=${this.fetchData}
.label=${this.hass.localize("ui.common.refresh")}
></ha-icon-button>
<ha-dropdown @wa-select=${this._handleOverflowAction}>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
.label=${this.hass.localize("ui.common.menu")}
></ha-icon-button>
<ha-dropdown-item value="show-full-logs">
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.logs.show_full_logs"
)}
</ha-dropdown-item>
</ha-dropdown>
</div>
</div>
${this._items.length === 0
? html`
<div class="card-content empty-content">
${this.hass.localize("ui.panel.config.logs.no_issues")}
</div>
const hasActiveFilters = this.activeFiltersCount > 0;
const listContent =
this._items.length === 0
? html`
<div class="card-content empty-content">
${this.hass.localize("ui.panel.config.logs.no_issues")}
</div>
`
: filteredItems.length === 0 && (this.filter || hasActiveFilters)
? html`
<div class="card-content">
${this.filter
? this.hass.localize(
"ui.panel.config.logs.no_issues_search",
{
term: this.filter,
}
)
: this.hass.localize("ui.panel.config.logs.no_issues")}
</div>
`
: html`
<div class="list-wrapper">
<ha-list>
${filteredItems.map(
(item, idx) => html`
<ha-list-item
@click=${this._openLog}
.logItem=${item}
twoline
>
${item.message[0]}
<span slot="secondary" class="secondary">
${this._timestamp(item)}
${html`(<span class=${item.level}
>${this.hass.localize(
`ui.panel.config.logs.level.${item.level}`
)}</span
>) `}
${item.error_type
? html`(<span class="error-type-text"
>${this.hass.localize(
`ui.panel.config.logs.error_type.${item.error_type}`
)}</span
>) `
: nothing}
${integrations[idx]
? `${domainToName(
this.hass.localize,
integrations[idx]!
)}${
isCustomIntegrationError(item)
? ` (${this.hass.localize(
"ui.panel.config.logs.custom_integration"
)})`
: ""
}`
: item.source[0]}
${item.count > 1
? html` - ${this._multipleMessages(item)} `
: nothing}
</span>
</ha-list-item>
`
: filteredItems.length === 0 && this.filter
? html`<div class="card-content">
${this.hass.localize(
"ui.panel.config.logs.no_issues_search",
{ term: this.filter }
)}
</div>`
: html`<ha-list
>${filteredItems.map(
(item, idx) => html`
<ha-list-item
@click=${this._openLog}
.logItem=${item}
twoline
>
${item.message[0]}
<span slot="secondary" class="secondary">
${this._timestamp(item)}
${html`(<span class=${item.level}
>${this.hass.localize(
`ui.panel.config.logs.level.${item.level}`
)}</span
>) `}
${integrations[idx]
? `${domainToName(
this.hass!.localize,
integrations[idx]!
)}${
isCustomIntegrationError(item)
? ` (${this.hass.localize(
"ui.panel.config.logs.custom_integration"
)})`
: ""
}`
: item.source[0]}
${item.count > 1
? html` - ${this._multipleMessages(item)} `
: nothing}
</span>
</ha-list-item>
`
)}</ha-list
>`}
)}
</ha-list>
</div>
`;
<div class="card-actions">
<ha-call-service-button
.hass=${this.hass}
domain="system_log"
service="clear"
>${this.hass.localize(
"ui.panel.config.logs.clear"
)}</ha-call-service-button
>
</div>
`}
</ha-card>
</div>
`;
return this.showFilters
? html`
<div class="content-layout">
<div class="pane">
<div class="pane-content">
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.logs.level_filter"
)}
.states=${levels.map((level) => ({
value: level,
label: this.hass.localize(
`ui.panel.config.logs.level.${level}`
),
}))}
.value=${this._levelFilter}
@data-table-filter-changed=${this._levelFilterChanged}
></ha-filter-states>
<ha-filter-states
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.logs.classification"
)}
.states=${[
...errorTypes.map((errorType) => ({
value: errorType,
label: this.hass.localize(
`ui.panel.config.logs.error_type.${errorType}`
),
})),
{
value: "unknown",
label: this.hass.localize("ui.panel.config.logs.other"),
},
]}
.value=${this._errorTypeFilter}
@data-table-filter-changed=${this._errorTypeFilterChanged}
></ha-filter-states>
</div>
</div>
<div class="content-main">${listContent}</div>
</div>
`
: listContent;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
this.fetchData();
this.loaded = true;
this.addEventListener("hass-service-called", (ev) =>
this.serviceCalled(ev)
this._notifyFiltersState();
}
private _levelFilterChanged(ev): void {
this._levelFilter = ev.detail.value || [];
this._notifyFiltersState();
}
private _errorTypeFilterChanged(ev): void {
this._errorTypeFilter = ev.detail.value || [];
this._notifyFiltersState();
}
private _notifyFiltersState(): void {
this.dispatchEvent(
new CustomEvent("system-log-filters-changed", {
detail: {
open: this.showFilters,
count: this.activeFiltersCount,
},
})
);
}
protected serviceCalled(ev): void {
// Check if this is for us
if (ev.detail.success && ev.detail.domain === "system_log") {
// Do the right thing depending on service
if (ev.detail.service === "clear") {
this._items = [];
}
}
}
private _handleOverflowAction(ev: HaDropdownSelectEvent) {
if (ev.detail.item.value === "show-full-logs") {
// @ts-ignore
fireEvent(this, "switch-log-view");
}
}
private async _downloadLogs() {
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = getErrorLogDownloadUrl(this.hass);
const logFileName = `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);
}
private _openLog(ev: Event): void {
const item = (ev.currentTarget as any).logItem;
showSystemLogDetailDialog(this, { item });
}
static styles = css`
ha-card {
padding-top: 8px;
:host {
display: block;
direction: var(--direction);
min-height: 0;
height: 100%;
background: var(--primary-background-color);
}
:host {
direction: var(--direction);
}
ha-list {
direction: ltr;
}
.header {
display: flex;
justify-content: space-between;
padding: 0 16px;
}
.header-buttons {
display: flex;
align-items: flex-start;
}
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
display: block;
margin-block-start: 0px;
font-weight: var(--ha-font-weight-normal);
}
.system-log-intro {
margin: 16px;
background: var(--card-background-color);
}
.loading-container {
@@ -288,6 +341,39 @@ export class SystemLogCard extends LitElement {
justify-content: center;
}
.content-layout {
display: flex;
min-height: 100%;
height: 100%;
}
.content-main {
flex: 1;
min-width: 0;
background: var(--card-background-color);
}
.list-wrapper {
border-top: 1px solid var(--divider-color);
min-height: 100%;
background: var(--card-background-color);
}
.pane {
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
border-inline-end: 1px solid var(--divider-color);
display: flex;
flex-direction: column;
min-height: 0;
background: var(--primary-background-color);
}
.pane-content {
overflow: auto;
background: var(--primary-background-color);
}
.error {
color: var(--error-color);
}
@@ -296,15 +382,33 @@ export class SystemLogCard extends LitElement {
color: var(--warning-color);
}
.error-type-text {
color: var(--secondary-text-color);
}
.card-content {
border-top: 1px solid var(--divider-color);
padding-top: 16px;
padding-bottom: 16px;
min-height: 100%;
background: var(--card-background-color);
}
.row-secondary {
text-align: left;
}
@media (max-width: 900px) {
.content-layout {
flex-direction: column;
}
.pane {
width: 100%;
border-inline-end: none;
border-top: 1px solid var(--divider-color);
}
}
`;
}
@@ -143,8 +143,7 @@ class HaConfigRepairs extends LitElement {
}
} else if (
issue.domain === "vacuum" &&
(issue.translation_key === "segments_changed" ||
issue.translation_key === "segments_mapping_not_configured")
issue.translation_key === "segments_changed"
) {
const data = await fetchRepairsIssueData(
this.hass.connection,
+146 -292
View File
@@ -1,5 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiCog,
@@ -22,15 +21,13 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
@@ -40,7 +37,6 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import { substituteBlueprint } from "../../../data/blueprint";
import { validateConfig } from "../../../data/config";
import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
type EntityRegistryEntry,
@@ -67,88 +63,47 @@ import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types";
import type { Entries } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import "./blueprint-script-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "../automation/ha-automation-script-editor-mixin";
import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("ha-script-editor")
export class HaScriptEditor extends SubscribeMixin(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
AutomationScriptEditorMixin<ScriptConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
)
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public scriptId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: ScriptConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaScriptEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@query("manual-script-editor")
private _manualEditor?: HaManualScriptEditor;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintScriptConfig;
@state() private _saving = false;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newScriptId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
currentConfig: () => this.config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this.entityRegCreated &&
this._newScriptId &&
changedProps.has("entityRegistry")
) {
@@ -157,22 +112,22 @@ export class HaScriptEditor extends SubscribeMixin(
entity.platform === "script" && entity.unique_id === this._newScriptId
);
if (script) {
this._entityRegCreated(script);
this._entityRegCreated = undefined;
this.entityRegCreated(script);
this.entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return nothing;
if (!this.config) {
return this.renderLoading();
}
const stateObj = this._entityId
? this.hass.states[this._entityId]
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
: undefined;
const useBlueprint = "use_blueprint" in this._config;
const useBlueprint = "use_blueprint" in this.config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -182,11 +137,11 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.header=${this._config.alias ||
.backCallback=${this.backTapped}
.header=${this.config.alias ||
this.hass.localize("ui.panel.config.script.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
${this.mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -252,7 +207,7 @@ export class HaScriptEditor extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
${this.mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -286,7 +241,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -307,10 +262,10 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-svg-icon>
</ha-dropdown-item>`
: nothing}
${!useBlueprint && !("fields" in this._config)
${!useBlueprint && !("fields" in this.config)
? html`
<ha-dropdown-item
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
value="add_fields"
>
${this.hass.localize(
@@ -326,9 +281,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item
value="rename"
.disabled=${!this.scriptId ||
this._readOnly ||
this._mode === "yaml"}
.disabled=${!this.scriptId || this.readOnly || this.mode === "yaml"}
>
${this.hass.localize("ui.panel.config.script.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -337,7 +290,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="change_mode"
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.script.editor.change_mode"
@@ -351,12 +304,12 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
<ha-dropdown-item
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.scriptId)}
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.scriptId)}
value="duplicate"
>
${this.hass.localize(
this._readOnly
this.readOnly
? "ui.panel.config.script.editor.migrate"
: "ui.panel.config.script.editor.duplicate"
)}
@@ -370,7 +323,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this._readOnly}
.disabled=${this.readOnly}
>
${this.hass.localize(
"ui.panel.config.script.editor.take_control"
@@ -382,7 +335,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -390,7 +343,7 @@ export class HaScriptEditor extends SubscribeMixin(
<wa-divider></wa-divider>
<ha-dropdown-item
.disabled=${this._readOnly || !this.scriptId}
.disabled=${this.readOnly || !this.scriptId}
value="delete"
.variant=${this.scriptId ? "danger" : "default"}
>
@@ -403,8 +356,8 @@ export class HaScriptEditor extends SubscribeMixin(
</ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
${this._mode === "gui"
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
${this.mode === "gui"
? html`
<div>
${useBlueprint
@@ -413,10 +366,10 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-script=${this._handleSaveScript}
></blueprint-script-editor>
@@ -426,16 +379,16 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@save-script=${this._handleSaveScript}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
${this.errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -444,7 +397,7 @@ export class HaScriptEditor extends SubscribeMixin(
)
: undefined}
>
${this._errors || this._validationErrors}
${this.errors || this.validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -453,7 +406,7 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
</ha-alert>`
: nothing}
${this._blueprintConfig
${this.blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.script.editor.confirm_take_control"
@@ -461,21 +414,21 @@ export class HaScriptEditor extends SubscribeMixin(
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
@click=${this.takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
@click=${this.revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
: this.readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -498,11 +451,11 @@ export class HaScriptEditor extends SubscribeMixin(
`}
</div>
`
: this._mode === "yaml"
: this.mode === "yaml"
? html`<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
.readOnly=${this.readOnly}
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveScript}
@@ -510,9 +463,9 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${!this._readOnly && this._dirty ? "dirty" : ""}
class=${!this.readOnly && this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this._saving}
.disabled=${this.saving}
extended
@click=${this._handleSaveScript}
>
@@ -551,26 +504,26 @@ export class HaScriptEditor extends SubscribeMixin(
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
this.currentEntityId = entity?.entity_id;
}
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this._dirty = !!initData;
this.dirty = !!initData;
const baseConfig: Partial<ScriptConfig> = {};
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [];
}
this._config = {
this.config = {
...baseConfig,
...initData,
} as ScriptConfig;
this._readOnly = false;
this.readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getScriptStateConfig(this.hass, this.entityId).then((c) => {
this._config = normalizeScriptConfig(c.config);
this.config = normalizeScriptConfig(c.config);
this._checkValidation();
});
const regEntry = this.entityRegistry.find(
@@ -579,25 +532,25 @@ export class HaScriptEditor extends SubscribeMixin(
if (regEntry?.unique_id) {
this.scriptId = regEntry.unique_id;
}
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
actions: this._config.sequence,
actions: this.config.sequence,
});
this._validationErrors = (
this.validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -612,13 +565,13 @@ export class HaScriptEditor extends SubscribeMixin(
private async _loadConfig() {
fetchScriptFileConfig(this.hass, this.scriptId!).then(
(config) => {
this._dirty = false;
this._readOnly = false;
this._config = normalizeScriptConfig(config);
this.dirty = false;
this.readOnly = false;
this.config = normalizeScriptConfig(config);
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
this.currentEntityId = entity?.entity_id;
this._checkValidation();
},
(resp) => {
@@ -647,19 +600,19 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _valueChanged(ev) {
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._config = ev.detail.value;
this._errors = undefined;
this._dirty = true;
this.config = ev.detail.value;
this.errors = undefined;
this.dirty = true;
}
private async _runScript() {
if (hasScriptFields(this.hass, this._entityId!)) {
if (hasScriptFields(this.hass, this.currentEntityId!)) {
showMoreInfoDialog(this, {
entityId: this._entityId!,
entityId: this.currentEntityId!,
});
return;
}
@@ -667,20 +620,13 @@ export class HaScriptEditor extends SubscribeMixin(
await triggerScript(this.hass, this.scriptId!);
showToast(this, {
message: this.hass.localize("ui.notification_toast.triggered", {
name: this._config!.alias,
name: this.config!.alias,
}),
});
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
}
private _editCategory() {
if (!this._registryEntry) {
if (!this.registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -693,7 +639,7 @@ export class HaScriptEditor extends SubscribeMixin(
}
showAssignCategoryDialog(this, {
scope: "script",
entityReg: this._registryEntry,
entityReg: this.registryEntry,
});
}
@@ -730,7 +676,7 @@ export class HaScriptEditor extends SubscribeMixin(
private async _showTrace() {
if (this.scriptId) {
const result = await this._confirmUnsavedChanged();
const result = await this.confirmUnsavedChanged();
if (result) {
navigate(`/config/script/trace/${this.scriptId}`);
}
@@ -738,47 +684,47 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _addFields() {
if ("fields" in this._config!) {
if ("fields" in this.config!) {
return;
}
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._manualEditor?.addFields();
this._dirty = true;
this.dirty = true;
}
private _preprocessYaml() {
return this._config;
return this.config;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
this.dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
this.yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = ev.detail.value;
this._errors = undefined;
this.yamlErrors = undefined;
this.config = ev.detail.value;
this.errors = undefined;
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
const id = this.scriptId || String(Date.now());
@@ -794,8 +740,8 @@ export class HaScriptEditor extends SubscribeMixin(
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
title: this.hass.localize(
this.scriptId
? "ui.panel.config.script.editor.leave.unsaved_confirm_title"
@@ -811,15 +757,8 @@ export class HaScriptEditor extends SubscribeMixin(
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this._config as BlueprintScriptConfig;
const config = this.config as BlueprintScriptConfig;
try {
const result = await substituteBlueprint(
@@ -835,35 +774,20 @@ export class HaScriptEditor extends SubscribeMixin(
description: config.description,
};
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this._readOnly = true;
this._errors = undefined;
this.readOnly = true;
this.errors = undefined;
} catch (err: any) {
this._errors = err.message;
this.errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this._readOnly
const result = this.readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.script.picker.migrate_script"
@@ -872,14 +796,14 @@ export class HaScriptEditor extends SubscribeMixin(
"ui.panel.config.script.picker.migrate_script_description"
),
})
: await this._confirmUnsavedChanged();
: await this.confirmUnsavedChanged();
if (result) {
this._entityId = undefined;
this.currentEntityId = undefined;
showScriptEditor({
...this._config,
alias: this._readOnly
? this._config?.alias
: `${this._config?.alias} (${this.hass.localize(
...this.config,
alias: this.readOnly
? this.config?.alias
: `${this.config?.alias} (${this.hass.localize(
"ui.panel.config.script.picker.duplicate"
)})`,
});
@@ -893,7 +817,7 @@ export class HaScriptEditor extends SubscribeMixin(
),
text: this.hass.localize(
"ui.panel.config.script.editor.delete_confirm_text",
{ name: this._config?.alias }
{ name: this.config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -907,42 +831,20 @@ export class HaScriptEditor extends SubscribeMixin(
goBack("/config");
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptScriptAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.entityRegistry.find(
(entry) => entry.unique_id === this.scriptId
),
@@ -953,10 +855,10 @@ export class HaScriptEditor extends SubscribeMixin(
private async _promptScriptMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this._config!,
config: this.config!,
updateConfig: (config) => {
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
this.requestUpdate();
resolve();
},
@@ -966,9 +868,9 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _handleSaveScript() {
if (this._yamlErrors) {
if (this.yamlErrors) {
showToast(this, {
message: this._yamlErrors,
message: this.yamlErrors,
});
return;
}
@@ -980,9 +882,9 @@ export class HaScriptEditor extends SubscribeMixin(
if (!saved) {
return;
}
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
this.currentEntityId = this._computeEntityIdFromAlias(this.config!.alias);
}
const id = this.scriptId || this._entityId || Date.now();
const id = this.scriptId || this.currentEntityId || Date.now();
await this._saveScript(id);
if (!this.scriptId) {
@@ -991,13 +893,13 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _saveScript(id): Promise<void> {
this._saving = true;
this.saving = true;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
if (this.entityRegistryUpdate !== undefined && !this.scriptId) {
this._newScriptId = id.toString();
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
this.entityRegCreated = resolve;
});
}
@@ -1005,11 +907,11 @@ export class HaScriptEditor extends SubscribeMixin(
await this.hass!.callApi(
"POST",
"config/script/config/" + id,
this._config
this.config
);
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
// wait for new script to appear in entity registry
if (entityRegPromise) {
@@ -1044,23 +946,23 @@ export class HaScriptEditor extends SubscribeMixin(
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
script: this._entityRegistryUpdate.category || null,
script: this.entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
});
}
}
this._dirty = false;
this.dirty = false;
} catch (errors: any) {
this._errors = errors.body?.message || errors.error || errors.body;
this.errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this._saving = false;
this.saving = false;
}
}
@@ -1077,14 +979,6 @@ export class HaScriptEditor extends SubscribeMixin(
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1109,8 +1003,8 @@ export class HaScriptEditor extends SubscribeMixin(
private _applyUndoRedo(config: ScriptConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
}
private _undo() {
@@ -1139,7 +1033,7 @@ export class HaScriptEditor extends SubscribeMixin(
this._showInfo();
break;
case "settings":
this._showSettings();
this.showSettings();
break;
case "category":
this._editCategory();
@@ -1163,11 +1057,11 @@ export class HaScriptEditor extends SubscribeMixin(
this._takeControl();
break;
case "toggle_yaml_mode":
if (this._mode === "gui") {
this._switchYamlMode();
if (this.mode === "gui") {
this.switchYamlMode();
break;
}
this._switchUiMode();
this.switchUiMode();
break;
case "delete":
this._deleteConfirm();
@@ -1181,19 +1075,8 @@ export class HaScriptEditor extends SubscribeMixin(
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-script-editor,
blueprint-script-editor {
margin: 0 auto;
@@ -1244,29 +1127,9 @@ export class HaScriptEditor extends SubscribeMixin(
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-fab {
position: fixed;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
.header {
display: flex;
margin: 16px 0;
@@ -1280,15 +1143,6 @@ export class HaScriptEditor extends SubscribeMixin(
.header a {
color: var(--secondary-text-color);
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}
@@ -670,10 +670,10 @@ export class AssistPipelineDebug extends LitElement {
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
--primary-text-color: var(
--text-light-primary-color,
var(--primary-text-color)
);
}
.tool_result [slot="header"] {
color: var(--text-light-primary-color, var(--primary-text-color));
}
.message.user,
+4 -79
View File
@@ -1,7 +1,6 @@
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 { navigate } from "../../common/navigate";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-alert";
@@ -11,13 +10,9 @@ import "../../components/ha-top-app-bar-fixed";
import type { EnergyPreferences } from "../../data/energy";
import { getEnergyDataCollection } from "../../data/energy";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import {
isStrategyView,
type LovelaceViewConfig,
} from "../../data/lovelace/config/view";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import "../lovelace/components/hui-energy-period-selector";
import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
@@ -37,7 +32,6 @@ const OVERVIEW_VIEW = {
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -46,7 +40,6 @@ const ENERGY_VIEW = {
strategy: {
type: "energy",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -55,7 +48,6 @@ const WATER_VIEW = {
strategy: {
type: "water",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -64,7 +56,6 @@ const GAS_VIEW = {
strategy: {
type: "gas",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
show_period_selector: true,
},
} as LovelaceViewConfig;
@@ -189,7 +180,9 @@ class PanelEnergy extends LitElement {
return html`
<div class="centered">
<ha-alert alert-type="error">
An error occurred loading energy preferences: ${this._error}
${this.hass.localize("ui.panel.energy.error_loading_preferences", {
error: this._error,
})}
</ha-alert>
</div>
`;
@@ -208,16 +201,6 @@ class PanelEnergy extends LitElement {
return nothing;
}
const routePath = this.route?.path?.split("/")[1] || "";
const currentView = this._lovelace.config.views.find(
(view) => view.path === routePath
);
const showEnergySelector =
currentView &&
isStrategyView(currentView) &&
currentView.strategy?.show_period_selector;
return html`
<hui-root
.hass=${this.hass}
@@ -228,22 +211,8 @@ class PanelEnergy extends LitElement {
.backButton=${this._searchParms.has("historyBack")}
.backPath=${this._searchParms.get("backPath") || "/"}
@reload-energy-panel=${this._reloadConfig}
class=${classMap({ "has-period-selector": showEnergySelector })}
>
</hui-root>
${showEnergySelector
? html`
<ha-card class="period-selector">
<hui-energy-period-selector
.hass=${this.hass}
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
opening-direction="right"
vertical-opening-direction="up"
fixed
></hui-energy-period-selector>
</ha-card>
`
: nothing}
`;
}
@@ -354,50 +323,6 @@ class PanelEnergy extends LitElement {
align-items: center;
justify-content: center;
}
hui-root.has-period-selector {
--view-container-padding-bottom: var(--ha-space-18);
}
.period-selector {
position: fixed;
z-index: 4;
bottom: max(var(--ha-space-4), var(--safe-area-inset-bottom, 0px));
left: max(
var(--mdc-drawer-width, 0px),
var(--safe-area-inset-left, 0px)
);
right: var(--safe-area-inset-right, 0);
inset-inline-start: max(
var(--mdc-drawer-width, 0px),
var(--safe-area-inset-left, 0px)
);
inset-inline-end: var(--safe-area-inset-right, 0);
transition:
left var(--ha-animation-duration-normal) ease,
right var(--ha-animation-duration-normal) ease,
inset-inline-start var(--ha-animation-duration-normal) ease,
inset-inline-end var(--ha-animation-duration-normal) ease;
margin: 0 auto;
max-width: calc(min(470px, 100% - var(--ha-space-4)));
box-sizing: border-box;
padding-left: var(--ha-space-2);
padding-right: 0;
padding-inline-start: var(--ha-space-4);
padding-inline-end: 0;
--ha-card-box-shadow:
0px 3px 5px -1px rgba(0, 0, 0, 0.2),
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
0px 1px 18px 0px rgba(0, 0, 0, 0.12);
--ha-card-border-color: var(--divider-color);
--ha-card-border-width: var(--ha-card-border-width, 1px);
}
@media all and (max-width: 450px), all and (max-height: 500px) {
hui-root.has-period-selector {
--view-container-padding-bottom: var(--ha-space-14);
}
.period-selector {
bottom: max(var(--ha-space-2), var(--safe-area-inset-bottom, 0px));
}
}
`,
];
}
@@ -13,16 +13,22 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const view: LovelaceViewConfig = {
type: "sections",
sections: [],
dense_section_placement: true,
max_columns: 2,
max_columns: 3,
footer: {
card: {
type: "energy-date-selection",
collection_key: collectionKey,
},
},
};
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
@@ -2,6 +2,7 @@ import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
import { getEnergyDataCollection } from "../../../data/energy";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
@@ -14,11 +15,21 @@ export class EnergyViewStrategy extends ReactiveElement {
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const view: LovelaceViewConfig = {
type: "sections",
max_columns: 3,
sections: [],
footer: {
card: {
type: "energy-date-selection",
collection_key: collectionKey,
},
},
};
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
@@ -36,8 +47,6 @@ export class EnergyViewStrategy extends ReactiveElement {
return view;
}
view.type = "sidebar";
const hasGrid = prefs.energy_sources.find(
(source): source is GridSourceTypeEnergyPreference =>
source.type === "grid" &&
@@ -50,83 +59,95 @@ export class EnergyViewStrategy extends ReactiveElement {
const hasBattery = prefs.energy_sources.some(
(source) => source.type === "battery"
);
view.cards!.push({
const mainCards: LovelaceCardConfig[] = [];
const gaugeCards: LovelaceCardConfig[] = [];
// Only include if we have a grid source & return.
if (hasReturn) {
const card = {
type: "energy-grid-neutrality-gauge",
collection_key: collectionKey,
};
gaugeCards.push(card);
}
// Only include if we have a solar source.
if (hasSolar) {
if (hasReturn) {
const card = {
type: "energy-solar-consumed-gauge",
collection_key: collectionKey,
};
gaugeCards.push(card);
}
if (hasGrid) {
const card = {
type: "energy-self-sufficiency-gauge",
collection_key: collectionKey,
};
gaugeCards.push(card);
}
}
// Only include if we have a grid
if (hasGrid) {
const card = {
type: "energy-carbon-consumed-gauge",
collection_key: collectionKey,
};
gaugeCards.push(card);
}
if (gaugeCards.length) {
view.sections!.push({
type: "grid",
column_span: 3,
cards:
gaugeCards.length === 1
? [gaugeCards[0]]
: gaugeCards.map((card) => ({
...card,
grid_options: { columns: 6 },
})),
});
}
mainCards.push({
type: "energy-compare",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
// Only include if we have a grid or battery.
if (hasGrid || hasBattery) {
view.cards!.push({
mainCards.push({
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
type: "energy-usage-graph",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
}
// Only include if we have a solar source.
if (hasSolar) {
view.cards!.push({
mainCards.push({
title: hass.localize("ui.panel.energy.cards.energy_solar_graph_title"),
type: "energy-solar-graph",
collection_key: collectionKey,
});
}
// Only include if we have a grid or battery.
if (hasGrid || hasBattery) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
type: "energy-distribution",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
grid_options: { columns: 36 },
});
}
if (hasGrid || hasSolar || hasBattery) {
view.cards!.push({
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["grid", "solar", "battery"],
});
}
// Only include if we have a grid source & return.
if (hasReturn) {
view.cards!.push({
type: "energy-grid-neutrality-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
// Only include if we have a solar source.
if (hasSolar) {
if (hasReturn) {
view.cards!.push({
type: "energy-solar-consumed-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
if (hasGrid) {
view.cards!.push({
type: "energy-self-sufficiency-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
}
// Only include if we have a grid
if (hasGrid) {
view.cards!.push({
type: "energy-carbon-consumed-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
grid_options: { columns: 36 },
});
}
@@ -137,29 +158,38 @@ export class EnergyViewStrategy extends ReactiveElement {
hass,
(d) => d.stat_consumption
);
view.cards!.push({
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_detail_graph_title"
),
type: "energy-devices-detail-graph",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
view.cards!.push({
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_graph_title"
),
type: "energy-devices-graph",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
view.cards!.push({
mainCards.push({
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
type: "energy-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsAndAreas,
group_by_area: showFloorsAndAreas,
grid_options: { columns: 36 },
});
}
view.sections!.push({
type: "grid",
column_span: 3,
cards: mainCards,
});
return view;
}
}
@@ -13,14 +13,21 @@ export class GasViewStrategy extends ReactiveElement {
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = {
type: "sections",
sections: [{ type: "grid", cards: [] }],
};
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const view: LovelaceViewConfig = {
type: "sections",
max_columns: 3,
sections: [{ type: "grid", cards: [], column_span: 3 }],
footer: {
card: {
type: "energy-date-selection",
collection_key: collectionKey,
},
},
};
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
@@ -49,6 +56,9 @@ export class GasViewStrategy extends ReactiveElement {
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
type: "energy-gas-graph",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
section.cards!.push({
@@ -56,6 +66,9 @@ export class GasViewStrategy extends ReactiveElement {
type: "energy-sources-table",
collection_key: collectionKey,
types: ["gas"],
grid_options: {
columns: 12,
},
});
return view;
@@ -14,14 +14,21 @@ export class WaterViewStrategy extends ReactiveElement {
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = {
type: "sections",
sections: [{ type: "grid", cards: [] }],
};
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const view: LovelaceViewConfig = {
type: "sections",
max_columns: 3,
sections: [{ type: "grid", cards: [], column_span: 3 }],
footer: {
card: {
type: "energy-date-selection",
collection_key: collectionKey,
},
},
};
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
@@ -52,6 +59,9 @@ export class WaterViewStrategy extends ReactiveElement {
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
type: "energy-water-graph",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
section.cards!.push({
title: hass.localize(
@@ -60,6 +70,9 @@ export class WaterViewStrategy extends ReactiveElement {
type: "energy-sources-table",
collection_key: collectionKey,
types: ["water"],
grid_options: {
columns: 12,
},
});
}
@@ -76,6 +89,9 @@ export class WaterViewStrategy extends ReactiveElement {
collection_key: collectionKey,
group_by_floor: showFloorsAndAreas,
group_by_area: showFloorsAndAreas,
grid_options: {
columns: 24,
},
});
}
@@ -17,6 +17,7 @@ import {
SMALL_SCREEN_CONDITION,
} from "../../lovelace/strategies/helpers/screen-conditions";
import type { ToggleGroupCardConfig } from "../../lovelace/cards/types";
import type { ButtonHeadingBadgeConfig } from "../../lovelace/heading-badges/types";
export interface LightViewStrategyConfig {
type: "light";
@@ -75,6 +76,7 @@ const processAreasForLight = (
{
type: "button",
icon: "mdi:power",
text: hass.localize("ui.panel.lovelace.strategy.light.off"),
tap_action: {
action: "perform-action",
perform_action: "light.turn_on",
@@ -89,11 +91,12 @@ const processAreasForLight = (
conditions: [anyOnCondition],
},
],
},
} satisfies ButtonHeadingBadgeConfig,
{
type: "button",
icon: "mdi:power",
color: "amber",
color: "orange",
text: hass.localize("ui.panel.lovelace.strategy.light.on"),
tap_action: {
action: "perform-action",
perform_action: "light.turn_off",
@@ -102,7 +105,7 @@ const processAreasForLight = (
},
},
visibility: [SMALL_SCREEN_CONDITION, anyOnCondition],
},
} satisfies ButtonHeadingBadgeConfig,
] satisfies LovelaceCardConfig[],
});
@@ -49,6 +49,14 @@ class HuiClimateFanModesCardFeature
@state() _currentFanMode?: string;
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="fan_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -175,14 +183,8 @@ class HuiClimateFanModesCardFeature
.value=${this._currentFanMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "fan_mode",
attributeValue: option.value,
},
}))}
.options=${options}
.renderIcon=${this._renderFanModeIcon}
><ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
</ha-control-select-menu>
`;
@@ -48,6 +48,14 @@ class HuiClimatePresetModesCardFeature
@state() _currentPresetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -179,14 +187,8 @@ class HuiClimatePresetModesCardFeature
.value=${this._currentPresetMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "preset_mode",
attributeValue: option.value,
},
}))}
.options=${options}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -48,6 +48,14 @@ class HuiClimateSwingHorizontalModesCardFeature
@state() _currentSwingHorizontalMode?: string;
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -187,14 +195,8 @@ class HuiClimateSwingHorizontalModesCardFeature
.value=${this._currentSwingHorizontalMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "swing_horizontal_mode",
attributeValue: option.value,
},
}))}
.options=${options}
.renderIcon=${this._renderSwingHorizontalModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
</ha-control-select-menu>
@@ -48,6 +48,14 @@ class HuiClimateSwingModesCardFeature
@state() _currentSwingMode?: string;
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="swing_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -179,14 +187,8 @@ class HuiClimateSwingModesCardFeature
.value=${this._currentSwingMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: option.value,
},
}))}
.options=${options}
.renderIcon=${this._renderSwingModeIcon}
><ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
</ha-control-select-menu>
`;
@@ -47,6 +47,14 @@ class HuiFanPresetModesCardFeature
@state() _currentPresetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -173,14 +181,8 @@ class HuiFanPresetModesCardFeature
.value=${this._currentPresetMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "preset_mode",
attributeValue: option.value,
},
}))}
.options=${options}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -48,6 +48,14 @@ class HuiHumidifierModesCardFeature
@state() _currentMode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -174,14 +182,8 @@ class HuiHumidifierModesCardFeature
.value=${this._currentMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj,
attribute: "mode",
attributeValue: option.value,
},
}))}
.options=${options}
.renderIcon=${this._renderModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -49,6 +49,14 @@ class HuiWaterHeaterOperationModeCardFeature
@state() _currentOperationMode?: OperationMode;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="operation_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -153,14 +161,8 @@ class HuiWaterHeaterOperationModeCardFeature
.value=${this._currentOperationMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: this._stateObj,
attribute: "operation_mode",
attributeValue: option.value,
},
}))}
.options=${options}
.renderIcon=${this._renderOperationModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
</ha-control-select-menu>
@@ -218,7 +218,9 @@ function formatTooltip(
}
// when comparing the first value is offset to match the main period
// and the real date is in the third value
const date = new Date(params[0].value?.[2] ?? params[0].value?.[0]);
// find the first param with the real date to handle gap-filled entries
const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2];
const date = new Date(origDate ?? params[0].value?.[0]);
let period: string;
if (suggestedPeriod === "month") {
@@ -610,18 +610,21 @@ class HuiPowerSankeyCard
});
// Collect battery power (positive = discharge, negative = charge)
// Sum all battery values first, then determine net direction.
// Momentary power should only flow in one direction across all batteries.
let net_battery = 0;
prefs.energy_sources
.filter((source) => source.type === "battery")
.forEach((source) => {
if (source.type === "battery" && source.stat_rate) {
const value = this._getCurrentPower(source.stat_rate);
if (value > 0) {
from_battery += value;
} else if (value < 0) {
to_battery += Math.abs(value);
}
net_battery += this._getCurrentPower(source.stat_rate);
}
});
if (net_battery > 0) {
from_battery = net_battery;
} else if (net_battery < 0) {
to_battery = Math.abs(net_battery);
}
// Calculate total consumption
const used_total = from_grid + solar + from_battery - to_grid - to_battery;
@@ -131,9 +131,10 @@ export class HuiDiscoveredDevicesCard
}
// Update visibility based on admin status and discovered devices count
const shouldBeHidden =
const shouldBeHidden = Boolean(
!this.hass.user?.is_admin ||
(this._config.hide_empty && this._discoveredFlows.length === 0);
(this._config.hide_empty && this._discoveredFlows.length === 0)
);
if (shouldBeHidden !== this.hidden) {
this.style.display = shouldBeHidden ? "none" : "";
@@ -68,9 +68,9 @@ export class HuiDistributionCard
// Strategy 1: Try to find power sensors (W, kW) - most common use case
const powerFilter = (stateObj: HassEntity): boolean => {
const unit = stateObj.attributes.unit_of_measurement;
const stateValue = Number(stateObj.state);
return (unit === "W" || unit === "kW") && !isNaN(stateValue);
const deviceClass = stateObj.attributes.device_class;
return deviceClass === "power" && !isNaN(stateValue);
};
let foundEntities = findEntities(
@@ -97,9 +97,10 @@ export class HuiRepairsCard
}
// Update visibility based on admin status and repairs count
const shouldBeHidden =
const shouldBeHidden = Boolean(
!this.hass.user?.is_admin ||
(this._config.hide_empty && this._repairsIssues.length === 0);
(this._config.hide_empty && this._repairsIssues.length === 0)
);
if (shouldBeHidden !== this.hidden) {
this.style.display = shouldBeHidden ? "none" : "";
@@ -91,9 +91,10 @@ export class HuiUpdatesCard extends LitElement implements LovelaceCard {
const updateEntities = this._getUpdateEntities();
// Update visibility based on admin status and updates count
const shouldBeHidden =
const shouldBeHidden = Boolean(
!this.hass.user?.is_admin ||
(this._config.hide_empty && updateEntities.length === 0);
(this._config.hide_empty && updateEntities.length === 0)
);
if (shouldBeHidden !== this.hidden) {
this.style.display = shouldBeHidden ? "none" : "";
@@ -103,7 +104,7 @@ export class HuiUpdatesCard extends LitElement implements LovelaceCard {
}
protected render(): TemplateResult | typeof nothing {
if (!this._config || !this.hass || this.hidden) {
if (!this._config || !this.hass) {
return nothing;
}
@@ -125,7 +125,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
}
private _measure() {
this.narrow = this.offsetWidth < 450;
this.narrow = this.offsetWidth < 425;
this._collapseButtons = this.offsetWidth < 320;
}
@@ -226,6 +226,7 @@ export class HuiDialogEditBadge
.hass=${this.hass}
.lovelace=${this._params.lovelaceConfig}
.value=${this._badgeConfig}
in-dialog
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
@editor-save=${this._save}
@@ -314,7 +315,9 @@ export class HuiDialogEditBadge
}
private _toggleMode(): void {
this._badgeEditorEl?.toggleMode();
withViewTransition(() => {
this._badgeEditorEl?.toggleMode();
});
}
private _opened() {
@@ -97,6 +97,7 @@ export class HuiDialogSuggestBadge extends LitElement {
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._badgeConfig}
in-dialog
></ha-yaml-editor>
</div>
`
@@ -203,6 +203,7 @@ export class HuiDialogEditCard
.hass=${this.hass}
.lovelace=${this._params.lovelaceConfig}
.value=${this._cardConfig}
in-dialog
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
@editor-save=${this._save}
@@ -297,7 +298,9 @@ export class HuiDialogEditCard
}
private _toggleMode(): void {
this._cardEditorEl?.toggleMode();
withViewTransition(() => {
this._cardEditorEl?.toggleMode();
});
}
private _opened() {
@@ -133,6 +133,7 @@ export class HuiDialogSuggestCard extends LitElement {
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._cardConfig}
in-dialog
></ha-yaml-editor>
</div>
`
@@ -57,6 +57,9 @@ export abstract class HuiElementEditor<
@property({ attribute: false }) public context?: C;
@property({ type: Boolean, attribute: "in-dialog" })
public inDialog = false;
@state() private _config?: T;
@state() private _configElement?: LovelaceGenericElementEditor;
@@ -150,6 +153,9 @@ export abstract class HuiElementEditor<
}
public toggleMode() {
if (!this.GUImode) {
this._yamlEditor?.disableCodeEditorFullscreen();
}
this.GUImode = !this.GUImode;
}
@@ -243,6 +249,7 @@ export abstract class HuiElementEditor<
.defaultValue=${this._config}
autofocus
.hass=${this.hass}
.inDialog=${this.inDialog}
@value-changed=${this._handleYAMLChanged}
@blur=${this._onBlurYaml}
@keydown=${this._ignoreKeydown}
@@ -127,6 +127,7 @@ export class HuiDialogEditSection
<ha-yaml-editor
.hass=${this.hass}
autofocus
in-dialog
@value-changed=${this._viewYamlChanged}
></ha-yaml-editor>
`;
@@ -162,6 +162,7 @@ export class HuiDialogEditView extends LitElement {
<ha-yaml-editor
.hass=${this.hass}
autofocus
in-dialog
@value-changed=${this._viewYamlChanged}
></ha-yaml-editor>
`;
@@ -91,7 +91,6 @@ export class HuiDialogEditViewFooter extends LitElement {
<hui-view-footer-settings-editor
.hass=${this.hass}
.config=${this._config}
.maxColumns=${this._params.maxColumns}
@config-changed=${this._configChanged}
></hui-view-footer-settings-editor>
`;
@@ -106,7 +105,7 @@ export class HuiDialogEditViewFooter extends LitElement {
.hass=${this.hass}
.open=${this._open}
header-title=${title}
.width=${this._yamlMode ? "full" : "large"}
width="medium"
@closed=${this._dialogClosed}
class=${this._yamlMode ? "yaml-mode" : ""}
>
@@ -1,53 +1,48 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view";
import {
DEFAULT_FOOTER_MAX_WIDTH_PX,
type LovelaceViewFooterConfig,
} from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
const SCHEMA = [
{
name: "max_width",
selector: {
number: {
min: 100,
max: 1600,
step: 10,
unit_of_measurement: "px",
},
},
},
] as const satisfies HaFormSchema[];
@customElement("hui-view-footer-settings-editor")
export class HuiViewFooterSettingsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config?: LovelaceViewFooterConfig;
@property({ attribute: false }) public maxColumns = 4;
private _schema = memoizeOne(
(maxColumns: number) =>
[
{
name: "column_span",
selector: {
number: {
min: 1,
max: maxColumns,
slider_ticks: true,
},
},
},
] as const satisfies HaFormSchema[]
);
protected render() {
const data = {
column_span: this.config?.column_span || 1,
max_width: this.config?.max_width || DEFAULT_FOOTER_MAX_WIDTH_PX,
};
const schema = this._schema(this.maxColumns);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.schema=${SCHEMA}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged}
></ha-form>
`;
@@ -65,19 +60,10 @@ export class HuiViewFooterSettingsEditor extends LitElement {
fireEvent(this, "config-changed", { config });
}
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass.localize(
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}`
);
private _computeHelper = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass.localize(
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}_helper`
) || "";
}
declare global {
@@ -4,7 +4,6 @@ import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/
export interface EditViewFooterDialogParams {
saveConfig: (config: LovelaceViewFooterConfig) => void;
config: LovelaceViewFooterConfig;
maxColumns: number;
}
export const showEditViewFooterDialog = (

Some files were not shown because too many files have changed in this diff Show More