mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 14:39:38 +00:00 
			
		
		
		
	Compare commits
	
		
			58 Commits
		
	
	
		
			20240610.0
			...
			dashboard_
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ebe5207b6e | ||
|   | bd1ede4145 | ||
|   | 321a085c0e | ||
|   | 6a3041988a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 23fcdf876c | ||
|   | 00f325e961 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d00b3cfc61 | ||
|   | 4cc9e74ea8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a56b9a96ce | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d4b5f4bc14 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | cf1523ee73 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f5d571ca84 | ||
|   | 362e92f313 | ||
|   | 5ddf72b973 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 6e78c28f51 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 772f0bb669 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 846c2a848f | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 8495757005 | ||
|   | 9960d38b91 | ||
|   | d3222f8bb0 | ||
|   | 2e5cce5409 | ||
|   | f78946447f | ||
|   | eb0579ddc5 | ||
|   | 686424fc70 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 039e9b40bd | ||
|   | 8272bef890 | ||
|   | 62528b2413 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | fa24f529e0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 43a54f6cda | ||
|   | 9c153bbd58 | ||
|   | 27afe9ecb7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 72f989e2bd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a6ef46565f | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a35ac09688 | ||
|   | 27024135ea | ||
|   | 8759ed740a | ||
|   | bb3e8ae33d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b5b60c9bf0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3b6a2cf7d8 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7e10e14102 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a580abab4a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 11523c08c4 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7a8988528b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2a6380f083 | ||
|   | 29881c8bb4 | ||
|   | 56254ddf03 | ||
|   | 007ba70641 | ||
|   | 3e1227b064 | ||
|   | 067e179f26 | ||
|   | 9a3f7df25e | ||
|   | c7b4e8f37c | ||
|   | bfa8b886ab | ||
|   | 433c00b73a | ||
|   | a497f42f73 | ||
|   | 165723cb5b | ||
|   | 42b5fa696a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 59062d96a8 | ||
|   | d36bbfe07d | 
							
								
								
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -57,7 +57,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -58,7 +58,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -76,7 +76,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -100,7 +100,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           # We must fetch at least the immediate parents so that if this is | ||||
|           # a pull request then we can checkout the head. | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -58,7 +58,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ jobs: | ||||
|     if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,7 @@ jobs: | ||||
|       contents: write | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@v5 | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|       contents: write # Required to upload release assets | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Verify version | ||||
|         uses: home-assistant/actions/helpers/verify-version@master | ||||
| @@ -55,7 +55,7 @@ jobs: | ||||
|           script/release | ||||
|  | ||||
|       - name: Upload release assets | ||||
|         uses: softprops/action-gh-release@v2.0.5 | ||||
|         uses: softprops/action-gh-release@v2.0.6 | ||||
|         with: | ||||
|           files: | | ||||
|             dist/*.whl | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -13,7 +13,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Upload Translations | ||||
|         run: | | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ enableGlobalCache: false | ||||
|  | ||||
| nodeLinker: node-modules | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-4.2.2.cjs | ||||
| yarnPath: .yarn/releases/yarn-4.3.1.cjs | ||||
|   | ||||
| @@ -92,8 +92,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|     [ | ||||
|       "@babel/preset-env", | ||||
|       { | ||||
|         useBuiltIns: latestBuild ? false : "usage", | ||||
|         corejs: latestBuild ? false : dependencies["core-js"], | ||||
|         useBuiltIns: "usage", | ||||
|         corejs: dependencies["core-js"], | ||||
|         bugfixes: true, | ||||
|         shippedProposals: true, | ||||
|       }, | ||||
| @@ -157,7 +157,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|       exclude: [ | ||||
|         path.join(paths.polymer_dir, "src/resources/polyfills"), | ||||
|         ...[ | ||||
|           "@formatjs/intl-\\w+", | ||||
|           "@formatjs/(?:ecma402-abstract|intl-\\w+)", | ||||
|           "@lit-labs/virtualizer/polyfills", | ||||
|           "@webcomponents/scoped-custom-element-registry", | ||||
|           "element-internals-polyfill", | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { ActionDetail } from "@material/mwc-list/mwc-list"; | ||||
| import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; | ||||
| import "@polymer/paper-item/paper-icon-item"; | ||||
| import "@polymer/paper-listbox/paper-listbox"; | ||||
| import { Auth, Connection } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| @@ -28,6 +27,7 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view"; | ||||
| import "../../../../src/layouts/hass-loading-screen"; | ||||
| import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; | ||||
| import "./hc-layout"; | ||||
| import "../../../../src/components/ha-list-item"; | ||||
|  | ||||
| @customElement("hc-cast") | ||||
| class HcCast extends LitElement { | ||||
| @@ -83,37 +83,37 @@ class HcCast extends LitElement { | ||||
|               ` | ||||
|             : html` | ||||
|                 <div class="section-header">PICK A VIEW</div> | ||||
|                 <paper-listbox | ||||
|                   attr-for-selected="data-path" | ||||
|                   .selected=${this.castManager.status.lovelacePath || ""} | ||||
|                 > | ||||
|                 <mwc-list @action=${this._handlePickView} activatable> | ||||
|                   ${( | ||||
|                     this.lovelaceViews ?? [ | ||||
|                       generateDefaultViewConfig({}, {}, {}, {}, () => ""), | ||||
|                     ] | ||||
|                   ).map( | ||||
|                     (view, idx) => html` | ||||
|                       <paper-icon-item | ||||
|                         @click=${this._handlePickView} | ||||
|                         data-path=${view.path || idx} | ||||
|                     (view, idx) => | ||||
|                       html`<ha-list-item | ||||
|                         graphic="avatar" | ||||
|                         .activated=${this.castManager.status?.lovelacePath === | ||||
|                         (view.path ?? idx)} | ||||
|                         .selected=${this.castManager.status?.lovelacePath === | ||||
|                         (view.path ?? idx)} | ||||
|                       > | ||||
|                         ${view.title || view.path || "Unnamed view"} | ||||
|                         ${view.icon | ||||
|                           ? html` | ||||
|                               <ha-icon | ||||
|                                 .icon=${view.icon} | ||||
|                                 slot="item-icon" | ||||
|                                 slot="graphic" | ||||
|                               ></ha-icon> | ||||
|                             ` | ||||
|                           : html`<ha-svg-icon | ||||
|                               slot="item-icon" | ||||
|                               .path=${mdiViewDashboard} | ||||
|                             ></ha-svg-icon>`} | ||||
|                         ${view.title || view.path || "Unnamed view"} | ||||
|                       </paper-icon-item> | ||||
|                     ` | ||||
|                   )} | ||||
|                 </paper-listbox> | ||||
|                             ></ha-svg-icon>`}</ha-list-item | ||||
|                       > ` | ||||
|                   )}</mwc-list | ||||
|                 > | ||||
|               `} | ||||
|  | ||||
|         <div class="card-actions"> | ||||
|           ${this.castManager.status | ||||
|             ? html` | ||||
| @@ -185,8 +185,8 @@ class HcCast extends LitElement { | ||||
|     this.castManager.requestSession(); | ||||
|   } | ||||
|  | ||||
|   private async _handlePickView(ev: Event) { | ||||
|     const path = (ev.currentTarget as any).getAttribute("data-path"); | ||||
|   private async _handlePickView(ev: CustomEvent<ActionDetail>) { | ||||
|     const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index; | ||||
|     await ensureConnectedCastSession(this.castManager!, this.auth!); | ||||
|     castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); | ||||
|   } | ||||
| @@ -249,26 +249,14 @@ class HcCast extends LitElement { | ||||
|         height: 18px; | ||||
|       } | ||||
|  | ||||
|       paper-listbox { | ||||
|         padding-top: 0; | ||||
|       } | ||||
|  | ||||
|       paper-listbox ha-icon, | ||||
|       paper-listbox ha-svg-icon { | ||||
|       ha-list-item ha-icon, | ||||
|       ha-list-item ha-svg-icon { | ||||
|         padding: 12px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|       paper-icon-item { | ||||
|         cursor: pointer; | ||||
|       } | ||||
|  | ||||
|       paper-icon-item[disabled] { | ||||
|         cursor: initial; | ||||
|       } | ||||
|  | ||||
|       :host([hide-icons]) paper-icon-item { | ||||
|         --paper-item-icon-width: 0px; | ||||
|       :host([hide-icons]) ha-icon { | ||||
|         display: none; | ||||
|       } | ||||
|  | ||||
|       .spacer { | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| import "@material/mwc-button"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { until } from "lit/directives/until"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
| import "../../../src/components/ha-card"; | ||||
| import "../../../src/components/ha-button"; | ||||
| import "../../../src/components/ha-circular-progress"; | ||||
| import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card"; | ||||
| import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||
| @@ -11,7 +12,6 @@ import { | ||||
|   demoConfigs, | ||||
|   selectedDemoConfig, | ||||
|   selectedDemoConfigIndex, | ||||
|   setDemoConfig, | ||||
| } from "../configs/demo-configs"; | ||||
|  | ||||
| @customElement("ha-demo-card") | ||||
| @@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|                 )} | ||||
|           </div> | ||||
|  | ||||
|           <mwc-button @click=${this._nextConfig} .disabled=${this._switching}> | ||||
|           <ha-button @click=${this._nextConfig} .disabled=${this._switching}> | ||||
|             ${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")} | ||||
|           </mwc-button> | ||||
|           </ha-button> | ||||
|         </div> | ||||
|         <div class="content"> | ||||
|           <p class="small-hidden"> | ||||
| @@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|         </div> | ||||
|         <div class="actions small-hidden"> | ||||
|           <a href="https://www.home-assistant.io" target="_blank"> | ||||
|             <mwc-button> | ||||
|             <ha-button> | ||||
|               ${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")} | ||||
|             </mwc-button> | ||||
|             </ha-button> | ||||
|           </a> | ||||
|         </div> | ||||
|       </ha-card> | ||||
| @@ -113,13 +113,7 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|   private async _updateConfig(index: number) { | ||||
|     this._switching = true; | ||||
|     try { | ||||
|       await setDemoConfig(this.hass, this.lovelace!, index); | ||||
|     } catch (err: any) { | ||||
|       alert("Failed to switch config :-("); | ||||
|     } finally { | ||||
|       this._switching = false; | ||||
|     } | ||||
|     fireEvent(this, "set-demo-config" as any, { index }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
| @@ -149,7 +143,7 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|           height: 60px; | ||||
|         } | ||||
|  | ||||
|         .picker mwc-button { | ||||
|         .picker ha-button { | ||||
|           margin-right: 8px; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import type { LocalizeFunc } from "../../../src/common/translations/localize"; | ||||
| import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||
| import { selectedDemoConfig } from "../configs/demo-configs"; | ||||
| import { | ||||
|   selectedDemoConfig, | ||||
|   selectedDemoConfigIndex, | ||||
|   setDemoConfig, | ||||
| } from "../configs/demo-configs"; | ||||
| import "../custom-cards/cast-demo-row"; | ||||
| import "../custom-cards/ha-demo-card"; | ||||
| import type { HADemoCard } from "../custom-cards/ha-demo-card"; | ||||
|  | ||||
| export const mockLovelace = ( | ||||
|   hass: MockHomeAssistant, | ||||
| @@ -19,17 +22,22 @@ export const mockLovelace = ( | ||||
|   hass.mockWS("lovelace/resources", () => Promise.resolve([])); | ||||
| }; | ||||
|  | ||||
| customElements.whenDefined("hui-card").then(() => { | ||||
| customElements.whenDefined("hui-root").then(() => { | ||||
|   // eslint-disable-next-line | ||||
|   const HUIView = customElements.get("hui-card"); | ||||
|   // Patch HUI-VIEW to make the lovelace object available to the demo card | ||||
|   const oldCreateCard = HUIView!.prototype.createElement; | ||||
|   const HUIRoot = customElements.get("hui-root")!; | ||||
|  | ||||
|   HUIView!.prototype.createElement = function (config) { | ||||
|     const el = oldCreateCard.call(this, config); | ||||
|     if (config.type === "custom:ha-demo-card") { | ||||
|       (el as HADemoCard).lovelace = this.lovelace; | ||||
|     } | ||||
|     return el; | ||||
|   const oldFirstUpdated = HUIRoot.prototype.firstUpdated; | ||||
|  | ||||
|   HUIRoot.prototype.firstUpdated = function (changedProperties) { | ||||
|     oldFirstUpdated.call(this, changedProperties); | ||||
|     this.addEventListener("set-demo-config", async (ev) => { | ||||
|       const index = (ev as CustomEvent).detail.index; | ||||
|       try { | ||||
|         await setDemoConfig(this.hass, this.lovelace!, index); | ||||
|       } catch (err: any) { | ||||
|         setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex); | ||||
|         alert("Failed to switch config :-("); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| }); | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import { load } from "js-yaml"; | ||||
| import { html, css, LitElement, PropertyValues } from "lit"; | ||||
| import { LitElement, PropertyValueMap, css, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import "../../../src/panels/lovelace/cards/hui-card"; | ||||
| import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card"; | ||||
| import { HomeAssistant } from "../../../src/types"; | ||||
|  | ||||
| export interface DemoCardConfig { | ||||
| @@ -19,7 +21,12 @@ class DemoCard extends LitElement { | ||||
|  | ||||
|   @state() private _size?: number; | ||||
|  | ||||
|   @query("#card") private _card!: HTMLElement; | ||||
|   @query("hui-card", false) private _card?: HuiCard; | ||||
|  | ||||
|   private _config = memoizeOne((config: string) => { | ||||
|     const c = (load(config) as any)[0]; | ||||
|     return c; | ||||
|   }); | ||||
|  | ||||
|   render() { | ||||
|     return html` | ||||
| @@ -30,63 +37,32 @@ class DemoCard extends LitElement { | ||||
|           : ""} | ||||
|       </h2> | ||||
|       <div class="root"> | ||||
|         <div id="card"></div> | ||||
|         ${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""} | ||||
|         <hui-card | ||||
|           .config=${this._config(this.config.config)} | ||||
|           .hass=${this.hass} | ||||
|           @card-updated=${this._cardUpdated} | ||||
|         ></hui-card> | ||||
|         ${this.showConfig | ||||
|           ? html`<pre>${this.config.config.trim()}</pre>` | ||||
|           : nothing} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|  | ||||
|     if (changedProps.has("config")) { | ||||
|       const card = this._card; | ||||
|       while (card.lastChild) { | ||||
|         card.removeChild(card.lastChild); | ||||
|       } | ||||
|  | ||||
|       const el = this._createCardElement((load(this.config.config) as any)[0]); | ||||
|       card.appendChild(el); | ||||
|       this._getSize(el); | ||||
|     } | ||||
|  | ||||
|     if (changedProps.has("hass")) { | ||||
|       const card = this._card.lastChild; | ||||
|       if (card) { | ||||
|         (card as any).hass = this.hass; | ||||
|       } | ||||
|     } | ||||
|   private async _cardUpdated(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     this._updateSize(); | ||||
|   } | ||||
|  | ||||
|   async _getSize(el) { | ||||
|     await customElements.whenDefined(el.localName); | ||||
|  | ||||
|     if (!("getCardSize" in el)) { | ||||
|       this._size = undefined; | ||||
|       return; | ||||
|     } | ||||
|     this._size = await el.getCardSize(); | ||||
|   private async _updateSize() { | ||||
|     this._size = await this._card?.getCardSize(); | ||||
|   } | ||||
|  | ||||
|   _createCardElement(cardConfig) { | ||||
|     const element = createCardElement(cardConfig); | ||||
|     if (this.hass) { | ||||
|       element.hass = this.hass; | ||||
|     } | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev) => { | ||||
|         ev.stopPropagation(); | ||||
|         this._rebuildCard(element, cardConfig); | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   _rebuildCard(cardElToReplace, config) { | ||||
|     const newCardEl = this._createCardElement(config); | ||||
|     cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace); | ||||
|   protected update( | ||||
|     _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown> | ||||
|   ): void { | ||||
|     super.update(_changedProperties); | ||||
|     this._updateSize(); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
| @@ -101,7 +77,7 @@ class DemoCard extends LitElement { | ||||
|       font-size: 0.5em; | ||||
|       color: var(--primary-text-color); | ||||
|     } | ||||
|     #card { | ||||
|     hui-card { | ||||
|       max-width: 400px; | ||||
|       width: 100vw; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										34
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								package.json
									
									
									
									
									
								
							| @@ -26,14 +26,14 @@ | ||||
|   "type": "module", | ||||
|   "dependencies": { | ||||
|     "@babel/runtime": "7.24.7", | ||||
|     "@braintree/sanitize-url": "7.0.2", | ||||
|     "@codemirror/autocomplete": "6.16.2", | ||||
|     "@braintree/sanitize-url": "7.0.3", | ||||
|     "@codemirror/autocomplete": "6.16.3", | ||||
|     "@codemirror/commands": "6.6.0", | ||||
|     "@codemirror/language": "6.10.2", | ||||
|     "@codemirror/legacy-modes": "6.4.0", | ||||
|     "@codemirror/search": "6.5.6", | ||||
|     "@codemirror/state": "6.4.1", | ||||
|     "@codemirror/view": "6.27.0", | ||||
|     "@codemirror/view": "6.28.2", | ||||
|     "@egjs/hammerjs": "2.0.17", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.5", | ||||
|     "@formatjs/intl-displaynames": "6.6.8", | ||||
| @@ -88,8 +88,8 @@ | ||||
|     "@polymer/paper-tabs": "3.1.0", | ||||
|     "@polymer/polymer": "3.5.1", | ||||
|     "@thomasloven/round-slider": "0.6.0", | ||||
|     "@vaadin/combo-box": "24.3.13", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.3.13", | ||||
|     "@vaadin/combo-box": "24.4.0", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.4.0", | ||||
|     "@vibrant/color": "3.2.1-alpha.1", | ||||
|     "@vibrant/core": "3.2.1-alpha.1", | ||||
|     "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", | ||||
| @@ -110,7 +110,7 @@ | ||||
|     "fuse.js": "7.0.0", | ||||
|     "google-timezones-json": "1.2.0", | ||||
|     "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", | ||||
|     "home-assistant-js-websocket": "9.3.0", | ||||
|     "home-assistant-js-websocket": "9.4.0", | ||||
|     "idb-keyval": "6.2.1", | ||||
|     "intl-messageformat": "10.5.14", | ||||
|     "js-yaml": "4.1.0", | ||||
| @@ -160,15 +160,15 @@ | ||||
|     "@lokalise/node-api": "12.5.0", | ||||
|     "@octokit/auth-oauth-device": "7.1.1", | ||||
|     "@octokit/plugin-retry": "7.1.1", | ||||
|     "@octokit/rest": "20.1.1", | ||||
|     "@octokit/rest": "21.0.0", | ||||
|     "@open-wc/dev-server-hmr": "0.1.4", | ||||
|     "@rollup/plugin-babel": "6.0.4", | ||||
|     "@rollup/plugin-commonjs": "25.0.8", | ||||
|     "@rollup/plugin-commonjs": "26.0.1", | ||||
|     "@rollup/plugin-json": "6.1.0", | ||||
|     "@rollup/plugin-node-resolve": "15.2.3", | ||||
|     "@rollup/plugin-replace": "5.0.7", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||
|     "@types/chromecast-caf-receiver": "6.0.14", | ||||
|     "@types/chromecast-caf-receiver": "6.0.15", | ||||
|     "@types/chromecast-caf-sender": "1.0.10", | ||||
|     "@types/color-name": "1.1.4", | ||||
|     "@types/glob": "8.1.0", | ||||
| @@ -185,8 +185,8 @@ | ||||
|     "@types/tar": "6.1.13", | ||||
|     "@types/ua-parser-js": "0.7.39", | ||||
|     "@types/webspeechapi": "0.0.29", | ||||
|     "@typescript-eslint/eslint-plugin": "7.12.0", | ||||
|     "@typescript-eslint/parser": "7.12.0", | ||||
|     "@typescript-eslint/eslint-plugin": "7.13.1", | ||||
|     "@typescript-eslint/parser": "7.13.1", | ||||
|     "@web/dev-server": "0.1.38", | ||||
|     "@web/dev-server-rollup": "0.4.1", | ||||
|     "babel-loader": "9.1.3", | ||||
| @@ -205,7 +205,7 @@ | ||||
|     "eslint-plugin-wc": "2.1.0", | ||||
|     "fancy-log": "2.0.0", | ||||
|     "fs-extra": "11.2.0", | ||||
|     "glob": "10.4.1", | ||||
|     "glob": "10.4.2", | ||||
|     "gulp": "5.0.0", | ||||
|     "gulp-json-transform": "0.5.0", | ||||
|     "gulp-rename": "2.0.0", | ||||
| @@ -214,7 +214,7 @@ | ||||
|     "husky": "9.0.11", | ||||
|     "instant-mocha": "1.5.2", | ||||
|     "jszip": "3.10.1", | ||||
|     "lint-staged": "15.2.5", | ||||
|     "lint-staged": "15.2.7", | ||||
|     "lit-analyzer": "2.0.3", | ||||
|     "lodash.merge": "4.6.2", | ||||
|     "lodash.template": "4.5.0", | ||||
| @@ -224,7 +224,7 @@ | ||||
|     "object-hash": "3.0.0", | ||||
|     "open": "10.1.0", | ||||
|     "pinst": "3.0.0", | ||||
|     "prettier": "3.3.1", | ||||
|     "prettier": "3.3.2", | ||||
|     "rollup": "2.79.1", | ||||
|     "rollup-plugin-string": "3.0.0", | ||||
|     "rollup-plugin-terser": "7.0.2", | ||||
| @@ -233,12 +233,12 @@ | ||||
|     "sinon": "18.0.0", | ||||
|     "source-map-url": "0.4.1", | ||||
|     "systemjs": "6.15.1", | ||||
|     "tar": "7.2.0", | ||||
|     "tar": "7.4.0", | ||||
|     "terser-webpack-plugin": "5.3.10", | ||||
|     "transform-async-modules-webpack-plugin": "1.1.1", | ||||
|     "ts-lit-plugin": "2.0.2", | ||||
|     "typescript": "5.4.5", | ||||
|     "webpack": "5.91.0", | ||||
|     "webpack": "5.92.1", | ||||
|     "webpack-cli": "5.1.4", | ||||
|     "webpack-dev-server": "5.0.4", | ||||
|     "webpack-manifest-plugin": "5.0.0", | ||||
| @@ -257,5 +257,5 @@ | ||||
|     "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", | ||||
|     "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" | ||||
|   }, | ||||
|   "packageManager": "yarn@4.2.2" | ||||
|   "packageManager": "yarn@4.3.1" | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import { | ||||
|   mdiFormatListBulleted, | ||||
|   mdiFormatListCheckbox, | ||||
|   mdiFormTextbox, | ||||
|   mdiForumOutline, | ||||
|   mdiGauge, | ||||
|   mdiGoogleAssistant, | ||||
|   mdiGoogleCirclesCommunities, | ||||
| @@ -98,7 +99,7 @@ export const FIXED_DOMAIN_ICONS = { | ||||
|   calendar: mdiCalendar, | ||||
|   climate: mdiThermostat, | ||||
|   configurator: mdiCog, | ||||
|   conversation: mdiMicrophoneMessage, | ||||
|   conversation: mdiForumOutline, | ||||
|   counter: mdiCounter, | ||||
|   date: mdiCalendar, | ||||
|   datetime: mdiCalendarClock, | ||||
| @@ -235,6 +236,8 @@ export const SENSOR_ENTITIES = [ | ||||
|   "weather", | ||||
| ]; | ||||
|  | ||||
| export const ASSIST_ENTITIES = ["conversation", "stt", "tts"]; | ||||
|  | ||||
| /** Domains that render an input element instead of a text value when displayed in a row. | ||||
|  *  Those rows should then not show a cursor pointer when hovered (which would normally | ||||
|  *  be the default) unless the element itself enforces it (e.g. a button). Also those elements | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/common/dom/prevent_default.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/common/dom/prevent_default.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export const preventDefault = (ev) => ev.preventDefault(); | ||||
| @@ -47,6 +47,8 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|  | ||||
|   @property({ type: Boolean }) public readOnly = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public linewrap = false; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "autocomplete-entities" }) | ||||
|   public autocompleteEntities = false; | ||||
|  | ||||
| @@ -134,6 +136,13 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|         ), | ||||
|       }); | ||||
|     } | ||||
|     if (changedProps.has("linewrap")) { | ||||
|       transactions.push({ | ||||
|         effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure( | ||||
|           this.linewrap ? this._loadedCodeMirror!.EditorView.lineWrapping : [] | ||||
|         ), | ||||
|       }); | ||||
|     } | ||||
|     if (changedProps.has("_value") && this._value !== this.value) { | ||||
|       transactions.push({ | ||||
|         changes: { | ||||
| @@ -181,6 +190,9 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|       this._loadedCodeMirror.readonlyCompartment.of( | ||||
|         this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) | ||||
|       ), | ||||
|       this._loadedCodeMirror.linewrapCompartment.of( | ||||
|         this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : [] | ||||
|       ), | ||||
|       this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate), | ||||
|     ]; | ||||
|  | ||||
|   | ||||
| @@ -89,13 +89,18 @@ export class HaFilterDomains extends LitElement { | ||||
|     }); | ||||
|  | ||||
|     return Array.from(domains.values()) | ||||
|       .map((domain) => ({ | ||||
|         domain, | ||||
|         name: domainToName(this.hass.localize, domain), | ||||
|       })) | ||||
|       .filter( | ||||
|         (entry) => | ||||
|           !filter || | ||||
|           entry.toLowerCase().includes(filter) || | ||||
|           domainToName(this.hass.localize, entry).toLowerCase().includes(filter) | ||||
|           entry.domain.toLowerCase().includes(filter) || | ||||
|           entry.name.toLowerCase().includes(filter) | ||||
|       ) | ||||
|       .sort((a, b) => stringCompare(a, b, this.hass.locale.language)); | ||||
|       .sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language)) | ||||
|       .map((entry) => entry.domain); | ||||
|   }); | ||||
|  | ||||
|   protected updated(changed) { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import type { Selector } from "../../data/selector"; | ||||
| import type { HaFormSchema } from "./types"; | ||||
|  | ||||
| export const computeInitialHaFormData = ( | ||||
|   schema: HaFormSchema[] | ||||
|   schema: HaFormSchema[] | readonly HaFormSchema[] | ||||
| ): Record<string, any> => { | ||||
|   const data = {}; | ||||
|   schema.forEach((field) => { | ||||
| @@ -36,6 +36,8 @@ export const computeInitialHaFormData = ( | ||||
|         minutes: 0, | ||||
|         seconds: 0, | ||||
|       }; | ||||
|     } else if (field.type === "expandable") { | ||||
|       data[field.name] = computeInitialHaFormData(field.schema); | ||||
|     } else if ("selector" in field) { | ||||
|       const selector: Selector = field.selector; | ||||
|  | ||||
|   | ||||
							
								
								
									
										233
									
								
								src/components/ha-grid-size-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								src/components/ha-grid-size-picker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | ||||
| import { LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "./ha-icon-button"; | ||||
| import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; | ||||
|  | ||||
| import { mdiRestore } from "@mdi/js"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| type GridSizeValue = { | ||||
|   rows?: number; | ||||
|   columns?: number; | ||||
| }; | ||||
|  | ||||
| @customElement("ha-grid-size-picker") | ||||
| export class HaGridSizeEditor extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public value?: GridSizeValue; | ||||
|  | ||||
|   @property({ attribute: false }) public rows = 6; | ||||
|  | ||||
|   @property({ attribute: false }) public columns = 4; | ||||
|  | ||||
|   @property({ attribute: false }) public rowMin?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public rowMax?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public columnMin?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public columnMax?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public isDefault?: boolean; | ||||
|  | ||||
|   @state() public _localValue?: GridSizeValue = undefined; | ||||
|  | ||||
|   protected willUpdate(changedProperties) { | ||||
|     if (changedProperties.has("value")) { | ||||
|       this._localValue = this.value; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <div class="grid"> | ||||
|         <ha-grid-layout-slider | ||||
|           aria-label=${this.hass.localize( | ||||
|             "ui.components.grid-size-picker.columns" | ||||
|           )} | ||||
|           id="columns" | ||||
|           .min=${this.columnMin ?? 1} | ||||
|           .max=${this.columnMax ?? this.columns} | ||||
|           .range=${this.columns} | ||||
|           .value=${this.value?.columns} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @slider-moved=${this._sliderMoved} | ||||
|         ></ha-grid-layout-slider> | ||||
|         <ha-grid-layout-slider | ||||
|           aria-label=${this.hass.localize( | ||||
|             "ui.components.grid-size-picker.rows" | ||||
|           )} | ||||
|           id="rows" | ||||
|           .min=${this.rowMin ?? 1} | ||||
|           .max=${this.rowMax ?? this.rows} | ||||
|           .range=${this.rows} | ||||
|           vertical | ||||
|           .value=${this.value?.rows} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @slider-moved=${this._sliderMoved} | ||||
|         ></ha-grid-layout-slider> | ||||
|         ${!this.isDefault | ||||
|           ? html` | ||||
|               <ha-icon-button | ||||
|                 @click=${this._reset} | ||||
|                 class="reset" | ||||
|                 .path=${mdiRestore} | ||||
|                 label=${this.hass.localize( | ||||
|                   "ui.components.grid-size-picker.reset_default" | ||||
|                 )} | ||||
|                 title=${this.hass.localize( | ||||
|                   "ui.components.grid-size-picker.reset_default" | ||||
|                 )} | ||||
|               > | ||||
|               </ha-icon-button> | ||||
|             ` | ||||
|           : nothing} | ||||
|         <div | ||||
|           class="preview" | ||||
|           style=${styleMap({ | ||||
|             "--total-rows": this.rows, | ||||
|             "--total-columns": this.columns, | ||||
|             "--rows": this._localValue?.rows, | ||||
|             "--columns": this._localValue?.columns, | ||||
|           })} | ||||
|         > | ||||
|           <div> | ||||
|             ${Array(this.rows * this.columns) | ||||
|               .fill(0) | ||||
|               .map((_, index) => { | ||||
|                 const row = Math.floor(index / this.columns) + 1; | ||||
|                 const column = (index % this.columns) + 1; | ||||
|                 const disabled = | ||||
|                   (this.rowMin !== undefined && row < this.rowMin) || | ||||
|                   (this.rowMax !== undefined && row > this.rowMax) || | ||||
|                   (this.columnMin !== undefined && column < this.columnMin) || | ||||
|                   (this.columnMax !== undefined && column > this.columnMax); | ||||
|                 return html` | ||||
|                   <div | ||||
|                     class="cell" | ||||
|                     data-row=${row} | ||||
|                     data-column=${column} | ||||
|                     ?disabled=${disabled} | ||||
|                     @click=${this._cellClick} | ||||
|                   ></div> | ||||
|                 `; | ||||
|               })} | ||||
|           </div> | ||||
|           <div class="selected"> | ||||
|             <div class="cell"></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   _cellClick(ev) { | ||||
|     const cell = ev.currentTarget as HTMLElement; | ||||
|     if (cell.getAttribute("disabled") !== null) return; | ||||
|     const rows = Number(cell.getAttribute("data-row")); | ||||
|     const columns = Number(cell.getAttribute("data-column")); | ||||
|     fireEvent(this, "value-changed", { | ||||
|       value: { rows, columns }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     const key = ev.currentTarget.id; | ||||
|     const newValue = { | ||||
|       ...this.value, | ||||
|       [key]: ev.detail.value, | ||||
|     }; | ||||
|     fireEvent(this, "value-changed", { value: newValue }); | ||||
|   } | ||||
|  | ||||
|   private _reset(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     fireEvent(this, "value-changed", { | ||||
|       value: { | ||||
|         rows: undefined, | ||||
|         columns: undefined, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _sliderMoved(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     const key = ev.currentTarget.id; | ||||
|     const value = ev.detail.value; | ||||
|     if (value === undefined) return; | ||||
|     this._localValue = { | ||||
|       ...this.value, | ||||
|       [key]: ev.detail.value, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   static styles = [ | ||||
|     css` | ||||
|       .grid { | ||||
|         display: grid; | ||||
|         grid-template-areas: | ||||
|           "reset column-slider" | ||||
|           "row-slider preview"; | ||||
|         grid-template-rows: auto 1fr; | ||||
|         grid-template-columns: auto 1fr; | ||||
|         gap: 8px; | ||||
|       } | ||||
|       #columns { | ||||
|         grid-area: column-slider; | ||||
|       } | ||||
|       #rows { | ||||
|         grid-area: row-slider; | ||||
|       } | ||||
|       .reset { | ||||
|         grid-area: reset; | ||||
|       } | ||||
|       .preview { | ||||
|         position: relative; | ||||
|         grid-area: preview; | ||||
|         aspect-ratio: 1 / 1; | ||||
|       } | ||||
|       .preview > div { | ||||
|         position: absolute; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(var(--total-columns), 1fr); | ||||
|         grid-template-rows: repeat(var(--total-rows), 1fr); | ||||
|         gap: 4px; | ||||
|       } | ||||
|       .preview .cell { | ||||
|         background-color: var(--disabled-color); | ||||
|         grid-column: span 1; | ||||
|         grid-row: span 1; | ||||
|         border-radius: 4px; | ||||
|         opacity: 0.2; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       .preview .cell[disabled] { | ||||
|         opacity: 0.05; | ||||
|         cursor: initial; | ||||
|       } | ||||
|       .selected { | ||||
|         pointer-events: none; | ||||
|       } | ||||
|       .selected .cell { | ||||
|         background-color: var(--primary-color); | ||||
|         grid-column: 1 / span var(--columns, 0); | ||||
|         grid-row: 1 / span var(--rows, 0); | ||||
|         opacity: 0.5; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-grid-size-picker": HaGridSizeEditor; | ||||
|   } | ||||
| } | ||||
| @@ -32,6 +32,7 @@ export class HaTemplateSelector extends LitElement { | ||||
|         autocomplete-icons | ||||
|         @value-changed=${this._handleChange} | ||||
|         dir="ltr" | ||||
|         linewrap | ||||
|       ></ha-code-editor> | ||||
|       ${this.helper | ||||
|         ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` | ||||
|   | ||||
| @@ -327,6 +327,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { | ||||
|     for (const entityId of Object.keys(this.hass.states)) { | ||||
|       if ( | ||||
|         entityId.startsWith("update.") && | ||||
|         !this.hass.entities[entityId]?.hidden && | ||||
|         updateCanInstall(this.hass.states[entityId] as UpdateEntity) | ||||
|       ) { | ||||
|         updateCount++; | ||||
|   | ||||
| @@ -138,6 +138,17 @@ export const adminChangePassword = ( | ||||
|     password, | ||||
|   }); | ||||
|  | ||||
| export const adminChangeUsername = ( | ||||
|   hass: HomeAssistant, | ||||
|   userId: string, | ||||
|   username: string | ||||
| ) => | ||||
|   hass.callWS<void>({ | ||||
|     type: "config/auth_provider/homeassistant/admin_change_username", | ||||
|     user_id: userId, | ||||
|     username, | ||||
|   }); | ||||
|  | ||||
| export const deleteAllRefreshTokens = ( | ||||
|   hass: HomeAssistant, | ||||
|   token_type?: RefreshTokenType, | ||||
|   | ||||
| @@ -6,6 +6,7 @@ export interface ConfigUpdateValues { | ||||
|   latitude: number; | ||||
|   longitude: number; | ||||
|   elevation: number; | ||||
|   radius: number; | ||||
|   unit_system: "metric" | "us_customary"; | ||||
|   time_zone: string; | ||||
|   external_url?: string | null; | ||||
|   | ||||
| @@ -249,6 +249,22 @@ export const localizeDeviceAutomationTrigger = ( | ||||
|   ) || | ||||
|   (trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!); | ||||
|  | ||||
| export const localizeExtraFieldsComputeLabelCallback = | ||||
|   (hass: HomeAssistant, deviceAutomation: DeviceAutomation) => | ||||
|   // Returns a callback for ha-form to calculate labels per schema object | ||||
|   (schema): string => | ||||
|     hass.localize( | ||||
|       `component.${deviceAutomation.domain}.device_automation.extra_fields.${schema.name}` | ||||
|     ) || schema.name; | ||||
|  | ||||
| export const localizeExtraFieldsComputeHelperCallback = | ||||
|   (hass: HomeAssistant, deviceAutomation: DeviceAutomation) => | ||||
|   // Returns a callback for ha-form to calculate helper texts per schema object | ||||
|   (schema): string | undefined => | ||||
|     hass.localize( | ||||
|       `component.${deviceAutomation.domain}.device_automation.extra_fields_descriptions.${schema.name}` | ||||
|     ); | ||||
|  | ||||
| export const sortDeviceAutomations = ( | ||||
|   automationA: DeviceAutomation, | ||||
|   automationB: DeviceAutomation | ||||
|   | ||||
| @@ -30,6 +30,7 @@ export interface LovelaceViewElement extends HTMLElement { | ||||
| export interface LovelaceSectionElement extends HTMLElement { | ||||
|   hass?: HomeAssistant; | ||||
|   lovelace?: Lovelace; | ||||
|   preview?: boolean; | ||||
|   viewIndex?: number; | ||||
|   index?: number; | ||||
|   cards?: HuiCard[]; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { HomeAssistant } from "../types"; | ||||
|  | ||||
| export interface OTBRInfo { | ||||
|   active_dataset_tlvs: string; | ||||
|   border_agent_id: string; | ||||
|   channel: number; | ||||
|   extended_address: string; | ||||
|   url: string; | ||||
|   | ||||
							
								
								
									
										21
									
								
								src/data/threshold.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/data/threshold.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| export interface ThresholdPreview { | ||||
|   state: string; | ||||
|   attributes: Record<string, any>; | ||||
| } | ||||
|  | ||||
| export const subscribePreviewThreshold = ( | ||||
|   hass: HomeAssistant, | ||||
|   flow_id: string, | ||||
|   flow_type: "config_flow" | "options_flow", | ||||
|   user_input: Record<string, any>, | ||||
|   callback: (preview: ThresholdPreview) => void | ||||
| ): Promise<UnsubscribeFunc> => | ||||
|   hass.connection.subscribeMessage(callback, { | ||||
|     type: "threshold/start_preview", | ||||
|     flow_id, | ||||
|     flow_type, | ||||
|     user_input, | ||||
|   }); | ||||
| @@ -1,6 +1,9 @@ | ||||
| import { | ||||
|   mdiAlertCircleOutline, | ||||
|   mdiGauge, | ||||
|   mdiThermometer, | ||||
|   mdiThermometerWater, | ||||
|   mdiSunWireless, | ||||
|   mdiWaterPercent, | ||||
|   mdiWeatherCloudy, | ||||
|   mdiWeatherFog, | ||||
| @@ -114,10 +117,15 @@ export const weatherIcons = { | ||||
| }; | ||||
|  | ||||
| export const weatherAttrIcons = { | ||||
|   apparent_temperature: mdiThermometer, | ||||
|   cloud_coverage: mdiWeatherCloudy, | ||||
|   dew_point: mdiThermometerWater, | ||||
|   humidity: mdiWaterPercent, | ||||
|   wind_bearing: mdiWeatherWindy, | ||||
|   wind_speed: mdiWeatherWindy, | ||||
|   pressure: mdiGauge, | ||||
|   temperature: mdiThermometer, | ||||
|   uv_index: mdiSunWireless, | ||||
|   visibility: mdiWeatherFog, | ||||
|   precipitation: mdiWeatherRainy, | ||||
| }; | ||||
| @@ -221,6 +229,8 @@ export const getWeatherUnit = ( | ||||
|         stateObj.attributes.pressure_unit || | ||||
|         (lengthUnit === "km" ? "hPa" : "inHg") | ||||
|       ); | ||||
|     case "apparent_temperature": | ||||
|     case "dew_point": | ||||
|     case "temperature": | ||||
|     case "templow": | ||||
|       return ( | ||||
| @@ -228,6 +238,7 @@ export const getWeatherUnit = ( | ||||
|       ); | ||||
|     case "wind_speed": | ||||
|       return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`; | ||||
|     case "cloud_coverage": | ||||
|     case "humidity": | ||||
|     case "precipitation_probability": | ||||
|       return "%"; | ||||
|   | ||||
| @@ -14,6 +14,7 @@ export interface Zone { | ||||
| export interface HomeZoneMutableParams { | ||||
|   latitude: number; | ||||
|   longitude: number; | ||||
|   radius: number; | ||||
| } | ||||
|  | ||||
| export interface ZoneMutableParams { | ||||
|   | ||||
							
								
								
									
										108
									
								
								src/dialogs/config-flow/previews/flow-preview-threshold.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/dialogs/config-flow/previews/flow-preview-threshold.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { LitElement, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { FlowType } from "../../../data/data_entry_flow"; | ||||
| import { | ||||
|   ThresholdPreview, | ||||
|   subscribePreviewThreshold, | ||||
| } from "../../../data/threshold"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import "./entity-preview-row"; | ||||
| import { debounce } from "../../../common/util/debounce"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
|  | ||||
| @customElement("flow-preview-threshold") | ||||
| class FlowPreviewThreshold extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public flowType!: FlowType; | ||||
|  | ||||
|   public handler!: string; | ||||
|  | ||||
|   @property() public stepId!: string; | ||||
|  | ||||
|   @property() public flowId!: string; | ||||
|  | ||||
|   @property() public stepData!: Record<string, any>; | ||||
|  | ||||
|   @state() private _preview?: HassEntity; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   private _unsub?: Promise<UnsubscribeFunc>; | ||||
|  | ||||
|   disconnectedCallback(): void { | ||||
|     super.disconnectedCallback(); | ||||
|     if (this._unsub) { | ||||
|       this._unsub.then((unsub) => unsub()); | ||||
|       this._unsub = undefined; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   willUpdate(changedProps) { | ||||
|     if (changedProps.has("stepData")) { | ||||
|       this._debouncedSubscribePreview(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (this._error) { | ||||
|       return html`<ha-alert alert-type="error">${this._error}</ha-alert>`; | ||||
|     } | ||||
|     return html`<entity-preview-row | ||||
|       .hass=${this.hass} | ||||
|       .stateObj=${this._preview} | ||||
|     ></entity-preview-row>`; | ||||
|   } | ||||
|  | ||||
|   private _setPreview = (preview: ThresholdPreview) => { | ||||
|     const now = new Date().toISOString(); | ||||
|     this._preview = { | ||||
|       entity_id: `${this.stepId}.___flow_preview___`, | ||||
|       last_changed: now, | ||||
|       last_updated: now, | ||||
|       context: { id: "", parent_id: null, user_id: null }, | ||||
|       ...preview, | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   private _debouncedSubscribePreview = debounce(() => { | ||||
|     this._subscribePreview(); | ||||
|   }, 250); | ||||
|  | ||||
|   private async _subscribePreview() { | ||||
|     if (this._unsub) { | ||||
|       (await this._unsub)(); | ||||
|       this._unsub = undefined; | ||||
|     } | ||||
|     if (this.flowType === "repair_flow") { | ||||
|       return; | ||||
|     } | ||||
|     try { | ||||
|       this._unsub = subscribePreviewThreshold( | ||||
|         this.hass, | ||||
|         this.flowId, | ||||
|         this.flowType, | ||||
|         this.stepData, | ||||
|         this._setPreview | ||||
|       ); | ||||
|       await this._unsub; | ||||
|       fireEvent(this, "set-flow-errors", { errors: {} }); | ||||
|     } catch (err: any) { | ||||
|       if (typeof err.message === "string") { | ||||
|         this._error = err.message; | ||||
|       } else { | ||||
|         this._error = undefined; | ||||
|         fireEvent(this, "set-flow-errors", err.message); | ||||
|       } | ||||
|       this._unsub = undefined; | ||||
|       this._preview = undefined; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "flow-preview-threshold": FlowPreviewThreshold; | ||||
|   } | ||||
| } | ||||
| @@ -126,7 +126,6 @@ class MoreInfoUpdate extends LitElement { | ||||
|               ></ha-checkbox> | ||||
|             </ha-formfield> ` | ||||
|         : ""} | ||||
|       <hr /> | ||||
|       <div class="actions"> | ||||
|         ${this.stateObj.attributes.auto_update | ||||
|           ? "" | ||||
| @@ -240,10 +239,20 @@ class MoreInfoUpdate extends LitElement { | ||||
|         justify-content: space-between; | ||||
|       } | ||||
|       .actions { | ||||
|         border-top: 1px solid var(--divider-color); | ||||
|         background: var( | ||||
|           --ha-dialog-surface-background, | ||||
|           var(--mdc-theme-surface, #fff) | ||||
|         ); | ||||
|         margin: 8px 0 0; | ||||
|         display: flex; | ||||
|         flex-wrap: wrap; | ||||
|         justify-content: center; | ||||
|         position: sticky; | ||||
|         bottom: 0; | ||||
|         padding: 12px 0; | ||||
|         margin-bottom: -24px; | ||||
|         z-index: 1; | ||||
|       } | ||||
|  | ||||
|       .actions mwc-button { | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
| import "../resources/compatibility"; | ||||
| import "../auth/ha-authorize"; | ||||
| import "../resources/safari-14-attachshadow-patch"; | ||||
| import "../resources/array.flat.polyfill"; | ||||
|  | ||||
| import("../resources/ha-style"); | ||||
| import("@polymer/polymer/lib/utils/settings").then( | ||||
|   | ||||
| @@ -25,7 +25,6 @@ import { subscribePanels } from "../data/ws-panels"; | ||||
| import { subscribeThemes } from "../data/ws-themes"; | ||||
| import { subscribeUser } from "../data/ws-user"; | ||||
| import type { ExternalAuth } from "../external_app/external_auth"; | ||||
| import "../resources/array.flat.polyfill"; | ||||
| import "../resources/safari-14-attachshadow-patch"; | ||||
|  | ||||
| window.name = MAIN_WINDOW_NAME; | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
| import "../resources/compatibility"; | ||||
| import "../onboarding/ha-onboarding"; | ||||
| import "../resources/safari-14-attachshadow-patch"; | ||||
| import "../resources/array.flat.polyfill"; | ||||
|  | ||||
| import("../resources/ha-style"); | ||||
| import("@polymer/polymer/lib/utils/settings").then( | ||||
|   | ||||
| @@ -5,6 +5,7 @@ export const demoConfig: HassConfig = { | ||||
|   elevation: 300, | ||||
|   latitude: 52.3731339, | ||||
|   longitude: 4.8903147, | ||||
|   radius: 100, | ||||
|   unit_system: { | ||||
|     length: "km", | ||||
|     mass: "kg", | ||||
|   | ||||
| @@ -96,7 +96,7 @@ export class HaConfigApplicationCredentials extends LitElement { | ||||
|         .hass=${this.hass} | ||||
|         .narrow=${this.narrow} | ||||
|         .route=${this.route} | ||||
|         backPath="/config" | ||||
|         back-path="/config" | ||||
|         .tabs=${configSections.devices} | ||||
|         .columns=${this._columns(this.narrow, this.hass.localize)} | ||||
|         .data=${this._getApplicationCredentials( | ||||
|   | ||||
| @@ -12,6 +12,8 @@ import { | ||||
|   deviceAutomationsEqual, | ||||
|   DeviceCapabilities, | ||||
|   fetchDeviceActionCapabilities, | ||||
|   localizeExtraFieldsComputeLabelCallback, | ||||
|   localizeExtraFieldsComputeHelperCallback, | ||||
| } from "../../../../../data/device_automation"; | ||||
| import { EntityRegistryEntry } from "../../../../../data/entity_registry"; | ||||
| import { HomeAssistant } from "../../../../../types"; | ||||
| @@ -84,8 +86,13 @@ export class HaDeviceAction extends LitElement { | ||||
|               .data=${this._extraFieldsData(this.action, this._capabilities)} | ||||
|               .schema=${this._capabilities.extra_fields} | ||||
|               .disabled=${this.disabled} | ||||
|               .computeLabel=${this._extraFieldsComputeLabelCallback( | ||||
|                 this.hass.localize | ||||
|               .computeLabel=${localizeExtraFieldsComputeLabelCallback( | ||||
|                 this.hass, | ||||
|                 this.action | ||||
|               )} | ||||
|               .computeHelper=${localizeExtraFieldsComputeHelperCallback( | ||||
|                 this.hass, | ||||
|                 this.action | ||||
|               )} | ||||
|               @value-changed=${this._extraFieldsChanged} | ||||
|             ></ha-form> | ||||
| @@ -152,14 +159,6 @@ export class HaDeviceAction extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _extraFieldsComputeLabelCallback(localize) { | ||||
|     // Returns a callback for ha-form to calculate labels per schema object | ||||
|     return (schema) => | ||||
|       localize( | ||||
|         `ui.panel.config.automation.editor.actions.type.device_id.extra_fields.${schema.name}` | ||||
|       ) || schema.name; | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     ha-device-picker { | ||||
|       display: block; | ||||
|   | ||||
| @@ -12,6 +12,8 @@ import { | ||||
|   DeviceCapabilities, | ||||
|   DeviceCondition, | ||||
|   fetchDeviceConditionCapabilities, | ||||
|   localizeExtraFieldsComputeLabelCallback, | ||||
|   localizeExtraFieldsComputeHelperCallback, | ||||
| } from "../../../../../data/device_automation"; | ||||
| import { EntityRegistryEntry } from "../../../../../data/entity_registry"; | ||||
| import type { HomeAssistant } from "../../../../../types"; | ||||
| @@ -84,8 +86,13 @@ export class HaDeviceCondition extends LitElement { | ||||
|               .data=${this._extraFieldsData(this.condition, this._capabilities)} | ||||
|               .schema=${this._capabilities.extra_fields} | ||||
|               .disabled=${this.disabled} | ||||
|               .computeLabel=${this._extraFieldsComputeLabelCallback( | ||||
|                 this.hass.localize | ||||
|               .computeLabel=${localizeExtraFieldsComputeLabelCallback( | ||||
|                 this.hass, | ||||
|                 this.condition | ||||
|               )} | ||||
|               .computeHelper=${localizeExtraFieldsComputeHelperCallback( | ||||
|                 this.hass, | ||||
|                 this.condition | ||||
|               )} | ||||
|               @value-changed=${this._extraFieldsChanged} | ||||
|             ></ha-form> | ||||
| @@ -153,14 +160,6 @@ export class HaDeviceCondition extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _extraFieldsComputeLabelCallback(localize) { | ||||
|     // Returns a callback for ha-form to calculate labels per schema object | ||||
|     return (schema) => | ||||
|       localize( | ||||
|         `ui.panel.config.automation.editor.conditions.type.device.extra_fields.${schema.name}` | ||||
|       ) || schema.name; | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     ha-device-picker { | ||||
|       display: block; | ||||
|   | ||||
| @@ -14,6 +14,8 @@ import { | ||||
|   DeviceCapabilities, | ||||
|   DeviceTrigger, | ||||
|   fetchDeviceTriggerCapabilities, | ||||
|   localizeExtraFieldsComputeLabelCallback, | ||||
|   localizeExtraFieldsComputeHelperCallback, | ||||
| } from "../../../../../data/device_automation"; | ||||
| import { EntityRegistryEntry } from "../../../../../data/entity_registry"; | ||||
| import { HomeAssistant } from "../../../../../types"; | ||||
| @@ -88,8 +90,13 @@ export class HaDeviceTrigger extends LitElement { | ||||
|               .data=${this._extraFieldsData(this.trigger, this._capabilities)} | ||||
|               .schema=${this._capabilities.extra_fields} | ||||
|               .disabled=${this.disabled} | ||||
|               .computeLabel=${this._extraFieldsComputeLabelCallback( | ||||
|                 this.hass.localize | ||||
|               .computeLabel=${localizeExtraFieldsComputeLabelCallback( | ||||
|                 this.hass, | ||||
|                 this.trigger | ||||
|               )} | ||||
|               .computeHelper=${localizeExtraFieldsComputeHelperCallback( | ||||
|                 this.hass, | ||||
|                 this.trigger | ||||
|               )} | ||||
|               @value-changed=${this._extraFieldsChanged} | ||||
|             ></ha-form> | ||||
| @@ -177,14 +184,6 @@ export class HaDeviceTrigger extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _extraFieldsComputeLabelCallback(localize) { | ||||
|     // Returns a callback for ha-form to calculate labels per schema object | ||||
|     return (schema) => | ||||
|       localize( | ||||
|         `ui.panel.config.automation.editor.triggers.type.device.extra_fields.${schema.name}` | ||||
|       ) || schema.name; | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     ha-device-picker { | ||||
|       display: block; | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { | ||||
|   mdiPower, | ||||
|   mdiRefresh, | ||||
| } from "@mdi/js"; | ||||
| import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
| @@ -177,7 +177,10 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const { updates: canInstallUpdates, total: totalUpdates } = | ||||
|       this._filterUpdateEntitiesWithInstall(this.hass.states); | ||||
|       this._filterUpdateEntitiesWithInstall( | ||||
|         this.hass.states, | ||||
|         this.hass.entities | ||||
|       ); | ||||
|  | ||||
|     const { issues: repairsIssues, total: totalRepairIssues } = | ||||
|       this._repairsIssues; | ||||
| @@ -306,8 +309,13 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   private _filterUpdateEntitiesWithInstall = memoizeOne( | ||||
|     (entities: HassEntities): { updates: UpdateEntity[]; total: number } => { | ||||
|       const updates = filterUpdateEntitiesWithInstall(entities); | ||||
|     ( | ||||
|       entities: HomeAssistant["states"], | ||||
|       entityRegistry: HomeAssistant["entities"] | ||||
|     ): { updates: UpdateEntity[]; total: number } => { | ||||
|       const updates = filterUpdateEntitiesWithInstall(entities).filter( | ||||
|         (entity) => !entityRegistry[entity.entity_id]?.hidden | ||||
|       ); | ||||
|  | ||||
|       return { | ||||
|         updates: updates.slice(0, updates.length === 3 ? updates.length : 2), | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||
| import { SENSOR_ENTITIES } from "../../../common/const"; | ||||
| import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| @@ -190,26 +190,42 @@ export class HaConfigDevicePage extends LitElement { | ||||
|  | ||||
|   private _entitiesByCategory = memoizeOne( | ||||
|     (entities: EntityRegistryEntry[]) => { | ||||
|       const result = groupBy(entities, (entry) => | ||||
|         entry.entity_category | ||||
|           ? entry.entity_category | ||||
|           : computeDomain(entry.entity_id) === "event" | ||||
|             ? "event" | ||||
|             : SENSOR_ENTITIES.includes(computeDomain(entry.entity_id)) | ||||
|               ? "sensor" | ||||
|               : "control" | ||||
|       ) as Record< | ||||
|       const result = groupBy(entities, (entry) => { | ||||
|         const domain = computeDomain(entry.entity_id); | ||||
|  | ||||
|         if (entry.entity_category) { | ||||
|           return entry.entity_category; | ||||
|         } | ||||
|  | ||||
|         if (domain === "event" || domain === "notify") { | ||||
|           return domain; | ||||
|         } | ||||
|  | ||||
|         if (SENSOR_ENTITIES.includes(domain)) { | ||||
|           return "sensor"; | ||||
|         } | ||||
|  | ||||
|         if (ASSIST_ENTITIES.includes(domain)) { | ||||
|           return "assist"; | ||||
|         } | ||||
|  | ||||
|         return "control"; | ||||
|       }) as Record< | ||||
|         | "control" | ||||
|         | "event" | ||||
|         | "sensor" | ||||
|         | "assist" | ||||
|         | "notify" | ||||
|         | NonNullable<EntityRegistryEntry["entity_category"]>, | ||||
|         EntityRegistryStateEntry[] | ||||
|       >; | ||||
|       for (const key of [ | ||||
|         "assist", | ||||
|         "config", | ||||
|         "control", | ||||
|         "diagnostic", | ||||
|         "event", | ||||
|         "notify", | ||||
|         "sensor", | ||||
|       ]) { | ||||
|         if (!(key in result)) { | ||||
| @@ -854,7 +870,15 @@ export class HaConfigDevicePage extends LitElement { | ||||
|           </div> | ||||
|           <div class="column"> | ||||
|             ${( | ||||
|               ["control", "sensor", "event", "config", "diagnostic"] as const | ||||
|               [ | ||||
|                 "control", | ||||
|                 "sensor", | ||||
|                 "notify", | ||||
|                 "event", | ||||
|                 "assist", | ||||
|                 "config", | ||||
|                 "diagnostic", | ||||
|               ] as const | ||||
|             ).map((category) => | ||||
|               // Make sure we render controls if no other cards will be rendered | ||||
|               entitiesByCategory[category].length > 0 || | ||||
| @@ -1004,6 +1028,9 @@ export class HaConfigDevicePage extends LitElement { | ||||
|                   : this.hass.localize( | ||||
|                       `ui.panel.config.devices.confirm_delete` | ||||
|                     ), | ||||
|               confirmText: this.hass.localize("ui.common.delete"), | ||||
|               dismissText: this.hass.localize("ui.common.cancel"), | ||||
|               destructive: true, | ||||
|             }); | ||||
|  | ||||
|             if (!confirmed) { | ||||
|   | ||||
| @@ -164,6 +164,9 @@ export class EntitySettingsHelperTab extends LitElement { | ||||
|         text: this.hass.localize( | ||||
|           "ui.dialogs.entity_registry.editor.confirm_delete" | ||||
|         ), | ||||
|         confirmText: this.hass.localize("ui.common.delete"), | ||||
|         dismissText: this.hass.localize("ui.common.cancel"), | ||||
|         destructive: true, | ||||
|       })) | ||||
|     ) { | ||||
|       return; | ||||
|   | ||||
| @@ -215,6 +215,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { | ||||
|         text: this.hass.localize( | ||||
|           "ui.dialogs.entity_registry.editor.confirm_delete" | ||||
|         ), | ||||
|         confirmText: this.hass.localize("ui.common.delete"), | ||||
|         dismissText: this.hass.localize("ui.common.cancel"), | ||||
|         destructive: true, | ||||
|       })) | ||||
|     ) { | ||||
|       return; | ||||
|   | ||||
| @@ -839,7 +839,7 @@ ${ | ||||
|     ></ha-svg-icon> | ||||
|     <div slot="headline"> | ||||
|       ${this.hass.localize( | ||||
|         "ui.panel.config.entities.picker.remove_selected.button" | ||||
|         "ui.panel.config.entities.picker.delete_selected.button" | ||||
|       )} | ||||
|     </div> | ||||
|   </ha-menu-item> | ||||
| @@ -1256,25 +1256,23 @@ ${rejected | ||||
|     }); | ||||
|     showConfirmationDialog(this, { | ||||
|       title: this.hass.localize( | ||||
|         `ui.panel.config.entities.picker.remove_selected.confirm_${ | ||||
|           removeableEntities.length !== this._selected.length ? "partly_" : "" | ||||
|         }title`, | ||||
|         { number: removeableEntities.length } | ||||
|         `ui.panel.config.entities.picker.delete_selected.confirm_title` | ||||
|       ), | ||||
|       text: | ||||
|         removeableEntities.length === this._selected.length | ||||
|           ? this.hass.localize( | ||||
|               "ui.panel.config.entities.picker.remove_selected.confirm_text" | ||||
|               "ui.panel.config.entities.picker.delete_selected.confirm_text" | ||||
|             ) | ||||
|           : this.hass.localize( | ||||
|               "ui.panel.config.entities.picker.remove_selected.confirm_partly_text", | ||||
|               "ui.panel.config.entities.picker.delete_selected.confirm_partly_text", | ||||
|               { | ||||
|                 removable: removeableEntities.length, | ||||
|                 deletable: removeableEntities.length, | ||||
|                 selected: this._selected.length, | ||||
|               } | ||||
|             ), | ||||
|       confirmText: this.hass.localize("ui.common.remove"), | ||||
|       confirmText: this.hass.localize("ui.common.delete"), | ||||
|       dismissText: this.hass.localize("ui.common.cancel"), | ||||
|       destructive: true, | ||||
|       confirm: () => { | ||||
|         removeableEntities.forEach((entity) => | ||||
|           removeEntityRegistryEntry(this.hass, entity) | ||||
|   | ||||
| @@ -754,7 +754,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { | ||||
|         ${item.disabled_by && devices.length | ||||
|           ? html` | ||||
|               <ha-menu-item | ||||
|                 .href=${devices.length === 1 | ||||
|                 href=${devices.length === 1 | ||||
|                   ? `/config/devices/device/${devices[0].id}` | ||||
|                   : `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`} | ||||
|               > | ||||
| @@ -769,7 +769,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { | ||||
|           : ""} | ||||
|         ${item.disabled_by && services.length | ||||
|           ? html`<ha-menu-item | ||||
|               .href=${services.length === 1 | ||||
|               href=${services.length === 1 | ||||
|                 ? `/config/devices/device/${services[0].id}` | ||||
|                 : `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`} | ||||
|             > | ||||
| @@ -787,7 +787,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { | ||||
|         ${item.disabled_by && entities.length | ||||
|           ? html` | ||||
|               <ha-menu-item | ||||
|                 .href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`} | ||||
|                 href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`} | ||||
|               > | ||||
|                 <ha-svg-icon | ||||
|                   .path=${mdiShapeOutline} | ||||
| @@ -827,7 +827,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { | ||||
|         ${this._diagnosticHandler && item.state === "loaded" | ||||
|           ? html` | ||||
|               <ha-menu-item | ||||
|                 .href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)} | ||||
|                 href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)} | ||||
|                 target="_blank" | ||||
|                 @click=${this._signUrl} | ||||
|               > | ||||
| @@ -1414,14 +1414,17 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { | ||||
|         ha-alert:first-of-type { | ||||
|           margin-top: 16px; | ||||
|         } | ||||
|         ha-list-item-new { | ||||
|           position: relative; | ||||
|         } | ||||
|         ha-list-item-new.discovered { | ||||
|           height: 72px; | ||||
|         } | ||||
|         ha-list-item-new.config_entry::after { | ||||
|           position: absolute; | ||||
|           top: 8px; | ||||
|           top: 0; | ||||
|           right: 0; | ||||
|           bottom: 8px; | ||||
|           bottom: 0; | ||||
|           left: 0; | ||||
|           opacity: 0.12; | ||||
|           pointer-events: none; | ||||
|   | ||||
| @@ -36,14 +36,8 @@ class DialogThreadDataset extends LitElement implements HassDialog { | ||||
|       dataset.extended_pan_id && | ||||
|       otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id); | ||||
|  | ||||
|     const canImportKeychain = | ||||
|       hasOTBR && | ||||
|       !this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain && | ||||
|       network.routers?.length; | ||||
|  | ||||
|     return html`<ha-dialog | ||||
|       open | ||||
|       .hideActions=${!canImportKeychain} | ||||
|       @closed=${this.closeDialog} | ||||
|       .heading=${createCloseHeading(this.hass, network.name)} | ||||
|     > | ||||
| @@ -59,28 +53,8 @@ class DialogThreadDataset extends LitElement implements HassDialog { | ||||
|               Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}` | ||||
|           : nothing} | ||||
|       </div> | ||||
|       ${canImportKeychain | ||||
|         ? html`<ha-button slot="primary-action" @click=${this._sendCredentials} | ||||
|             >Send credentials to phone</ha-button | ||||
|           >` | ||||
|         : nothing} | ||||
|     </ha-dialog>`; | ||||
|   } | ||||
|  | ||||
|   private _sendCredentials() { | ||||
|     this.hass.auth.external!.fireMessage({ | ||||
|       type: "thread/store_in_platform_keychain", | ||||
|       payload: { | ||||
|         mac_extended_address: | ||||
|           this._params?.network.dataset?.preferred_extended_address || | ||||
|           this._params!.network.routers![0]!.extended_address, | ||||
|         border_agent_id: | ||||
|           this._params?.network.dataset?.preferred_border_agent_id || | ||||
|           this._params!.network.routers![0]!.border_agent_id, | ||||
|         active_operational_dataset: this._params!.otbrInfo!.active_dataset_tlvs, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -151,7 +151,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { | ||||
|               slot="fab" | ||||
|               @click=${this._importExternalThreadCredentials} | ||||
|               extended | ||||
|               label="Import credentials" | ||||
|               label="Send credentials to Home Assistant" | ||||
|               ><ha-svg-icon slot="icon" .path=${mdiCellphoneKey}></ha-svg-icon | ||||
|             ></ha-fab>` | ||||
|           : nothing} | ||||
| @@ -160,6 +160,14 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   private _renderNetwork(network: ThreadNetwork) { | ||||
|     const canImportKeychain = | ||||
|       this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain && | ||||
|       network.dataset?.extended_pan_id && | ||||
|       this._otbrInfo && | ||||
|       this._otbrInfo?.active_dataset_tlvs?.includes( | ||||
|         network.dataset.extended_pan_id | ||||
|       ); | ||||
|  | ||||
|     return html`<ha-card> | ||||
|       <div class="card-header"> | ||||
|         ${network.name}${network.dataset | ||||
| @@ -303,9 +311,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { | ||||
|             > | ||||
|           </div>` | ||||
|         : ""} | ||||
|       ${canImportKeychain | ||||
|         ? html`<div class="card-actions"> | ||||
|             <mwc-button @click=${this._sendCredentials} | ||||
|               >Send credentials to phone</mwc-button | ||||
|             > | ||||
|           </div>` | ||||
|         : ""} | ||||
|     </ha-card>`; | ||||
|   } | ||||
|  | ||||
|   private _sendCredentials() { | ||||
|     if (!this._otbrInfo) { | ||||
|       return; | ||||
|     } | ||||
|     this.hass.auth.external!.fireMessage({ | ||||
|       type: "thread/store_in_platform_keychain", | ||||
|       payload: { | ||||
|         mac_extended_address: this._otbrInfo.extended_address, | ||||
|         border_agent_id: this._otbrInfo.border_agent_id ?? "", | ||||
|         active_operational_dataset: this._otbrInfo.active_dataset_tlvs ?? "", | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async _showDatasetInfo(ev: Event) { | ||||
|     const network = (ev.currentTarget as any).network as ThreadNetwork; | ||||
|     showThreadDatasetDialog(this, { network, otbrInfo: this._otbrInfo }); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import { fireEvent } from "../../../../../common/dom/fire_event"; | ||||
| import "../../../../../components/ha-alert"; | ||||
| import type { HaCheckbox } from "../../../../../components/ha-checkbox"; | ||||
| @@ -60,7 +61,8 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|     | "finished" | ||||
|     | "provisioned" | ||||
|     | "validate_dsk_enter_pin" | ||||
|     | "grant_security_classes"; | ||||
|     | "grant_security_classes" | ||||
|     | "waiting_for_device"; | ||||
|  | ||||
|   @state() private _device?: ZWaveJSAddNodeDevice; | ||||
|  | ||||
| @@ -86,6 +88,11 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|  | ||||
|   private _qrProcessing = false; | ||||
|  | ||||
|   public connectedCallback(): void { | ||||
|     super.connectedCallback(); | ||||
|     window.addEventListener("beforeunload", this._onBeforeUnload); | ||||
|   } | ||||
|  | ||||
|   public disconnectedCallback(): void { | ||||
|     super.disconnectedCallback(); | ||||
|     this._unsubscribe(); | ||||
| @@ -106,14 +113,22 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     // Prevent accidentally closing the dialog in certain stages | ||||
|     const preventClose = this._shouldPreventClose(); | ||||
|  | ||||
|     const heading = this.hass.localize( | ||||
|       "ui.panel.config.zwave_js.add_node.title" | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <ha-dialog | ||||
|         open | ||||
|         @closed=${this.closeDialog} | ||||
|         .heading=${createCloseHeading( | ||||
|           this.hass, | ||||
|           this.hass.localize("ui.panel.config.zwave_js.add_node.title") | ||||
|         )} | ||||
|         .heading=${preventClose | ||||
|           ? heading | ||||
|           : createCloseHeading(this.hass, heading)} | ||||
|         scrimClickAction=${ifDefined(preventClose ? "" : undefined)} | ||||
|         escapeKeyAction=${ifDefined(preventClose ? "" : undefined)} | ||||
|       > | ||||
|         ${this._status === "loading" | ||||
|           ? html`<div style="display: flex; justify-content: center;"> | ||||
| @@ -122,81 +137,93 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|                 indeterminate | ||||
|               ></ha-circular-progress> | ||||
|             </div>` | ||||
|           : this._status === "choose_strategy" | ||||
|             ? html`<h3>Choose strategy</h3> | ||||
|                 <div class="flex-column"> | ||||
|                   <ha-formfield | ||||
|                     .label=${html`<b>Secure if possible</b> | ||||
|                       <div class="secondary"> | ||||
|                         Requires user interaction during inclusion. Fast and | ||||
|                         secure with S2 when supported. Fallback to legacy S0 or | ||||
|                         no encryption when necessary. | ||||
|                       </div>`} | ||||
|                   > | ||||
|                     <ha-radio | ||||
|                       name="strategy" | ||||
|                       @change=${this._handleStrategyChange} | ||||
|                       .value=${InclusionStrategy.Default} | ||||
|                       .checked=${this._inclusionStrategy === | ||||
|                         InclusionStrategy.Default || | ||||
|                       this._inclusionStrategy === undefined} | ||||
|           : this._status === "waiting_for_device" | ||||
|             ? html`<div class="flex-container"> | ||||
|                 <ha-circular-progress indeterminate></ha-circular-progress> | ||||
|                 <p> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.zwave_js.add_node.waiting_for_device" | ||||
|                   )} | ||||
|                 </p> | ||||
|               </div>` | ||||
|             : this._status === "choose_strategy" | ||||
|               ? html`<h3>Choose strategy</h3> | ||||
|                   <div class="flex-column"> | ||||
|                     <ha-formfield | ||||
|                       .label=${html`<b>Secure if possible</b> | ||||
|                         <div class="secondary"> | ||||
|                           Requires user interaction during inclusion. Fast and | ||||
|                           secure with S2 when supported. Fallback to legacy S0 | ||||
|                           or no encryption when necessary. | ||||
|                         </div>`} | ||||
|                     > | ||||
|                     </ha-radio> | ||||
|                   </ha-formfield> | ||||
|                   <ha-formfield | ||||
|                     .label=${html`<b>Legacy Secure</b> | ||||
|                       <div class="secondary"> | ||||
|                         Uses the older S0 security that is secure, but slow due | ||||
|                         to a lot of overhead. Allows securely including S2 | ||||
|                         capable devices which fail to be included with S2. | ||||
|                       </div>`} | ||||
|                   > | ||||
|                     <ha-radio | ||||
|                       name="strategy" | ||||
|                       @change=${this._handleStrategyChange} | ||||
|                       .value=${InclusionStrategy.Security_S0} | ||||
|                       .checked=${this._inclusionStrategy === | ||||
|                       InclusionStrategy.Security_S0} | ||||
|                       <ha-radio | ||||
|                         name="strategy" | ||||
|                         @change=${this._handleStrategyChange} | ||||
|                         .value=${InclusionStrategy.Default} | ||||
|                         .checked=${this._inclusionStrategy === | ||||
|                           InclusionStrategy.Default || | ||||
|                         this._inclusionStrategy === undefined} | ||||
|                       > | ||||
|                       </ha-radio> | ||||
|                     </ha-formfield> | ||||
|                     <ha-formfield | ||||
|                       .label=${html`<b>Legacy Secure</b> | ||||
|                         <div class="secondary"> | ||||
|                           Uses the older S0 security that is secure, but slow | ||||
|                           due to a lot of overhead. Allows securely including S2 | ||||
|                           capable devices which fail to be included with S2. | ||||
|                         </div>`} | ||||
|                     > | ||||
|                     </ha-radio> | ||||
|                   </ha-formfield> | ||||
|                   <ha-formfield | ||||
|                     .label=${html`<b>Insecure</b> | ||||
|                       <div class="secondary">Do not use encryption.</div>`} | ||||
|                   > | ||||
|                     <ha-radio | ||||
|                       name="strategy" | ||||
|                       @change=${this._handleStrategyChange} | ||||
|                       .value=${InclusionStrategy.Insecure} | ||||
|                       .checked=${this._inclusionStrategy === | ||||
|                       InclusionStrategy.Insecure} | ||||
|                       <ha-radio | ||||
|                         name="strategy" | ||||
|                         @change=${this._handleStrategyChange} | ||||
|                         .value=${InclusionStrategy.Security_S0} | ||||
|                         .checked=${this._inclusionStrategy === | ||||
|                         InclusionStrategy.Security_S0} | ||||
|                       > | ||||
|                       </ha-radio> | ||||
|                     </ha-formfield> | ||||
|                     <ha-formfield | ||||
|                       .label=${html`<b>Insecure</b> | ||||
|                         <div class="secondary">Do not use encryption.</div>`} | ||||
|                     > | ||||
|                     </ha-radio> | ||||
|                   </ha-formfield> | ||||
|                 </div> | ||||
|                 <mwc-button | ||||
|                   slot="primaryAction" | ||||
|                   @click=${this._startManualInclusion} | ||||
|                 > | ||||
|                   Search device | ||||
|                 </mwc-button>` | ||||
|             : this._status === "qr_scan" | ||||
|               ? html`${this._error | ||||
|                     ? html`<ha-alert alert-type="error" | ||||
|                         >${this._error}</ha-alert | ||||
|                       >` | ||||
|                     : ""} | ||||
|                   <ha-qr-scanner | ||||
|                     .localize=${this.hass.localize} | ||||
|                     @qr-code-scanned=${this._qrCodeScanned} | ||||
|                   ></ha-qr-scanner> | ||||
|                   <mwc-button slot="secondaryAction" @click=${this._startOver}> | ||||
|                     ${this.hass.localize( | ||||
|                       "ui.panel.config.zwave_js.common.back" | ||||
|                     )} | ||||
|                       <ha-radio | ||||
|                         name="strategy" | ||||
|                         @change=${this._handleStrategyChange} | ||||
|                         .value=${InclusionStrategy.Insecure} | ||||
|                         .checked=${this._inclusionStrategy === | ||||
|                         InclusionStrategy.Insecure} | ||||
|                       > | ||||
|                       </ha-radio> | ||||
|                     </ha-formfield> | ||||
|                   </div> | ||||
|                   <mwc-button | ||||
|                     slot="primaryAction" | ||||
|                     @click=${this._startManualInclusion} | ||||
|                   > | ||||
|                     Search device | ||||
|                   </mwc-button>` | ||||
|               : this._status === "validate_dsk_enter_pin" | ||||
|                 ? html` | ||||
|               : this._status === "qr_scan" | ||||
|                 ? html`${this._error | ||||
|                       ? html`<ha-alert alert-type="error" | ||||
|                           >${this._error}</ha-alert | ||||
|                         >` | ||||
|                       : ""} | ||||
|                     <ha-qr-scanner | ||||
|                       .localize=${this.hass.localize} | ||||
|                       @qr-code-scanned=${this._qrCodeScanned} | ||||
|                     ></ha-qr-scanner> | ||||
|                     <mwc-button | ||||
|                       slot="secondaryAction" | ||||
|                       @click=${this._startOver} | ||||
|                     > | ||||
|                       ${this.hass.localize( | ||||
|                         "ui.panel.config.zwave_js.common.back" | ||||
|                       )} | ||||
|                     </mwc-button>` | ||||
|                 : this._status === "validate_dsk_enter_pin" | ||||
|                   ? html` | ||||
|                 <p> | ||||
|                   Please enter the 5-digit PIN for your device and verify that | ||||
|                   the rest of the device-specific key matches the one that can | ||||
| @@ -225,198 +252,160 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|                 </mwc-button> | ||||
|               </div> | ||||
|             ` | ||||
|                 : this._status === "grant_security_classes" | ||||
|                   ? html` | ||||
|                       <h3> | ||||
|                         The device has requested the following security classes: | ||||
|                       </h3> | ||||
|                       ${this._error | ||||
|                         ? html`<ha-alert alert-type="error" | ||||
|                             >${this._error}</ha-alert | ||||
|                           >` | ||||
|                         : ""} | ||||
|                       <div class="flex-column"> | ||||
|                         ${this._requestedGrant?.securityClasses | ||||
|                           .sort((a, b) => { | ||||
|                             // Put highest security classes at the top, S0 at the bottom | ||||
|                             if (a === SecurityClass.S0_Legacy) return 1; | ||||
|                             if (b === SecurityClass.S0_Legacy) return -1; | ||||
|                             return b - a; | ||||
|                           }) | ||||
|                           .map( | ||||
|                             (securityClass) => | ||||
|                               html`<ha-formfield | ||||
|                                 .label=${html`<b | ||||
|                                     >${this.hass.localize( | ||||
|                                       `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title` | ||||
|                                     )}</b | ||||
|                                   > | ||||
|                                   <div class="secondary"> | ||||
|                                     ${this.hass.localize( | ||||
|                                       `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description` | ||||
|                                     )} | ||||
|                                   </div>`} | ||||
|                               > | ||||
|                                 <ha-checkbox | ||||
|                                   @change=${this._handleSecurityClassChange} | ||||
|                                   .value=${securityClass} | ||||
|                                   .checked=${this._securityClasses.includes( | ||||
|                                     securityClass | ||||
|                                   )} | ||||
|                                 > | ||||
|                                 </ha-checkbox> | ||||
|                               </ha-formfield>` | ||||
|                           )} | ||||
|                       </div> | ||||
|                       <mwc-button | ||||
|                         slot="primaryAction" | ||||
|                         .disabled=${!this._securityClasses.length} | ||||
|                         @click=${this._grantSecurityClasses} | ||||
|                       > | ||||
|                         Submit | ||||
|                       </mwc-button> | ||||
|                     ` | ||||
|                   : this._status === "timed_out" | ||||
|                   : this._status === "grant_security_classes" | ||||
|                     ? html` | ||||
|                         <h3>Timed out!</h3> | ||||
|                         <p> | ||||
|                           We have not found any device in inclusion mode. Make | ||||
|                           sure the device is active and in inclusion mode. | ||||
|                         </p> | ||||
|                         <mwc-button | ||||
|                           slot="primaryAction" | ||||
|                           @click=${this._startOver} | ||||
|                         > | ||||
|                           Retry | ||||
|                         </mwc-button> | ||||
|                       ` | ||||
|                     : this._status === "started_specific" | ||||
|                       ? html`<h3> | ||||
|                             ${this.hass.localize( | ||||
|                               "ui.panel.config.zwave_js.add_node.searching_device" | ||||
|                             )} | ||||
|                           </h3> | ||||
|                           <ha-circular-progress | ||||
|                             indeterminate | ||||
|                           ></ha-circular-progress> | ||||
|                           <p> | ||||
|                             ${this.hass.localize( | ||||
|                               "ui.panel.config.zwave_js.add_node.follow_device_instructions" | ||||
|                             )} | ||||
|                           </p>` | ||||
|                       : this._status === "started" | ||||
|                         ? html` | ||||
|                             <div class="select-inclusion"> | ||||
|                               <div class="outline"> | ||||
|                                 <h2> | ||||
|                                   ${this.hass.localize( | ||||
|                                     "ui.panel.config.zwave_js.add_node.searching_device" | ||||
|                                   )} | ||||
|                                 </h2> | ||||
|                                 <ha-circular-progress | ||||
|                                   indeterminate | ||||
|                                 ></ha-circular-progress> | ||||
|                                 <p> | ||||
|                                   ${this.hass.localize( | ||||
|                                     "ui.panel.config.zwave_js.add_node.follow_device_instructions" | ||||
|                                   )} | ||||
|                                 </p> | ||||
|                                 <p> | ||||
|                                   <button | ||||
|                                     class="link" | ||||
|                                     @click=${this._chooseInclusionStrategy} | ||||
|                                   > | ||||
|                                     ${this.hass.localize( | ||||
|                                       "ui.panel.config.zwave_js.add_node.choose_inclusion_strategy" | ||||
|                                     )} | ||||
|                                   </button> | ||||
|                                 </p> | ||||
|                               </div> | ||||
|                               ${this._supportsSmartStart | ||||
|                                 ? html` <div class="outline"> | ||||
|                                     <h2> | ||||
|                                       ${this.hass.localize( | ||||
|                                         "ui.panel.config.zwave_js.add_node.qr_code" | ||||
|                                       )} | ||||
|                                     </h2> | ||||
|                                     <ha-svg-icon | ||||
|                                       .path=${mdiQrcodeScan} | ||||
|                                     ></ha-svg-icon> | ||||
|                                     <p> | ||||
|                                       ${this.hass.localize( | ||||
|                                         "ui.panel.config.zwave_js.add_node.qr_code_paragraph" | ||||
|                                       )} | ||||
|                                     </p> | ||||
|                                     <p> | ||||
|                                       <mwc-button @click=${this._scanQRCode}> | ||||
|                                         ${this.hass.localize( | ||||
|                                           "ui.panel.config.zwave_js.add_node.scan_qr_code" | ||||
|                                         )} | ||||
|                                       </mwc-button> | ||||
|                                     </p> | ||||
|                                   </div>` | ||||
|                                 : ""} | ||||
|                             </div> | ||||
|                             <mwc-button | ||||
|                               slot="primaryAction" | ||||
|                               @click=${this.closeDialog} | ||||
|                             > | ||||
|                               ${this.hass.localize("ui.common.cancel")} | ||||
|                             </mwc-button> | ||||
|                           ` | ||||
|                         : this._status === "interviewing" | ||||
|                           ? html` | ||||
|                               <div class="flex-container"> | ||||
|                                 <ha-circular-progress | ||||
|                                   indeterminate | ||||
|                                 ></ha-circular-progress> | ||||
|                                 <div class="status"> | ||||
|                                   <p> | ||||
|                                     <b | ||||
|                         <h3> | ||||
|                           The device has requested the following security | ||||
|                           classes: | ||||
|                         </h3> | ||||
|                         ${this._error | ||||
|                           ? html`<ha-alert alert-type="error" | ||||
|                               >${this._error}</ha-alert | ||||
|                             >` | ||||
|                           : ""} | ||||
|                         <div class="flex-column"> | ||||
|                           ${this._requestedGrant?.securityClasses | ||||
|                             .sort((a, b) => { | ||||
|                               // Put highest security classes at the top, S0 at the bottom | ||||
|                               if (a === SecurityClass.S0_Legacy) return 1; | ||||
|                               if (b === SecurityClass.S0_Legacy) return -1; | ||||
|                               return b - a; | ||||
|                             }) | ||||
|                             .map( | ||||
|                               (securityClass) => | ||||
|                                 html`<ha-formfield | ||||
|                                   .label=${html`<b | ||||
|                                       >${this.hass.localize( | ||||
|                                         "ui.panel.config.zwave_js.add_node.interview_started" | ||||
|                                         `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title` | ||||
|                                       )}</b | ||||
|                                     > | ||||
|                                     <div class="secondary"> | ||||
|                                       ${this.hass.localize( | ||||
|                                         `ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description` | ||||
|                                       )} | ||||
|                                     </div>`} | ||||
|                                 > | ||||
|                                   <ha-checkbox | ||||
|                                     @change=${this._handleSecurityClassChange} | ||||
|                                     .value=${securityClass} | ||||
|                                     .checked=${this._securityClasses.includes( | ||||
|                                       securityClass | ||||
|                                     )} | ||||
|                                   > | ||||
|                                   </ha-checkbox> | ||||
|                                 </ha-formfield>` | ||||
|                             )} | ||||
|                         </div> | ||||
|                         <mwc-button | ||||
|                           slot="primaryAction" | ||||
|                           .disabled=${!this._securityClasses.length} | ||||
|                           @click=${this._grantSecurityClasses} | ||||
|                         > | ||||
|                           Submit | ||||
|                         </mwc-button> | ||||
|                       ` | ||||
|                     : this._status === "timed_out" | ||||
|                       ? html` | ||||
|                           <h3>Timed out!</h3> | ||||
|                           <p> | ||||
|                             We have not found any device in inclusion mode. Make | ||||
|                             sure the device is active and in inclusion mode. | ||||
|                           </p> | ||||
|                           <mwc-button | ||||
|                             slot="primaryAction" | ||||
|                             @click=${this._startOver} | ||||
|                           > | ||||
|                             Retry | ||||
|                           </mwc-button> | ||||
|                         ` | ||||
|                       : this._status === "started_specific" | ||||
|                         ? html`<h3> | ||||
|                               ${this.hass.localize( | ||||
|                                 "ui.panel.config.zwave_js.add_node.searching_device" | ||||
|                               )} | ||||
|                             </h3> | ||||
|                             <ha-circular-progress | ||||
|                               indeterminate | ||||
|                             ></ha-circular-progress> | ||||
|                             <p> | ||||
|                               ${this.hass.localize( | ||||
|                                 "ui.panel.config.zwave_js.add_node.follow_device_instructions" | ||||
|                               )} | ||||
|                             </p>` | ||||
|                         : this._status === "started" | ||||
|                           ? html` | ||||
|                               <div class="select-inclusion"> | ||||
|                                 <div class="outline"> | ||||
|                                   <h2> | ||||
|                                     ${this.hass.localize( | ||||
|                                       "ui.panel.config.zwave_js.add_node.searching_device" | ||||
|                                     )} | ||||
|                                   </h2> | ||||
|                                   <ha-circular-progress | ||||
|                                     indeterminate | ||||
|                                   ></ha-circular-progress> | ||||
|                                   <p> | ||||
|                                     ${this.hass.localize( | ||||
|                                       "ui.panel.config.zwave_js.add_node.follow_device_instructions" | ||||
|                                     )} | ||||
|                                   </p> | ||||
|                                   <p> | ||||
|                                     <button | ||||
|                                       class="link" | ||||
|                                       @click=${this._chooseInclusionStrategy} | ||||
|                                     > | ||||
|                                       ${this.hass.localize( | ||||
|                                         "ui.panel.config.zwave_js.add_node.choose_inclusion_strategy" | ||||
|                                       )} | ||||
|                                     </button> | ||||
|                                   </p> | ||||
|                                   ${this._stages | ||||
|                                     ? html` <div class="stages"> | ||||
|                                         ${this._stages.map( | ||||
|                                           (stage) => html` | ||||
|                                             <span class="stage"> | ||||
|                                               <ha-svg-icon | ||||
|                                                 .path=${mdiCheckCircle} | ||||
|                                                 class="success" | ||||
|                                               ></ha-svg-icon> | ||||
|                                               ${stage} | ||||
|                                             </span> | ||||
|                                           ` | ||||
|                                         )} | ||||
|                                       </div>` | ||||
|                                     : ""} | ||||
|                                 </div> | ||||
|                                 ${this._supportsSmartStart | ||||
|                                   ? html` <div class="outline"> | ||||
|                                       <h2> | ||||
|                                         ${this.hass.localize( | ||||
|                                           "ui.panel.config.zwave_js.add_node.qr_code" | ||||
|                                         )} | ||||
|                                       </h2> | ||||
|                                       <ha-svg-icon | ||||
|                                         .path=${mdiQrcodeScan} | ||||
|                                       ></ha-svg-icon> | ||||
|                                       <p> | ||||
|                                         ${this.hass.localize( | ||||
|                                           "ui.panel.config.zwave_js.add_node.qr_code_paragraph" | ||||
|                                         )} | ||||
|                                       </p> | ||||
|                                       <p> | ||||
|                                         <mwc-button @click=${this._scanQRCode}> | ||||
|                                           ${this.hass.localize( | ||||
|                                             "ui.panel.config.zwave_js.add_node.scan_qr_code" | ||||
|                                           )} | ||||
|                                         </mwc-button> | ||||
|                                       </p> | ||||
|                                     </div>` | ||||
|                                   : ""} | ||||
|                               </div> | ||||
|                               <mwc-button | ||||
|                                 slot="primaryAction" | ||||
|                                 @click=${this.closeDialog} | ||||
|                               > | ||||
|                                 ${this.hass.localize("ui.common.close")} | ||||
|                                 ${this.hass.localize("ui.common.cancel")} | ||||
|                               </mwc-button> | ||||
|                             ` | ||||
|                           : this._status === "failed" | ||||
|                           : this._status === "interviewing" | ||||
|                             ? html` | ||||
|                                 <div class="flex-container"> | ||||
|                                   <ha-circular-progress | ||||
|                                     indeterminate | ||||
|                                   ></ha-circular-progress> | ||||
|                                   <div class="status"> | ||||
|                                     <ha-alert | ||||
|                                       alert-type="error" | ||||
|                                       .title=${this.hass.localize( | ||||
|                                         "ui.panel.config.zwave_js.add_node.inclusion_failed" | ||||
|                                       )} | ||||
|                                     > | ||||
|                                       ${this._error || | ||||
|                                       this.hass.localize( | ||||
|                                         "ui.panel.config.zwave_js.add_node.check_logs" | ||||
|                                       )} | ||||
|                                     </ha-alert> | ||||
|                                     <p> | ||||
|                                       <b | ||||
|                                         >${this.hass.localize( | ||||
|                                           "ui.panel.config.zwave_js.add_node.interview_started" | ||||
|                                         )}</b | ||||
|                                       > | ||||
|                                     </p> | ||||
|                                     ${this._stages | ||||
|                                       ? html` <div class="stages"> | ||||
|                                           ${this._stages.map( | ||||
| @@ -441,45 +430,21 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|                                   ${this.hass.localize("ui.common.close")} | ||||
|                                 </mwc-button> | ||||
|                               ` | ||||
|                             : this._status === "finished" | ||||
|                             : this._status === "failed" | ||||
|                               ? html` | ||||
|                                   <div class="flex-container"> | ||||
|                                     <ha-svg-icon | ||||
|                                       .path=${this._lowSecurity | ||||
|                                         ? mdiAlertCircle | ||||
|                                         : mdiCheckCircle} | ||||
|                                       class=${this._lowSecurity | ||||
|                                         ? "warning" | ||||
|                                         : "success"} | ||||
|                                     ></ha-svg-icon> | ||||
|                                     <div class="status"> | ||||
|                                       <p> | ||||
|                                         ${this.hass.localize( | ||||
|                                           "ui.panel.config.zwave_js.add_node.inclusion_finished" | ||||
|                                       <ha-alert | ||||
|                                         alert-type="error" | ||||
|                                         .title=${this.hass.localize( | ||||
|                                           "ui.panel.config.zwave_js.add_node.inclusion_failed" | ||||
|                                         )} | ||||
|                                       </p> | ||||
|                                       ${this._lowSecurity | ||||
|                                         ? html`<ha-alert | ||||
|                                             alert-type="warning" | ||||
|                                             title="The device was added insecurely" | ||||
|                                           > | ||||
|                                             There was an error during secure | ||||
|                                             inclusion. You can try again by | ||||
|                                             excluding the device and adding it | ||||
|                                             again. | ||||
|                                           </ha-alert>` | ||||
|                                         : ""} | ||||
|                                       <a | ||||
|                                         href=${`/config/devices/device/${ | ||||
|                                           this._device!.id | ||||
|                                         }`} | ||||
|                                       > | ||||
|                                         <mwc-button> | ||||
|                                           ${this.hass.localize( | ||||
|                                             "ui.panel.config.zwave_js.add_node.view_device" | ||||
|                                           )} | ||||
|                                         </mwc-button> | ||||
|                                       </a> | ||||
|                                         ${this._error || | ||||
|                                         this.hass.localize( | ||||
|                                           "ui.panel.config.zwave_js.add_node.check_logs" | ||||
|                                         )} | ||||
|                                       </ha-alert> | ||||
|                                       ${this._stages | ||||
|                                         ? html` <div class="stages"> | ||||
|                                             ${this._stages.map( | ||||
| @@ -504,18 +469,60 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|                                     ${this.hass.localize("ui.common.close")} | ||||
|                                   </mwc-button> | ||||
|                                 ` | ||||
|                               : this._status === "provisioned" | ||||
|                                 ? html` <div class="flex-container"> | ||||
|                               : this._status === "finished" | ||||
|                                 ? html` | ||||
|                                     <div class="flex-container"> | ||||
|                                       <ha-svg-icon | ||||
|                                         .path=${mdiCheckCircle} | ||||
|                                         class="success" | ||||
|                                         .path=${this._lowSecurity | ||||
|                                           ? mdiAlertCircle | ||||
|                                           : mdiCheckCircle} | ||||
|                                         class=${this._lowSecurity | ||||
|                                           ? "warning" | ||||
|                                           : "success"} | ||||
|                                       ></ha-svg-icon> | ||||
|                                       <div class="status"> | ||||
|                                         <p> | ||||
|                                           ${this.hass.localize( | ||||
|                                             "ui.panel.config.zwave_js.add_node.provisioning_finished" | ||||
|                                             "ui.panel.config.zwave_js.add_node.inclusion_finished" | ||||
|                                           )} | ||||
|                                         </p> | ||||
|                                         ${this._lowSecurity | ||||
|                                           ? html`<ha-alert | ||||
|                                               alert-type="warning" | ||||
|                                               title="The device was added insecurely" | ||||
|                                             > | ||||
|                                               There was an error during secure | ||||
|                                               inclusion. You can try again by | ||||
|                                               excluding the device and adding it | ||||
|                                               again. | ||||
|                                             </ha-alert>` | ||||
|                                           : ""} | ||||
|                                         <a | ||||
|                                           href=${`/config/devices/device/${ | ||||
|                                             this._device?.id | ||||
|                                           }`} | ||||
|                                         > | ||||
|                                           <mwc-button> | ||||
|                                             ${this.hass.localize( | ||||
|                                               "ui.panel.config.zwave_js.add_node.view_device" | ||||
|                                             )} | ||||
|                                           </mwc-button> | ||||
|                                         </a> | ||||
|                                         ${this._stages | ||||
|                                           ? html` <div class="stages"> | ||||
|                                               ${this._stages.map( | ||||
|                                                 (stage) => html` | ||||
|                                                   <span class="stage"> | ||||
|                                                     <ha-svg-icon | ||||
|                                                       .path=${mdiCheckCircle} | ||||
|                                                       class="success" | ||||
|                                                     ></ha-svg-icon> | ||||
|                                                     ${stage} | ||||
|                                                   </span> | ||||
|                                                 ` | ||||
|                                               )} | ||||
|                                             </div>` | ||||
|                                           : ""} | ||||
|                                       </div> | ||||
|                                     </div> | ||||
|                                     <mwc-button | ||||
| @@ -523,12 +530,42 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|                                       @click=${this.closeDialog} | ||||
|                                     > | ||||
|                                       ${this.hass.localize("ui.common.close")} | ||||
|                                     </mwc-button>` | ||||
|                                 : ""} | ||||
|                                     </mwc-button> | ||||
|                                   ` | ||||
|                                 : this._status === "provisioned" | ||||
|                                   ? html` <div class="flex-container"> | ||||
|                                         <ha-svg-icon | ||||
|                                           .path=${mdiCheckCircle} | ||||
|                                           class="success" | ||||
|                                         ></ha-svg-icon> | ||||
|                                         <div class="status"> | ||||
|                                           <p> | ||||
|                                             ${this.hass.localize( | ||||
|                                               "ui.panel.config.zwave_js.add_node.provisioning_finished" | ||||
|                                             )} | ||||
|                                           </p> | ||||
|                                         </div> | ||||
|                                       </div> | ||||
|                                       <mwc-button | ||||
|                                         slot="primaryAction" | ||||
|                                         @click=${this.closeDialog} | ||||
|                                       > | ||||
|                                         ${this.hass.localize("ui.common.close")} | ||||
|                                       </mwc-button>` | ||||
|                                   : ""} | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _shouldPreventClose(): boolean { | ||||
|     return ( | ||||
|       this._status === "started_specific" || | ||||
|       this._status === "validate_dsk_enter_pin" || | ||||
|       this._status === "grant_security_classes" || | ||||
|       this._status === "waiting_for_device" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private _chooseInclusionStrategy(): void { | ||||
|     this._unsubscribe(); | ||||
|     this._status = "choose_strategy"; | ||||
| @@ -639,7 +676,7 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _validateDskAndEnterPin(): Promise<void> { | ||||
|     this._status = "loading"; | ||||
|     this._status = "waiting_for_device"; | ||||
|     this._error = undefined; | ||||
|     try { | ||||
|       await zwaveValidateDskAndEnterPin( | ||||
| @@ -656,7 +693,7 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _grantSecurityClasses(): Promise<void> { | ||||
|     this._status = "loading"; | ||||
|     this._status = "waiting_for_device"; | ||||
|     this._error = undefined; | ||||
|     try { | ||||
|       await zwaveGrantSecurityClasses( | ||||
| @@ -719,6 +756,12 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|           this._addNodeTimeoutHandle = undefined; | ||||
|         } | ||||
|  | ||||
|         if (message.event === "node found") { | ||||
|           // The user may have to enter a PIN. Until then prevent accidentally | ||||
|           // closing the dialog | ||||
|           this._status = "waiting_for_device"; | ||||
|         } | ||||
|  | ||||
|         if (message.event === "validate dsk and enter pin") { | ||||
|           this._status = "validate_dsk_enter_pin"; | ||||
|           this._dsk = message.dsk; | ||||
| @@ -775,6 +818,13 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|     }, 90000); | ||||
|   } | ||||
|  | ||||
|   private _onBeforeUnload = (event: BeforeUnloadEvent) => { | ||||
|     if (this._shouldPreventClose()) { | ||||
|       event.preventDefault(); | ||||
|     } | ||||
|     event.returnValue = true; | ||||
|   }; | ||||
|  | ||||
|   private _unsubscribe(): void { | ||||
|     if (this._subscribed) { | ||||
|       this._subscribed.then((unsub) => unsub()); | ||||
| @@ -791,6 +841,7 @@ class DialogZWaveJSAddNode extends LitElement { | ||||
|       clearTimeout(this._addNodeTimeoutHandle); | ||||
|     } | ||||
|     this._addNodeTimeoutHandle = undefined; | ||||
|     window.removeEventListener("beforeunload", this._onBeforeUnload); | ||||
|   } | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|   | ||||
| @@ -2,17 +2,21 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import { | ||||
|   mdiCheck, | ||||
|   mdiCheckCircleOutline, | ||||
|   mdiDelete, | ||||
|   mdiDotsVertical, | ||||
|   mdiOpenInNew, | ||||
|   mdiPencil, | ||||
|   mdiPlus, | ||||
|   mdiStar, | ||||
| } from "@mdi/js"; | ||||
| import { LitElement, PropertyValues, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import memoize from "memoize-one"; | ||||
| import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
| import { navigate } from "../../../../common/navigate"; | ||||
| import { stringCompare } from "../../../../common/string/compare"; | ||||
| import { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
| @@ -22,6 +26,9 @@ import "../../../../components/ha-clickable-list-item"; | ||||
| import "../../../../components/ha-fab"; | ||||
| import "../../../../components/ha-icon"; | ||||
| import "../../../../components/ha-icon-button"; | ||||
| import "../../../../components/ha-menu"; | ||||
| import type { HaMenu } from "../../../../components/ha-menu"; | ||||
| import "../../../../components/ha-menu-item"; | ||||
| import "../../../../components/ha-svg-icon"; | ||||
| import { LovelacePanelConfig } from "../../../../data/lovelace"; | ||||
| import { | ||||
| @@ -41,13 +48,11 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog- | ||||
| import "../../../../layouts/hass-loading-screen"; | ||||
| import "../../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { HomeAssistant, Route } from "../../../../types"; | ||||
| import { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import { getLovelaceStrategy } from "../../../lovelace/strategies/get-strategy"; | ||||
| import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboard"; | ||||
| import { lovelaceTabs } from "../ha-config-lovelace"; | ||||
| import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy"; | ||||
| import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
|  | ||||
| type DataTableItem = Pick< | ||||
|   LovelaceDashboard, | ||||
| @@ -85,6 +90,10 @@ export class HaConfigLovelaceDashboards extends LitElement { | ||||
|   }) | ||||
|   private _activeSorting?: SortingChangedEvent; | ||||
|  | ||||
|   @state() private _overflowDashboard?: LovelaceDashboard; | ||||
|  | ||||
|   @query("#overflow-menu") private _overflowMenu!: HaMenu; | ||||
|  | ||||
|   public willUpdate() { | ||||
|     if (!this.hasUpdated) { | ||||
|       this.hass.loadFragmentTranslation("lovelace"); | ||||
| @@ -210,40 +219,36 @@ export class HaConfigLovelaceDashboards extends LitElement { | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       columns.url_path = { | ||||
|       columns.actions = { | ||||
|         title: "", | ||||
|         label: localize( | ||||
|           "ui.panel.config.lovelace.dashboards.picker.headers.url" | ||||
|         ), | ||||
|         filterable: true, | ||||
|         width: "100px", | ||||
|         template: (dashboard) => | ||||
|           narrow | ||||
|             ? html` | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiOpenInNew} | ||||
|                   .urlPath=${dashboard.url_path} | ||||
|                   @click=${this._navigate} | ||||
|                   .label=${this.hass.localize( | ||||
|                     "ui.panel.config.lovelace.dashboards.picker.open" | ||||
|                   )} | ||||
|                 ></ha-icon-button> | ||||
|               ` | ||||
|             : html` | ||||
|                 <mwc-button | ||||
|                   .urlPath=${dashboard.url_path} | ||||
|                   @click=${this._navigate} | ||||
|                   >${this.hass.localize( | ||||
|                     "ui.panel.config.lovelace.dashboards.picker.open" | ||||
|                   )}</mwc-button | ||||
|                 > | ||||
|               `, | ||||
|         width: "64px", | ||||
|         type: "icon-button", | ||||
|         template: (dashboard) => html` | ||||
|           <ha-icon-button | ||||
|             .dashboard=${dashboard} | ||||
|             .label=${this.hass.localize("ui.common.overflow_menu")} | ||||
|             .path=${mdiDotsVertical} | ||||
|             @click=${this._showOverflowMenu} | ||||
|           ></ha-icon-button> | ||||
|         `, | ||||
|       }; | ||||
|  | ||||
|       return columns; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private _showOverflowMenu = (ev) => { | ||||
|     if ( | ||||
|       this._overflowMenu.open && | ||||
|       ev.target === this._overflowMenu.anchorElement | ||||
|     ) { | ||||
|       this._overflowMenu.close(); | ||||
|       return; | ||||
|     } | ||||
|     this._overflowDashboard = ev.target.dashboard; | ||||
|     this._overflowMenu.anchorElement = ev.target; | ||||
|     this._overflowMenu.show(); | ||||
|   }; | ||||
|  | ||||
|   private _getItems = memoize((dashboards: LovelaceDashboard[]) => { | ||||
|     const defaultMode = ( | ||||
|       this.hass.panels?.lovelace?.config as LovelacePanelConfig | ||||
| @@ -314,7 +319,7 @@ export class HaConfigLovelaceDashboards extends LitElement { | ||||
|         @sorting-changed=${this._handleSortingChanged} | ||||
|         .filter=${this._filter} | ||||
|         @search-changed=${this._handleSearchChange} | ||||
|         @row-click=${this._editDashboard} | ||||
|         @row-click=${this._navigate} | ||||
|         id="url_path" | ||||
|         hasFab | ||||
|         clickable | ||||
| @@ -346,6 +351,22 @@ export class HaConfigLovelaceDashboards extends LitElement { | ||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||
|         </ha-fab> | ||||
|       </hass-tabs-subpage-data-table> | ||||
|       <ha-menu id="overflow-menu" positioning="fixed"> | ||||
|         <ha-menu-item @click=${this._editDashboard}> | ||||
|           <ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon> | ||||
|           <div slot="headline">Edit</div> | ||||
|         </ha-menu-item> | ||||
|  | ||||
|         <ha-menu-item> | ||||
|           <ha-svg-icon .path=${mdiStar} slot="start"></ha-svg-icon> | ||||
|           <div slot="headline">Set to default</div> | ||||
|         </ha-menu-item> | ||||
|         <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|         <ha-menu-item class="warning"> | ||||
|           <ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon> | ||||
|           <div slot="headline">Delete</div> | ||||
|         </ha-menu-item> | ||||
|       </ha-menu> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -358,21 +379,23 @@ export class HaConfigLovelaceDashboards extends LitElement { | ||||
|     this._dashboards = await fetchDashboards(this.hass); | ||||
|   } | ||||
|  | ||||
|   private _navigate(ev: Event) { | ||||
|     ev.stopPropagation(); | ||||
|     navigate(`/${(ev.target as any).urlPath}`); | ||||
|   private _navigate(ev: CustomEvent) { | ||||
|     const urlPath = (ev.detail as RowClickedEvent).id; | ||||
|     navigate(`/${urlPath}`); | ||||
|   } | ||||
|  | ||||
|   private _editDashboard(ev: CustomEvent) { | ||||
|     const urlPath = (ev.detail as RowClickedEvent).id; | ||||
|   private _editDashboard = (ev) => { | ||||
|     ev.stopPropagation(); | ||||
|     const dashboard = ev.currentTarget.parentElement.anchorElement.automation; | ||||
|  | ||||
|     const urlPath = (ev.currentTarget as any).urlPath; | ||||
|  | ||||
|     if (urlPath === "energy") { | ||||
|       navigate("/config/energy"); | ||||
|       return; | ||||
|     } | ||||
|     const dashboard = this._dashboards.find((res) => res.url_path === urlPath); | ||||
|     this._openDetailDialog(dashboard, urlPath); | ||||
|   } | ||||
|   }; | ||||
|  | ||||
|   private async _addDashboard() { | ||||
|     showNewDashboardDialog(this, { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import "../../../components/ha-formfield"; | ||||
| import "../../../components/ha-picture-upload"; | ||||
| import type { HaPictureUpload } from "../../../components/ha-picture-upload"; | ||||
| import "../../../components/ha-textfield"; | ||||
| import { adminChangeUsername } from "../../../data/auth"; | ||||
| import { PersonMutableParams } from "../../../data/person"; | ||||
| import { | ||||
|   deleteUser, | ||||
| @@ -19,10 +20,11 @@ import { | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
|   showPromptDialog, | ||||
| } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; | ||||
| import { ValueChangedEvent, HomeAssistant } from "../../../types"; | ||||
| import { haStyleDialog } from "../../../resources/styles"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../../../types"; | ||||
| import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { showAddUserDialog } from "../users/show-dialog-add-user"; | ||||
| import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password"; | ||||
| @@ -136,9 +138,9 @@ class DialogPersonDetail extends LitElement { | ||||
|             ></ha-picture-upload> | ||||
|  | ||||
|             <ha-formfield | ||||
|               .label=${this.hass!.localize( | ||||
|               .label=${`${this.hass!.localize( | ||||
|                 "ui.panel.config.person.detail.allow_login" | ||||
|               )} | ||||
|               )}${this._user ? ` (${this._user.username})` : ""}`} | ||||
|             > | ||||
|               <ha-switch | ||||
|                 @change=${this._allowLoginChanged} | ||||
| @@ -244,13 +246,21 @@ class DialogPersonDetail extends LitElement { | ||||
|               </mwc-button> | ||||
|               ${this._user && this.hass.user?.is_owner | ||||
|                 ? html`<mwc-button | ||||
|                     slot="secondaryAction" | ||||
|                     @click=${this._changePassword} | ||||
|                   > | ||||
|                     ${this.hass.localize( | ||||
|                       "ui.panel.config.users.editor.change_password" | ||||
|                     )} | ||||
|                   </mwc-button>` | ||||
|                       slot="secondaryAction" | ||||
|                       @click=${this._changeUsername} | ||||
|                     > | ||||
|                       ${this.hass.localize( | ||||
|                         "ui.panel.config.users.editor.change_username" | ||||
|                       )} | ||||
|                     </mwc-button> | ||||
|                     <mwc-button | ||||
|                       slot="secondaryAction" | ||||
|                       @click=${this._changePassword} | ||||
|                     > | ||||
|                       ${this.hass.localize( | ||||
|                         "ui.panel.config.users.editor.change_password" | ||||
|                       )} | ||||
|                     </mwc-button>` | ||||
|                 : ""} | ||||
|             ` | ||||
|           : nothing} | ||||
| @@ -292,11 +302,14 @@ class DialogPersonDetail extends LitElement { | ||||
|         userAddedCallback: async (user?: User) => { | ||||
|           if (user) { | ||||
|             target.checked = true; | ||||
|             if (this._params!.entry) { | ||||
|               await this._params!.updateEntry({ user_id: user.id }); | ||||
|             } | ||||
|             this._params?.refreshUsers(); | ||||
|             this._user = user; | ||||
|             this._userId = user.id; | ||||
|             this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); | ||||
|             this._localOnly = user.local_only; | ||||
|             this._params?.refreshUsers(); | ||||
|           } | ||||
|         }, | ||||
|         name: this._name, | ||||
| @@ -320,6 +333,9 @@ class DialogPersonDetail extends LitElement { | ||||
|       await deleteUser(this.hass, this._userId); | ||||
|       this._params?.refreshUsers(); | ||||
|       this._userId = undefined; | ||||
|       this._user = undefined; | ||||
|       this._isAdmin = undefined; | ||||
|       this._localOnly = undefined; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -349,6 +365,53 @@ class DialogPersonDetail extends LitElement { | ||||
|     showAdminChangePasswordDialog(this, { userId: this._user.id }); | ||||
|   } | ||||
|  | ||||
|   private async _changeUsername() { | ||||
|     if (!this._user) { | ||||
|       return; | ||||
|     } | ||||
|     const credential = this._user.credentials.find( | ||||
|       (cred) => cred.type === "homeassistant" | ||||
|     ); | ||||
|     if (!credential) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "No Home Assistant credentials found.", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const newUsername = await showPromptDialog(this, { | ||||
|       inputLabel: this.hass.localize( | ||||
|         "ui.panel.config.users.change_username.new_username" | ||||
|       ), | ||||
|       confirmText: this.hass.localize( | ||||
|         "ui.panel.config.users.change_username.change" | ||||
|       ), | ||||
|       title: this.hass.localize( | ||||
|         "ui.panel.config.users.change_username.caption" | ||||
|       ), | ||||
|       defaultValue: this._user.username!, | ||||
|     }); | ||||
|     if (newUsername) { | ||||
|       try { | ||||
|         await adminChangeUsername(this.hass, this._user.id, newUsername); | ||||
|         this._params?.refreshUsers(); | ||||
|         this._user = { ...this._user, username: newUsername }; | ||||
|         showAlertDialog(this, { | ||||
|           text: this.hass.localize( | ||||
|             "ui.panel.config.users.change_username.username_changed" | ||||
|           ), | ||||
|         }); | ||||
|       } catch (err: any) { | ||||
|         showAlertDialog(this, { | ||||
|           title: this.hass.localize( | ||||
|             "ui.panel.config.users.change_username.failed" | ||||
|           ), | ||||
|           text: err.message, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _updateEntry() { | ||||
|     this._submitting = true; | ||||
|     try { | ||||
|   | ||||
| @@ -31,8 +31,6 @@ class DialogIntegrationStartup extends LitElement { | ||||
|     return html` | ||||
|       <ha-dialog | ||||
|         open | ||||
|         scrimClickAction | ||||
|         escapeKeyAction | ||||
|         hideActions | ||||
|         .heading=${createCloseHeading( | ||||
|           this.hass, | ||||
|   | ||||
| @@ -143,8 +143,6 @@ class DialogSystemInformation extends LitElement { | ||||
|       <ha-dialog | ||||
|         open | ||||
|         @closed=${this.closeDialog} | ||||
|         scrimClickAction | ||||
|         escapeKeyAction | ||||
|         .heading=${createCloseHeading( | ||||
|           this.hass, | ||||
|           this.hass.localize("ui.panel.config.repairs.system_information") | ||||
|   | ||||
| @@ -10,12 +10,16 @@ import "../../../components/ha-label"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import "../../../components/ha-switch"; | ||||
| import "../../../components/ha-textfield"; | ||||
| import { adminChangeUsername } from "../../../data/auth"; | ||||
| import { | ||||
|   computeUserBadges, | ||||
|   SYSTEM_GROUP_ID_ADMIN, | ||||
|   SYSTEM_GROUP_ID_USER, | ||||
| } from "../../../data/user"; | ||||
| import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showPromptDialog, | ||||
| } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { haStyleDialog } from "../../../resources/styles"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password"; | ||||
| @@ -172,11 +176,15 @@ class DialogUserDetail extends LitElement { | ||||
|               ` | ||||
|             : ""} | ||||
|           ${!user.system_generated && this.hass.user?.is_owner | ||||
|             ? html`<mwc-button @click=${this._changePassword}> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.config.users.editor.change_password" | ||||
|                 )} | ||||
|               </mwc-button>` | ||||
|             ? html`<mwc-button @click=${this._changeUsername}> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.users.editor.change_username" | ||||
|                   )} </mwc-button | ||||
|                 ><mwc-button @click=${this._changePassword}> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.users.editor.change_password" | ||||
|                   )} | ||||
|                 </mwc-button>` | ||||
|             : ""} | ||||
|         </div> | ||||
|  | ||||
| @@ -250,6 +258,56 @@ class DialogUserDetail extends LitElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _changeUsername() { | ||||
|     const credential = this._params?.entry.credentials.find( | ||||
|       (cred) => cred.type === "homeassistant" | ||||
|     ); | ||||
|     if (!credential) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "No Home Assistant credentials found.", | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     const newUsername = await showPromptDialog(this, { | ||||
|       inputLabel: this.hass.localize( | ||||
|         "ui.panel.config.users.change_username.new_username" | ||||
|       ), | ||||
|       confirmText: this.hass.localize( | ||||
|         "ui.panel.config.users.change_username.change" | ||||
|       ), | ||||
|       title: this.hass.localize( | ||||
|         "ui.panel.config.users.change_username.caption" | ||||
|       ), | ||||
|       defaultValue: this._params!.entry.username!, | ||||
|     }); | ||||
|     if (newUsername) { | ||||
|       try { | ||||
|         await adminChangeUsername( | ||||
|           this.hass, | ||||
|           this._params!.entry.id, | ||||
|           newUsername | ||||
|         ); | ||||
|         this._params = { | ||||
|           ...this._params!, | ||||
|           entry: { ...this._params!.entry, username: newUsername }, | ||||
|         }; | ||||
|         this._params.replaceEntry(this._params.entry); | ||||
|         showAlertDialog(this, { | ||||
|           text: this.hass.localize( | ||||
|             "ui.panel.config.users.change_username.username_changed" | ||||
|           ), | ||||
|         }); | ||||
|       } catch (err: any) { | ||||
|         showAlertDialog(this, { | ||||
|           title: this.hass.localize( | ||||
|             "ui.panel.config.users.change_username.failed" | ||||
|           ), | ||||
|           text: err.message, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _changePassword() { | ||||
|     const credential = this._params?.entry.credentials.find( | ||||
|       (cred) => cred.type === "homeassistant" | ||||
|   | ||||
| @@ -182,7 +182,7 @@ export class HaConfigUsers extends LitElement { | ||||
|         .hass=${this.hass} | ||||
|         .narrow=${this.narrow} | ||||
|         .route=${this.route} | ||||
|         backPath="/config" | ||||
|         back-path="/config" | ||||
|         .tabs=${configSections.persons} | ||||
|         .columns=${this._columns(this.narrow, this.hass.localize)} | ||||
|         .data=${this._userData(this._users, this.hass.localize)} | ||||
| @@ -237,6 +237,11 @@ export class HaConfigUsers extends LitElement { | ||||
|  | ||||
|     showUserDetailDialog(this, { | ||||
|       entry, | ||||
|       replaceEntry: (newEntry: User) => { | ||||
|         this._users = this._users!.map((ent) => | ||||
|           ent.id === newEntry.id ? newEntry : ent | ||||
|         ); | ||||
|       }, | ||||
|       updateEntry: async (values) => { | ||||
|         const updated = await updateUser(this.hass!, entry!.id, values); | ||||
|         this._users = this._users!.map((ent) => | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { UpdateUserParams, User } from "../../../data/user"; | ||||
| export interface UserDetailDialogParams { | ||||
|   entry: User; | ||||
|   updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>; | ||||
|   replaceEntry: (entry: User) => void; | ||||
|   removeEntry: () => Promise<boolean>; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,7 @@ const SCHEMA = [ | ||||
|   { | ||||
|     name: "location", | ||||
|     required: true, | ||||
|     selector: { location: { radius: true, radius_readonly: true } }, | ||||
|     selector: { location: { radius: true } }, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @@ -35,6 +35,7 @@ class DialogHomeZoneDetail extends LitElement { | ||||
|     this._data = { | ||||
|       latitude: this.hass.config.latitude, | ||||
|       longitude: this.hass.config.longitude, | ||||
|       radius: this.hass.config.radius, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -73,11 +74,6 @@ class DialogHomeZoneDetail extends LitElement { | ||||
|             .computeLabel=${this._computeLabel} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></ha-form> | ||||
|           <p> | ||||
|             ${this.hass!.localize( | ||||
|               "ui.panel.config.zone.detail.no_edit_home_zone_radius" | ||||
|             )} | ||||
|           </p> | ||||
|         </div> | ||||
|         <mwc-button | ||||
|           slot="primaryAction" | ||||
| @@ -95,7 +91,7 @@ class DialogHomeZoneDetail extends LitElement { | ||||
|     location: { | ||||
|       latitude: data.latitude, | ||||
|       longitude: data.longitude, | ||||
|       radius: this.hass.states["zone.home"]?.attributes?.radius || 100, | ||||
|       radius: data.radius || 100, | ||||
|     }, | ||||
|   })); | ||||
|  | ||||
| @@ -104,6 +100,7 @@ class DialogHomeZoneDetail extends LitElement { | ||||
|     const value = { ...ev.detail.value }; | ||||
|     value.latitude = value.location.latitude; | ||||
|     value.longitude = value.location.longitude; | ||||
|     value.radius = value.location.radius; | ||||
|     delete value.location; | ||||
|     this._data = value; | ||||
|   } | ||||
|   | ||||
| @@ -101,7 +101,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { | ||||
|                 : zoneRadiusColor, | ||||
|           location_editable: | ||||
|             entityState.entity_id === "zone.home" && this._canEditCore, | ||||
|           radius_editable: false, | ||||
|           radius_editable: | ||||
|             entityState.entity_id === "zone.home" && this._canEditCore, | ||||
|         }) | ||||
|       ); | ||||
|       const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({ | ||||
| @@ -381,8 +382,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _radiusUpdated(ev: CustomEvent) { | ||||
|   private async _radiusUpdated(ev: CustomEvent) { | ||||
|     this._activeEntry = ev.detail.id; | ||||
|     if (ev.detail.id === "zone.home" && this._canEditCore) { | ||||
|       await saveCoreConfig(this.hass, { | ||||
|         radius: Math.round(ev.detail.radius), | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     const entry = this._storageItems!.find((item) => item.id === ev.detail.id); | ||||
|     if (!entry) { | ||||
|       return; | ||||
| @@ -478,6 +485,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { | ||||
|     await saveCoreConfig(this.hass, { | ||||
|       latitude: values.latitude, | ||||
|       longitude: values.longitude, | ||||
|       radius: values.radius, | ||||
|     }); | ||||
|     this._zoomZone("zone.home"); | ||||
|   } | ||||
|   | ||||
| @@ -282,6 +282,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { | ||||
|               { statistic_id: issue.data.statistic_id } | ||||
|             )}`, | ||||
|           confirmText: this.hass.localize("ui.common.delete"), | ||||
|           destructive: true, | ||||
|           confirm: async () => { | ||||
|             await clearStatistics(this.hass, [issue.data.statistic_id]); | ||||
|             this._deletedStatistics.add(issue.data.statistic_id); | ||||
| @@ -314,7 +315,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { | ||||
|         }); | ||||
|         break; | ||||
|       case "entity_no_longer_recorded": | ||||
|         showAlertDialog(this, { | ||||
|         showConfirmationDialog(this, { | ||||
|           title: this.hass.localize( | ||||
|             "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title" | ||||
|           ), | ||||
| @@ -335,7 +336,17 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { | ||||
|               ${this.hass.localize( | ||||
|                 "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link" | ||||
|               )}</a | ||||
|             >`, | ||||
|             ><br /><br /> | ||||
|             ${this.hass.localize( | ||||
|               "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4" | ||||
|             )}`, | ||||
|           confirmText: this.hass.localize("ui.common.delete"), | ||||
|           destructive: true, | ||||
|           confirm: async () => { | ||||
|             await clearStatistics(this.hass, [issue.data.statistic_id]); | ||||
|             this._deletedStatistics.add(issue.data.statistic_id); | ||||
|             this._validateStatistics(); | ||||
|           }, | ||||
|         }); | ||||
|         break; | ||||
|       case "unsupported_state_class": | ||||
| @@ -381,6 +392,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { | ||||
|               { statistic_id: issue.data.statistic_id } | ||||
|             )}`, | ||||
|           confirmText: this.hass.localize("ui.common.delete"), | ||||
|           destructive: true, | ||||
|           confirm: async () => { | ||||
|             await clearStatistics(this.hass, [issue.data.statistic_id]); | ||||
|             this._deletedStatistics.add(issue.data.statistic_id); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| export const filterModes = ( | ||||
|   supportedModes: string[] | undefined, | ||||
|   selectedModes: string[] | undefined | ||||
| ): string[] => | ||||
| export const filterModes = <T extends string = string>( | ||||
|   supportedModes: T[] | undefined, | ||||
|   selectedModes: T[] | undefined | ||||
| ): T[] => | ||||
|   selectedModes | ||||
|     ? selectedModes.filter((mode) => (supportedModes || []).includes(mode)) | ||||
|     : supportedModes || []; | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { stateColorCss } from "../../../common/entity/state_color"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import "../../../components/ha-control-button"; | ||||
| import "../../../components/ha-control-button-group"; | ||||
| import "../../../components/ha-control-select"; | ||||
| @@ -70,37 +69,18 @@ class HuiAlarmModeCardFeature | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _modes = memoizeOne( | ||||
|     ( | ||||
|       stateObj: AlarmControlPanelEntity, | ||||
|       selectedModes: AlarmMode[] | undefined | ||||
|     ) => { | ||||
|       if (!selectedModes) { | ||||
|         return []; | ||||
|       } | ||||
|  | ||||
|       return (Object.keys(ALARM_MODES) as AlarmMode[]).filter((mode) => { | ||||
|         const feature = ALARM_MODES[mode].feature; | ||||
|         return ( | ||||
|           (!feature || supportsFeature(stateObj, feature)) && | ||||
|           selectedModes.includes(mode) | ||||
|         ); | ||||
|       }); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private _getCurrentMode(stateObj: AlarmControlPanelEntity) { | ||||
|     return this._modes(stateObj, this._config?.modes).find( | ||||
|       (mode) => mode === stateObj.state | ||||
|     ); | ||||
|   } | ||||
|   private _getCurrentMode = memoizeOne((stateObj: AlarmControlPanelEntity) => { | ||||
|     const supportedModes = supportedAlarmModes(stateObj); | ||||
|     return supportedModes.find((mode) => mode === stateObj.state); | ||||
|   }); | ||||
|  | ||||
|   private async _valueChanged(ev: CustomEvent) { | ||||
|     if (!this.stateObj) return; | ||||
|     const mode = (ev.detail as any).value as AlarmMode; | ||||
|  | ||||
|     if (mode === this.stateObj!.state) return; | ||||
|     if (mode === this.stateObj.state) return; | ||||
|  | ||||
|     const oldMode = this._getCurrentMode(this.stateObj!); | ||||
|     const oldMode = this._getCurrentMode(this.stateObj); | ||||
|     this._currentMode = mode; | ||||
|  | ||||
|     try { | ||||
| @@ -153,6 +133,7 @@ class HuiAlarmModeCardFeature | ||||
|         </ha-control-button-group> | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="container"> | ||||
|         <ha-control-select | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; | ||||
| import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { LovelaceCard } from "../../types"; | ||||
| import type { EnergyGridGaugeCardConfig } from "../types"; | ||||
| import type { EnergyGridNeutralityGaugeCardConfig } from "../types"; | ||||
| import { hasConfigChanged } from "../../common/has-changed"; | ||||
|  | ||||
| const LEVELS: LevelDefinition[] = [ | ||||
| @@ -39,7 +39,7 @@ class HuiEnergyGridGaugeCard | ||||
| { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @state() private _config?: EnergyGridGaugeCardConfig; | ||||
|   @state() private _config?: EnergyGridNeutralityGaugeCardConfig; | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
| @@ -59,7 +59,7 @@ class HuiEnergyGridGaugeCard | ||||
|     return 4; | ||||
|   } | ||||
|  | ||||
|   public setConfig(config: EnergyGridGaugeCardConfig): void { | ||||
|   public setConfig(config: EnergyGridNeutralityGaugeCardConfig): void { | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { PropertyValues, ReactiveElement } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { MediaQueriesListener } from "../../../common/dom/media_query"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| @@ -10,23 +11,41 @@ import { | ||||
|   checkConditionsMet, | ||||
| } from "../common/validate-condition"; | ||||
| import { createCardElement } from "../create-element/create-card-element"; | ||||
| import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types"; | ||||
| import { createErrorCardConfig } from "../create-element/create-element-base"; | ||||
| import type { LovelaceCard, LovelaceLayoutOptions } from "../types"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
|     "card-visibility-changed": { value: boolean }; | ||||
|     "card-updated": undefined; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @customElement("hui-card") | ||||
| export class HuiCard extends ReactiveElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public lovelace?: Lovelace; | ||||
|   @property({ type: Boolean }) public preview = false; | ||||
|  | ||||
|   @property({ attribute: false }) public isPanel = false; | ||||
|   @property({ type: Boolean }) public isPanel = false; | ||||
|  | ||||
|   @state() public _config?: LovelaceCardConfig; | ||||
|   set config(config: LovelaceCardConfig | undefined) { | ||||
|     if (!config) return; | ||||
|     if (config.type !== this._config?.type) { | ||||
|       this._buildElement(config); | ||||
|     } else if (config !== this.config) { | ||||
|       this._element?.setConfig(config); | ||||
|       fireEvent(this, "card-updated"); | ||||
|     } | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public get config() { | ||||
|     return this._config; | ||||
|   } | ||||
|  | ||||
|   private _config?: LovelaceCardConfig; | ||||
|  | ||||
|   private _element?: LovelaceCard; | ||||
|  | ||||
| @@ -44,7 +63,7 @@ export class HuiCard extends ReactiveElement { | ||||
|   public connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     this._listenMediaQueries(); | ||||
|     this._updateElement(); | ||||
|     this._updateVisibility(); | ||||
|   } | ||||
|  | ||||
|   public getCardSize(): number | Promise<number> { | ||||
| @@ -56,7 +75,7 @@ export class HuiCard extends ReactiveElement { | ||||
|   } | ||||
|  | ||||
|   public getLayoutOptions(): LovelaceLayoutOptions { | ||||
|     const configOptions = this._config?.layout_options ?? {}; | ||||
|     const configOptions = this.config?.layout_options ?? {}; | ||||
|     if (this._element) { | ||||
|       const cardOptions = this._element.getLayoutOptions?.() ?? {}; | ||||
|       return { | ||||
| @@ -67,51 +86,84 @@ export class HuiCard extends ReactiveElement { | ||||
|     return configOptions; | ||||
|   } | ||||
|  | ||||
|   // Public to make demo happy | ||||
|   public createElement(config: LovelaceCardConfig) { | ||||
|     const element = createCardElement(config) as LovelaceCard; | ||||
|   public getElementLayoutOptions(): LovelaceLayoutOptions { | ||||
|     return this._element?.getLayoutOptions?.() ?? {}; | ||||
|   } | ||||
|  | ||||
|   private _createElement(config: LovelaceCardConfig) { | ||||
|     const element = createCardElement(config); | ||||
|     element.hass = this.hass; | ||||
|     element.editMode = this.lovelace?.editMode; | ||||
|     element.preview = this.preview; | ||||
|     // For backwards compatibility | ||||
|     (element as any).editMode = this.preview; | ||||
|     // Update element when the visibility of the card changes (e.g. conditional card or filter card) | ||||
|     element.addEventListener("card-visibility-changed", (ev) => { | ||||
|     element.addEventListener("card-visibility-changed", (ev: Event) => { | ||||
|       ev.stopPropagation(); | ||||
|       this._updateElement(); | ||||
|       this._updateVisibility(); | ||||
|     }); | ||||
|     element.addEventListener( | ||||
|       "ll-upgrade", | ||||
|       (ev: Event) => { | ||||
|         ev.stopPropagation(); | ||||
|         fireEvent(this, "card-updated"); | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev: Event) => { | ||||
|         ev.stopPropagation(); | ||||
|         this._buildElement(config); | ||||
|         fireEvent(this, "card-updated"); | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   public setConfig(config: LovelaceCardConfig): void { | ||||
|     if (this._config === config) { | ||||
|       return; | ||||
|     } | ||||
|     this._config = config; | ||||
|     this._element = this.createElement(config); | ||||
|   private _buildElement(config: LovelaceCardConfig) { | ||||
|     this._element = this._createElement(config); | ||||
|  | ||||
|     while (this.lastChild) { | ||||
|       this.removeChild(this.lastChild); | ||||
|     } | ||||
|     this.appendChild(this._element!); | ||||
|     this._updateVisibility(); | ||||
|   } | ||||
|  | ||||
|   protected update(changedProperties: PropertyValues<typeof this>) { | ||||
|     super.update(changedProperties); | ||||
|   protected update(changedProps: PropertyValues<typeof this>) { | ||||
|     super.update(changedProps); | ||||
|  | ||||
|     if (this._element) { | ||||
|       if (changedProperties.has("hass")) { | ||||
|         this._element.hass = this.hass; | ||||
|       if (changedProps.has("hass")) { | ||||
|         try { | ||||
|           this._element.hass = this.hass; | ||||
|         } catch (e: any) { | ||||
|           this._buildElement(createErrorCardConfig(e.message, null)); | ||||
|         } | ||||
|       } | ||||
|       if (changedProperties.has("lovelace")) { | ||||
|         this._element.editMode = this.lovelace?.editMode; | ||||
|       if (changedProps.has("preview")) { | ||||
|         try { | ||||
|           this._element.preview = this.preview; | ||||
|           // For backwards compatibility | ||||
|           (this._element as any).editMode = this.preview; | ||||
|         } catch (e: any) { | ||||
|           this._buildElement(createErrorCardConfig(e.message, null)); | ||||
|         } | ||||
|       } | ||||
|       if (changedProperties.has("hass") || changedProperties.has("lovelace")) { | ||||
|         this._updateElement(); | ||||
|       } | ||||
|       if (changedProperties.has("isPanel")) { | ||||
|       if (changedProps.has("isPanel")) { | ||||
|         this._element.isPanel = this.isPanel; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected willUpdate( | ||||
|     changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown> | ||||
|   ): void { | ||||
|     if (changedProps.has("hass") || changedProps.has("preview")) { | ||||
|       this._updateVisibility(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _clearMediaQueries() { | ||||
|     this._listeners.forEach((unsub) => unsub()); | ||||
|     this._listeners = []; | ||||
| @@ -119,42 +171,50 @@ export class HuiCard extends ReactiveElement { | ||||
|  | ||||
|   private _listenMediaQueries() { | ||||
|     this._clearMediaQueries(); | ||||
|     if (!this._config?.visibility) { | ||||
|     if (!this.config?.visibility) { | ||||
|       return; | ||||
|     } | ||||
|     const conditions = this._config.visibility; | ||||
|     const conditions = this.config.visibility; | ||||
|     const hasOnlyMediaQuery = | ||||
|       conditions.length === 1 && | ||||
|       conditions[0].condition === "screen" && | ||||
|       !!conditions[0].media_query; | ||||
|  | ||||
|     this._listeners = attachConditionMediaQueriesListeners( | ||||
|       this._config.visibility, | ||||
|       this.config.visibility, | ||||
|       (matches) => { | ||||
|         this._updateElement(hasOnlyMediaQuery && matches); | ||||
|         this._updateVisibility(hasOnlyMediaQuery && matches); | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private _updateElement(forceVisible?: boolean) { | ||||
|     if (!this._element) { | ||||
|   private _updateVisibility(forceVisible?: boolean) { | ||||
|     if (!this._element || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (this._element.hidden) { | ||||
|       this.style.setProperty("display", "none"); | ||||
|       this.toggleAttribute("hidden", true); | ||||
|       this._setElementVisibility(false); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const visible = | ||||
|       forceVisible || | ||||
|       this.lovelace?.editMode || | ||||
|       !this._config?.visibility || | ||||
|       checkConditionsMet(this._config.visibility, this.hass); | ||||
|       this.preview || | ||||
|       !this.config?.visibility || | ||||
|       checkConditionsMet(this.config.visibility, this.hass); | ||||
|     this._setElementVisibility(visible); | ||||
|   } | ||||
|  | ||||
|   private _setElementVisibility(visible: boolean) { | ||||
|     if (!this._element) return; | ||||
|  | ||||
|     if (this.hidden !== !visible) { | ||||
|       this.style.setProperty("display", visible ? "" : "none"); | ||||
|       this.toggleAttribute("hidden", !visible); | ||||
|       fireEvent(this, "card-visibility-changed", { value: visible }); | ||||
|     } | ||||
|  | ||||
|     this.style.setProperty("display", visible ? "" : "none"); | ||||
|     this.toggleAttribute("hidden", !visible); | ||||
|     if (!visible && this._element.parentElement) { | ||||
|       this.removeChild(this._element); | ||||
|     } else if (visible && !this._element.parentElement) { | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import { computeCardSize } from "../common/compute-card-size"; | ||||
| import { HuiConditionalBase } from "../components/hui-conditional-base"; | ||||
| import { createCardElement } from "../create-element/create-card-element"; | ||||
| import { LovelaceCard, LovelaceCardEditor } from "../types"; | ||||
| import { ConditionalCardConfig } from "./types"; | ||||
|  | ||||
| @@ -38,30 +37,15 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard { | ||||
|   } | ||||
|  | ||||
|   private _createCardElement(cardConfig: LovelaceCardConfig) { | ||||
|     const element = createCardElement(cardConfig) as LovelaceCard; | ||||
|     if (this.hass) { | ||||
|       element.hass = this.hass; | ||||
|     } | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev) => { | ||||
|         ev.stopPropagation(); | ||||
|         this._rebuildCard(cardConfig); | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     const element = document.createElement("hui-card"); | ||||
|     element.hass = this.hass; | ||||
|     element.preview = this.preview; | ||||
|     element.config = cardConfig; | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   private _rebuildCard(config: LovelaceCardConfig): void { | ||||
|     this._element = this._createCardElement(config); | ||||
|     if (this.lastChild) { | ||||
|       this.replaceChild(this._element, this.lastChild); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected setVisibility(conditionMet: boolean): void { | ||||
|     const visible = this.editMode || conditionMet; | ||||
|     const visible = this.preview || conditionMet; | ||||
|     const previouslyHidden = this.hidden; | ||||
|     super.setVisibility(conditionMet); | ||||
|     if (previouslyHidden !== this.hidden) { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { PropertyValues, ReactiveElement } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import { computeCardSize } from "../common/compute-card-size"; | ||||
| @@ -11,11 +12,10 @@ import { | ||||
|   checkConditionsMet, | ||||
|   extractConditionEntityIds, | ||||
| } from "../common/validate-condition"; | ||||
| import { createCardElement } from "../create-element/create-card-element"; | ||||
| import { EntityFilterEntityConfig } from "../entity-rows/types"; | ||||
| import { LovelaceCard } from "../types"; | ||||
| import { HuiCard } from "./hui-card"; | ||||
| import { EntityFilterCardConfig } from "./types"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
|  | ||||
| @customElement("hui-entity-filter-card") | ||||
| export class HuiEntityFilterCard | ||||
| @@ -55,11 +55,11 @@ export class HuiEntityFilterCard | ||||
|  | ||||
|   @property({ type: Boolean }) public isPanel = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public editMode = false; | ||||
|   @property({ type: Boolean }) public preview = false; | ||||
|  | ||||
|   @state() private _config?: EntityFilterCardConfig; | ||||
|  | ||||
|   private _element?: LovelaceCard; | ||||
|   private _element?: HuiCard; | ||||
|  | ||||
|   private _configEntities?: EntityFilterEntityConfig[]; | ||||
|  | ||||
| @@ -117,7 +117,7 @@ export class HuiEntityFilterCard | ||||
|   protected shouldUpdate(changedProps: PropertyValues): boolean { | ||||
|     if (this._element) { | ||||
|       this._element.hass = this.hass; | ||||
|       this._element.editMode = this.editMode; | ||||
|       this._element.preview = this.preview; | ||||
|       this._element.isPanel = this.isPanel; | ||||
|     } | ||||
|  | ||||
| @@ -173,12 +173,12 @@ export class HuiEntityFilterCard | ||||
|     } | ||||
|  | ||||
|     if (!this.lastChild) { | ||||
|       this._element.setConfig({ | ||||
|       this._element.config = { | ||||
|         ...this._baseCardConfig!, | ||||
|         entities: entitiesList, | ||||
|       }); | ||||
|       }; | ||||
|       this._oldEntities = entitiesList; | ||||
|     } else if (this._element.tagName !== "HUI-ERROR-CARD") { | ||||
|     } else { | ||||
|       const isSame = | ||||
|         this._oldEntities && | ||||
|         entitiesList.length === this._oldEntities.length && | ||||
| @@ -186,10 +186,10 @@ export class HuiEntityFilterCard | ||||
|  | ||||
|       if (!isSame) { | ||||
|         this._oldEntities = entitiesList; | ||||
|         this._element.setConfig({ | ||||
|         this._element.config = { | ||||
|           ...this._baseCardConfig!, | ||||
|           entities: entitiesList, | ||||
|         }); | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -245,33 +245,12 @@ export class HuiEntityFilterCard | ||||
|   } | ||||
|  | ||||
|   private _createCardElement(cardConfig: LovelaceCardConfig) { | ||||
|     const element = createCardElement(cardConfig) as LovelaceCard; | ||||
|     if (this.hass) { | ||||
|       element.hass = this.hass; | ||||
|     } | ||||
|     element.isPanel = this.isPanel; | ||||
|     element.editMode = this.editMode; | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev) => { | ||||
|         ev.stopPropagation(); | ||||
|         this._rebuildCard(element, cardConfig); | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     const element = document.createElement("hui-card"); | ||||
|     element.hass = this.hass; | ||||
|     element.preview = this.preview; | ||||
|     element.config = cardConfig; | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   private _rebuildCard( | ||||
|     cardElToReplace: LovelaceCard, | ||||
|     config: LovelaceCardConfig | ||||
|   ): void { | ||||
|     const newCardEl = this._createCardElement(config); | ||||
|     if (cardElToReplace.parentElement) { | ||||
|       cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace); | ||||
|     } | ||||
|     this._element = newCardEl; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { dump } from "js-yaml"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, state } from "lit/decorators"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "../../../components/ha-alert"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import { LovelaceCard } from "../types"; | ||||
| @@ -10,6 +10,8 @@ import { ErrorCardConfig } from "./types"; | ||||
| export class HuiErrorCard extends LitElement implements LovelaceCard { | ||||
|   public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public preview = false; | ||||
|  | ||||
|   @state() private _config?: ErrorCardConfig; | ||||
|  | ||||
|   public getCardSize(): number { | ||||
|   | ||||
| @@ -38,7 +38,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ type: Boolean }) public editMode = false; | ||||
|   @property({ type: Boolean }) public preview = false; | ||||
|  | ||||
|   @state() private _config?: MarkdownCardConfig; | ||||
|  | ||||
| @@ -163,12 +163,12 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { | ||||
|             user: this.hass.user!.name, | ||||
|           }, | ||||
|           strict: true, | ||||
|           report_errors: this.editMode, | ||||
|           report_errors: this.preview, | ||||
|         } | ||||
|       ); | ||||
|       await this._unsubRenderTemplate; | ||||
|     } catch (e: any) { | ||||
|       if (this.editMode) { | ||||
|       if (this.preview) { | ||||
|         this._error = e.message; | ||||
|         this._errorLevel = undefined; | ||||
|       } | ||||
|   | ||||
| @@ -1,19 +1,12 @@ | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeRTLDirection } from "../../../common/util/compute_rtl"; | ||||
| import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import { createCardElement } from "../create-element/create-card-element"; | ||||
| import { LovelaceCard, LovelaceCardEditor } from "../types"; | ||||
| import "./hui-card"; | ||||
| import type { HuiCard } from "./hui-card"; | ||||
| import { StackCardConfig } from "./types"; | ||||
| import { computeRTLDirection } from "../../../common/util/compute_rtl"; | ||||
|  | ||||
| export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | ||||
|   extends LitElement | ||||
| @@ -30,9 +23,9 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | ||||
|  | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ type: Boolean }) public editMode = false; | ||||
|   @property({ type: Boolean }) public preview = false; | ||||
|  | ||||
|   @state() protected _cards?: LovelaceCard[]; | ||||
|   @state() protected _cards?: HuiCard[]; | ||||
|  | ||||
|   @state() protected _config?: T; | ||||
|  | ||||
| @@ -49,30 +42,36 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | ||||
|     } | ||||
|     this._config = config; | ||||
|     this._cards = config.cards.map((card) => { | ||||
|       const element = this._createCardElement(card) as LovelaceCard; | ||||
|       const element = this._createCardElement(card); | ||||
|       return element; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|     if ( | ||||
|       !this._cards || | ||||
|       (!changedProps.has("hass") && !changedProps.has("editMode")) | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|   protected update(changedProperties) { | ||||
|     super.update(changedProperties); | ||||
|  | ||||
|     for (const element of this._cards) { | ||||
|       if (this.hass) { | ||||
|         element.hass = this.hass; | ||||
|     if (this._cards) { | ||||
|       if (changedProperties.has("hass")) { | ||||
|         this._cards.forEach((card) => { | ||||
|           card.hass = this.hass; | ||||
|         }); | ||||
|       } | ||||
|       if (this.editMode !== undefined) { | ||||
|         element.editMode = this.editMode; | ||||
|       if (changedProperties.has("editMode")) { | ||||
|         this._cards.forEach((card) => { | ||||
|           card.preview = this.preview; | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _createCardElement(cardConfig: LovelaceCardConfig) { | ||||
|     const element = document.createElement("hui-card"); | ||||
|     element.hass = this.hass; | ||||
|     element.preview = this.preview; | ||||
|     element.config = cardConfig; | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this._config || !this._cards) { | ||||
|       return nothing; | ||||
| @@ -110,34 +109,4 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _createCardElement(cardConfig: LovelaceCardConfig) { | ||||
|     const element = createCardElement(cardConfig) as LovelaceCard; | ||||
|     if (this.hass) { | ||||
|       element.hass = this.hass; | ||||
|     } | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev) => { | ||||
|         ev.stopPropagation(); | ||||
|         this._rebuildCard(element, cardConfig); | ||||
|         fireEvent(this, "ll-rebuild"); | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   private _rebuildCard( | ||||
|     cardElToReplace: LovelaceCard, | ||||
|     config: LovelaceCardConfig | ||||
|   ): void { | ||||
|     const newCardEl = this._createCardElement(config); | ||||
|     if (cardElToReplace.parentElement) { | ||||
|       cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace); | ||||
|     } | ||||
|     this._cards = this._cards!.map((curCardEl) => | ||||
|       curCardEl === cardElToReplace ? newCardEl : curCardEl | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -118,85 +118,74 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig { | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergySummaryCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergySummaryCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-summary"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergyDistributionCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-distribution"; | ||||
|   title?: string; | ||||
|   link_dashboard?: boolean; | ||||
|   collection_key?: string; | ||||
| } | ||||
| export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergyUsageGraphCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-usage-graph"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergySolarGraphCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergySolarGraphCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-solar-graph"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergyGasGraphCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergyGasGraphCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-gas-graph"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergyWaterGraphCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergyWaterGraphCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-water-graph"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-devices-graph"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
|   max_devices?: number; | ||||
| } | ||||
|  | ||||
| export interface EnergyDevicesDetailGraphCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergyDevicesDetailGraphCardConfig | ||||
|   extends EnergyCardBaseConfig { | ||||
|   type: "energy-devices-detail-graph"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
|   max_devices?: number; | ||||
| } | ||||
|  | ||||
| export interface EnergySourcesTableCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergySourcesTableCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-sources-table"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergySolarGaugeCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-solar-consumed-gauge"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergySelfSufficiencyGaugeCardConfig | ||||
|   extends LovelaceCardConfig { | ||||
|   extends EnergyCardBaseConfig { | ||||
|   type: "energy-self-sufficiency-gauge"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { | ||||
|   type: "energy-grid-result-gauge"; | ||||
| export interface EnergyGridNeutralityGaugeCardConfig | ||||
|   extends EnergyCardBaseConfig { | ||||
|   type: "energy-grid-neutrality-gauge"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig { | ||||
| export interface EnergyCarbonGaugeCardConfig extends EnergyCardBaseConfig { | ||||
|   type: "energy-carbon-consumed-gauge"; | ||||
|   title?: string; | ||||
|   collection_key?: string; | ||||
| } | ||||
|  | ||||
| export interface EntityFilterCardConfig extends LovelaceCardConfig { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { HassEntities, HassEntity } from "home-assistant-js-websocket"; | ||||
| import { SENSOR_ENTITIES } from "../../../common/const"; | ||||
| import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| @@ -35,17 +35,16 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types"; | ||||
| const HIDE_DOMAIN = new Set([ | ||||
|   "automation", | ||||
|   "configurator", | ||||
|   "conversation", | ||||
|   "device_tracker", | ||||
|   "event", | ||||
|   "geo_location", | ||||
|   "notify", | ||||
|   "persistent_notification", | ||||
|   "script", | ||||
|   "sun", | ||||
|   "zone", | ||||
|   "event", | ||||
|   "tts", | ||||
|   "stt", | ||||
|   "todo", | ||||
|   "zone", | ||||
|   ...ASSIST_ENTITIES, | ||||
| ]); | ||||
|  | ||||
| const HIDE_PLATFORM = new Set(["mobile_app"]); | ||||
|   | ||||
| @@ -353,7 +353,7 @@ export class HuiCardOptions extends LitElement { | ||||
|       allowDashboardChange: true, | ||||
|       header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"), | ||||
|       viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => { | ||||
|         const view = this.lovelace!.config.views[viewIndex]; | ||||
|         const view = selectedDashConfig.views[viewIndex]; | ||||
|  | ||||
|         if (!isStrategyView(view) && view.type === SECTION_VIEW_LAYOUT) { | ||||
|           showAlertDialog(this, { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { MediaQueriesListener } from "../../../common/dom/media_query"; | ||||
| import { deepEqual } from "../../../common/util/deep-equal"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import { HuiCard } from "../cards/hui-card"; | ||||
| import { ConditionalCardConfig } from "../cards/types"; | ||||
| import { | ||||
|   Condition, | ||||
| @@ -12,7 +13,6 @@ import { | ||||
|   validateConditionalConfig, | ||||
| } from "../common/validate-condition"; | ||||
| import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; | ||||
| import { LovelaceCard } from "../types"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -24,11 +24,11 @@ declare global { | ||||
| export class HuiConditionalBase extends ReactiveElement { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ type: Boolean }) public editMode = false; | ||||
|   @property({ type: Boolean }) public preview = false; | ||||
|  | ||||
|   @state() protected _config?: ConditionalCardConfig | ConditionalRowConfig; | ||||
|  | ||||
|   protected _element?: LovelaceCard | LovelaceRow; | ||||
|   protected _element?: HuiCard | LovelaceRow; | ||||
|  | ||||
|   private _listeners: MediaQueriesListener[] = []; | ||||
|  | ||||
| @@ -116,7 +116,7 @@ export class HuiConditionalBase extends ReactiveElement { | ||||
|       changed.has("_element") || | ||||
|       changed.has("_config") || | ||||
|       changed.has("hass") || | ||||
|       changed.has("editMode") | ||||
|       changed.has("preview") | ||||
|     ) { | ||||
|       this._listenMediaQueries(); | ||||
|       this._updateVisibility(); | ||||
| @@ -128,7 +128,7 @@ export class HuiConditionalBase extends ReactiveElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._element.editMode = this.editMode; | ||||
|     this._element.preview = this.preview; | ||||
|  | ||||
|     const conditionMet = checkConditionsMet( | ||||
|       this._config!.conditions, | ||||
| @@ -142,7 +142,7 @@ export class HuiConditionalBase extends ReactiveElement { | ||||
|     if (!this._element || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const visible = this.editMode || conditionMet; | ||||
|     const visible = this.preview || conditionMet; | ||||
|     if (this.hidden !== !visible) { | ||||
|       this.toggleAttribute("hidden", !visible); | ||||
|       this.style.setProperty("display", visible ? "" : "none"); | ||||
|   | ||||
| @@ -152,6 +152,7 @@ const _lazyCreate = <T extends keyof CreateElementConfigTypes>( | ||||
|   customElements.whenDefined(tag).then(() => { | ||||
|     try { | ||||
|       customElements.upgrade(element); | ||||
|       fireEvent(element, "ll-upgrade"); | ||||
|       // @ts-ignore | ||||
|       element.setConfig(config); | ||||
|     } catch (err: any) { | ||||
|   | ||||
							
								
								
									
										389
									
								
								src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										389
									
								
								src/panels/lovelace/editor/card-editor/ha-grid-layout-slider.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,389 @@ | ||||
| import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
|     "slider-moved": { value?: number }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const A11Y_KEY_CODES = new Set([ | ||||
|   "ArrowRight", | ||||
|   "ArrowUp", | ||||
|   "ArrowLeft", | ||||
|   "ArrowDown", | ||||
|   "PageUp", | ||||
|   "PageDown", | ||||
|   "Home", | ||||
|   "End", | ||||
| ]); | ||||
|  | ||||
| @customElement("ha-grid-layout-slider") | ||||
| export class HaGridLayoutSlider extends LitElement { | ||||
|   @property({ type: Boolean, reflect: true }) | ||||
|   public disabled = false; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) | ||||
|   public vertical = false; | ||||
|  | ||||
|   @property({ attribute: "touch-action" }) | ||||
|   public touchAction?: string; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public value?: number; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public step = 1; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public min = 1; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public max = 4; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public range?: number; | ||||
|  | ||||
|   @state() | ||||
|   public pressed = false; | ||||
|  | ||||
|   private _mc?: HammerManager; | ||||
|  | ||||
|   private get _range() { | ||||
|     return this.range ?? this.max; | ||||
|   } | ||||
|  | ||||
|   private _valueToPercentage(value: number) { | ||||
|     const percentage = this._boundedValue(value) / this._range; | ||||
|     return percentage; | ||||
|   } | ||||
|  | ||||
|   private _percentageToValue(percentage: number) { | ||||
|     return this._range * percentage; | ||||
|   } | ||||
|  | ||||
|   private _steppedValue(value: number) { | ||||
|     return Math.round(value / this.step) * this.step; | ||||
|   } | ||||
|  | ||||
|   private _boundedValue(value: number) { | ||||
|     return Math.min(Math.max(value, this.min), this.max); | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated(changedProperties: PropertyValues): void { | ||||
|     super.firstUpdated(changedProperties); | ||||
|     this.setupListeners(); | ||||
|     this.setAttribute("role", "slider"); | ||||
|     if (!this.hasAttribute("tabindex")) { | ||||
|       this.setAttribute("tabindex", "0"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|     if (changedProps.has("value")) { | ||||
|       const valuenow = this._steppedValue(this.value ?? 0); | ||||
|       this.setAttribute("aria-valuenow", valuenow.toString()); | ||||
|       this.setAttribute("aria-valuetext", valuenow.toString()); | ||||
|     } | ||||
|     if (changedProps.has("min")) { | ||||
|       this.setAttribute("aria-valuemin", this.min.toString()); | ||||
|     } | ||||
|     if (changedProps.has("max")) { | ||||
|       this.setAttribute("aria-valuemax", this.max.toString()); | ||||
|     } | ||||
|     if (changedProps.has("vertical")) { | ||||
|       const orientation = this.vertical ? "vertical" : "horizontal"; | ||||
|       this.setAttribute("aria-orientation", orientation); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   connectedCallback(): void { | ||||
|     super.connectedCallback(); | ||||
|     this.setupListeners(); | ||||
|   } | ||||
|  | ||||
|   disconnectedCallback(): void { | ||||
|     super.disconnectedCallback(); | ||||
|     this.destroyListeners(); | ||||
|   } | ||||
|  | ||||
|   @query("#slider") | ||||
|   private slider; | ||||
|  | ||||
|   setupListeners() { | ||||
|     if (this.slider && !this._mc) { | ||||
|       this._mc = new Manager(this.slider, { | ||||
|         touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"), | ||||
|       }); | ||||
|       this._mc.add( | ||||
|         new Pan({ | ||||
|           threshold: 10, | ||||
|           direction: DIRECTION_ALL, | ||||
|           enable: true, | ||||
|         }) | ||||
|       ); | ||||
|  | ||||
|       this._mc.add(new Tap({ event: "singletap" })); | ||||
|  | ||||
|       let savedValue; | ||||
|       this._mc.on("panstart", () => { | ||||
|         if (this.disabled) return; | ||||
|         this.pressed = true; | ||||
|         savedValue = this.value; | ||||
|       }); | ||||
|       this._mc.on("pancancel", () => { | ||||
|         if (this.disabled) return; | ||||
|         this.pressed = false; | ||||
|         this.value = savedValue; | ||||
|       }); | ||||
|       this._mc.on("panmove", (e) => { | ||||
|         if (this.disabled) return; | ||||
|         const percentage = this._getPercentageFromEvent(e); | ||||
|         this.value = this._percentageToValue(percentage); | ||||
|         const value = this._steppedValue(this._boundedValue(this.value)); | ||||
|         fireEvent(this, "slider-moved", { value }); | ||||
|       }); | ||||
|       this._mc.on("panend", (e) => { | ||||
|         if (this.disabled) return; | ||||
|         this.pressed = false; | ||||
|         const percentage = this._getPercentageFromEvent(e); | ||||
|         const value = this._percentageToValue(percentage); | ||||
|         this.value = this._steppedValue(this._boundedValue(value)); | ||||
|         fireEvent(this, "slider-moved", { value: undefined }); | ||||
|         fireEvent(this, "value-changed", { value: this.value }); | ||||
|       }); | ||||
|  | ||||
|       this._mc.on("singletap", (e) => { | ||||
|         if (this.disabled) return; | ||||
|         const percentage = this._getPercentageFromEvent(e); | ||||
|         const value = this._percentageToValue(percentage); | ||||
|         this.value = this._steppedValue(this._boundedValue(value)); | ||||
|         fireEvent(this, "value-changed", { value: this.value }); | ||||
|       }); | ||||
|  | ||||
|       this.addEventListener("keydown", this._handleKeyDown); | ||||
|       this.addEventListener("keyup", this._handleKeyUp); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   destroyListeners() { | ||||
|     if (this._mc) { | ||||
|       this._mc.destroy(); | ||||
|       this._mc = undefined; | ||||
|     } | ||||
|     this.removeEventListener("keydown", this._handleKeyDown); | ||||
|     this.removeEventListener("keyup", this._handleKeyUp); | ||||
|   } | ||||
|  | ||||
|   private get _tenPercentStep() { | ||||
|     return Math.max(this.step, (this.max - this.min) / 10); | ||||
|   } | ||||
|  | ||||
|   _handleKeyDown(e: KeyboardEvent) { | ||||
|     if (!A11Y_KEY_CODES.has(e.code)) return; | ||||
|     e.preventDefault(); | ||||
|     switch (e.code) { | ||||
|       case "ArrowRight": | ||||
|       case "ArrowUp": | ||||
|         this.value = this._boundedValue((this.value ?? 0) + this.step); | ||||
|         break; | ||||
|       case "ArrowLeft": | ||||
|       case "ArrowDown": | ||||
|         this.value = this._boundedValue((this.value ?? 0) - this.step); | ||||
|         break; | ||||
|       case "PageUp": | ||||
|         this.value = this._steppedValue( | ||||
|           this._boundedValue((this.value ?? 0) + this._tenPercentStep) | ||||
|         ); | ||||
|         break; | ||||
|       case "PageDown": | ||||
|         this.value = this._steppedValue( | ||||
|           this._boundedValue((this.value ?? 0) - this._tenPercentStep) | ||||
|         ); | ||||
|         break; | ||||
|       case "Home": | ||||
|         this.value = this.min; | ||||
|         break; | ||||
|       case "End": | ||||
|         this.value = this.max; | ||||
|         break; | ||||
|     } | ||||
|     fireEvent(this, "slider-moved", { value: this.value }); | ||||
|   } | ||||
|  | ||||
|   _handleKeyUp(e: KeyboardEvent) { | ||||
|     if (!A11Y_KEY_CODES.has(e.code)) return; | ||||
|     e.preventDefault(); | ||||
|     fireEvent(this, "value-changed", { value: this.value }); | ||||
|   } | ||||
|  | ||||
|   private _getPercentageFromEvent = (e: HammerInput) => { | ||||
|     if (this.vertical) { | ||||
|       const y = e.center.y; | ||||
|       const offset = e.target.getBoundingClientRect().top; | ||||
|       const total = e.target.clientHeight; | ||||
|       return Math.max(Math.min(1, (y - offset) / total), 0); | ||||
|     } | ||||
|     const x = e.center.x; | ||||
|     const offset = e.target.getBoundingClientRect().left; | ||||
|     const total = e.target.clientWidth; | ||||
|     return Math.max(Math.min(1, (x - offset) / total), 0); | ||||
|   }; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <div | ||||
|         class="container${classMap({ | ||||
|           pressed: this.pressed, | ||||
|         })}" | ||||
|         style=${styleMap({ | ||||
|           "--value": `${this._valueToPercentage(this.value ?? 0)}`, | ||||
|         })} | ||||
|       > | ||||
|         <div id="slider" class="slider"> | ||||
|           <div class="track"> | ||||
|             <div class="background"> | ||||
|               <div | ||||
|                 class="active" | ||||
|                 style=${styleMap({ | ||||
|                   "--min": `${this.min / this._range}`, | ||||
|                   "--max": `${1 - this.max / this._range}`, | ||||
|                 })} | ||||
|               ></div> | ||||
|             </div> | ||||
|           </div> | ||||
|           ${this.value !== undefined | ||||
|             ? html`<div class="handle"></div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|         --grid-layout-slider: 48px; | ||||
|         height: var(--grid-layout-slider); | ||||
|         width: 100%; | ||||
|         outline: none; | ||||
|         transition: box-shadow 180ms ease-in-out; | ||||
|       } | ||||
|       :host(:focus-visible) { | ||||
|         box-shadow: 0 0 0 2px var(--primary-color); | ||||
|       } | ||||
|       :host([vertical]) { | ||||
|         width: var(--grid-layout-slider); | ||||
|         height: 100%; | ||||
|       } | ||||
|       .container { | ||||
|         position: relative; | ||||
|         height: 100%; | ||||
|         width: 100%; | ||||
|       } | ||||
|       .slider { | ||||
|         position: relative; | ||||
|         height: 100%; | ||||
|         width: 100%; | ||||
|         transform: translateZ(0); | ||||
|         overflow: visible; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       .slider * { | ||||
|         pointer-events: none; | ||||
|       } | ||||
|       .track { | ||||
|         position: absolute; | ||||
|         inset: 0; | ||||
|         margin: auto; | ||||
|         height: 16px; | ||||
|         width: 100%; | ||||
|         border-radius: 8px; | ||||
|         overflow: hidden; | ||||
|       } | ||||
|       :host([vertical]) .track { | ||||
|         width: 16px; | ||||
|         height: 100%; | ||||
|       } | ||||
|       .background { | ||||
|         position: absolute; | ||||
|         inset: 0; | ||||
|         background: var(--disabled-color); | ||||
|         opacity: 0.5; | ||||
|       } | ||||
|       .active { | ||||
|         position: absolute; | ||||
|         background: grey; | ||||
|         top: 0; | ||||
|         right: calc(var(--max) * 100%); | ||||
|         bottom: 0; | ||||
|         left: calc(var(--min) * 100%); | ||||
|       } | ||||
|       :host([vertical]) .active { | ||||
|         top: calc(var(--min) * 100%); | ||||
|         right: 0; | ||||
|         bottom: calc(var(--max) * 100%); | ||||
|         left: 0; | ||||
|       } | ||||
|       .handle { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         height: 100%; | ||||
|         width: 16px; | ||||
|         transform: translate(-50%, 0); | ||||
|         background: var(--card-background-color); | ||||
|         left: calc(var(--value, 0%) * 100%); | ||||
|         transition: | ||||
|           left 180ms ease-in-out, | ||||
|           top 180ms ease-in-out; | ||||
|       } | ||||
|       :host([vertical]) .handle { | ||||
|         transform: translate(0, -50%); | ||||
|         left: 0; | ||||
|         top: calc(var(--value, 0%) * 100%); | ||||
|         height: 16px; | ||||
|         width: 100%; | ||||
|       } | ||||
|       .handle::after { | ||||
|         position: absolute; | ||||
|         inset: 0; | ||||
|         width: 4px; | ||||
|         border-radius: 2px; | ||||
|         height: 100%; | ||||
|         margin: auto; | ||||
|         background: grey; | ||||
|         content: ""; | ||||
|       } | ||||
|       :host([vertical]) .handle::after { | ||||
|         height: 4px; | ||||
|         width: 100%; | ||||
|       } | ||||
|       :host(:disabled) .slider { | ||||
|         cursor: not-allowed; | ||||
|       } | ||||
|       .pressed .handle { | ||||
|         transition: none; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-grid-layout-slider": HaGridLayoutSlider; | ||||
|   } | ||||
| } | ||||
| @@ -6,17 +6,21 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; | ||||
| import { getCardElementClass } from "../../create-element/create-card-element"; | ||||
| import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types"; | ||||
| import { HuiElementEditor } from "../hui-element-editor"; | ||||
| import "./hui-card-layout-editor"; | ||||
| import "./hui-card-visibility-editor"; | ||||
|  | ||||
| const TABS = ["config", "visibility"] as const; | ||||
| type Tab = "config" | "visibility" | "layout"; | ||||
|  | ||||
| @customElement("hui-card-element-editor") | ||||
| export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> { | ||||
|   @state() private _curTab: (typeof TABS)[number] = TABS[0]; | ||||
|   @state() private _curTab: Tab = "config"; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "show-visibility-tab" }) | ||||
|   public showVisibilityTab = false; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "show-layout-tab" }) | ||||
|   public showLayoutTab = false; | ||||
|  | ||||
|   protected async getConfigElement(): Promise<LovelaceCardEditor | undefined> { | ||||
|     const elClass = await getCardElementClass(this.configElementType!); | ||||
|  | ||||
| @@ -52,7 +56,11 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> { | ||||
|   } | ||||
|  | ||||
|   protected renderConfigElement(): TemplateResult { | ||||
|     if (!this.showVisibilityTab) return super.renderConfigElement(); | ||||
|     const displayedTabs: Tab[] = ["config"]; | ||||
|     if (this.showVisibilityTab) displayedTabs.push("visibility"); | ||||
|     if (this.showLayoutTab) displayedTabs.push("layout"); | ||||
|  | ||||
|     if (displayedTabs.length === 1) return super.renderConfigElement(); | ||||
|  | ||||
|     let content: TemplateResult<1> | typeof nothing = nothing; | ||||
|  | ||||
| @@ -69,19 +77,28 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> { | ||||
|           ></hui-card-visibility-editor> | ||||
|         `; | ||||
|         break; | ||||
|       case "layout": | ||||
|         content = html` | ||||
|           <hui-card-layout-editor | ||||
|             .hass=${this.hass} | ||||
|             .config=${this.value} | ||||
|             @value-changed=${this._configChanged} | ||||
|           > | ||||
|           </hui-card-layout-editor> | ||||
|         `; | ||||
|     } | ||||
|     return html` | ||||
|       <paper-tabs | ||||
|         scrollable | ||||
|         hide-scroll-buttons | ||||
|         .selected=${TABS.indexOf(this._curTab)} | ||||
|         .selected=${displayedTabs.indexOf(this._curTab)} | ||||
|         @selected-item-changed=${this._handleTabSelected} | ||||
|       > | ||||
|         ${TABS.map( | ||||
|         ${displayedTabs.map( | ||||
|           (tab, index) => html` | ||||
|             <paper-tab id=${tab} .dialogInitialFocus=${index === 0}> | ||||
|               ${this.hass.localize( | ||||
|                 `ui.panel.lovelace.editor.edit_card.tab-${tab}` | ||||
|                 `ui.panel.lovelace.editor.edit_card.tab_${tab}` | ||||
|               )} | ||||
|             </paper-tab> | ||||
|           ` | ||||
|   | ||||
							
								
								
									
										266
									
								
								src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,266 @@ | ||||
| import type { ActionDetail } from "@material/mwc-list"; | ||||
| import { mdiCheck, mdiDotsVertical } from "@mdi/js"; | ||||
| import { LitElement, PropertyValues, css, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { preventDefault } from "../../../../common/dom/prevent_default"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import "../../../../components/ha-button"; | ||||
| import "../../../../components/ha-button-menu"; | ||||
| import "../../../../components/ha-grid-size-picker"; | ||||
| import "../../../../components/ha-icon-button"; | ||||
| import "../../../../components/ha-list-item"; | ||||
| import "../../../../components/ha-slider"; | ||||
| import "../../../../components/ha-svg-icon"; | ||||
| import "../../../../components/ha-yaml-editor"; | ||||
| import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; | ||||
| import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; | ||||
| import { haStyle } from "../../../../resources/styles"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { HuiCard } from "../../cards/hui-card"; | ||||
| import { DEFAULT_GRID_OPTIONS } from "../../sections/hui-grid-section"; | ||||
| import { LovelaceLayoutOptions } from "../../types"; | ||||
|  | ||||
| @customElement("hui-card-layout-editor") | ||||
| export class HuiCardLayoutEditor extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public config!: LovelaceCardConfig; | ||||
|  | ||||
|   @state() _defaultLayoutOptions?: LovelaceLayoutOptions; | ||||
|  | ||||
|   @state() public _yamlMode = false; | ||||
|  | ||||
|   @state() public _uiAvailable = true; | ||||
|  | ||||
|   @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; | ||||
|  | ||||
|   private _cardElement?: HuiCard; | ||||
|  | ||||
|   private _gridSizeValue = memoizeOne( | ||||
|     ( | ||||
|       options?: LovelaceLayoutOptions, | ||||
|       defaultOptions?: LovelaceLayoutOptions | ||||
|     ) => ({ | ||||
|       rows: | ||||
|         options?.grid_rows ?? | ||||
|         defaultOptions?.grid_rows ?? | ||||
|         DEFAULT_GRID_OPTIONS.grid_rows, | ||||
|       columns: | ||||
|         options?.grid_columns ?? | ||||
|         defaultOptions?.grid_columns ?? | ||||
|         DEFAULT_GRID_OPTIONS.grid_columns, | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
|   private _isDefault = memoizeOne( | ||||
|     (options?: LovelaceLayoutOptions) => | ||||
|       options?.grid_columns === undefined && options?.grid_rows === undefined | ||||
|   ); | ||||
|  | ||||
|   render() { | ||||
|     return html` | ||||
|       <div class="header"> | ||||
|         <p class="intro"> | ||||
|           ${this.hass.localize( | ||||
|             `ui.panel.lovelace.editor.edit_card.layout.explanation` | ||||
|           )} | ||||
|         </p> | ||||
|         <ha-button-menu | ||||
|           slot="icons" | ||||
|           @action=${this._handleAction} | ||||
|           @click=${preventDefault} | ||||
|           @closed=${stopPropagation} | ||||
|           fixed | ||||
|           .corner=${"BOTTOM_END"} | ||||
|           .menuCorner=${"END"} | ||||
|         > | ||||
|           <ha-icon-button | ||||
|             slot="trigger" | ||||
|             .label=${this.hass.localize("ui.common.menu")} | ||||
|             .path=${mdiDotsVertical} | ||||
|           > | ||||
|           </ha-icon-button> | ||||
|  | ||||
|           <ha-list-item graphic="icon" .disabled=${!this._uiAvailable}> | ||||
|             ${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit_ui")} | ||||
|             ${!this._yamlMode | ||||
|               ? html` | ||||
|                   <ha-svg-icon | ||||
|                     class="selected_menu_item" | ||||
|                     slot="graphic" | ||||
|                     .path=${mdiCheck} | ||||
|                   ></ha-svg-icon> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           </ha-list-item> | ||||
|  | ||||
|           <ha-list-item graphic="icon"> | ||||
|             ${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.edit_card.edit_yaml" | ||||
|             )} | ||||
|             ${this._yamlMode | ||||
|               ? html` | ||||
|                   <ha-svg-icon | ||||
|                     class="selected_menu_item" | ||||
|                     slot="graphic" | ||||
|                     .path=${mdiCheck} | ||||
|                   ></ha-svg-icon> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           </ha-list-item> | ||||
|         </ha-button-menu> | ||||
|       </div> | ||||
|       ${this._yamlMode | ||||
|         ? html` | ||||
|             <ha-yaml-editor | ||||
|               .hass=${this.hass} | ||||
|               .defaultValue=${this.config.layout_options} | ||||
|               @value-changed=${this._valueChanged} | ||||
|             ></ha-yaml-editor> | ||||
|           ` | ||||
|         : html` | ||||
|             <ha-grid-size-picker | ||||
|               .hass=${this.hass} | ||||
|               .value=${this._gridSizeValue( | ||||
|                 this.config.layout_options, | ||||
|                 this._defaultLayoutOptions | ||||
|               )} | ||||
|               .isDefault=${this._isDefault(this.config.layout_options)} | ||||
|               @value-changed=${this._gridSizeChanged} | ||||
|             ></ha-grid-size-picker> | ||||
|           `} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated(changedProps: PropertyValues<this>): void { | ||||
|     super.firstUpdated(changedProps); | ||||
|     try { | ||||
|       this._cardElement = document.createElement("hui-card"); | ||||
|       this._cardElement.hass = this.hass; | ||||
|       this._cardElement.preview = true; | ||||
|       this._cardElement.config = this.config; | ||||
|       this._cardElement.addEventListener("card-updated", (ev: Event) => { | ||||
|         ev.stopPropagation(); | ||||
|         this._defaultLayoutOptions = | ||||
|           this._cardElement?.getElementLayoutOptions(); | ||||
|       }); | ||||
|       this._defaultLayoutOptions = this._cardElement.getElementLayoutOptions(); | ||||
|     } catch (err) { | ||||
|       // eslint-disable-next-line no-console | ||||
|       console.error(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues<this>): void { | ||||
|     super.updated(changedProps); | ||||
|     if (this._cardElement) { | ||||
|       if (changedProps.has("hass")) { | ||||
|         this._cardElement.hass = this.hass; | ||||
|       } | ||||
|       if (changedProps.has("config")) { | ||||
|         this._cardElement.config = this.config; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _handleAction(ev: CustomEvent<ActionDetail>) { | ||||
|     switch (ev.detail.index) { | ||||
|       case 0: | ||||
|         this._yamlMode = false; | ||||
|         break; | ||||
|       case 1: | ||||
|         this._yamlMode = true; | ||||
|         break; | ||||
|       case 2: | ||||
|         this._reset(); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _reset() { | ||||
|     const newConfig = { ...this.config }; | ||||
|     delete newConfig.layout_options; | ||||
|     this._yamlEditor?.setValue({}); | ||||
|     fireEvent(this, "value-changed", { value: newConfig }); | ||||
|   } | ||||
|  | ||||
|   private _gridSizeChanged(ev: CustomEvent): void { | ||||
|     ev.stopPropagation(); | ||||
|     const value = ev.detail.value; | ||||
|  | ||||
|     const newConfig: LovelaceCardConfig = { | ||||
|       ...this.config, | ||||
|       layout_options: { | ||||
|         ...this.config.layout_options, | ||||
|         grid_columns: value.columns, | ||||
|         grid_rows: value.rows, | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
|     if (newConfig.layout_options!.grid_columns === undefined) { | ||||
|       delete newConfig.layout_options!.grid_columns; | ||||
|     } | ||||
|     if (newConfig.layout_options!.grid_rows === undefined) { | ||||
|       delete newConfig.layout_options!.grid_rows; | ||||
|     } | ||||
|     if (Object.keys(newConfig.layout_options!).length === 0) { | ||||
|       delete newConfig.layout_options; | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "value-changed", { value: newConfig }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     ev.stopPropagation(); | ||||
|     const options = ev.detail.value as LovelaceLayoutOptions; | ||||
|     const newConfig: LovelaceCardConfig = { | ||||
|       ...this.config, | ||||
|       layout_options: options, | ||||
|     }; | ||||
|     fireEvent(this, "value-changed", { value: newConfig }); | ||||
|   } | ||||
|  | ||||
|   static styles = [ | ||||
|     haStyle, | ||||
|     css` | ||||
|       .header { | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: flex-start; | ||||
|       } | ||||
|       .header .intro { | ||||
|         flex: 1; | ||||
|         margin: 0; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|       .header ha-button-menu { | ||||
|         --mdc-theme-text-primary-on-background: var(--primary-text-color); | ||||
|         margin-top: -8px; | ||||
|       } | ||||
|       .selected_menu_item { | ||||
|         color: var(--primary-color); | ||||
|       } | ||||
|       .disabled { | ||||
|         opacity: 0.5; | ||||
|         pointer-events: none; | ||||
|       } | ||||
|       ha-grid-size-picker { | ||||
|         display: block; | ||||
|         max-width: 250px; | ||||
|         margin: 16px auto; | ||||
|       } | ||||
|       ha-yaml-editor { | ||||
|         display: block; | ||||
|         margin: 16px 0; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "hui-card-layout-editor": HuiCardLayoutEditor; | ||||
|   } | ||||
| } | ||||
| @@ -1,106 +0,0 @@ | ||||
| import { PropertyValues, ReactiveElement } from "lit"; | ||||
| import { property } from "lit/decorators"; | ||||
| import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { createCardElement } from "../../create-element/create-card-element"; | ||||
| import { createErrorCardConfig } from "../../create-element/create-element-base"; | ||||
| import { LovelaceCard } from "../../types"; | ||||
|  | ||||
| export class HuiCardPreview extends ReactiveElement { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public config?: LovelaceCardConfig; | ||||
|  | ||||
|   private _element?: LovelaceCard; | ||||
|  | ||||
|   private get _error() { | ||||
|     return this._element?.tagName === "HUI-ERROR-CARD"; | ||||
|   } | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.addEventListener("ll-rebuild", () => { | ||||
|       this._cleanup(); | ||||
|       if (this.config) { | ||||
|         this._createCard(this.config); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   protected createRenderRoot() { | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   protected update(changedProperties: PropertyValues) { | ||||
|     super.update(changedProperties); | ||||
|  | ||||
|     if (changedProperties.has("config")) { | ||||
|       const oldConfig = changedProperties.get("config") as | ||||
|         | undefined | ||||
|         | LovelaceCardConfig; | ||||
|  | ||||
|       if (!this.config) { | ||||
|         this._cleanup(); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!this.config.type) { | ||||
|         this._createCard( | ||||
|           createErrorCardConfig("No card type found", this.config) | ||||
|         ); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!this._element) { | ||||
|         this._createCard(this.config); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // in case the element was an error element we always want to recreate it | ||||
|       if (!this._error && oldConfig && this.config.type === oldConfig.type) { | ||||
|         try { | ||||
|           this._element.setConfig(this.config); | ||||
|         } catch (err: any) { | ||||
|           this._createCard(createErrorCardConfig(err.message, this.config)); | ||||
|         } | ||||
|       } else { | ||||
|         this._createCard(this.config); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (changedProperties.has("hass")) { | ||||
|       if (this._element) { | ||||
|         this._element.hass = this.hass; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _createCard(configValue: LovelaceCardConfig): void { | ||||
|     this._cleanup(); | ||||
|     this._element = createCardElement(configValue); | ||||
|  | ||||
|     this._element.editMode = true; | ||||
|  | ||||
|     if (this.hass) { | ||||
|       this._element!.hass = this.hass; | ||||
|     } | ||||
|  | ||||
|     this.appendChild(this._element!); | ||||
|   } | ||||
|  | ||||
|   private _cleanup() { | ||||
|     if (!this._element) { | ||||
|       return; | ||||
|     } | ||||
|     this.removeChild(this._element); | ||||
|     this._element = undefined; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "hui-card-preview": HuiCardPreview; | ||||
|   } | ||||
| } | ||||
|  | ||||
| customElements.define("hui-card-preview", HuiCardPreview); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { LitElement, html } from "lit"; | ||||
| import { LitElement, html, css } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/ha-alert"; | ||||
| @@ -16,11 +16,11 @@ export class HuiCardVisibilityEditor extends LitElement { | ||||
|   render() { | ||||
|     const conditions = this.config.visibility ?? []; | ||||
|     return html` | ||||
|       <ha-alert alert-type="info"> | ||||
|       <p class="intro"> | ||||
|         ${this.hass.localize( | ||||
|           `ui.panel.lovelace.editor.edit_card.visibility.explanation` | ||||
|         )} | ||||
|       </ha-alert> | ||||
|       </p> | ||||
|       <ha-card-conditions-editor | ||||
|         .hass=${this.hass} | ||||
|         .conditions=${conditions} | ||||
| @@ -42,6 +42,14 @@ export class HuiCardVisibilityEditor extends LitElement { | ||||
|     } | ||||
|     fireEvent(this, "value-changed", { value: newConfig }); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     .intro { | ||||
|       margin: 0; | ||||
|       color: var(--secondary-text-color); | ||||
|       margin-bottom: 8px; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; | ||||
| import { haStyleDialog } from "../../../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import "./hui-card-preview"; | ||||
| import "../../cards/hui-card"; | ||||
| import type { DeleteCardDialogParams } from "./show-delete-card-dialog"; | ||||
|  | ||||
| @customElement("hui-dialog-delete-card") | ||||
| @@ -45,10 +45,11 @@ export class HuiDialogDeleteCard extends LitElement { | ||||
|           ${this._cardConfig | ||||
|             ? html` | ||||
|                 <div class="element-preview"> | ||||
|                   <hui-card-preview | ||||
|                   <hui-card | ||||
|                     .hass=${this.hass} | ||||
|                     .config=${this._cardConfig} | ||||
|                   ></hui-card-preview> | ||||
|                     preview | ||||
|                   ></hui-card> | ||||
|                 </div> | ||||
|               ` | ||||
|             : ""} | ||||
| @@ -74,7 +75,7 @@ export class HuiDialogDeleteCard extends LitElement { | ||||
|         .element-preview { | ||||
|           position: relative; | ||||
|         } | ||||
|         hui-card-preview { | ||||
|         hui-card { | ||||
|           margin: 4px auto; | ||||
|           max-width: 500px; | ||||
|           display: block; | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import { | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { computeRTLDirection } from "../../../../common/util/compute_rtl"; | ||||
| @@ -29,6 +30,7 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; | ||||
| import { haStyleDialog } from "../../../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; | ||||
| import "../../sections/hui-section"; | ||||
| import { addCard, replaceCard } from "../config-util"; | ||||
| import { getCardDocumentationURL } from "../get-card-documentation-url"; | ||||
| import type { ConfigChangedEvent } from "../hui-element-editor"; | ||||
| @@ -36,7 +38,7 @@ import { findLovelaceContainer } from "../lovelace-path"; | ||||
| import type { GUIModeChangedEvent } from "../types"; | ||||
| import "./hui-card-element-editor"; | ||||
| import type { HuiCardElementEditor } from "./hui-card-element-editor"; | ||||
| import "./hui-card-preview"; | ||||
| import "../../cards/hui-card"; | ||||
| import type { EditCardDialogParams } from "./show-edit-card-dialog"; | ||||
|  | ||||
| declare global { | ||||
| @@ -234,6 +236,7 @@ export class HuiDialogEditCard | ||||
|         <div class="content"> | ||||
|           <div class="element-editor"> | ||||
|             <hui-card-element-editor | ||||
|               .showLayoutTab=${this._shouldShowLayoutTab()} | ||||
|               .showVisibilityTab=${this._cardConfig?.type !== "conditional"} | ||||
|               .hass=${this.hass} | ||||
|               .lovelace=${this._params.lovelaceConfig} | ||||
| @@ -245,11 +248,23 @@ export class HuiDialogEditCard | ||||
|             ></hui-card-element-editor> | ||||
|           </div> | ||||
|           <div class="element-preview"> | ||||
|             <hui-card-preview | ||||
|               .hass=${this.hass} | ||||
|               .config=${this._cardConfig} | ||||
|               class=${this._error ? "blur" : ""} | ||||
|             ></hui-card-preview> | ||||
|             ${this._isInSection | ||||
|               ? html` | ||||
|                   <hui-section | ||||
|                     .hass=${this.hass} | ||||
|                     .config=${this._cardConfigInSection(this._cardConfig)} | ||||
|                     preview | ||||
|                     class=${this._error ? "blur" : ""} | ||||
|                   ></hui-section> | ||||
|                 ` | ||||
|               : html` | ||||
|                   <hui-card | ||||
|                     .hass=${this.hass} | ||||
|                     .config=${this._cardConfig} | ||||
|                     preview | ||||
|                     class=${this._error ? "blur" : ""} | ||||
|                   ></hui-card> | ||||
|                 `} | ||||
|             ${this._error | ||||
|               ? html` | ||||
|                   <ha-circular-progress | ||||
| @@ -334,6 +349,34 @@ export class HuiDialogEditCard | ||||
|     this._cardEditorEl?.focusYamlEditor(); | ||||
|   } | ||||
|  | ||||
|   private get _isInSection() { | ||||
|     return this._params!.path.length === 2; | ||||
|   } | ||||
|  | ||||
|   private _shouldShowLayoutTab(): boolean { | ||||
|     /** | ||||
|      * Only show layout tab for cards in a grid section | ||||
|      * In the future, every section and view should be able to bring their own editor for layout. | ||||
|      * For now, we limit it to grid sections as it's the only section type | ||||
|      * */ | ||||
|     return ( | ||||
|       this._isInSection && | ||||
|       (!this._containerConfig.type || this._containerConfig.type === "grid") | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private _cardConfigInSection = memoizeOne( | ||||
|     (cardConfig?: LovelaceCardConfig) => { | ||||
|       const { cards, title, ...containerConfig } = this | ||||
|         ._containerConfig as LovelaceSectionConfig; | ||||
|  | ||||
|       return { | ||||
|         ...containerConfig, | ||||
|         cards: cardConfig ? [cardConfig] : [], | ||||
|       }; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private get _canSave(): boolean { | ||||
|     if (this._saving) { | ||||
|       return false; | ||||
| @@ -452,10 +495,18 @@ export class HuiDialogEditCard | ||||
|           flex-direction: column; | ||||
|         } | ||||
|  | ||||
|         .content hui-card-preview { | ||||
|           margin: 4px auto; | ||||
|         .content hui-card { | ||||
|           display: block; | ||||
|           padding: 4px; | ||||
|           margin: 0 auto; | ||||
|           max-width: 390px; | ||||
|         } | ||||
|         .content hui-section { | ||||
|           display: block; | ||||
|           padding: 4px; | ||||
|           margin: 0 auto; | ||||
|           max-width: var(--ha-view-sections-column-max-width, 500px); | ||||
|         } | ||||
|         .content .element-editor { | ||||
|           margin: 0 10px; | ||||
|         } | ||||
| @@ -470,11 +521,16 @@ export class HuiDialogEditCard | ||||
|             flex-shrink: 1; | ||||
|             min-width: 0; | ||||
|           } | ||||
|           .content hui-card-preview { | ||||
|           .content hui-card { | ||||
|             padding: 8px 10px; | ||||
|             margin: auto 0px; | ||||
|             max-width: 500px; | ||||
|           } | ||||
|           .content hui-section { | ||||
|             padding: 8px 10px; | ||||
|             margin: auto 0px; | ||||
|             max-width: var(--ha-view-sections-column-max-width, 500px); | ||||
|           } | ||||
|         } | ||||
|         .hidden { | ||||
|           display: none; | ||||
| @@ -498,7 +554,7 @@ export class HuiDialogEditCard | ||||
|           position: absolute; | ||||
|           z-index: 10; | ||||
|         } | ||||
|         hui-card-preview { | ||||
|         hui-card { | ||||
|           padding-top: 8px; | ||||
|           margin-bottom: 4px; | ||||
|           display: block; | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import { | ||||
|   LovelaceContainerPath, | ||||
|   parseLovelaceContainerPath, | ||||
| } from "../lovelace-path"; | ||||
| import "./hui-card-preview"; | ||||
| import "../../cards/hui-card"; | ||||
| import { showCreateCardDialog } from "./show-create-card-dialog"; | ||||
| import { SuggestCardDialogParams } from "./show-suggest-card-dialog"; | ||||
|  | ||||
| @@ -75,6 +75,7 @@ export class HuiDialogSuggestCard extends LitElement { | ||||
|           <hui-section | ||||
|             .hass=${this.hass} | ||||
|             .config=${this._sectionConfig} | ||||
|             preview | ||||
|           ></hui-section> | ||||
|         </div> | ||||
|       `; | ||||
| @@ -84,10 +85,11 @@ export class HuiDialogSuggestCard extends LitElement { | ||||
|         <div class="element-preview"> | ||||
|           ${this._cardConfig.map( | ||||
|             (cardConfig) => html` | ||||
|               <hui-card-preview | ||||
|               <hui-card | ||||
|                 .hass=${this.hass} | ||||
|                 .config=${cardConfig} | ||||
|               ></hui-card-preview> | ||||
|                 preview | ||||
|               ></hui-card> | ||||
|             ` | ||||
|           )} | ||||
|         </div> | ||||
| @@ -191,7 +193,7 @@ export class HuiDialogSuggestCard extends LitElement { | ||||
|         .element-preview { | ||||
|           position: relative; | ||||
|         } | ||||
|         hui-card-preview, | ||||
|         hui-card, | ||||
|         hui-section { | ||||
|           padding-top: 8px; | ||||
|           margin: 4px auto; | ||||
|   | ||||
| @@ -1,104 +0,0 @@ | ||||
| import { PropertyValues, ReactiveElement } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { LovelaceSectionElement } from "../../../../data/lovelace"; | ||||
| import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { createSectionElement } from "../../create-element/create-section-element"; | ||||
| import { createErrorSectionConfig } from "../../sections/hui-error-section"; | ||||
| import { LovelaceConfig } from "../../../../data/lovelace/config/types"; | ||||
|  | ||||
| @customElement("hui-section-preview") | ||||
| export class HuiSectionPreview extends ReactiveElement { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public lovelace?: LovelaceConfig; | ||||
|  | ||||
|   @property({ attribute: false }) public config?: LovelaceSectionConfig; | ||||
|  | ||||
|   private _element?: LovelaceSectionElement; | ||||
|  | ||||
|   private get _error() { | ||||
|     return this._element?.tagName === "HUI-ERROR-SECTION"; | ||||
|   } | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.addEventListener("ll-rebuild", () => { | ||||
|       this._cleanup(); | ||||
|       if (this.config) { | ||||
|         this._createSection(this.config); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   protected createRenderRoot() { | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   protected update(changedProperties: PropertyValues) { | ||||
|     super.update(changedProperties); | ||||
|  | ||||
|     if (changedProperties.has("config")) { | ||||
|       const oldConfig = changedProperties.get("config") as | ||||
|         | undefined | ||||
|         | LovelaceSectionConfig; | ||||
|  | ||||
|       if (!this.config) { | ||||
|         this._cleanup(); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!this.config.type) { | ||||
|         this._createSection(createErrorSectionConfig("No section type found")); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!this._element) { | ||||
|         this._createSection(this.config); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // in case the element was an error element we always want to recreate it | ||||
|       if (!this._error && oldConfig && this.config.type === oldConfig.type) { | ||||
|         try { | ||||
|           this._element.setConfig(this.config); | ||||
|         } catch (err: any) { | ||||
|           this._createSection(createErrorSectionConfig(err.message)); | ||||
|         } | ||||
|       } else { | ||||
|         this._createSection(this.config); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (changedProperties.has("hass")) { | ||||
|       if (this._element) { | ||||
|         this._element.hass = this.hass; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _createSection(configValue: LovelaceSectionConfig): void { | ||||
|     this._cleanup(); | ||||
|     this._element = createSectionElement(configValue) as LovelaceSectionElement; | ||||
|  | ||||
|     if (this.hass) { | ||||
|       this._element!.hass = this.hass; | ||||
|     } | ||||
|  | ||||
|     this.appendChild(this._element!); | ||||
|   } | ||||
|  | ||||
|   private _cleanup() { | ||||
|     if (!this._element) { | ||||
|       return; | ||||
|     } | ||||
|     this.removeChild(this._element); | ||||
|     this._element = undefined; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "hui-section-preview": HuiSectionPreview; | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,3 @@ | ||||
| import { preventDefault } from "@fullcalendar/core/internal"; | ||||
| import { ActionDetail } from "@material/mwc-list"; | ||||
| import { mdiCheck, mdiDelete, mdiDotsVertical, mdiFlask } from "@mdi/js"; | ||||
| import { LitElement, PropertyValues, css, html, nothing } from "lit"; | ||||
| @@ -6,6 +5,7 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { preventDefault } from "../../../../common/dom/prevent_default"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import { handleStructError } from "../../../../common/structs/handle-errors"; | ||||
| import "../../../../components/ha-alert"; | ||||
|   | ||||
| @@ -81,7 +81,7 @@ export type LovelaceRowConfig = | ||||
|  | ||||
| export interface LovelaceRow extends HTMLElement { | ||||
|   hass?: HomeAssistant; | ||||
|   editMode?: boolean; | ||||
|   preview?: boolean; | ||||
|   setConfig(config: LovelaceRowConfig); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -11,10 +11,10 @@ import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { HuiCard } from "../cards/hui-card"; | ||||
| import "../components/hui-card-edit-mode"; | ||||
| import { moveCard } from "../editor/config-util"; | ||||
| import type { Lovelace } from "../types"; | ||||
| import { HuiCard } from "../cards/hui-card"; | ||||
| import type { Lovelace, LovelaceLayoutOptions } from "../types"; | ||||
|  | ||||
| const CARD_SORTABLE_OPTIONS: HaSortableOptions = { | ||||
|   delay: 100, | ||||
| @@ -23,6 +23,11 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = { | ||||
|   invertedSwapThreshold: 0.7, | ||||
| } as HaSortableOptions; | ||||
|  | ||||
| export const DEFAULT_GRID_OPTIONS: LovelaceLayoutOptions = { | ||||
|   grid_columns: 4, | ||||
|   grid_rows: 1, | ||||
| }; | ||||
|  | ||||
| export class GridSection extends LitElement implements LovelaceSectionElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
| @@ -95,11 +100,15 @@ export class GridSection extends LitElement implements LovelaceSectionElement { | ||||
|               const card = this.cards![idx]; | ||||
|               const layoutOptions = card.getLayoutOptions(); | ||||
|  | ||||
|               const columnSize = | ||||
|                 layoutOptions.grid_columns ?? DEFAULT_GRID_OPTIONS.grid_columns; | ||||
|               const rowSize = | ||||
|                 layoutOptions.grid_rows ?? DEFAULT_GRID_OPTIONS.grid_rows; | ||||
|               return html` | ||||
|                 <div | ||||
|                   style=${styleMap({ | ||||
|                     "--column-size": layoutOptions.grid_columns, | ||||
|                     "--row-size": layoutOptions.grid_rows, | ||||
|                     "--column-size": columnSize, | ||||
|                     "--row-size": rowSize, | ||||
|                   })} | ||||
|                   class="card ${classMap({ | ||||
|                     "fit-rows": typeof layoutOptions?.grid_rows === "number", | ||||
| @@ -202,6 +211,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement { | ||||
|           margin: 0px; | ||||
|           letter-spacing: 0.1px; | ||||
|           line-height: 32px; | ||||
|           text-align: var(--ha-view-sections-title-text-align, start); | ||||
|           min-height: 32px; | ||||
|           display: block; | ||||
|           padding: 24px 10px 10px; | ||||
| @@ -215,8 +225,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement { | ||||
|         .card { | ||||
|           border-radius: var(--ha-card-border-radius, 12px); | ||||
|           position: relative; | ||||
|           grid-row: span var(--row-size, 1); | ||||
|           grid-column: span var(--column-size, 4); | ||||
|           grid-row: span var(--row-size); | ||||
|           grid-column: span var(--column-size); | ||||
|         } | ||||
|  | ||||
|         .card.fit-rows { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { PropertyValues, ReactiveElement } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { MediaQueriesListener } from "../../../common/dom/media_query"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import type { LovelaceSectionElement } from "../../../data/lovelace"; | ||||
| @@ -16,7 +17,6 @@ import { | ||||
|   attachConditionMediaQueriesListeners, | ||||
|   checkConditionsMet, | ||||
| } from "../common/validate-condition"; | ||||
| import { createErrorCardConfig } from "../create-element/create-element-base"; | ||||
| import { createSectionElement } from "../create-element/create-section-element"; | ||||
| import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; | ||||
| import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; | ||||
| @@ -26,7 +26,6 @@ import { parseLovelaceCardPath } from "../editor/lovelace-path"; | ||||
| import { generateLovelaceSectionStrategy } from "../strategies/get-strategy"; | ||||
| import type { Lovelace } from "../types"; | ||||
| import { DEFAULT_SECTION_LAYOUT } from "./const"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -42,6 +41,8 @@ export class HuiSection extends ReactiveElement { | ||||
|  | ||||
|   @property({ attribute: false }) public lovelace?: Lovelace; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) public preview = false; | ||||
|  | ||||
|   @property({ type: Number }) public index!: number; | ||||
|  | ||||
|   @property({ type: Number }) public viewIndex!: number; | ||||
| @@ -54,23 +55,15 @@ export class HuiSection extends ReactiveElement { | ||||
|  | ||||
|   private _listeners: MediaQueriesListener[] = []; | ||||
|  | ||||
|   // Public to make demo happy | ||||
|   public createCardElement(cardConfig: LovelaceCardConfig) { | ||||
|   private _createCardElement(cardConfig: LovelaceCardConfig) { | ||||
|     const element = document.createElement("hui-card"); | ||||
|     element.hass = this.hass; | ||||
|     element.lovelace = this.lovelace; | ||||
|     element.setConfig(cardConfig); | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev: Event) => { | ||||
|         // In edit mode let it go to hui-root and rebuild whole section. | ||||
|         if (!this.lovelace!.editMode) { | ||||
|           ev.stopPropagation(); | ||||
|           this._rebuildCard(element, cardConfig); | ||||
|         } | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     element.preview = this.preview; | ||||
|     element.config = cardConfig; | ||||
|     element.addEventListener("card-updated", (ev: Event) => { | ||||
|       ev.stopPropagation(); | ||||
|       this._cards = [...this._cards]; | ||||
|     }); | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
| @@ -121,28 +114,23 @@ export class HuiSection extends ReactiveElement { | ||||
|       // Config has not changed. Just props | ||||
|       if (changedProperties.has("hass")) { | ||||
|         this._cards.forEach((element) => { | ||||
|           try { | ||||
|             element.hass = this.hass; | ||||
|           } catch (e: any) { | ||||
|             this._rebuildCard(element, createErrorCardConfig(e.message, null)); | ||||
|           } | ||||
|           element.hass = this.hass; | ||||
|         }); | ||||
|         this._layoutElement.hass = this.hass; | ||||
|       } | ||||
|       if (changedProperties.has("lovelace")) { | ||||
|         this._layoutElement.lovelace = this.lovelace; | ||||
|       } | ||||
|       if (changedProperties.has("preview")) { | ||||
|         this._layoutElement.preview = this.preview; | ||||
|         this._cards.forEach((element) => { | ||||
|           try { | ||||
|             element.lovelace = this.lovelace; | ||||
|           } catch (e: any) { | ||||
|             this._rebuildCard(element, createErrorCardConfig(e.message, null)); | ||||
|           } | ||||
|           element.preview = this.preview; | ||||
|         }); | ||||
|       } | ||||
|       if (changedProperties.has("_cards")) { | ||||
|         this._layoutElement.cards = this._cards; | ||||
|       } | ||||
|       if (changedProperties.has("hass") || changedProperties.has("lovelace")) { | ||||
|       if (changedProperties.has("hass") || changedProperties.has("preview")) { | ||||
|         this._updateElement(); | ||||
|       } | ||||
|     } | ||||
| @@ -222,7 +210,7 @@ export class HuiSection extends ReactiveElement { | ||||
|     } | ||||
|     const visible = | ||||
|       forceVisible || | ||||
|       this.lovelace?.editMode || | ||||
|       this.preview || | ||||
|       !this.config.visibility || | ||||
|       checkConditionsMet(this.config.visibility, this.hass); | ||||
|  | ||||
| @@ -283,22 +271,8 @@ export class HuiSection extends ReactiveElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._cards = config.cards.map((cardConfig) => { | ||||
|       const element = this.createCardElement(cardConfig); | ||||
|       return element; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _rebuildCard( | ||||
|     cardElToReplace: HuiCard, | ||||
|     config: LovelaceCardConfig | ||||
|   ): void { | ||||
|     const newCardEl = this.createCardElement(config); | ||||
|     if (cardElToReplace.parentElement) { | ||||
|       cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace); | ||||
|     } | ||||
|     this._cards = this._cards!.map((curCardEl) => | ||||
|       curCardEl === cardElToReplace ? newCardEl : curCardEl | ||||
|     this._cards = config.cards.map((cardConfig) => | ||||
|       this._createCardElement(cardConfig) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -17,6 +17,7 @@ declare global { | ||||
|   // eslint-disable-next-line | ||||
|   interface HASSDomEvents { | ||||
|     "ll-rebuild": Record<string, unknown>; | ||||
|     "ll-upgrade": Record<string, unknown>; | ||||
|     "ll-badge-rebuild": Record<string, unknown>; | ||||
|   } | ||||
| } | ||||
| @@ -47,7 +48,7 @@ export type LovelaceLayoutOptions = { | ||||
| export interface LovelaceCard extends HTMLElement { | ||||
|   hass?: HomeAssistant; | ||||
|   isPanel?: boolean; | ||||
|   editMode?: boolean; | ||||
|   preview?: boolean; | ||||
|   getCardSize(): number | Promise<number>; | ||||
|   getLayoutOptions?(): LovelaceLayoutOptions; | ||||
|   setConfig(config: LovelaceCardConfig): void; | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { HuiCard } from "../cards/hui-card"; | ||||
| import { computeCardSize } from "../common/compute-card-size"; | ||||
| import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types"; | ||||
| import type { Lovelace, LovelaceBadge } from "../types"; | ||||
|  | ||||
| // Find column with < 5 size, else smallest column | ||||
| const getColumnIndex = (columnSizes: number[], size: number) => { | ||||
| @@ -248,17 +248,17 @@ export class MasonryView extends LitElement implements LovelaceViewElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _addCardToColumn(columnEl, index, editMode) { | ||||
|     const card: LovelaceCard = this.cards[index]; | ||||
|     if (!editMode || this.isStrategy) { | ||||
|       card.editMode = false; | ||||
|   private _addCardToColumn(columnEl, index, preview) { | ||||
|     const card: HuiCard = this.cards[index]; | ||||
|     if (!preview || this.isStrategy) { | ||||
|       card.preview = false; | ||||
|       columnEl.appendChild(card); | ||||
|     } else { | ||||
|       const wrapper = document.createElement("hui-card-options"); | ||||
|       wrapper.hass = this.hass; | ||||
|       wrapper.lovelace = this.lovelace; | ||||
|       wrapper.path = [this.index!, index]; | ||||
|       card.editMode = true; | ||||
|       card.preview = true; | ||||
|       wrapper.appendChild(card); | ||||
|       columnEl.appendChild(wrapper); | ||||
|     } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import type { HomeAssistant } from "../../../types"; | ||||
| import { HuiCard } from "../cards/hui-card"; | ||||
| import { HuiCardOptions } from "../components/hui-card-options"; | ||||
| import { HuiWarning } from "../components/hui-warning"; | ||||
| import type { Lovelace, LovelaceCard } from "../types"; | ||||
| import type { Lovelace } from "../types"; | ||||
|  | ||||
| let editCodeLoaded = false; | ||||
|  | ||||
| @@ -32,7 +32,7 @@ export class PanelView extends LitElement implements LovelaceViewElement { | ||||
|  | ||||
|   @property({ attribute: false }) public cards: HuiCard[] = []; | ||||
|  | ||||
|   @state() private _card?: LovelaceCard | HuiWarning | HuiCardOptions; | ||||
|   @state() private _card?: HuiCard | HuiWarning | HuiCardOptions; | ||||
|  | ||||
|   public setConfig(_config: LovelaceViewConfig): void {} | ||||
|  | ||||
| @@ -104,11 +104,11 @@ export class PanelView extends LitElement implements LovelaceViewElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const card: LovelaceCard = this.cards[0]; | ||||
|     const card: HuiCard = this.cards[0]; | ||||
|     card.isPanel = true; | ||||
|  | ||||
|     if (this.isStrategy || !this.lovelace?.editMode) { | ||||
|       card.editMode = false; | ||||
|       card.preview = false; | ||||
|       this._card = card; | ||||
|       return; | ||||
|     } | ||||
| @@ -118,7 +118,7 @@ export class PanelView extends LitElement implements LovelaceViewElement { | ||||
|     wrapper.lovelace = this.lovelace; | ||||
|     wrapper.path = [this.index!, 0]; | ||||
|     wrapper.hidePosition = true; | ||||
|     card.editMode = true; | ||||
|     card.preview = true; | ||||
|     wrapper.appendChild(card); | ||||
|     this._card = wrapper; | ||||
|   } | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import type { HomeAssistant } from "../../../types"; | ||||
| import { HuiCard } from "../cards/hui-card"; | ||||
| import { HuiCardOptions } from "../components/hui-card-options"; | ||||
| import { replaceCard } from "../editor/config-util"; | ||||
| import type { Lovelace, LovelaceCard } from "../types"; | ||||
| import type { Lovelace } from "../types"; | ||||
|  | ||||
| export class SideBarView extends LitElement implements LovelaceViewElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -140,18 +140,18 @@ export class SideBarView extends LitElement implements LovelaceViewElement { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     this.cards.forEach((card: LovelaceCard, idx) => { | ||||
|     this.cards.forEach((card, idx) => { | ||||
|       const cardConfig = this._config?.cards?.[idx]; | ||||
|       let element: LovelaceCard | HuiCardOptions; | ||||
|       let element: HuiCard | HuiCardOptions; | ||||
|       if (this.isStrategy || !this.lovelace?.editMode) { | ||||
|         card.editMode = false; | ||||
|         card.preview = false; | ||||
|         element = card; | ||||
|       } else { | ||||
|         element = document.createElement("hui-card-options"); | ||||
|         element.hass = this.hass; | ||||
|         element.lovelace = this.lovelace; | ||||
|         element.path = [this.index!, idx]; | ||||
|         card.editMode = true; | ||||
|         card.preview = true; | ||||
|         const movePositionButton = document.createElement("ha-icon-button"); | ||||
|         movePositionButton.slot = "buttons"; | ||||
|         const moveIcon = document.createElement("ha-svg-icon"); | ||||
|   | ||||
| @@ -21,7 +21,6 @@ import "../cards/hui-card"; | ||||
| import type { HuiCard } from "../cards/hui-card"; | ||||
| import { processConfigEntities } from "../common/process-config-entities"; | ||||
| import { createBadgeElement } from "../create-element/create-badge-element"; | ||||
| import { createErrorCardConfig } from "../create-element/create-element-base"; | ||||
| import { createViewElement } from "../create-element/create-view-element"; | ||||
| import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; | ||||
| import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; | ||||
| @@ -77,19 +76,12 @@ export class HUIView extends ReactiveElement { | ||||
|   private _createCardElement(cardConfig: LovelaceCardConfig) { | ||||
|     const element = document.createElement("hui-card"); | ||||
|     element.hass = this.hass; | ||||
|     element.lovelace = this.lovelace; | ||||
|     element.setConfig(cardConfig); | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev: Event) => { | ||||
|         // In edit mode let it go to hui-root and rebuild whole view. | ||||
|         if (!this.lovelace!.editMode) { | ||||
|           ev.stopPropagation(); | ||||
|           this._rebuildCard(element, cardConfig); | ||||
|         } | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     element.preview = this.lovelace.editMode; | ||||
|     element.config = cardConfig; | ||||
|     element.addEventListener("card-updated", (ev: Event) => { | ||||
|       ev.stopPropagation(); | ||||
|       this._cards = [...this._cards]; | ||||
|     }); | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
| @@ -117,6 +109,7 @@ export class HUIView extends ReactiveElement { | ||||
|     element.lovelace = this.lovelace; | ||||
|     element.config = sectionConfig; | ||||
|     element.viewIndex = this.index; | ||||
|     element.preview = this.lovelace.editMode; | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev: Event) => { | ||||
| @@ -183,11 +176,7 @@ export class HUIView extends ReactiveElement { | ||||
|         }); | ||||
|  | ||||
|         this._cards.forEach((element) => { | ||||
|           try { | ||||
|             element.hass = this.hass; | ||||
|           } catch (e: any) { | ||||
|             this._rebuildCard(element, createErrorCardConfig(e.message, null)); | ||||
|           } | ||||
|           element.hass = this.hass; | ||||
|         }); | ||||
|  | ||||
|         this._sections.forEach((element) => { | ||||
| @@ -221,17 +210,13 @@ export class HUIView extends ReactiveElement { | ||||
|           try { | ||||
|             element.hass = this.hass; | ||||
|             element.lovelace = this.lovelace; | ||||
|             element.preview = this.lovelace.editMode; | ||||
|           } catch (e: any) { | ||||
|             this._rebuildSection(element, createErrorSectionConfig(e.message)); | ||||
|           } | ||||
|         }); | ||||
|         this._cards.forEach((element) => { | ||||
|           try { | ||||
|             element.hass = this.hass; | ||||
|             element.lovelace = this.lovelace; | ||||
|           } catch (e: any) { | ||||
|             this._rebuildCard(element, createErrorCardConfig(e.message, null)); | ||||
|           } | ||||
|           element.preview = this.lovelace.editMode; | ||||
|         }); | ||||
|       } | ||||
|       if (changedProperties.has("_cards")) { | ||||
| @@ -388,19 +373,6 @@ export class HUIView extends ReactiveElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _rebuildCard( | ||||
|     cardElToReplace: HuiCard, | ||||
|     config: LovelaceCardConfig | ||||
|   ): void { | ||||
|     const newCardEl = this._createCardElement(config); | ||||
|     if (cardElToReplace.parentElement) { | ||||
|       cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace); | ||||
|     } | ||||
|     this._cards = this._cards!.map((curCardEl) => | ||||
|       curCardEl === cardElToReplace ? newCardEl : curCardEl | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private _rebuildBadge( | ||||
|     badgeElToReplace: LovelaceBadge, | ||||
|     config: LovelaceBadgeConfig | ||||
|   | ||||
| @@ -301,7 +301,7 @@ class HaRefreshTokens extends LitElement { | ||||
|         text: this.hass.localize( | ||||
|           "ui.panel.profile.refresh_tokens.confirm_delete_all" | ||||
|         ), | ||||
|         confirmText: this.hass.localize("ui.common.delete"), | ||||
|         confirmText: this.hass.localize("ui.common.delete_all"), | ||||
|         destructive: true, | ||||
|       })) | ||||
|     ) { | ||||
|   | ||||
| @@ -17,12 +17,13 @@ import { | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { isComponentLoaded } from "../../common/config/is_component_loaded"; | ||||
| import { storage } from "../../common/decorators/storage"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { computeStateName } from "../../common/entity/compute_state_name"; | ||||
| import { supportsFeature } from "../../common/entity/supports-feature"; | ||||
| import { navigate } from "../../common/navigate"; | ||||
| import { constructUrlCurrentPath } from "../../common/url/construct-url"; | ||||
| import { | ||||
| @@ -40,6 +41,7 @@ import "../../components/ha-two-pane-top-app-bar-fixed"; | ||||
| import { deleteConfigEntry } from "../../data/config_entries"; | ||||
| import { getExtendedEntityRegistryEntry } from "../../data/entity_registry"; | ||||
| import { fetchIntegrationManifest } from "../../data/integration"; | ||||
| import { LovelaceCardConfig } from "../../data/lovelace/config/card"; | ||||
| import { TodoListEntityFeature, getTodoLists } from "../../data/todo"; | ||||
| import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; | ||||
| import { | ||||
| @@ -49,11 +51,8 @@ import { | ||||
| import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; | ||||
| import { haStyle } from "../../resources/styles"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import { HuiErrorCard } from "../lovelace/cards/hui-error-card"; | ||||
| import { createCardElement } from "../lovelace/create-element/create-card-element"; | ||||
| import { LovelaceCard } from "../lovelace/types"; | ||||
| import "../lovelace/cards/hui-card"; | ||||
| import { showTodoItemEditDialog } from "./show-dialog-todo-item-editor"; | ||||
| import { supportsFeature } from "../../common/entity/supports-feature"; | ||||
|  | ||||
| @customElement("ha-panel-todo") | ||||
| class PanelTodo extends LitElement { | ||||
| @@ -63,8 +62,6 @@ class PanelTodo extends LitElement { | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) public mobile = false; | ||||
|  | ||||
|   @state() private _card?: LovelaceCard | HuiErrorCard; | ||||
|  | ||||
|   @storage({ | ||||
|     key: "selectedTodoEntity", | ||||
|     state: true, | ||||
| @@ -128,15 +125,10 @@ class PanelTodo extends LitElement { | ||||
|     if (changedProperties.has("_entityId") || !this.hasUpdated) { | ||||
|       this._setupTodoElement(); | ||||
|     } | ||||
|  | ||||
|     if (changedProperties.has("hass") && this._card) { | ||||
|       this._card.hass = this.hass; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _setupTodoElement(): void { | ||||
|     if (!this._entityId) { | ||||
|       this._card = undefined; | ||||
|       navigate(constructUrlCurrentPath(""), { replace: true }); | ||||
|       return; | ||||
|     } | ||||
| @@ -144,13 +136,16 @@ class PanelTodo extends LitElement { | ||||
|       constructUrlCurrentPath(createSearchParam({ entity_id: this._entityId })), | ||||
|       { replace: true } | ||||
|     ); | ||||
|     this._card = createCardElement({ | ||||
|       type: "todo-list", | ||||
|       entity: this._entityId, | ||||
|     }) as LovelaceCard; | ||||
|     this._card.hass = this.hass; | ||||
|   } | ||||
|  | ||||
|   private _cardConfig = memoizeOne( | ||||
|     (entityId: string) => | ||||
|       ({ | ||||
|         type: "todo-list", | ||||
|         entity: entityId, | ||||
|       }) as LovelaceCardConfig | ||||
|   ); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const entityRegistryEntry = this._entityId | ||||
|       ? this.hass.entities[this._entityId] | ||||
| @@ -274,7 +269,16 @@ class PanelTodo extends LitElement { | ||||
|             : nothing} | ||||
|         </ha-button-menu> | ||||
|         <div id="columns"> | ||||
|           <div class="column">${this._card}</div> | ||||
|           <div class="column"> | ||||
|             ${this._entityId | ||||
|               ? html` | ||||
|                   <hui-card | ||||
|                     .hass=${this.hass} | ||||
|                     .config=${this._cardConfig(this._entityId)} | ||||
|                   ></hui-card> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           </div> | ||||
|         </div> | ||||
|         ${entityState && | ||||
|         supportsFeature(entityState, TodoListEntityFeature.CREATE_TODO_ITEM) | ||||
|   | ||||
| @@ -1,28 +0,0 @@ | ||||
| /* eslint-disable no-extend-native */ | ||||
|  | ||||
| export {}; // for Babel to treat as a module | ||||
|  | ||||
| if (!Array.prototype.flat) { | ||||
|   Object.defineProperty(Array.prototype, "flat", { | ||||
|     configurable: true, | ||||
|     writable: true, | ||||
|     value: function (...args) { | ||||
|       const depth = typeof args[0] === "undefined" ? 1 : Number(args[0]) || 0; | ||||
|       const result = []; | ||||
|       const forEach = result.forEach; | ||||
|  | ||||
|       const flatDeep = (arr: Array<any>, dpth: number) => { | ||||
|         forEach.call(arr, (val) => { | ||||
|           if (dpth > 0 && Array.isArray(val)) { | ||||
|             flatDeep(val, dpth - 1); | ||||
|           } else { | ||||
|             result.push(val); | ||||
|           } | ||||
|         }); | ||||
|       }; | ||||
|  | ||||
|       flatDeep(this, depth); | ||||
|       return result; | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| @@ -33,6 +33,7 @@ export const langs = { | ||||
|  | ||||
| export const langCompartment = new Compartment(); | ||||
| export const readonlyCompartment = new Compartment(); | ||||
| export const linewrapCompartment = new Compartment(); | ||||
|  | ||||
| export const tabKeyBindings: KeyBinding[] = [ | ||||
|   { key: "Tab", run: indentMore }, | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "core-js/modules/web.url.can-parse"; | ||||
| import { fromError } from "stacktrace-js"; | ||||
| import { UAParser } from "ua-parser-js"; | ||||
|  | ||||
|   | ||||
| @@ -326,6 +326,7 @@ | ||||
|       "refresh": "Refresh", | ||||
|       "cancel": "Cancel", | ||||
|       "delete": "Delete", | ||||
|       "delete_all": "Delete all", | ||||
|       "download": "[%key:supervisor::backup::download%]", | ||||
|       "duplicate": "Duplicate", | ||||
|       "remove": "Remove", | ||||
| @@ -740,6 +741,11 @@ | ||||
|           "last_year": "Last year" | ||||
|         } | ||||
|       }, | ||||
|       "grid-size-picker": { | ||||
|         "reset_default": "Reset to default size", | ||||
|         "columns": "Number of columns", | ||||
|         "rows": "Number of rows" | ||||
|       }, | ||||
|       "relative_time": { | ||||
|         "never": "Never" | ||||
|       }, | ||||
| @@ -2863,12 +2869,6 @@ | ||||
|                 "device": { | ||||
|                   "label": "Device", | ||||
|                   "trigger": "Trigger", | ||||
|                   "extra_fields": { | ||||
|                     "above": "Above", | ||||
|                     "below": "Below", | ||||
|                     "for": "Duration (optional)", | ||||
|                     "zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]" | ||||
|                   }, | ||||
|                   "description": { | ||||
|                     "picker": "When something happens to a device. Great way to start." | ||||
|                   } | ||||
| @@ -3106,13 +3106,6 @@ | ||||
|                 "device": { | ||||
|                   "label": "Device", | ||||
|                   "condition": "Condition", | ||||
|                   "extra_fields": { | ||||
|                     "above": "Above", | ||||
|                     "below": "Below", | ||||
|                     "for": "Duration", | ||||
|                     "hvac_mode": "HVAC mode", | ||||
|                     "preset_mode": "Preset mode" | ||||
|                   }, | ||||
|                   "description": { | ||||
|                     "picker": "Set of conditions provided by your device. Great way to start." | ||||
|                   } | ||||
| @@ -3343,17 +3336,6 @@ | ||||
|                 "device_id": { | ||||
|                   "label": "Device", | ||||
|                   "action": "Action", | ||||
|                   "extra_fields": { | ||||
|                     "code": "Code", | ||||
|                     "message": "Message", | ||||
|                     "title": "Title", | ||||
|                     "position": "[%key:ui::card::cover::position%]", | ||||
|                     "mode": "Mode", | ||||
|                     "humidity": "Humidity", | ||||
|                     "value": "Value", | ||||
|                     "brightness_pct": "[%key:ui::card::light::brightness%]", | ||||
|                     "flash": "Flash" | ||||
|                   }, | ||||
|                   "description": { | ||||
|                     "picker": "Do something on a device. Great way to start.", | ||||
|                     "no_device": "Device action" | ||||
| @@ -4036,6 +4018,8 @@ | ||||
|             "event": "Events", | ||||
|             "sensor": "Sensors", | ||||
|             "diagnostic": "Diagnostic", | ||||
|             "notify": "Notifiers", | ||||
|             "assist": "[%key:ui::panel::lovelace::menu::assist%]", | ||||
|             "config": "Configuration", | ||||
|             "add_entities_lovelace": "Add to dashboard", | ||||
|             "none": "This device has no entities", | ||||
| @@ -4116,12 +4100,11 @@ | ||||
|               "confirm_title": "Do you want to disable {number} {number, plural,\n  one {entity}\n  other {entities}\n}?", | ||||
|               "confirm_text": "Disabled entities will not be added to Home Assistant." | ||||
|             }, | ||||
|             "remove_selected": { | ||||
|               "button": "Remove selected", | ||||
|               "confirm_title": "Do you want to remove {number} {number, plural,\n  one {entity}\n  other {entities}\n}?", | ||||
|               "confirm_partly_title": "Only {number} {number, plural,\n  one {selected entity}\n  other {selected entities}\n} can be removed.", | ||||
|               "confirm_text": "You should remove them from your dashboard config and automations if they contain these entities.", | ||||
|               "confirm_partly_text": "You can only remove {removable} of the selected {selected} entities. Entities can only be removed when the integration is no longer providing the entities. Sometimes you have to restart Home Assistant before you can remove the entities of a removed integration. Are you sure you want to remove the removable entities?" | ||||
|             "delete_selected": { | ||||
|               "button": "Delete selected", | ||||
|               "confirm_title": "Delete selected entities?", | ||||
|               "confirm_text": "Are you sure you want to delete the entities?\n\nRemove them from your dashboard and automations if they include these entities.", | ||||
|               "confirm_partly_text": "You can only delete {deletable} of the {selected} entities. The others require the integration to stop providing them, and sometimes a Home Assistant restart is needed. Are you sure you want to delete the deletable entities?\n\nRemove them from your dashboard and automations if they include these entities." | ||||
|             }, | ||||
|             "hide_selected": { | ||||
|               "button": "Hide selected", | ||||
| @@ -4192,8 +4175,7 @@ | ||||
|             "required_error_msg": "This field is required", | ||||
|             "delete": "Delete", | ||||
|             "create": "Add", | ||||
|             "update": "Update", | ||||
|             "no_edit_home_zone_radius": "The radius of the home zone is not editable in the UI." | ||||
|             "update": "Update" | ||||
|           }, | ||||
|           "core_location_dialog": "Home Assistant location" | ||||
|         }, | ||||
| @@ -4405,6 +4387,7 @@ | ||||
|             "name": "Display name", | ||||
|             "username": "Username", | ||||
|             "change_password": "Change password", | ||||
|             "change_username": "Change username", | ||||
|             "activate_user": "Activate user", | ||||
|             "deactivate_user": "Deactivate user", | ||||
|             "delete_user": "Delete user", | ||||
| @@ -4438,6 +4421,13 @@ | ||||
|             "change": "Change", | ||||
|             "password_no_match": "Passwords don't match", | ||||
|             "password_changed": "The password has been changed successfully." | ||||
|           }, | ||||
|           "change_username": { | ||||
|             "caption": "Change username", | ||||
|             "new_username": "New username", | ||||
|             "change": "Change", | ||||
|             "username_changed": "The username has been changed successfully.", | ||||
|             "failed": "Failed to change username" | ||||
|           } | ||||
|         }, | ||||
|         "application_credentials": { | ||||
| @@ -4863,7 +4853,8 @@ | ||||
|             "provisioning_finished": "The device has been added. Once you power it on, it will become available.", | ||||
|             "view_device": "View Device", | ||||
|             "interview_started": "The device is being interviewed. This may take some time.", | ||||
|             "interview_failed": "The device interview failed. Additional information may be available in the logs." | ||||
|             "interview_failed": "The device interview failed. Additional information may be available in the logs.", | ||||
|             "waiting_for_device": "Communicating with the device. Please wait." | ||||
|           }, | ||||
|           "provisioned": { | ||||
|             "dsk": "DSK", | ||||
| @@ -5521,10 +5512,14 @@ | ||||
|             "increase_position": "Increase card position", | ||||
|             "options": "More options", | ||||
|             "search_cards": "Search cards", | ||||
|             "tab-config": "Config", | ||||
|             "tab-visibility": "Visibility", | ||||
|             "tab_config": "Config", | ||||
|             "tab_visibility": "Visibility", | ||||
|             "tab_layout": "Layout", | ||||
|             "visibility": { | ||||
|               "explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown." | ||||
|             }, | ||||
|             "layout": { | ||||
|               "explanation": "Configure how the card will appear on the dashboard. This settings will override the default size and position of the card." | ||||
|             } | ||||
|           }, | ||||
|           "move_card": { | ||||
| @@ -6412,10 +6407,10 @@ | ||||
|           "disable_expiration_failed": "Failed to disable refresh token expiration", | ||||
|           "enable_expiration_failed": "Failed to enable refresh token expiration", | ||||
|           "confirm_delete_title": "Delete refresh token?", | ||||
|           "confirm_delete_text": "The refresh token for ''{name}'' will be permanently deleted. This will end the login session on the associated device.", | ||||
|           "confirm_delete_text": "The refresh token for ''{name}'' will be permanently deleted. This associated device will not have access anymore.", | ||||
|           "delete_all_tokens": "Delete all tokens", | ||||
|           "confirm_delete_all_title": "Delete all refresh tokens?", | ||||
|           "confirm_delete_all": "Are you sure you want to delete all refresh tokens? Your current session token will not be removed. Your long-lived access tokens will not be removed.", | ||||
|           "confirm_delete_all": "Are you sure you want to delete all refresh tokens? These associated devices will not have access anymore. Your current session and long-lived access tokens will not be deleted.", | ||||
|           "delete_failed": "Failed to delete the refresh token.", | ||||
|           "current_token_tooltip": "Unable to delete current refresh token" | ||||
|         }, | ||||
| @@ -6744,7 +6739,8 @@ | ||||
|                 "title": "Entity no longer recorded", | ||||
|                 "info_text_1": "We have generated statistics for this entity in the past, but state changes of this entity are no longer recorded, therefore, we cannot track long term statistics for it anymore.", | ||||
|                 "info_text_2": "You probably excluded this entity, or have just included some entities.", | ||||
|                 "info_text_3_link": "See the recorder documentation for more information." | ||||
|                 "info_text_3_link": "See the recorder documentation for more information.", | ||||
|                 "info_text_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now." | ||||
|               }, | ||||
|               "unsupported_state_class": { | ||||
|                 "title": "Unsupported state class", | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user