mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-29 21:49:45 +00:00 
			
		
		
		
	Compare commits
	
		
			53 Commits
		
	
	
		
			20220223.0
			...
			update-int
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3703ffc42d | ||
|   | 9ea8e13c87 | ||
|   | 604b79696e | ||
|   | 8c445f6409 | ||
|   | 797c871137 | ||
|   | 24829bd903 | ||
|   | add92a559d | ||
|   | 17018c0f26 | ||
|   | cd6a478130 | ||
|   | 4f6d7ca5c9 | ||
|   | c2994343b4 | ||
|   | e5f77c35d4 | ||
|   | a9e5a5dd44 | ||
|   | 1159798b8d | ||
|   | 437de42c55 | ||
|   | 89e0bb3f16 | ||
|   | 28c9631b6c | ||
|   | a769f84755 | ||
|   | 7abf9c2473 | ||
|   | 298296a81f | ||
|   | 6907fa5c8e | ||
|   | 546461b70f | ||
|   | 4031009c26 | ||
|   | 91e4557625 | ||
|   | f0c4b92dbb | ||
|   | 04ae8c9d14 | ||
|   | 0158610d42 | ||
|   | 5ab6121581 | ||
|   | 3d9c31aef9 | ||
|   | acfeea5c92 | ||
|   | 75e8e17073 | ||
|   | 976fd4b32d | ||
|   | 49beafbe5f | ||
|   | 151f8d5524 | ||
|   | 48355aa98e | ||
|   | fc31929f41 | ||
|   | b7c149fcc1 | ||
|   | 02d058561b | ||
|   | 4e57fb1ec1 | ||
|   | 30f79c5a46 | ||
|   | 30f7252d84 | ||
|   | 8af795a7ce | ||
|   | 8576eeae41 | ||
|   | cd740ed135 | ||
|   | 892f774792 | ||
|   | aa504fe1f8 | ||
|   | be491451d5 | ||
|   | bad184210d | ||
|   | a43b3b64b3 | ||
|   | aa831a9adf | ||
|   | 43d4f55392 | ||
|   | 130c66fb24 | ||
|   | 684c232c8c | 
							
								
								
									
										631
									
								
								.yarn/releases/yarn-3.0.2.cjs
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										631
									
								
								.yarn/releases/yarn-3.0.2.cjs
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										785
									
								
								.yarn/releases/yarn-3.2.0.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										785
									
								
								.yarn/releases/yarn-3.2.0.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ plugins: | ||||
|   - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs | ||||
|     spec: "@yarnpkg/plugin-interactive-tools" | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-3.0.2.cjs | ||||
| yarnPath: .yarn/releases/yarn-3.2.0.cjs | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| const gulp = require("gulp"); | ||||
| const fs = require("fs"); | ||||
| const path = require("path"); | ||||
| const marked = require("marked"); | ||||
| const { marked } = require("marked"); | ||||
| const glob = require("glob"); | ||||
| const yaml = require("js-yaml"); | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ const source = require("vinyl-source-stream"); | ||||
| const vinylBuffer = require("vinyl-buffer"); | ||||
| const gulp = require("gulp"); | ||||
| const fs = require("fs"); | ||||
| const foreach = require("gulp-foreach"); | ||||
| const flatmap = require("gulp-flatmap"); | ||||
| const merge = require("gulp-merge-json"); | ||||
| const rename = require("gulp-rename"); | ||||
| const transform = require("gulp-json-transform"); | ||||
| @@ -183,7 +183,7 @@ gulp.task("build-merged-translations", () => | ||||
|     }) | ||||
|     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) | ||||
|     .pipe( | ||||
|       foreach((stream, file) => { | ||||
|       flatmap((stream, file) => { | ||||
|         // For each language generate a merged json file. It begins with the master | ||||
|         // translation as a failsafe for untranslated strings, and merges all parent | ||||
|         // tags into one file for each specific subtag | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||
| import { LovelaceConfig } from "../../../../src/data/lovelace"; | ||||
| import { Lovelace } from "../../../../src/panels/lovelace/types"; | ||||
| @@ -20,6 +20,8 @@ class HcLovelace extends LitElement { | ||||
|  | ||||
|   @property() public urlPath: string | null = null; | ||||
|  | ||||
|   @query("hui-view") private _huiView?: HTMLElement; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const index = this._viewIndex; | ||||
|     if (index === undefined) { | ||||
| @@ -78,12 +80,12 @@ class HcLovelace extends LitElement { | ||||
|           this.lovelaceConfig.background; | ||||
|  | ||||
|         if (configBackground) { | ||||
|           (this.shadowRoot!.querySelector( | ||||
|             "hui-view" | ||||
|           ) as HTMLElement)!.style.setProperty( | ||||
|           this._huiView!.style.setProperty( | ||||
|             "--lovelace-background", | ||||
|             configBackground | ||||
|           ); | ||||
|         } else { | ||||
|           this._huiView!.style.removeProperty("--lovelace-background"); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -116,6 +118,9 @@ class HcLovelace extends LitElement { | ||||
|       :host > * { | ||||
|         flex: 1; | ||||
|       } | ||||
|       hui-view { | ||||
|         background: var(--lovelace-background, var(--primary-background-color)); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -279,7 +279,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { | ||||
|           can_play: true, | ||||
|           can_expand: false, | ||||
|           children_media_class: null, | ||||
|           thumbnail: null, | ||||
|           thumbnail: "https://brands.home-assistant.io/_/image/logo.png", | ||||
|         }, | ||||
|         { | ||||
|           title: "movie.mp4", | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import memoizeOne from "memoize-one"; | ||||
| import { atLeastVersion } from "../../../src/common/config/version"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
| import { navigate } from "../../../src/common/navigate"; | ||||
| import "../../../src/common/search/search-input"; | ||||
| import "../../../src/components/search-input"; | ||||
| import { extractSearchParam } from "../../../src/common/url/search-params"; | ||||
| import "../../../src/components/ha-button-menu"; | ||||
| import "../../../src/components/ha-icon-button"; | ||||
| @@ -110,8 +110,6 @@ class HassioAddonStore extends LitElement { | ||||
|               <div class="search"> | ||||
|                 <search-input | ||||
|                   .hass=${this.hass} | ||||
|                   no-label-float | ||||
|                   no-underline | ||||
|                   .filter=${this._filter} | ||||
|                   @value-changed=${this._filterChanged} | ||||
|                 ></search-input> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||
| import "../../../../src/common/search/search-input"; | ||||
| import "../../../../src/components/search-input"; | ||||
| import { stringCompare } from "../../../../src/common/string/compare"; | ||||
| import "../../../../src/components/ha-dialog"; | ||||
| import "../../../../src/components/ha-expansion-panel"; | ||||
| @@ -80,8 +80,6 @@ class HassioHardwareDialog extends LitElement { | ||||
|           ></ha-icon-button> | ||||
|           <search-input | ||||
|             .hass=${this.hass} | ||||
|             dialogInitialFocus | ||||
|             no-label-float | ||||
|             .filter=${this._filter} | ||||
|             @value-changed=${this._handleSearchChange} | ||||
|             .label=${this._dialogParams.supervisor.localize( | ||||
|   | ||||
| @@ -106,6 +106,9 @@ class HassioRepositoriesDialog extends LitElement { | ||||
|                     </paper-item-body> | ||||
|                     <div class="delete"> | ||||
|                       <ha-icon-button | ||||
|                         .label=${this._dialogParams!.supervisor.localize( | ||||
|                           "dialog.repositories.remove" | ||||
|                         )} | ||||
|                         .disabled=${usedRepositories.includes(repo.slug)} | ||||
|                         .slug=${repo.slug} | ||||
|                         .path=${usedRepositories.includes(repo.slug) | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| // Compat needs to be first import | ||||
| import "../../src/resources/compatibility"; | ||||
| import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; | ||||
| import "../../src/resources/roboto"; | ||||
| import "../../src/resources/safari-14-attachshadow-patch"; | ||||
| import "./hassio-main"; | ||||
|  | ||||
| setCancelSyntheticClickEvents(false); | ||||
|  | ||||
| const styleEl = document.createElement("style"); | ||||
| styleEl.innerHTML = ` | ||||
| body { | ||||
|   | ||||
| @@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||
| import "../../../src/layouts/hass-loading-screen"; | ||||
| import "../../../src/layouts/hass-subpage"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| import { addonArchIsSupported, extractChangelog } from "../util/addon"; | ||||
|  | ||||
| @@ -57,6 +56,12 @@ declare global { | ||||
|  | ||||
| type updateType = "os" | "supervisor" | "core" | "addon"; | ||||
|  | ||||
| const SUPERVISOR_UPDATE_NAMES = { | ||||
|   core: "Home Assistant Core", | ||||
|   os: "Home Assistant Operating System", | ||||
|   supervisor: "Home Assistant Supervisor", | ||||
| }; | ||||
|  | ||||
| const changelogUrl = ( | ||||
|   entry: updateType, | ||||
|   version: string | ||||
|   | ||||
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
								
							| @@ -117,7 +117,7 @@ | ||||
|     "leaflet-draw": "^1.0.4", | ||||
|     "lit": "^2.1.2", | ||||
|     "lit-vaadin-helpers": "^0.3.0", | ||||
|     "marked": "^3.0.2", | ||||
|     "marked": "^4.0.12", | ||||
|     "memoize-one": "^5.2.1", | ||||
|     "node-vibrant": "3.2.1-alpha.1", | ||||
|     "proxy-polyfill": "^0.3.2", | ||||
| @@ -137,12 +137,12 @@ | ||||
|     "vue": "^2.6.12", | ||||
|     "vue2-daterange-picker": "^0.5.1", | ||||
|     "web-animations-js": "^2.3.2", | ||||
|     "workbox-cacheable-response": "^6.1.5", | ||||
|     "workbox-core": "^6.1.5", | ||||
|     "workbox-expiration": "^6.1.5", | ||||
|     "workbox-precaching": "^6.1.5", | ||||
|     "workbox-routing": "^6.1.5", | ||||
|     "workbox-strategies": "^6.1.5", | ||||
|     "workbox-cacheable-response": "^6.4.2", | ||||
|     "workbox-core": "^6.4.2", | ||||
|     "workbox-expiration": "^6.4.2", | ||||
|     "workbox-precaching": "^6.4.2", | ||||
|     "workbox-routing": "^6.4.2", | ||||
|     "workbox-strategies": "^6.4.2", | ||||
|     "xss": "^1.0.9" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
| @@ -171,7 +171,7 @@ | ||||
|     "@types/js-yaml": "^4", | ||||
|     "@types/leaflet": "^1", | ||||
|     "@types/leaflet-draw": "^1", | ||||
|     "@types/marked": "^2", | ||||
|     "@types/marked": "^4", | ||||
|     "@types/mocha": "^8", | ||||
|     "@types/qrcode": "^1.4.2", | ||||
|     "@types/sortablejs": "^1", | ||||
| @@ -198,7 +198,7 @@ | ||||
|     "fs-extra": "^7.0.1", | ||||
|     "glob": "^7.2.0", | ||||
|     "gulp": "^4.0.2", | ||||
|     "gulp-foreach": "^0.1.0", | ||||
|     "gulp-flatmap": "^1.0.2", | ||||
|     "gulp-json-transform": "^0.4.6", | ||||
|     "gulp-merge-json": "^1.3.1", | ||||
|     "gulp-rename": "^2.0.0", | ||||
| @@ -235,7 +235,7 @@ | ||||
|     "webpack-dev-server": "^4.3.0", | ||||
|     "webpack-manifest-plugin": "^4.0.2", | ||||
|     "webpackbar": "^5.0.0-3", | ||||
|     "workbox-build": "^6.1.5" | ||||
|     "workbox-build": "^6.4.2" | ||||
|   }, | ||||
|   "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", | ||||
|   "resolutions": { | ||||
| @@ -255,5 +255,6 @@ | ||||
|   "prettier": { | ||||
|     "trailingComma": "es5", | ||||
|     "arrowParens": "always" | ||||
|   } | ||||
|   }, | ||||
|   "packageManager": "yarn@3.2.0" | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,6 @@ | ||||
| from pathlib import Path | ||||
|  | ||||
|  | ||||
| def where(): | ||||
| def where() -> Path: | ||||
|     """Return path to the frontend.""" | ||||
|     return Path(__file__).parent | ||||
|   | ||||
							
								
								
									
										0
									
								
								public/py.typed
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								public/py.typed
									
									
									
									
									
										Normal file
									
								
							| @@ -1,6 +1,6 @@ | ||||
| [metadata] | ||||
| name         = home-assistant-frontend | ||||
| version      = 20220223.0 | ||||
| version      = 20220301.0 | ||||
| author       = The Home Assistant Authors | ||||
| author_email = hello@home-assistant.io | ||||
| license      = Apache-2.0 | ||||
| @@ -19,3 +19,8 @@ python_requires = >= 3.4.0 | ||||
| [options.packages.find] | ||||
| include = | ||||
|     hass_frontend* | ||||
|  | ||||
| [mypy] | ||||
| python_version = 3.4 | ||||
| show_error_codes = True | ||||
| strict = True | ||||
|   | ||||
| @@ -21,7 +21,7 @@ import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { restoreScroll } from "../../common/decorators/restore-scroll"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import "../../common/search/search-input"; | ||||
| import "../search-input"; | ||||
| import { debounce } from "../../common/util/debounce"; | ||||
| import { nextRender } from "../../common/util/render-status"; | ||||
| import { haStyleScrollbar } from "../../resources/styles"; | ||||
|   | ||||
| @@ -115,6 +115,9 @@ class DateRangePickerElement extends WrappedElement { | ||||
|             color: var(--primary-text-color); | ||||
|             min-width: initial !important; | ||||
|           } | ||||
|           .daterangepicker:before { | ||||
|             display: none; | ||||
|           } | ||||
|           .daterangepicker:after { | ||||
|             border-bottom: 6px solid var(--card-background-color); | ||||
|           } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { SelectBase } from "@material/mwc-select/mwc-select-base"; | ||||
| import { styles } from "@material/mwc-select/mwc-select.css"; | ||||
| import { html, nothing } from "lit"; | ||||
| import { css, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { debounce } from "../common/util/debounce"; | ||||
| import { nextRender } from "../common/util/render-status"; | ||||
| @@ -20,8 +20,6 @@ export class HaSelect extends SelectBase { | ||||
|     ></span>`; | ||||
|   } | ||||
|  | ||||
|   static override styles = [styles]; | ||||
|  | ||||
|   connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     window.addEventListener("translations-updated", this._translationsUpdated); | ||||
| @@ -39,6 +37,15 @@ export class HaSelect extends SelectBase { | ||||
|     await nextRender(); | ||||
|     this.layoutOptions(); | ||||
|   }, 500); | ||||
|  | ||||
|   static override styles = [ | ||||
|     styles, | ||||
|     css` | ||||
|       .mdc-select:not(.mdc-select--disabled) .mdc-select__icon { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|   | ||||
| @@ -51,10 +51,11 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) { | ||||
|   private _filterEntities = (entity: HassEntity): boolean => { | ||||
|     if (this.selector.entity?.domain) { | ||||
|       const filterDomain = this.selector.entity.domain; | ||||
|       const filterDomainIsArray = Array.isArray(filterDomain); | ||||
|       const entityDomain = computeStateDomain(entity); | ||||
|       if ( | ||||
|         (Array.isArray(filterDomain) && !filterDomain.includes(entityDomain)) || | ||||
|         entityDomain !== filterDomain | ||||
|         (filterDomainIsArray && !filterDomain.includes(entityDomain)) || | ||||
|         (!filterDomainIsArray && entityDomain !== filterDomain) | ||||
|       ) { | ||||
|         return false; | ||||
|       } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { | ||||
| } from "../../data/media-player"; | ||||
| import type { MediaSelector, MediaSelectorValue } from "../../data/selector"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url"; | ||||
| import "../ha-alert"; | ||||
| import "../ha-form/ha-form"; | ||||
| import type { HaFormSchema } from "../ha-form/types"; | ||||
| @@ -50,6 +51,18 @@ export class HaMediaSelector extends LitElement { | ||||
|         getSignedPath(this.hass, thumbnail).then((signedPath) => { | ||||
|           this._thumbnailUrl = signedPath.path; | ||||
|         }); | ||||
|       } else if ( | ||||
|         thumbnail && | ||||
|         thumbnail.startsWith("https://brands.home-assistant.io") | ||||
|       ) { | ||||
|         // The backend is not aware of the theme used by the users, | ||||
|         // so we rewrite the URL to show a proper icon | ||||
|         this._thumbnailUrl = brandsUrl({ | ||||
|           domain: extractDomainFromBrandUrl(thumbnail), | ||||
|           type: "icon", | ||||
|           useFallback: true, | ||||
|           darkOptimized: this.hass.themes?.darkMode, | ||||
|         }); | ||||
|       } else { | ||||
|         this._thumbnailUrl = thumbnail; | ||||
|       } | ||||
|   | ||||
| @@ -42,9 +42,7 @@ export class HaTab extends LitElement { | ||||
|         @keydown=${this._handleKeyDown} | ||||
|       > | ||||
|         ${this.narrow ? html`<slot name="icon"></slot>` : ""} | ||||
|         ${!this.narrow || this.active | ||||
|           ? html`<span class="name">${this.name}</span>` | ||||
|           : ""} | ||||
|         <span class="name">${this.name}</span> | ||||
|         ${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""} | ||||
|       </div> | ||||
|     `; | ||||
|   | ||||
| @@ -53,6 +53,10 @@ export class HaTextField extends TextFieldBase { | ||||
|         padding-right: var(--text-field-suffix-padding-right, 0px); | ||||
|       } | ||||
|  | ||||
|       .mdc-text-field__icon { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|       input { | ||||
|         text-align: var(--text-field-text-align); | ||||
|       } | ||||
|   | ||||
| @@ -34,23 +34,24 @@ import { | ||||
|   MediaPickedEvent, | ||||
|   MediaPlayerBrowseAction, | ||||
| } from "../../data/media-player"; | ||||
| import { browseLocalMediaPlayer } from "../../data/media_source"; | ||||
| import { isTTSMediaSource } from "../../data/tts"; | ||||
| import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; | ||||
| import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; | ||||
| import { haStyle } from "../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url"; | ||||
| import { documentationUrl } from "../../util/documentation-url"; | ||||
| import "../entity/ha-entity-picker"; | ||||
| import "../ha-button-menu"; | ||||
| import "../ha-card"; | ||||
| import type { HaCard } from "../ha-card"; | ||||
| import "../ha-circular-progress"; | ||||
| import "../ha-fab"; | ||||
| import "../ha-icon-button"; | ||||
| import "../ha-svg-icon"; | ||||
| import "../ha-fab"; | ||||
| import { browseLocalMediaPlayer } from "../../data/media_source"; | ||||
| import { isTTSMediaSource } from "../../data/tts"; | ||||
| import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; | ||||
| import "./ha-browse-media-tts"; | ||||
| import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -681,6 +682,17 @@ export class HaMediaPlayerBrowse extends LitElement { | ||||
|                 // Thumbnails served by local API require authentication | ||||
|                 const signedPath = await getSignedPath(this.hass, thumbnailUrl); | ||||
|                 thumbnailUrl = signedPath.path; | ||||
|               } else if ( | ||||
|                 thumbnailUrl.startsWith("https://brands.home-assistant.io") | ||||
|               ) { | ||||
|                 // The backend is not aware of the theme used by the users, | ||||
|                 // so we rewrite the URL to show a proper icon | ||||
|                 thumbnailUrl = brandsUrl({ | ||||
|                   domain: extractDomainFromBrandUrl(thumbnailUrl), | ||||
|                   type: "icon", | ||||
|                   useFallback: true, | ||||
|                   darkOptimized: this.hass.themes?.darkMode, | ||||
|                 }); | ||||
|               } | ||||
|               thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`; | ||||
|               observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { mdiClose, mdiMagnify } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import "../../components/ha-icon-button"; | ||||
| import "../../components/ha-svg-icon"; | ||||
| import "../../components/ha-textfield"; | ||||
| import type { HaTextField } from "../../components/ha-textfield"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import { fireEvent } from "../dom/fire_event"; | ||||
| import "./ha-icon-button"; | ||||
| import "./ha-svg-icon"; | ||||
| import "./ha-textfield"; | ||||
| import type { HaTextField } from "./ha-textfield"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| 
 | ||||
| @customElement("search-input") | ||||
| class SearchInput extends LitElement { | ||||
| @@ -1,8 +1,13 @@ | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| interface ValidationResult { | ||||
|   valid: boolean; | ||||
|   error: string | null; | ||||
| interface ValidConfig { | ||||
|   valid: true; | ||||
|   error: null; | ||||
| } | ||||
|  | ||||
| interface InvalidConfig { | ||||
|   valid: false; | ||||
|   error: string; | ||||
| } | ||||
|  | ||||
| type ValidKeys = "trigger" | "action" | "condition"; | ||||
| @@ -12,7 +17,7 @@ export const validateConfig = < | ||||
| >( | ||||
|   hass: HomeAssistant, | ||||
|   config: T | ||||
| ): Promise<Record<keyof T, ValidationResult>> => | ||||
| ): Promise<Record<keyof T, ValidConfig | InvalidConfig>> => | ||||
|   hass.callWS({ | ||||
|     type: "validate_config", | ||||
|     ...config, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| export interface InputDateTime { | ||||
| @@ -17,6 +18,19 @@ export interface InputDateTimeMutableParams { | ||||
|   has_date: boolean; | ||||
| } | ||||
|  | ||||
| export const stateToIsoDateString = (entityState: HassEntity) => | ||||
|   `${entityState.attributes.year || "1970"}-${String( | ||||
|     entityState.attributes.month || "01" | ||||
|   ).padStart(2, "0")}-${String(entityState.attributes.day || "01").padStart( | ||||
|     2, | ||||
|     "0" | ||||
|   )}T${String(entityState.attributes.hour || "00").padStart(2, "0")}:${String( | ||||
|     entityState.attributes.minute || "00" | ||||
|   ).padStart(2, "0")}:${String(entityState.attributes.second || "00").padStart( | ||||
|     2, | ||||
|     "0" | ||||
|   )}`; | ||||
|  | ||||
| export const setInputDateTimeValue = ( | ||||
|   hass: HomeAssistant, | ||||
|   entityId: string, | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { Action } from "./script"; | ||||
|  | ||||
| export const callExecuteScript = (hass: HomeAssistant, sequence: Action[]) => | ||||
| export const callExecuteScript = ( | ||||
|   hass: HomeAssistant, | ||||
|   sequence: Action | Action[] | ||||
| ) => | ||||
|   hass.callWS({ | ||||
|     type: "execute_script", | ||||
|     sequence, | ||||
|   | ||||
| @@ -1,58 +0,0 @@ | ||||
| import { HomeAssistant } from "../../types"; | ||||
|  | ||||
| interface SupervisorBaseAvailableUpdates { | ||||
|   panel_path?: string; | ||||
|   update_type?: string; | ||||
|   version_latest?: string; | ||||
| } | ||||
|  | ||||
| interface SupervisorAddonAvailableUpdates | ||||
|   extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "addon"; | ||||
|   icon?: string; | ||||
|   name?: string; | ||||
| } | ||||
|  | ||||
| interface SupervisorCoreAvailableUpdates | ||||
|   extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "core"; | ||||
| } | ||||
|  | ||||
| interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "os"; | ||||
| } | ||||
|  | ||||
| interface SupervisorSupervisorAvailableUpdates | ||||
|   extends SupervisorBaseAvailableUpdates { | ||||
|   update_type?: "supervisor"; | ||||
| } | ||||
|  | ||||
| export type SupervisorAvailableUpdates = | ||||
|   | SupervisorAddonAvailableUpdates | ||||
|   | SupervisorCoreAvailableUpdates | ||||
|   | SupervisorOsAvailableUpdates | ||||
|   | SupervisorSupervisorAvailableUpdates; | ||||
|  | ||||
| export interface SupervisorAvailableUpdatesResponse { | ||||
|   available_updates: SupervisorAvailableUpdates[]; | ||||
| } | ||||
|  | ||||
| export const fetchSupervisorAvailableUpdates = async ( | ||||
|   hass: HomeAssistant | ||||
| ): Promise<SupervisorAvailableUpdates[]> => | ||||
|   ( | ||||
|     await hass.callWS<SupervisorAvailableUpdatesResponse>({ | ||||
|       type: "supervisor/api", | ||||
|       endpoint: "/available_updates", | ||||
|       method: "get", | ||||
|     }) | ||||
|   ).available_updates; | ||||
|  | ||||
| export const refreshSupervisorAvailableUpdates = async ( | ||||
|   hass: HomeAssistant | ||||
| ): Promise<void> => | ||||
|   hass.callWS<void>({ | ||||
|     type: "supervisor/api", | ||||
|     endpoint: "/refresh_updates", | ||||
|     method: "post", | ||||
|   }); | ||||
							
								
								
									
										37
									
								
								src/data/update.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/data/update.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| export interface UpdateDescription { | ||||
|   identifier: string; | ||||
|   name: string; | ||||
|   domain: string; | ||||
|   current_version: string; | ||||
|   available_version: string; | ||||
|   changelog_content: string | null; | ||||
|   changelog_url: string | null; | ||||
|   icon_url: string | null; | ||||
|   supports_backup: boolean; | ||||
| } | ||||
|  | ||||
| export interface SkipUpdateParams { | ||||
|   domain: string; | ||||
|   version: string; | ||||
|   identifier: string; | ||||
| } | ||||
|  | ||||
| export interface PerformUpdateParams extends SkipUpdateParams { | ||||
|   backup?: boolean; | ||||
| } | ||||
|  | ||||
| export const fetchUpdateInfo = ( | ||||
|   hass: HomeAssistant | ||||
| ): Promise<UpdateDescription[]> => hass.callWS({ type: "update/info" }); | ||||
|  | ||||
| export const skipUpdate = ( | ||||
|   hass: HomeAssistant, | ||||
|   params: SkipUpdateParams | ||||
| ): Promise<void> => hass.callWS({ type: "update/skip", ...params }); | ||||
|  | ||||
| export const performUpdate = ( | ||||
|   hass: HomeAssistant, | ||||
|   params: PerformUpdateParams | ||||
| ): Promise<void> => hass.callWS({ type: "update/update", ...params }); | ||||
| @@ -117,13 +117,17 @@ class DataEntryFlowDialog extends LitElement { | ||||
|         ); | ||||
|       } catch (err: any) { | ||||
|         this.closeDialog(); | ||||
|         let message = err.message || err.body || "Unknown error"; | ||||
|         if (typeof message !== "string") { | ||||
|           message = JSON.stringify(message); | ||||
|         } | ||||
|         showAlertDialog(this, { | ||||
|           title: this.hass.localize( | ||||
|             "ui.panel.config.integrations.config_flow.error" | ||||
|           ), | ||||
|           text: `${this.hass.localize( | ||||
|             "ui.panel.config.integrations.config_flow.could_not_load" | ||||
|           )}: ${err.message || err.body}`, | ||||
|           )}: ${message}`, | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import memoizeOne from "memoize-one"; | ||||
| import { isComponentLoaded } from "../../common/config/is_component_loaded"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { navigate } from "../../common/navigate"; | ||||
| import "../../common/search/search-input"; | ||||
| import "../../components/search-input"; | ||||
| import { caseInsensitiveStringCompare } from "../../common/string/compare"; | ||||
| import { LocalizeFunc } from "../../common/translations/localize"; | ||||
| import "../../components/ha-icon-next"; | ||||
|   | ||||
| @@ -4,7 +4,10 @@ import { customElement, property } from "lit/decorators"; | ||||
| import "../../../components/ha-date-input"; | ||||
| import "../../../components/ha-time-input"; | ||||
| import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; | ||||
| import { setInputDateTimeValue } from "../../../data/input_datetime"; | ||||
| import { | ||||
|   setInputDateTimeValue, | ||||
|   stateToIsoDateString, | ||||
| } from "../../../data/input_datetime"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
|  | ||||
| @customElement("more-info-input_datetime") | ||||
| @@ -24,7 +27,7 @@ class MoreInfoInputDatetime extends LitElement { | ||||
|             ? html` | ||||
|                 <ha-date-input | ||||
|                   .locale=${this.hass.locale} | ||||
|                   .value=${`${this.stateObj.attributes.year}-${this.stateObj.attributes.month}-${this.stateObj.attributes.day}`} | ||||
|                   .value=${stateToIsoDateString(this.stateObj)} | ||||
|                   .disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)} | ||||
|                   @value-changed=${this._dateChanged} | ||||
|                 > | ||||
|   | ||||
| @@ -50,12 +50,11 @@ class MoreInfoMediaPlayer extends LitElement { | ||||
|     const controls = computeMediaControls(stateObj); | ||||
|  | ||||
|     return html` | ||||
|       ${!controls | ||||
|         ? "" | ||||
|         : html` | ||||
|       <div class="controls"> | ||||
|         <div class="basic-controls"> | ||||
|                 ${controls!.map( | ||||
|           ${!controls | ||||
|             ? "" | ||||
|             : controls.map( | ||||
|                 (control) => html` | ||||
|                   <ha-icon-button | ||||
|                     action=${control.action} | ||||
| @@ -85,7 +84,6 @@ class MoreInfoMediaPlayer extends LitElement { | ||||
|             ` | ||||
|           : ""} | ||||
|       </div> | ||||
|           `} | ||||
|       ${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) || | ||||
|         supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) && | ||||
|       ![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) | ||||
|   | ||||
| @@ -86,11 +86,11 @@ export class QuickBar extends LitElement { | ||||
|  | ||||
|   @state() private _search = ""; | ||||
|  | ||||
|   @state() private _opened = false; | ||||
|   @state() private _open = false; | ||||
|  | ||||
|   @state() private _commandMode = false; | ||||
|  | ||||
|   @state() private _done = false; | ||||
|   @state() private _opened = false; | ||||
|  | ||||
|   @state() private _narrow = false; | ||||
|  | ||||
| @@ -109,12 +109,12 @@ export class QuickBar extends LitElement { | ||||
|       "all and (max-width: 450px), all and (max-height: 500px)" | ||||
|     ).matches; | ||||
|     this._initializeItemsIfNeeded(); | ||||
|     this._opened = true; | ||||
|     this._open = true; | ||||
|   } | ||||
|  | ||||
|   public closeDialog() { | ||||
|     this._open = false; | ||||
|     this._opened = false; | ||||
|     this._done = false; | ||||
|     this._focusSet = false; | ||||
|     this._filter = ""; | ||||
|     this._search = ""; | ||||
| @@ -133,7 +133,7 @@ export class QuickBar extends LitElement { | ||||
|   ); | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this._opened) { | ||||
|     if (!this._open) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
| @@ -218,7 +218,8 @@ export class QuickBar extends LitElement { | ||||
|             ` | ||||
|           : html` | ||||
|               <mwc-list> | ||||
|                 <lit-virtualizer | ||||
|                 ${this._opened | ||||
|                   ? html`<lit-virtualizer | ||||
|                       scroller | ||||
|                       @keydown=${this._handleListItemKeyDown} | ||||
|                       @rangechange=${this._handleRangeChanged} | ||||
| @@ -229,13 +230,14 @@ export class QuickBar extends LitElement { | ||||
|                           ? "calc(100vh - 56px)" | ||||
|                           : `${Math.min( | ||||
|                               items.length * (this._commandMode ? 56 : 72) + 26, | ||||
|                           this._done ? 500 : 0 | ||||
|                               500 | ||||
|                             )}px`, | ||||
|                       })} | ||||
|                       .items=${items} | ||||
|                       .renderItem=${this._renderItem} | ||||
|                     > | ||||
|                 </lit-virtualizer> | ||||
|                     </lit-virtualizer>` | ||||
|                   : ""} | ||||
|               </mwc-list> | ||||
|             `} | ||||
|         ${this._hint ? html`<div class="hint">${this._hint}</div>` : ""} | ||||
| @@ -252,9 +254,7 @@ export class QuickBar extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _handleOpened() { | ||||
|     this.updateComplete.then(() => { | ||||
|       this._done = true; | ||||
|     }); | ||||
|     this._opened = true; | ||||
|   } | ||||
|  | ||||
|   private async _handleRangeChanged(e) { | ||||
| @@ -454,9 +454,10 @@ export class QuickBar extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _handleItemClick(ev) { | ||||
|     const listItem = ev.target.closest("mwc-list-item"); | ||||
|     this.processItemAndCloseDialog( | ||||
|       (ev.target as any).item, | ||||
|       Number((ev.target as HTMLElement).getAttribute("index")) | ||||
|       listItem.item, | ||||
|       Number(listItem.getAttribute("index")) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
							
								
								
									
										211
									
								
								src/dialogs/update-dialog/ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								src/dialogs/update-dialog/ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { computeRTL } from "../../common/util/compute_rtl"; | ||||
| import "../../components/ha-alert"; | ||||
| import "../../components/ha-checkbox"; | ||||
| import "../../components/ha-circular-progress"; | ||||
| import { createCloseHeading } from "../../components/ha-dialog"; | ||||
| import "../../components/ha-faded"; | ||||
| import "../../components/ha-formfield"; | ||||
| import "../../components/ha-icon-button"; | ||||
| import "../../components/ha-markdown"; | ||||
| import { | ||||
|   performUpdate, | ||||
|   skipUpdate, | ||||
|   UpdateDescription, | ||||
| } from "../../data/update"; | ||||
| import { haStyleDialog } from "../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { UpdateDialogParams } from "./show-ha-update-dialog"; | ||||
|  | ||||
| @customElement("ha-update-dialog") | ||||
| export class HaUpdateDialog extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @state() private _opened = false; | ||||
|  | ||||
|   @state() private _updating = false; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   @state() private _update!: UpdateDescription; | ||||
|  | ||||
|   _refreshCallback!: () => void; | ||||
|  | ||||
|   public async showDialog(dialogParams: UpdateDialogParams): Promise<void> { | ||||
|     this._opened = true; | ||||
|     this._update = dialogParams.update; | ||||
|     this._refreshCallback = dialogParams.refreshCallback; | ||||
|   } | ||||
|  | ||||
|   public async closeDialog(): Promise<void> { | ||||
|     this._opened = false; | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this._opened) { | ||||
|       return html``; | ||||
|     } | ||||
|     return html` | ||||
|       <ha-dialog | ||||
|         open | ||||
|         @closed=${this.closeDialog} | ||||
|         scrimClickAction | ||||
|         .heading=${createCloseHeading( | ||||
|           this.hass, | ||||
|           this.hass.localize("ui.panel.config.updates.dialog.title", { | ||||
|             name: this._update.name, | ||||
|           }) | ||||
|         )} | ||||
|       > | ||||
|         <div> | ||||
|           ${this._error | ||||
|             ? html`<ha-alert alert-type="error" .rtl=${computeRTL(this.hass)}> | ||||
|                 ${this._error} | ||||
|               </ha-alert>` | ||||
|             : ""} | ||||
|           ${!this._updating | ||||
|             ? html` | ||||
|                 ${this._update.changelog_content | ||||
|                   ? html` | ||||
|                       <ha-faded> | ||||
|                         <ha-markdown .content=${this._update.changelog_content}> | ||||
|                         </ha-markdown> | ||||
|                       </ha-faded> | ||||
|                     ` | ||||
|                   : ""} | ||||
|                 ${this._update.changelog_url | ||||
|                   ? html`<a href=${this._update.changelog_url} target="_blank"> | ||||
|                       Full changelog | ||||
|                     </a> ` | ||||
|                   : ""} | ||||
|                 <p> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.updates.dialog.description", | ||||
|                     { | ||||
|                       name: this._update.name, | ||||
|                       version: this._update.current_version, | ||||
|                       newest_version: this._update.available_version, | ||||
|                     } | ||||
|                   )} | ||||
|                 </p> | ||||
|                 ${this._update.supports_backup | ||||
|                   ? html` | ||||
|                       <ha-formfield | ||||
|                         .label=${this.hass.localize( | ||||
|                           "ui.panel.config.updates.dialog.create_backup" | ||||
|                         )} | ||||
|                       > | ||||
|                         <ha-checkbox checked></ha-checkbox> | ||||
|                       </ha-formfield> | ||||
|                     ` | ||||
|                   : ""} | ||||
|               ` | ||||
|             : html`<ha-circular-progress alt="Updating" size="large" active> | ||||
|                 </ha-circular-progress> | ||||
|                 <p class="progress-text"> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.updates.dialog.updating", | ||||
|                     { | ||||
|                       name: this._update.name, | ||||
|                       version: this._update.available_version, | ||||
|                     } | ||||
|                   )} | ||||
|                 </p>`} | ||||
|         </div> | ||||
|         ${!this._updating | ||||
|           ? html` | ||||
|               <mwc-button slot="secondaryAction" @click=${this._skipUpdate}> | ||||
|                 ${this.hass.localize("ui.common.skip")} | ||||
|               </mwc-button> | ||||
|               <mwc-button | ||||
|                 .disabled=${this._updating} | ||||
|                 slot="primaryAction" | ||||
|                 @click=${this._performUpdate} | ||||
|               > | ||||
|                 ${this.hass.localize("ui.panel.config.updates.dialog.update")} | ||||
|               </mwc-button> | ||||
|             ` | ||||
|           : ""} | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   get _shouldCreateBackup(): boolean { | ||||
|     if (!this._update.supports_backup) { | ||||
|       return false; | ||||
|     } | ||||
|     const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); | ||||
|     if (checkbox) { | ||||
|       return checkbox.checked; | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   private async _performUpdate() { | ||||
|     this._error = undefined; | ||||
|     this._updating = true; | ||||
|     try { | ||||
|       await performUpdate(this.hass, { | ||||
|         domain: this._update.domain, | ||||
|         identifier: this._update.identifier, | ||||
|         version: this._update.available_version, | ||||
|         backup: this._shouldCreateBackup, | ||||
|       }); | ||||
|     } catch (err: any) { | ||||
|       this._error = err.message; | ||||
|       this._updating = false; | ||||
|       return; | ||||
|     } | ||||
|     this._updating = false; | ||||
|     this._refreshCallback(); | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|  | ||||
|   private async _skipUpdate() { | ||||
|     this._error = undefined; | ||||
|     try { | ||||
|       await skipUpdate(this.hass, { | ||||
|         domain: this._update.domain, | ||||
|         identifier: this._update.identifier, | ||||
|         version: this._update.available_version, | ||||
|       }); | ||||
|     } catch (err: any) { | ||||
|       this._error = err.message; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._refreshCallback(); | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleDialog, | ||||
|       css` | ||||
|         ha-circular-progress { | ||||
|           display: block; | ||||
|           margin: 32px; | ||||
|           text-align: center; | ||||
|         } | ||||
|  | ||||
|         .progress-text { | ||||
|           text-align: center; | ||||
|         } | ||||
|  | ||||
|         ha-markdown { | ||||
|           padding-bottom: 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-update-dialog": HaUpdateDialog; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/dialogs/update-dialog/show-ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/dialogs/update-dialog/show-ha-update-dialog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { UpdateDescription } from "../../data/update"; | ||||
|  | ||||
| export interface UpdateDialogParams { | ||||
|   update: UpdateDescription; | ||||
|   refreshCallback: () => void; | ||||
| } | ||||
|  | ||||
| export const showUpdateDialog = ( | ||||
|   element: HTMLElement, | ||||
|   dialogParams: UpdateDialogParams | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "ha-update-dialog", | ||||
|     dialogImport: () => import("./ha-update-dialog"), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
| @@ -13,6 +13,9 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>( | ||||
|   class SubscribeClass extends superClass { | ||||
|     @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|     // we wait with subscribing till these properties are set on the host element | ||||
|     protected hassSubscribeRequiredHostProps?: string[]; | ||||
|  | ||||
|     private __unsubs?: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>>; | ||||
|  | ||||
|     public connectedCallback() { | ||||
| @@ -39,6 +42,16 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>( | ||||
|       super.updated(changedProps); | ||||
|       if (changedProps.has("hass")) { | ||||
|         this.__checkSubscribed(); | ||||
|         return; | ||||
|       } | ||||
|       if (!this.hassSubscribeRequiredHostProps) { | ||||
|         return; | ||||
|       } | ||||
|       for (const key of changedProps.keys()) { | ||||
|         if (this.hassSubscribeRequiredHostProps.includes(key as string)) { | ||||
|           this.__checkSubscribed(); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -52,7 +65,10 @@ export const SubscribeMixin = <T extends Constructor<ReactiveElement>>( | ||||
|       if ( | ||||
|         this.__unsubs !== undefined || | ||||
|         !(this as unknown as Element).isConnected || | ||||
|         this.hass === undefined | ||||
|         this.hass === undefined || | ||||
|         this.hassSubscribeRequiredHostProps?.some( | ||||
|           (prop) => this[prop] === undefined | ||||
|         ) | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|   | ||||
| @@ -16,10 +16,16 @@ import "../../../../components/ha-icon-button"; | ||||
| import "../../../../components/ha-select"; | ||||
| import type { HaSelect } from "../../../../components/ha-select"; | ||||
| import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; | ||||
| import { validateConfig } from "../../../../data/config"; | ||||
| import { Action, getActionType } from "../../../../data/script"; | ||||
| import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; | ||||
| import { callExecuteScript } from "../../../../data/service"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| } from "../../../../dialogs/generic/show-dialog-box"; | ||||
| import { haStyle } from "../../../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { showToast } from "../../../../util/toast"; | ||||
| import "./types/ha-automation-action-activate_scene"; | ||||
| import "./types/ha-automation-action-choose"; | ||||
| import "./types/ha-automation-action-condition"; | ||||
| @@ -180,6 +186,11 @@ export default class HaAutomationActionRow extends LitElement { | ||||
|                 .label=${this.hass.localize("ui.common.menu")} | ||||
|                 .path=${mdiDotsVertical} | ||||
|               ></ha-icon-button> | ||||
|               <mwc-list-item> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.config.automation.editor.actions.run_action" | ||||
|                 )} | ||||
|               </mwc-list-item> | ||||
|               <mwc-list-item .disabled=${!this._uiModeAvailable}> | ||||
|                 ${yamlMode | ||||
|                   ? this.hass.localize( | ||||
| @@ -290,17 +301,54 @@ export default class HaAutomationActionRow extends LitElement { | ||||
|   private _handleAction(ev: CustomEvent<ActionDetail>) { | ||||
|     switch (ev.detail.index) { | ||||
|       case 0: | ||||
|         this._switchYamlMode(); | ||||
|         this._runAction(); | ||||
|         break; | ||||
|       case 1: | ||||
|         fireEvent(this, "duplicate"); | ||||
|         this._switchYamlMode(); | ||||
|         break; | ||||
|       case 2: | ||||
|         fireEvent(this, "duplicate"); | ||||
|         break; | ||||
|       case 3: | ||||
|         this._onDelete(); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _runAction() { | ||||
|     const validated = await validateConfig(this.hass, { | ||||
|       action: this.action, | ||||
|     }); | ||||
|  | ||||
|     if (!validated.action.valid) { | ||||
|       showAlertDialog(this, { | ||||
|         title: this.hass.localize( | ||||
|           "ui.panel.config.automation.editor.actions.invalid_action" | ||||
|         ), | ||||
|         text: validated.action.error, | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       await callExecuteScript(this.hass, this.action); | ||||
|     } catch (err: any) { | ||||
|       showAlertDialog(this, { | ||||
|         title: this.hass.localize( | ||||
|           "ui.panel.config.automation.editor.actions.run_action_error" | ||||
|         ), | ||||
|         text: err.message || err, | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     showToast(this, { | ||||
|       message: this.hass.localize( | ||||
|         "ui.panel.config.automation.editor.actions.run_action_success" | ||||
|       ), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _onDelete() { | ||||
|     showConfirmationDialog(this, { | ||||
|       text: this.hass.localize( | ||||
|   | ||||
| @@ -145,7 +145,7 @@ export class HaDeviceAction extends LitElement { | ||||
|   static styles = css` | ||||
|     ha-device-picker { | ||||
|       display: block; | ||||
|       margin-bottom: 24px; | ||||
|       margin-bottom: 16px; | ||||
|     } | ||||
|     ha-device-action-picker { | ||||
|       display: block; | ||||
|   | ||||
| @@ -50,7 +50,7 @@ export class HaEventAction extends LitElement implements ActionElement { | ||||
|       <ha-yaml-editor | ||||
|         .hass=${this.hass} | ||||
|         .label=${this.hass.localize( | ||||
|           "ui.panel.config.automation.editor.actions.type.event.service_data" | ||||
|           "ui.panel.config.automation.editor.actions.type.event.event_data" | ||||
|         )} | ||||
|         .name=${"event_data"} | ||||
|         .defaultValue=${event_data} | ||||
|   | ||||
| @@ -162,8 +162,8 @@ export class HaRepeatAction extends LitElement implements ActionElement { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         ha-select { | ||||
|           margin-top: 8px; | ||||
|         ha-textfield { | ||||
|           margin-top: 16px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -33,7 +33,6 @@ export class HaWaitForTriggerAction | ||||
|         .value=${timeout || ""} | ||||
|         @change=${this._valueChanged} | ||||
|       ></ha-textfield> | ||||
|       <br /> | ||||
|       <ha-formfield | ||||
|         .label=${this.hass.localize( | ||||
|           "ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout" | ||||
|   | ||||
| @@ -117,8 +117,8 @@ export class HaTimeCondition extends LitElement implements ConditionElement { | ||||
|     ); | ||||
|  | ||||
|     const data = { | ||||
|       mode_before: "value", | ||||
|       mode_after: "value", | ||||
|       mode_before: inputModeBefore ? "input" : "value", | ||||
|       mode_after: inputModeAfter ? "input" : "value", | ||||
|       ...this.condition, | ||||
|     }; | ||||
|  | ||||
| @@ -137,18 +137,11 @@ export class HaTimeCondition extends LitElement implements ConditionElement { | ||||
|     ev.stopPropagation(); | ||||
|     const newValue = ev.detail.value; | ||||
|  | ||||
|     const newModeAfter = newValue.mode_after === "input"; | ||||
|     const newModeBefore = newValue.mode_before === "input"; | ||||
|     this._inputModeAfter = newValue.mode_after === "input"; | ||||
|     this._inputModeBefore = newValue.mode_before === "input"; | ||||
|  | ||||
|     if (newModeAfter !== this._inputModeAfter) { | ||||
|       this._inputModeAfter = newModeAfter; | ||||
|       newValue.after = undefined; | ||||
|     } | ||||
|  | ||||
|     if (newModeBefore !== this._inputModeBefore) { | ||||
|       this._inputModeBefore = newModeBefore; | ||||
|       newValue.before = undefined; | ||||
|     } | ||||
|     delete newValue.mode_after; | ||||
|     delete newValue.mode_before; | ||||
|  | ||||
|     Object.keys(newValue).forEach((key) => | ||||
|       newValue[key] === undefined || newValue[key] === "" | ||||
|   | ||||
| @@ -346,7 +346,8 @@ export class HaManualAutomationEditor extends LitElement { | ||||
|         .link-button-row { | ||||
|           padding: 14px; | ||||
|         } | ||||
|         ha-textarea { | ||||
|         ha-textarea, | ||||
|         ha-textfield { | ||||
|           display: block; | ||||
|         } | ||||
|         span[slot="introduction"] a { | ||||
|   | ||||
| @@ -23,9 +23,9 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { | ||||
|  | ||||
|   private _schema = memoizeOne( | ||||
|     (localize: LocalizeFunc, inputMode?: boolean): HaFormSchema[] => { | ||||
|       const modeSchema = inputMode | ||||
|         ? { name: "at", selector: { entity: { domain: "input_datetime" } } } | ||||
|         : { name: "at", selector: { time: {} } }; | ||||
|       const atSelector = inputMode | ||||
|         ? { entity: { domain: "input_datetime" } } | ||||
|         : { time: {} }; | ||||
|  | ||||
|       return [ | ||||
|         { | ||||
| @@ -47,7 +47,7 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { | ||||
|             ], | ||||
|           ], | ||||
|         }, | ||||
|         modeSchema, | ||||
|         { name: "at", selector: atSelector }, | ||||
|       ]; | ||||
|     } | ||||
|   ); | ||||
| @@ -80,7 +80,7 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { | ||||
|     const schema: HaFormSchema[] = this._schema(this.hass.localize, inputMode); | ||||
|  | ||||
|     const data = { | ||||
|       mode: "value", | ||||
|       mode: inputMode ? "input" : "value", | ||||
|       ...this.trigger, | ||||
|     }; | ||||
|  | ||||
| @@ -99,7 +99,8 @@ export class HaTimeTrigger extends LitElement implements TriggerElement { | ||||
|     ev.stopPropagation(); | ||||
|     const newValue = ev.detail.value; | ||||
|  | ||||
|     this._inputMode = newValue.mode.value === "input"; | ||||
|     this._inputMode = newValue.mode === "input"; | ||||
|     delete newValue.mode; | ||||
|  | ||||
|     Object.keys(newValue).forEach((key) => | ||||
|       newValue[key] === undefined || newValue[key] === "" | ||||
|   | ||||
| @@ -29,6 +29,7 @@ import "./cloud-remote-pref"; | ||||
| import "./cloud-tts-pref"; | ||||
| import "./cloud-webhooks"; | ||||
| import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; | ||||
| import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; | ||||
|  | ||||
| @customElement("cloud-account") | ||||
| export class CloudAccount extends SubscribeMixin(LitElement) { | ||||
| @@ -276,10 +277,21 @@ export class CloudAccount extends SubscribeMixin(LitElement) { | ||||
|   private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { | ||||
|     switch (ev.detail.index) { | ||||
|       case 0: | ||||
|         showConfirmationDialog(this, { | ||||
|           text: this.hass.localize( | ||||
|             "ui.panel.config.cloud.account.sign_out_confirm" | ||||
|           ), | ||||
|           confirmText: this.hass!.localize("ui.common.yes"), | ||||
|           dismissText: this.hass!.localize("ui.common.no"), | ||||
|           confirm: () => this._logoutFromCloud(), | ||||
|         }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _logoutFromCloud() { | ||||
|     await cloudLogout(this.hass); | ||||
|     fireEvent(this, "ha-refresh-cloud-status"); | ||||
|   } | ||||
|   } | ||||
|  | ||||
|   _computeRTLDirection(hass) { | ||||
|     return computeRTLDirection(hass); | ||||
|   | ||||
| @@ -40,7 +40,7 @@ class ConfigNameForm extends LitElement { | ||||
|             )} | ||||
|             .disabled=${disabled} | ||||
|             .value=${this._nameValue} | ||||
|             @changed=${this._handleChange} | ||||
|             @change=${this._handleChange} | ||||
|           ></ha-textfield> | ||||
|         </div> | ||||
|         <div class="card-actions"> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import { | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
| } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||
| import "../../../components/ha-card"; | ||||
| import "../../../components/ha-icon-next"; | ||||
| @@ -26,10 +26,6 @@ import "../../../components/ha-menu-button"; | ||||
| import "../../../components/ha-button-menu"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { CloudStatus } from "../../../data/cloud"; | ||||
| import { | ||||
|   refreshSupervisorAvailableUpdates, | ||||
|   SupervisorAvailableUpdates, | ||||
| } from "../../../data/supervisor/root"; | ||||
| import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; | ||||
| import "../../../layouts/ha-app-layout"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| @@ -38,10 +34,70 @@ import "../ha-config-section"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import "./ha-config-navigation"; | ||||
| import "./ha-config-updates"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { showToast } from "../../../util/toast"; | ||||
| import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { UpdateDescription } from "../../../data/update"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeRTL } from "../../../common/util/compute_rtl"; | ||||
|  | ||||
| const randomTip = (hass: HomeAssistant) => { | ||||
|   const weighted: string[] = []; | ||||
|   const tips = [ | ||||
|     { | ||||
|       content: hass.localize( | ||||
|         "ui.panel.config.tips.join", | ||||
|         "forums", | ||||
|         html`<a | ||||
|           href="https://community.home-assistant.io" | ||||
|           target="_blank" | ||||
|           rel="noreferrer" | ||||
|           >Forums</a | ||||
|         >`, | ||||
|         "twitter", | ||||
|         html`<a | ||||
|           href=${documentationUrl(hass, `/twitter`)} | ||||
|           target="_blank" | ||||
|           rel="noreferrer" | ||||
|           >Twitter</a | ||||
|         >`, | ||||
|         "discord", | ||||
|         html`<a | ||||
|           href=${documentationUrl(hass, `/join-chat`)} | ||||
|           target="_blank" | ||||
|           rel="noreferrer" | ||||
|           >Chat</a | ||||
|         >`, | ||||
|         "blog", | ||||
|         html`<a | ||||
|           href=${documentationUrl(hass, `/blog`)} | ||||
|           target="_blank" | ||||
|           rel="noreferrer" | ||||
|           >Blog</a | ||||
|         >`, | ||||
|         "newsletter", | ||||
|         html`<span class="keep-together" | ||||
|           ><a | ||||
|             href=${documentationUrl(hass, `/newsletter`)} | ||||
|             target="_blank" | ||||
|             rel="noreferrer" | ||||
|             >Newsletter</a | ||||
|           > | ||||
|           <ha-svg-icon class="new" .path=${mdiNewBox}></ha-svg-icon | ||||
|         ></span>` | ||||
|       ), | ||||
|       weight: 2, | ||||
|     }, | ||||
|     { content: hass.localize("ui.dialogs.quick-bar.key_c_hint"), weight: 1 }, | ||||
|   ]; | ||||
|  | ||||
|   tips.forEach((tip) => { | ||||
|     for (let i = 0; i < tip.weight; i++) { | ||||
|       weighted.push(tip.content); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   return weighted[Math.floor(Math.random() * weighted.length)]; | ||||
| }; | ||||
|  | ||||
| @customElement("ha-config-dashboard") | ||||
| class HaConfigDashboard extends LitElement { | ||||
| @@ -55,11 +111,11 @@ class HaConfigDashboard extends LitElement { | ||||
|   @property() public cloudStatus?: CloudStatus; | ||||
|  | ||||
|   // null means not available | ||||
|   @property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null; | ||||
|   @property() public updates?: UpdateDescription[] | null; | ||||
|  | ||||
|   @property() public showAdvanced!: boolean; | ||||
|  | ||||
|   private _notifyUpdates = false; | ||||
|   @state() private _tip?: string; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
| @@ -72,6 +128,7 @@ class HaConfigDashboard extends LitElement { | ||||
|             ></ha-menu-button> | ||||
|             <div main-title>${this.hass.localize("panel.config")}</div> | ||||
|             <ha-icon-button | ||||
|               .label=${this.hass.localize("ui.dialogs.quick-bar.title")} | ||||
|               .path=${mdiMagnify} | ||||
|               @click=${this._showQuickBar} | ||||
|             ></ha-icon-button> | ||||
| @@ -98,20 +155,23 @@ class HaConfigDashboard extends LitElement { | ||||
|           .isWide=${this.isWide} | ||||
|           full-width | ||||
|         > | ||||
|           ${this.supervisorUpdates === undefined | ||||
|             ? // Hide everything until updates loaded | ||||
|               html`` | ||||
|             : html`${this.supervisorUpdates?.length | ||||
|           ${this.updates === undefined | ||||
|             ? html`<ha-alert .rtl=${computeRTL(this.hass)}> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.config.updates.checking_updates" | ||||
|                 )} | ||||
|               </ha-alert>` | ||||
|             : this.updates?.length | ||||
|             ? html`<ha-card> | ||||
|                 <ha-config-updates | ||||
|                   .hass=${this.hass} | ||||
|                   .narrow=${this.narrow} | ||||
|                         .supervisorUpdates=${this.supervisorUpdates} | ||||
|                   .updates=${this.updates} | ||||
|                 ></ha-config-updates> | ||||
|               </ha-card>` | ||||
|             : ""} | ||||
|           <ha-card> | ||||
|                   ${this.narrow && this.supervisorUpdates?.length | ||||
|             ${this.narrow && this.updates?.length | ||||
|               ? html`<div class="title"> | ||||
|                   ${this.hass.localize("panel.config")} | ||||
|                 </div>` | ||||
| @@ -141,51 +201,11 @@ class HaConfigDashboard extends LitElement { | ||||
|               .showAdvanced=${this.showAdvanced} | ||||
|               .pages=${configSections.dashboard} | ||||
|             ></ha-config-navigation> | ||||
|                 </ha-card>`} | ||||
|           </ha-card> | ||||
|           <div class="tips"> | ||||
|             <ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon> | ||||
|             <span class="tip-word">Tip!</span> | ||||
|             <span class="text"> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.panel.config.tips.join", | ||||
|                 "forums", | ||||
|                 html`<a | ||||
|                   href="https://community.home-assistant.io" | ||||
|                   target="_blank" | ||||
|                   rel="noreferrer" | ||||
|                   >Forums</a | ||||
|                 >`, | ||||
|                 "twitter", | ||||
|                 html`<a | ||||
|                   href=${documentationUrl(this.hass, `/twitter`)} | ||||
|                   target="_blank" | ||||
|                   rel="noreferrer" | ||||
|                   >Twitter</a | ||||
|                 >`, | ||||
|                 "discord", | ||||
|                 html`<a | ||||
|                   href=${documentationUrl(this.hass, `/join-chat`)} | ||||
|                   target="_blank" | ||||
|                   rel="noreferrer" | ||||
|                   >Chat</a | ||||
|                 >`, | ||||
|                 "blog", | ||||
|                 html`<a | ||||
|                   href=${documentationUrl(this.hass, `/blog`)} | ||||
|                   target="_blank" | ||||
|                   rel="noreferrer" | ||||
|                   >Blog</a | ||||
|                 >`, | ||||
|                 "newsletter", | ||||
|                 html`<a | ||||
|                     href=${documentationUrl(this.hass, `/newsletter`)} | ||||
|                     target="_blank" | ||||
|                     rel="noreferrer" | ||||
|                     >Newsletter</a | ||||
|                   > | ||||
|                   <ha-svg-icon class="new" .path=${mdiNewBox}></ha-svg-icon>` | ||||
|               )} | ||||
|             </span> | ||||
|             <span class="text">${this._tip}</span> | ||||
|           </div> | ||||
|         </ha-config-section> | ||||
|       </ha-app-layout> | ||||
| @@ -195,20 +215,8 @@ class HaConfigDashboard extends LitElement { | ||||
|   protected override updated(changedProps: PropertyValues): void { | ||||
|     super.updated(changedProps); | ||||
|  | ||||
|     if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) { | ||||
|       return; | ||||
|     } | ||||
|     this._notifyUpdates = false; | ||||
|     if (this.supervisorUpdates?.length) { | ||||
|       showToast(this, { | ||||
|         message: this.hass.localize( | ||||
|           "ui.panel.config.updates.updates_refreshed" | ||||
|         ), | ||||
|       }); | ||||
|     } else { | ||||
|       showToast(this, { | ||||
|         message: this.hass.localize("ui.panel.config.updates.no_new_updates"), | ||||
|       }); | ||||
|     if (!this._tip && changedProps.has("hass")) { | ||||
|       this._tip = randomTip(this.hass); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -222,18 +230,16 @@ class HaConfigDashboard extends LitElement { | ||||
|   private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { | ||||
|     switch (ev.detail.index) { | ||||
|       case 0: | ||||
|         if (isComponentLoaded(this.hass, "hassio")) { | ||||
|           this._notifyUpdates = true; | ||||
|           await refreshSupervisorAvailableUpdates(this.hass); | ||||
|           fireEvent(this, "ha-refresh-supervisor"); | ||||
|         if (isComponentLoaded(this.hass, "update")) { | ||||
|           fireEvent(this, "ha-refresh-updates"); | ||||
|           return; | ||||
|         } | ||||
|         showAlertDialog(this, { | ||||
|           title: this.hass.localize( | ||||
|             "ui.panel.config.updates.check_unavailable.title" | ||||
|             "ui.panel.config.updates.update_not_loaded.title" | ||||
|           ), | ||||
|           text: this.hass.localize( | ||||
|             "ui.panel.config.updates.check_unavailable.description" | ||||
|             "ui.panel.config.updates.update_not_loaded.description" | ||||
|           ), | ||||
|           warning: true, | ||||
|         }); | ||||
| @@ -293,6 +299,10 @@ class HaConfigDashboard extends LitElement { | ||||
|         .new { | ||||
|           color: var(--primary-color); | ||||
|         } | ||||
|  | ||||
|         .keep-together { | ||||
|           display: inline-block; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,21 +1,48 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { mdiPackageVariant } from "@mdi/js"; | ||||
| import "@polymer/paper-item/paper-icon-item"; | ||||
| import "@polymer/paper-item/paper-item-body"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import "../../../components/ha-alert"; | ||||
| import "../../../components/ha-icon-next"; | ||||
| import "../../../components/ha-logo-svg"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { SupervisorAvailableUpdates } from "../../../data/supervisor/root"; | ||||
| import { UpdateDescription } from "../../../data/update"; | ||||
| import { showUpdateDialog } from "../../../dialogs/update-dialog/show-ha-update-dialog"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import "../../../components/ha-icon-next"; | ||||
| import { brandsUrl } from "../../../util/brands-url"; | ||||
|  | ||||
| export const SUPERVISOR_UPDATE_NAMES = { | ||||
|   core: "Home Assistant Core", | ||||
|   os: "Home Assistant Operating System", | ||||
|   supervisor: "Home Assistant Supervisor", | ||||
| }; | ||||
| const sortUpdates = memoizeOne((a: UpdateDescription, b: UpdateDescription) => { | ||||
|   if (a.domain === "hassio" && b.domain === "hassio") { | ||||
|     if (a.identifier === "core") { | ||||
|       return -1; | ||||
|     } | ||||
|     if (b.identifier === "core") { | ||||
|       return 1; | ||||
|     } | ||||
|     if (a.identifier === "supervisor") { | ||||
|       return -1; | ||||
|     } | ||||
|     if (b.identifier === "supervisor") { | ||||
|       return 1; | ||||
|     } | ||||
|     if (a.identifier === "os") { | ||||
|       return -1; | ||||
|     } | ||||
|     if (b.identifier === "os") { | ||||
|       return 1; | ||||
|     } | ||||
|   } | ||||
|   if (a.domain === "hassio") { | ||||
|     return -1; | ||||
|   } | ||||
|   if (b.domain === "hassio") { | ||||
|     return 1; | ||||
|   } | ||||
|   return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1; | ||||
| }); | ||||
|  | ||||
| @customElement("ha-config-updates") | ||||
| class HaConfigUpdates extends LitElement { | ||||
| @@ -24,62 +51,62 @@ class HaConfigUpdates extends LitElement { | ||||
|   @property({ type: Boolean }) public narrow!: boolean; | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public supervisorUpdates?: SupervisorAvailableUpdates[] | null; | ||||
|   public updates?: UpdateDescription[] | null; | ||||
|  | ||||
|   @state() private _showAll = false; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.supervisorUpdates?.length) { | ||||
|     if (!this.updates?.length) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     // Make sure the first updates shown are for the Supervisor | ||||
|     const sortedUpdates = this.updates.sort((a, b) => sortUpdates(a, b)); | ||||
|  | ||||
|     const updates = | ||||
|       this._showAll || this.supervisorUpdates.length <= 3 | ||||
|         ? this.supervisorUpdates | ||||
|         : this.supervisorUpdates.slice(0, 2); | ||||
|       this._showAll || sortedUpdates.length <= 3 | ||||
|         ? sortedUpdates | ||||
|         : sortedUpdates.slice(0, 2); | ||||
|  | ||||
|     return html` | ||||
|       <div class="title"> | ||||
|         ${this.hass.localize("ui.panel.config.updates.title", { | ||||
|           count: this.supervisorUpdates.length, | ||||
|           count: sortedUpdates.length, | ||||
|         })} | ||||
|       </div> | ||||
|       ${updates.map( | ||||
|         (update) => html` | ||||
|           <a href="/hassio${update.panel_path}"> | ||||
|             <paper-icon-item> | ||||
|           <paper-icon-item @click=${this._showUpdate} .update=${update}> | ||||
|             <span slot="item-icon" class="icon"> | ||||
|                 ${update.update_type === "addon" | ||||
|                   ? update.icon | ||||
|                     ? html`<img src="/api/hassio${update.icon}" />` | ||||
|                     : html`<ha-svg-icon | ||||
|                         .path=${mdiPackageVariant} | ||||
|                       ></ha-svg-icon>` | ||||
|                   : html`<ha-logo-svg></ha-logo-svg>`} | ||||
|               <img | ||||
|                 src=${update.icon_url || | ||||
|                 brandsUrl({ | ||||
|                   domain: update.domain, | ||||
|                   type: "icon", | ||||
|                   useFallback: true, | ||||
|                   darkOptimized: this.hass.themes?.darkMode, | ||||
|                 })} | ||||
|               /> | ||||
|             </span> | ||||
|             <paper-item-body two-line> | ||||
|                 ${update.update_type === "addon" | ||||
|                   ? update.name | ||||
|                   : SUPERVISOR_UPDATE_NAMES[update.update_type!]} | ||||
|               ${update.name} | ||||
|               <div secondary> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.config.updates.version_available", | ||||
|                   { | ||||
|                       version_available: update.version_latest, | ||||
|                     version_available: update.available_version, | ||||
|                   } | ||||
|                 )} | ||||
|               </div> | ||||
|             </paper-item-body> | ||||
|               ${!this.narrow ? html`<ha-icon-next></ha-icon-next>` : ""} | ||||
|           </paper-icon-item> | ||||
|           </a> | ||||
|         ` | ||||
|       )} | ||||
|       ${!this._showAll && this.supervisorUpdates.length >= 4 | ||||
|       ${!this._showAll && this.updates.length >= 4 | ||||
|         ? html` | ||||
|             <button class="show-more" @click=${this._showAllClicked}> | ||||
|               ${this.hass.localize("ui.panel.config.updates.more_updates", { | ||||
|                 count: this.supervisorUpdates!.length - updates.length, | ||||
|                 count: this.updates!.length - updates.length, | ||||
|               })} | ||||
|             </button> | ||||
|           ` | ||||
| @@ -91,6 +118,14 @@ class HaConfigUpdates extends LitElement { | ||||
|     this._showAll = true; | ||||
|   } | ||||
|  | ||||
|   private _showUpdate(ev) { | ||||
|     const update = ev.currentTarget.update as UpdateDescription; | ||||
|     showUpdateDialog(this, { | ||||
|       update, | ||||
|       refreshCallback: () => fireEvent(this, "ha-refresh-updates"), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup[] { | ||||
|     return [ | ||||
|       css` | ||||
| @@ -139,6 +174,9 @@ class HaConfigUpdates extends LitElement { | ||||
|           outline: none; | ||||
|           text-decoration: underline; | ||||
|         } | ||||
|         paper-icon-item { | ||||
|           cursor: pointer; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -438,6 +438,13 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|             )} | ||||
|             .path=${mdiFilterVariant} | ||||
|           ></ha-icon-button> | ||||
|           ${this.narrow && activeFilters?.length | ||||
|             ? html`<mwc-list-item @click=${this._clearFilter} | ||||
|                 >${this.hass.localize("ui.components.data-table.filtering_by")} | ||||
|                 ${activeFilters.join(", ")} | ||||
|                 <span class="clear">Clear</span></mwc-list-item | ||||
|               >` | ||||
|             : ""} | ||||
|           <ha-check-list-item | ||||
|             left | ||||
|             @request-selected=${this._showDisabledChanged} | ||||
| @@ -523,6 +530,11 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|         ha-button-menu { | ||||
|           margin-left: 8px; | ||||
|         } | ||||
|         .clear { | ||||
|           color: var(--primary-color); | ||||
|           padding-left: 8px; | ||||
|           text-transform: uppercase; | ||||
|         } | ||||
|       `, | ||||
|       haStyle, | ||||
|     ]; | ||||
|   | ||||
| @@ -86,6 +86,7 @@ export class EnergyDeviceSettings extends LitElement { | ||||
|                     : device.stat_consumption}</span | ||||
|                 > | ||||
|                 <ha-icon-button | ||||
|                   .label=${this.hass.localize("ui.common.delete")} | ||||
|                   @click=${this._deleteDevice} | ||||
|                   .device=${device} | ||||
|                   .path=${mdiDelete} | ||||
|   | ||||
| @@ -585,6 +585,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|                 )} | ||||
|                 .path=${mdiFilterVariant} | ||||
|               ></ha-icon-button> | ||||
|               ${this.narrow && activeFilters?.length | ||||
|                 ? html`<mwc-list-item @click=${this._clearFilter} | ||||
|                     >${this.hass.localize( | ||||
|                       "ui.components.data-table.filtering_by" | ||||
|                     )} | ||||
|                     ${activeFilters.join(", ")} | ||||
|                     <span class="clear">Clear</span></mwc-list-item | ||||
|                   >` | ||||
|                 : ""} | ||||
|               <ha-check-list-item | ||||
|                 @request-selected=${this._showDisabledChanged} | ||||
|                 .selected=${this._showDisabled} | ||||
| @@ -896,6 +905,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|         ha-button-menu { | ||||
|           margin-left: 8px; | ||||
|         } | ||||
|         .clear { | ||||
|           color: var(--primary-color); | ||||
|           padding-left: 8px; | ||||
|           text-transform: uppercase; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -27,20 +27,18 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { isComponentLoaded } from "../../common/config/is_component_loaded"; | ||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | ||||
| import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; | ||||
| import { | ||||
|   fetchSupervisorAvailableUpdates, | ||||
|   SupervisorAvailableUpdates, | ||||
| } from "../../data/supervisor/root"; | ||||
| import { fetchUpdateInfo, UpdateDescription } from "../../data/update"; | ||||
| import "../../layouts/hass-loading-screen"; | ||||
| import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; | ||||
| import { PageNavigation } from "../../layouts/hass-tabs-subpage"; | ||||
| import { HomeAssistant, Route } from "../../types"; | ||||
| import { showToast } from "../../util/toast"; | ||||
|  | ||||
| declare global { | ||||
|   // for fire event | ||||
|   interface HASSDomEvents { | ||||
|     "ha-refresh-cloud-status": undefined; | ||||
|     "ha-refresh-supervisor": undefined; | ||||
|     "ha-refresh-updates": undefined; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -407,7 +405,7 @@ class HaPanelConfig extends HassRouterPage { | ||||
|  | ||||
|   @state() private _cloudStatus?: CloudStatus; | ||||
|  | ||||
|   @state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null; | ||||
|   @state() private _updates?: UpdateDescription[] | null; | ||||
|  | ||||
|   private _listeners: Array<() => void> = []; | ||||
|  | ||||
| @@ -443,18 +441,18 @@ class HaPanelConfig extends HassRouterPage { | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|     if (isComponentLoaded(this.hass, "hassio")) { | ||||
|       this._loadSupervisorUpdates(); | ||||
|       this.addEventListener("ha-refresh-supervisor", () => { | ||||
|         this._loadSupervisorUpdates(); | ||||
|     if (isComponentLoaded(this.hass, "update")) { | ||||
|       this._loadUpdates(); | ||||
|       this.addEventListener("ha-refresh-updates", () => { | ||||
|         this._loadUpdates(); | ||||
|       }); | ||||
|       this.addEventListener("connection-status", (ev) => { | ||||
|         if (ev.detail === "connected") { | ||||
|           this._loadSupervisorUpdates(); | ||||
|           this._loadUpdates(); | ||||
|         } | ||||
|       }); | ||||
|     } else { | ||||
|       this._supervisorUpdates = null; | ||||
|       this._updates = null; | ||||
|     } | ||||
|     this.addEventListener("ha-refresh-cloud-status", () => | ||||
|       this._updateCloudStatus() | ||||
| @@ -486,7 +484,7 @@ class HaPanelConfig extends HassRouterPage { | ||||
|         isWide, | ||||
|         narrow: this.narrow, | ||||
|         cloudStatus: this._cloudStatus, | ||||
|         supervisorUpdates: this._supervisorUpdates, | ||||
|         updates: this._updates, | ||||
|       }); | ||||
|     } else { | ||||
|       el.route = this.routeTail; | ||||
| @@ -495,7 +493,7 @@ class HaPanelConfig extends HassRouterPage { | ||||
|       el.isWide = isWide; | ||||
|       el.narrow = this.narrow; | ||||
|       el.cloudStatus = this._cloudStatus; | ||||
|       el.supervisorUpdates = this._supervisorUpdates; | ||||
|       el.updates = this._updates; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -514,13 +512,33 @@ class HaPanelConfig extends HassRouterPage { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _loadSupervisorUpdates(): Promise<void> { | ||||
|   private async _loadUpdates(): Promise<void> { | ||||
|     const _showToast = this._updates !== undefined; | ||||
|  | ||||
|     if (_showToast) { | ||||
|       showToast(this, { | ||||
|         message: this.hass.localize("ui.panel.config.updates.checking_updates"), | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       this._supervisorUpdates = await fetchSupervisorAvailableUpdates( | ||||
|         this.hass | ||||
|       ); | ||||
|       this._updates = await fetchUpdateInfo(this.hass); | ||||
|     } catch (err) { | ||||
|       this._supervisorUpdates = null; | ||||
|       this._updates = null; | ||||
|     } | ||||
|  | ||||
|     if (_showToast) { | ||||
|       if (this._updates?.length) { | ||||
|         showToast(this, { | ||||
|           message: this.hass.localize( | ||||
|             "ui.panel.config.updates.updates_refreshed" | ||||
|           ), | ||||
|         }); | ||||
|       } else { | ||||
|         showToast(this, { | ||||
|           message: this.hass.localize("ui.panel.config.updates.no_new_updates"), | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import { ifDefined } from "lit/directives/if-defined"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import type { HASSDomEvent } from "../../../common/dom/fire_event"; | ||||
| import { navigate } from "../../../common/navigate"; | ||||
| import "../../../common/search/search-input"; | ||||
| import "../../../components/search-input"; | ||||
| import { caseInsensitiveStringCompare } from "../../../common/string/compare"; | ||||
| import type { LocalizeFunc } from "../../../common/translations/localize"; | ||||
| import { extractSearchParam } from "../../../common/url/search-params"; | ||||
|   | ||||
| @@ -213,11 +213,11 @@ export class HaIntegrationCard extends LitElement { | ||||
|       } else { | ||||
|         stateTextExtra = html` | ||||
|           <br /> | ||||
|           <a href="/config/logs" | ||||
|             >${this.hass.localize( | ||||
|           <a href=${`/config/logs/?filter=${item.domain}`}> | ||||
|             ${this.hass.localize( | ||||
|               "ui.panel.config.integrations.config_entry.check_the_logs" | ||||
|             )}</a | ||||
|           > | ||||
|             )} | ||||
|           </a> | ||||
|         `; | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import { | ||||
|   Node, | ||||
| } from "vis-network/peer/esm/vis-network"; | ||||
| import { navigate } from "../../../../../common/navigate"; | ||||
| import "../../../../../common/search/search-input"; | ||||
| import "../../../../../components/search-input"; | ||||
| import "../../../../../components/device/ha-device-picker"; | ||||
| import "../../../../../components/ha-button-menu"; | ||||
| import "../../../../../components/ha-checkbox"; | ||||
| @@ -144,8 +144,6 @@ export class ZHANetworkVisualizationPage extends LitElement { | ||||
|               <div slot="header"> | ||||
|                 <search-input | ||||
|                   .hass=${this.hass} | ||||
|                   no-label-float | ||||
|                   no-underline | ||||
|                   class="header" | ||||
|                   @value-changed=${this._handleSearchChange} | ||||
|                   .filter=${this._filter} | ||||
| @@ -161,8 +159,6 @@ export class ZHANetworkVisualizationPage extends LitElement { | ||||
|           ${!this.narrow | ||||
|             ? html`<search-input | ||||
|                 .hass=${this.hass} | ||||
|                 no-label-float | ||||
|                 no-underline | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|                 .filter=${this._filter} | ||||
|                 .label=${this.hass.localize( | ||||
|   | ||||
| @@ -347,7 +347,7 @@ class ZWaveJSConfigDashboard extends LitElement { | ||||
|       } else { | ||||
|         stateTextExtra = html` | ||||
|           <br /> | ||||
|           <a href="/config/logs" | ||||
|           <a href="/config/logs?filter=zwave_js" | ||||
|             >${this.hass.localize( | ||||
|               "ui.panel.config.integrations.config_entry.check_the_logs" | ||||
|             )}</a | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import "../../../layouts/hass-tabs-subpage"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../types"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import "../../../common/search/search-input"; | ||||
| import "../../../components/search-input"; | ||||
| import { extractSearchParam } from "../../../common/url/search-params"; | ||||
| import "./error-log-card"; | ||||
| import "./system-log-card"; | ||||
| @@ -22,7 +22,7 @@ export class HaConfigLogs extends LitElement { | ||||
|  | ||||
|   @property() public route!: Route; | ||||
|  | ||||
|   @state() private _filter = extractSearchParam("filter") ?? ""; | ||||
|   @state() private _filter = extractSearchParam("filter") || ""; | ||||
|  | ||||
|   @query("system-log-card", true) private systemLog?: SystemLogCard; | ||||
|  | ||||
| @@ -43,8 +43,6 @@ export class HaConfigLogs extends LitElement { | ||||
|           <div slot="header"> | ||||
|             <search-input | ||||
|               class="header" | ||||
|               no-label-float | ||||
|               no-underline | ||||
|               @value-changed=${this._filterChanged} | ||||
|               .hass=${this.hass} | ||||
|               .filter=${this._filter} | ||||
| @@ -55,9 +53,6 @@ export class HaConfigLogs extends LitElement { | ||||
|       : html` | ||||
|           <div class="search"> | ||||
|             <search-input | ||||
|               autofocus | ||||
|               no-label-float | ||||
|               no-underline | ||||
|               @value-changed=${this._filterChanged} | ||||
|               .hass=${this.hass} | ||||
|               .filter=${this._filter} | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import "../../../components/ha-code-editor"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import "../../../components/ha-checkbox"; | ||||
| import "../../../components/search-input"; | ||||
| import "../../../components/ha-expansion-panel"; | ||||
| import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { EventsMixin } from "../../../mixins/events-mixin"; | ||||
| @@ -85,7 +86,8 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { | ||||
|           padding: 0; | ||||
|         } | ||||
|  | ||||
|         .filters ha-textfield { | ||||
|         .filters search-input { | ||||
|           display: block; | ||||
|           --mdc-text-field-fill-color: transparent; | ||||
|         } | ||||
|  | ||||
| @@ -252,28 +254,27 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { | ||||
|           </tr> | ||||
|           <tr class="filters"> | ||||
|             <th> | ||||
|               <ha-textfield | ||||
|               <search-input | ||||
|                 label="[[localize('ui.panel.developer-tools.tabs.states.filter_entities')]]" | ||||
|                 type="search" | ||||
|                 value="[[_entityFilter]]" | ||||
|                 on-input="_entityFilterChanged" | ||||
|               ></ha-textfield> | ||||
|                 on-value-changed="_entityFilterChanged" | ||||
|               ></search-input> | ||||
|             </th> | ||||
|             <th> | ||||
|               <ha-textfield | ||||
|               <search-input | ||||
|                 label="[[localize('ui.panel.developer-tools.tabs.states.filter_states')]]" | ||||
|                 type="search" | ||||
|                 value="[[_stateFilter]]" | ||||
|                 on-input="_stateFilterChanged" | ||||
|               ></ha-textfield> | ||||
|                 on-value-changed="_stateFilterChanged" | ||||
|               ></search-input> | ||||
|             </th> | ||||
|             <th hidden$="[[!computeShowAttributes(narrow, _showAttributes)]]"> | ||||
|               <ha-textfield | ||||
|               <search-input | ||||
|                 label="[[localize('ui.panel.developer-tools.tabs.states.filter_attributes')]]" | ||||
|                 type="search" | ||||
|                 value="[[_attributeFilter]]" | ||||
|                 on-input="_attributeFilterChanged" | ||||
|               ></ha-textfield> | ||||
|                 on-value-changed="_attributeFilterChanged" | ||||
|               ></search-input> | ||||
|             </th> | ||||
|           </tr> | ||||
|           <tr hidden$="[[!computeShowEntitiesPlaceholder(_entities)]]"> | ||||
| @@ -440,15 +441,15 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { | ||||
|   } | ||||
|  | ||||
|   _entityFilterChanged(ev) { | ||||
|     this._entityFilter = ev.target.value; | ||||
|     this._entityFilter = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   _stateFilterChanged(ev) { | ||||
|     this._stateFilter = ev.target.value; | ||||
|     this._stateFilter = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   _attributeFilterChanged(ev) { | ||||
|     this._attributeFilter = ev.target.value; | ||||
|     this._attributeFilter = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   _getHistoryURL(entityId, inputDate) { | ||||
|   | ||||
| @@ -289,6 +289,12 @@ class HaPanelDevTemplate extends LitElement { | ||||
|         .rendered.error { | ||||
|           color: var(--error-color); | ||||
|         } | ||||
|  | ||||
|         @media all and (max-width: 870px) { | ||||
|           .render-pane { | ||||
|             max-width: 100%; | ||||
|           } | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -32,6 +32,8 @@ class HuiEnergyCarbonGaugeCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public getCardSize(): number { | ||||
|     return 4; | ||||
|   } | ||||
|   | ||||
| @@ -49,6 +49,8 @@ export class HuiEnergyDevicesGraphCard | ||||
|  | ||||
|   @query("ha-chart-base") private _chart?: HaChartBase; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -43,6 +43,8 @@ class HuiEnergyDistrubutionCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public setConfig(config: EnergyDistributionCardConfig): void { | ||||
|     this._config = config; | ||||
|   } | ||||
|   | ||||
| @@ -62,6 +62,8 @@ export class HuiEnergyGasGraphCard | ||||
|  | ||||
|   @state() private _unit?: string; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -35,6 +35,8 @@ class HuiEnergyGridGaugeCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass!, { | ||||
|   | ||||
| @@ -30,6 +30,8 @@ class HuiEnergySolarGaugeCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass!, { | ||||
|   | ||||
| @@ -61,6 +61,8 @@ export class HuiEnergySolarGraphCard | ||||
|  | ||||
|   @state() private _end = endOfToday(); | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -45,6 +45,8 @@ export class HuiEnergySourcesTableCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -50,6 +50,8 @@ export class HuiEnergyUsageGraphCard | ||||
|  | ||||
|   @state() private _end = endOfToday(); | ||||
|  | ||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       getEnergyDataCollection(this.hass, { | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import { styleMap } from "lit/directives/style-map"; | ||||
| import { until } from "lit/directives/until"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../common/search/search-input"; | ||||
| import "../../../../components/search-input"; | ||||
| import "../../../../components/ha-circular-progress"; | ||||
| import { UNAVAILABLE_STATES } from "../../../../data/entity"; | ||||
| import type { | ||||
|   | ||||
| @@ -90,11 +90,9 @@ export class HuiAlarmPanelCardEditor | ||||
|  | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return `${this.hass!.localize( | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       )} (${this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.config.required" | ||||
|       )})`; | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (schema.name === "name") { | ||||
|   | ||||
| @@ -107,11 +107,9 @@ export class HuiEntityCardEditor | ||||
|  | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return `${this.hass!.localize( | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       )} (${this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.config.required" | ||||
|       )})`; | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return this.hass!.localize( | ||||
|   | ||||
| @@ -1,25 +1,18 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { assert } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import { domainIcon } from "../../../../common/entity/domain_icon"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-icon-picker"; | ||||
| import "../../../../components/ha-select"; | ||||
| import "../../../../components/ha-switch"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { EntitiesCardEntityConfig } from "../../cards/types"; | ||||
| import "../../components/hui-action-editor"; | ||||
| import "../../components/hui-entity-editor"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceRowEditor } from "../../types"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { EntitiesCardEntityConfig } from "../../cards/types"; | ||||
| import type { LovelaceRowEditor } from "../../types"; | ||||
| import { entitiesConfigStruct } from "../structs/entities-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const SecondaryInfoValues: { [key: string]: { domains?: string[] } } = { | ||||
|   "entity-id": {}, | ||||
| @@ -45,127 +38,105 @@ export class HuiGenericEntityRowEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|   private _schema = memoizeOne( | ||||
|     ( | ||||
|       entity: string, | ||||
|       icon: string | undefined, | ||||
|       entityState: HassEntity, | ||||
|       localize: LocalizeFunc | ||||
|     ): HaFormSchema[] => { | ||||
|       const domain = computeDomain(entity); | ||||
|  | ||||
|   get _name(): string { | ||||
|     return this._config!.name || ""; | ||||
|   } | ||||
|  | ||||
|   get _icon(): string { | ||||
|     return this._config!.icon || ""; | ||||
|   } | ||||
|  | ||||
|   get _secondary_info(): string { | ||||
|     return this._config!.secondary_info || ""; | ||||
|       return [ | ||||
|         { name: "entity", required: true, selector: { entity: {} } }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           name: "", | ||||
|           schema: [ | ||||
|             { name: "name", selector: { text: {} } }, | ||||
|             { | ||||
|               name: "icon", | ||||
|               selector: { | ||||
|                 icon: { | ||||
|                   placeholder: icon || entityState?.attributes.icon, | ||||
|                   fallbackPath: | ||||
|                     !icon && !entityState?.attributes.icon && entityState | ||||
|                       ? domainIcon(domain, entityState) | ||||
|                       : undefined, | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           name: "secondary_info", | ||||
|           selector: { | ||||
|             select: { | ||||
|               options: Object.keys(SecondaryInfoValues) | ||||
|                 .filter( | ||||
|                   (info) => | ||||
|                     !("domains" in SecondaryInfoValues[info]) || | ||||
|                     ("domains" in SecondaryInfoValues[info] && | ||||
|                       SecondaryInfoValues[info].domains!.includes(domain)) | ||||
|                 ) | ||||
|                 .map((info) => ({ | ||||
|                   value: info, | ||||
|                   label: localize( | ||||
|                     `ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}` | ||||
|                   ), | ||||
|                 })), | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       ]; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const domain = computeDomain(this._entity); | ||||
|     const entityState = this.hass.states[this._entity]; | ||||
|     const entityState = this.hass.states[this._config.entity]; | ||||
|  | ||||
|     const schema = this._schema( | ||||
|       this._config.entity, | ||||
|       this._config.icon, | ||||
|       entityState, | ||||
|       this.hass.localize | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           allow-custom-entity | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._config.entity} | ||||
|           .configValue=${"entity"} | ||||
|           @change=${this._valueChanged} | ||||
|         ></ha-entity-picker> | ||||
|         <div class="side-by-side"> | ||||
|           <paper-input | ||||
|             .label=${this.hass!.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.name" | ||||
|             )} | ||||
|             .value=${this._config.name} | ||||
|             .configValue=${"name"} | ||||
|         .data=${this._config} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|           <ha-icon-picker | ||||
|             .label=${this.hass!.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.icon" | ||||
|             )} | ||||
|             .value=${this._config.icon} | ||||
|             .configValue=${"icon"} | ||||
|             .placeholder=${entityState?.attributes.icon} | ||||
|             .fallbackPath=${!this._icon && | ||||
|             !entityState?.attributes.icon && | ||||
|             entityState | ||||
|               ? domainIcon(computeDomain(entityState.entity_id), entityState) | ||||
|               : undefined} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></ha-icon-picker> | ||||
|         </div> | ||||
|         <ha-select | ||||
|           label="Secondary Info" | ||||
|           .configValue=${"secondary_info"} | ||||
|           @selected=${this._valueChanged} | ||||
|           @closed=${stopPropagation} | ||||
|           .value=${this._config.secondary_info || "none"} | ||||
|           naturalMenuWidth | ||||
|           fixedMenuPosition | ||||
|         > | ||||
|           <mwc-list-item value="" | ||||
|             >${this.hass!.localize( | ||||
|               "ui.panel.lovelace.editor.card.entities.secondary_info_values.none" | ||||
|             )}</mwc-list-item | ||||
|           > | ||||
|           ${Object.keys(SecondaryInfoValues).map((info) => { | ||||
|             if ( | ||||
|               !("domains" in SecondaryInfoValues[info]) || | ||||
|               ("domains" in SecondaryInfoValues[info] && | ||||
|                 SecondaryInfoValues[info].domains!.includes(domain)) | ||||
|             ) { | ||||
|               return html` | ||||
|                 <mwc-list-item .value=${info}> | ||||
|                   ${this.hass!.localize( | ||||
|                     `ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}` | ||||
|                   )} | ||||
|                 </mwc-list-item> | ||||
|               `; | ||||
|             } | ||||
|             return ""; | ||||
|           })} | ||||
|         </ha-select> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|     const value = target.value || ev.detail?.item?.value; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === value) { | ||||
|       return; | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|     if (target.configValue) { | ||||
|       if (value === "" || !value) { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         this._config = { | ||||
|           ...this._config, | ||||
|           [target.configValue!]: value, | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|       ) || | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.entity-row.${schema.name}` | ||||
|       ) | ||||
|     ); | ||||
|   }; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,16 +1,13 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { HumidifierCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { HumidifierCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -21,7 +18,21 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const includeDomains = ["humidifier"]; | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { | ||||
|     name: "entity", | ||||
|     required: true, | ||||
|     selector: { entity: { domain: "humidifer" } }, | ||||
|   }, | ||||
|   { | ||||
|     type: "grid", | ||||
|     name: "", | ||||
|     schema: [ | ||||
|       { name: "name", selector: { text: {} } }, | ||||
|       { name: "theme", selector: { theme: {} } }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-humidifier-card-editor") | ||||
| export class HuiHumidifierCardEditor | ||||
| @@ -37,81 +48,37 @@ export class HuiHumidifierCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _name(): string { | ||||
|     return this._config!.name || ""; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.entity" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           .includeDomains=${includeDomains} | ||||
|           @change=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.name" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._name} | ||||
|           .configValue=${"name"} | ||||
|         .data=${this._config} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <hui-theme-select-editor | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></hui-theme-select-editor> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (target.configValue) { | ||||
|       if (target.value === "") { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         this._config = { ...this._config, [target.configValue!]: target.value }; | ||||
|       } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,14 +1,13 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { IframeCardConfig } from "../../cards/types"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { IframeCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -19,6 +18,18 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { name: "title", selector: { text: {} } }, | ||||
|   { | ||||
|     name: "", | ||||
|     type: "grid", | ||||
|     schema: [ | ||||
|       { name: "url", required: true, selector: { text: {} } }, | ||||
|       { name: "aspect_ratio", selector: { text: {} } }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-iframe-card-editor") | ||||
| export class HuiIframeCardEditor | ||||
|   extends LitElement | ||||
| @@ -33,85 +44,28 @@ export class HuiIframeCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _title(): string { | ||||
|     return this._config!.title || ""; | ||||
|   } | ||||
|  | ||||
|   get _url(): string { | ||||
|     return this._config!.url || ""; | ||||
|   } | ||||
|  | ||||
|   get _aspect_ratio(): string { | ||||
|     return this._config!.aspect_ratio || ""; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.url" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|           .value=${this._url} | ||||
|           .configValue=${"url"} | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${this._config} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <div class="side-by-side"> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.title" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._title} | ||||
|             .configValue=${"title"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.aspect_ratio" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._aspect_ratio} | ||||
|             .configValue=${"aspect_ratio"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|     const value = target.value; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === value) { | ||||
|       return; | ||||
|     } | ||||
|     if (target.configValue) { | ||||
|       if (value === "") { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         this._config = { ...this._config, [target.configValue!]: value }; | ||||
|       } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   } | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => | ||||
|     this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,22 +1,21 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, object, optional, string, assign } from "superstruct"; | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/ha-icon-picker"; | ||||
| import { ActionConfig } from "../../../../data/lovelace"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { LightCardConfig } from "../../cards/types"; | ||||
| import type { ActionConfig } from "../../../../data/lovelace"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { LightCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-action-editor"; | ||||
| import "../../components/hui-entity-editor"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { EditorTarget } from "../types"; | ||||
| import type { EditorTarget } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { domainIcon } from "../../../../common/entity/domain_icon"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -30,8 +29,6 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const includeDomains = ["light"]; | ||||
|  | ||||
| @customElement("hui-light-card-editor") | ||||
| export class HuiLightCardEditor | ||||
|   extends LitElement | ||||
| @@ -46,21 +43,39 @@ export class HuiLightCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _name(): string { | ||||
|     return this._config!.name || ""; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _icon(): string { | ||||
|     return this._config!.icon || ""; | ||||
|   } | ||||
|   private _schema = memoizeOne( | ||||
|     ( | ||||
|       entity: string, | ||||
|       icon: string | undefined, | ||||
|       entityState: HassEntity | ||||
|     ): HaFormSchema[] => [ | ||||
|       { | ||||
|         name: "entity", | ||||
|         required: true, | ||||
|         selector: { entity: { domain: "light" } }, | ||||
|       }, | ||||
|       { | ||||
|         type: "grid", | ||||
|         name: "", | ||||
|         schema: [ | ||||
|           { name: "name", selector: { text: {} } }, | ||||
|           { | ||||
|             name: "icon", | ||||
|             selector: { | ||||
|               icon: { | ||||
|                 placeholder: icon || entityState?.attributes.icon, | ||||
|                 fallbackPath: | ||||
|                   !icon && !entityState?.attributes.icon && entityState | ||||
|                     ? domainIcon(computeDomain(entity), entityState) | ||||
|                     : undefined, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { name: "theme", selector: { theme: {} } }, | ||||
|     ] | ||||
|   ); | ||||
|  | ||||
|   get _hold_action(): ActionConfig { | ||||
|     return this._config!.hold_action || { action: "more-info" }; | ||||
| @@ -84,59 +99,22 @@ export class HuiLightCardEditor | ||||
|       "none", | ||||
|     ]; | ||||
|  | ||||
|     const entityState = this.hass.states[this._entity]; | ||||
|     const entityState = this.hass.states[this._config.entity]; | ||||
|     const schema = this._schema( | ||||
|       this._config.entity, | ||||
|       this._config.icon, | ||||
|       entityState | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${this._config} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       ></ha-form> | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.entity" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           .includeDomains=${includeDomains} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <div class="side-by-side"> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.name" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._name} | ||||
|             .configValue=${"name"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|           <ha-icon-picker | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.icon" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._icon} | ||||
|             .placeholder=${this._icon || entityState?.attributes.icon} | ||||
|             .fallbackPath=${!this._icon && | ||||
|             !entityState?.attributes.icon && | ||||
|             entityState | ||||
|               ? domainIcon(computeDomain(entityState.entity_id), entityState) | ||||
|               : undefined} | ||||
|             .configValue=${"icon"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></ha-icon-picker> | ||||
|         </div> | ||||
|  | ||||
|         <hui-theme-select-editor | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></hui-theme-select-editor> | ||||
|  | ||||
|         <hui-action-editor | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.hold_action" | ||||
| @@ -147,7 +125,7 @@ export class HuiLightCardEditor | ||||
|           .config=${this._hold_action} | ||||
|           .actions=${actions} | ||||
|           .configValue=${"hold_action"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @value-changed=${this._actionChanged} | ||||
|         ></hui-action-editor> | ||||
|  | ||||
|         <hui-action-editor | ||||
| @@ -160,13 +138,13 @@ export class HuiLightCardEditor | ||||
|           .config=${this._double_tap_action} | ||||
|           .actions=${actions} | ||||
|           .configValue=${"double_tap_action"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @value-changed=${this._actionChanged} | ||||
|         ></hui-action-editor> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|   private _actionChanged(ev: CustomEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
| @@ -190,9 +168,33 @@ export class HuiLightCardEditor | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   static styles: CSSResultGroup = [ | ||||
|     configElementStyle, | ||||
|     css` | ||||
|       ha-form, | ||||
|       hui-action-editor { | ||||
|         display: block; | ||||
|         margin-bottom: 24px; | ||||
|         overflow: auto; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { | ||||
|   array, | ||||
| @@ -12,15 +12,11 @@ import { | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/entity/ha-entities-picker"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { LogbookCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-entity-editor"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { LogbookCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { EditorTarget } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -32,6 +28,18 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { name: "title", selector: { text: {} } }, | ||||
|   { | ||||
|     name: "", | ||||
|     type: "grid", | ||||
|     schema: [ | ||||
|       { name: "theme", selector: { theme: {} } }, | ||||
|       { name: "hours_to_show", selector: { number: { mode: "box", min: 1 } } }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-logbook-card-editor") | ||||
| export class HuiLogbookCardEditor | ||||
|   extends LitElement | ||||
| @@ -41,67 +49,28 @@ export class HuiLogbookCardEditor | ||||
|  | ||||
|   @state() private _config?: LogbookCardConfig; | ||||
|  | ||||
|   @state() private _configEntities?: string[]; | ||||
|  | ||||
|   public setConfig(config: LogbookCardConfig): void { | ||||
|     assert(config, cardConfigStruct); | ||||
|     this._config = config; | ||||
|     this._configEntities = config.entities; | ||||
|   } | ||||
|  | ||||
|   get _title(): string { | ||||
|     return this._config!.title || ""; | ||||
|   } | ||||
|  | ||||
|   get _entities(): string[] { | ||||
|     return this._config!.entities || []; | ||||
|   } | ||||
|  | ||||
|   get _hours_to_show(): number { | ||||
|     return this._config!.hours_to_show || 24; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.title" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._title} | ||||
|           .configValue=${"title"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <div class="side-by-side"> | ||||
|           <hui-theme-select-editor | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|             .value=${this._theme} | ||||
|             .configValue=${"theme"} | ||||
|         .data=${this._config} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|           ></hui-theme-select-editor> | ||||
|           <paper-input | ||||
|             type="number" | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.hours_to_show" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._hours_to_show} | ||||
|             min="1" | ||||
|             .configValue=${"hours_to_show"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|       ></ha-form> | ||||
|       <h3> | ||||
|         ${`${this.hass!.localize( | ||||
|           "ui.panel.lovelace.editor.card.generic.entities" | ||||
| @@ -111,48 +80,27 @@ export class HuiLogbookCardEditor | ||||
|       </h3> | ||||
|       <ha-entities-picker | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._configEntities} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         .value=${this._entities} | ||||
|         @value-changed=${this._entitiesChanged} | ||||
|       > | ||||
|       </ha-entities-picker> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (ev.detail && ev.detail.value && Array.isArray(ev.detail.value)) { | ||||
|       this._config = { ...this._config, entities: ev.detail.value }; | ||||
|     } else if (target.configValue) { | ||||
|       if (target.value === "") { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         let value: any = target.value; | ||||
|  | ||||
|         if (target.type === "number") { | ||||
|           value = Number(value); | ||||
|         } | ||||
|  | ||||
|         this._config = { | ||||
|           ...this._config, | ||||
|           [target.configValue!]: value, | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|   private _entitiesChanged(ev: CustomEvent): void { | ||||
|     this._config = { ...this._config!, entities: ev.detail.value }; | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => | ||||
|     this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ) || | ||||
|     this.hass!.localize(`ui.panel.lovelace.editor.card.logbook.${schema.name}`); | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| @@ -12,7 +13,6 @@ import { | ||||
|   assign, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { computeRTLDirection } from "../../../../common/util/compute_rtl"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-switch"; | ||||
| import { PolymerChangedEvent } from "../../../../polymer-types"; | ||||
| @@ -27,6 +27,7 @@ import { entitiesConfigStruct } from "../structs/entities-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -41,6 +42,20 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { name: "title", selector: { text: {} } }, | ||||
|   { | ||||
|     name: "", | ||||
|     type: "grid", | ||||
|     schema: [ | ||||
|       { name: "aspect_ratio", selector: { text: {} } }, | ||||
|       { name: "default_zoom", selector: { number: { mode: "box", min: 0 } } }, | ||||
|       { name: "dark_mode", selector: { boolean: {} } }, | ||||
|       { name: "hours_to_show", selector: { number: { mode: "box", min: 1 } } }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-map-card-editor") | ||||
| export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
| @@ -57,99 +72,24 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { | ||||
|       : []; | ||||
|   } | ||||
|  | ||||
|   get _title(): string { | ||||
|     return this._config!.title || ""; | ||||
|   } | ||||
|  | ||||
|   get _aspect_ratio(): string { | ||||
|     return this._config!.aspect_ratio || ""; | ||||
|   } | ||||
|  | ||||
|   get _default_zoom(): number { | ||||
|     return this._config!.default_zoom || 0; | ||||
|   } | ||||
|  | ||||
|   get _geo_location_sources(): string[] { | ||||
|     return this._config!.geo_location_sources || []; | ||||
|   } | ||||
|  | ||||
|   get _hours_to_show(): number { | ||||
|     return this._config!.hours_to_show || 0; | ||||
|   } | ||||
|  | ||||
|   get _dark_mode(): boolean { | ||||
|     return this._config!.dark_mode || false; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${this._config} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       ></ha-form> | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.title" | ||||
|           )} | ||||
|           (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._title} | ||||
|           .configValue=${"title"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <div class="side-by-side"> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.aspect_ratio" | ||||
|             )} | ||||
|             (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._aspect_ratio} | ||||
|             .configValue=${"aspect_ratio"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.map.default_zoom" | ||||
|             )} | ||||
|             (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             type="number" | ||||
|             .value=${this._default_zoom} | ||||
|             .configValue=${"default_zoom"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|         <div class="side-by-side"> | ||||
|           <ha-formfield | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.map.dark_mode" | ||||
|             )} | ||||
|             .dir=${computeRTLDirection(this.hass)} | ||||
|           > | ||||
|             <ha-switch | ||||
|               .checked=${this._dark_mode} | ||||
|               .configValue=${"dark_mode"} | ||||
|               @change=${this._valueChanged} | ||||
|             ></ha-switch | ||||
|           ></ha-formfield> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.map.hours_to_show" | ||||
|             )} | ||||
|             (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             type="number" | ||||
|             .value=${this._hours_to_show} | ||||
|             .configValue=${"hours_to_show"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|         <hui-entity-editor | ||||
|           .hass=${this.hass} | ||||
|           .entities=${this._configEntities} | ||||
| @@ -167,8 +107,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { | ||||
|             )} | ||||
|             .hass=${this.hass} | ||||
|             .value=${this._geo_location_sources} | ||||
|             .configValue=${"geo_location_sources"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|             @value-changed=${this._geoSourcesChanged} | ||||
|           ></hui-input-list-editor> | ||||
|         </div> | ||||
|       </div> | ||||
| @@ -176,18 +115,15 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { | ||||
|   } | ||||
|  | ||||
|   private _entitiesValueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     if (ev.detail && ev.detail.entities) { | ||||
|       this._config = { ...this._config, entities: ev.detail.entities }; | ||||
|       this._config = { ...this._config!, entities: ev.detail.entities }; | ||||
|  | ||||
|       this._configEntities = processEditorEntities(this._config.entities); | ||||
|       fireEvent(this, "config-changed", { config: this._config }); | ||||
|       fireEvent(this, "config-changed", { config: this._config! }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: PolymerChangedEvent<any>): void { | ||||
|   private _geoSourcesChanged(ev: PolymerChangedEvent<any>): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
| @@ -196,26 +132,34 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     let value = target.checked ?? ev.detail.value; | ||||
|     const value = ev.detail.value; | ||||
|  | ||||
|     if (value && target.type === "number") { | ||||
|       value = Number(value); | ||||
|     } | ||||
|     if (this[`_${target.configValue}`] === value) { | ||||
|     if (this._geo_location_sources === value) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (value === "") { | ||||
|       this._config = { ...this._config }; | ||||
|       delete this._config[target.configValue!]; | ||||
|     } else if (target.configValue) { | ||||
|       delete this._config.geo_location_sources; | ||||
|     } else { | ||||
|       this._config = { | ||||
|         ...this._config, | ||||
|         [target.configValue]: value, | ||||
|         geo_location_sources: value, | ||||
|       }; | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => | ||||
|     this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ) || | ||||
|     this.hass!.localize(`ui.panel.lovelace.editor.card.map.${schema.name}`); | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       configElementStyle, | ||||
|   | ||||
| @@ -1,16 +1,13 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "@polymer/paper-input/paper-textarea"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { MarkdownCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { MarkdownCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -21,6 +18,12 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { name: "title", selector: { text: {} } }, | ||||
|   { name: "content", required: true, selector: { text: { multiline: true } } }, | ||||
|   { name: "theme", selector: { theme: {} } }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-markdown-card-editor") | ||||
| export class HuiMarkdownCardEditor | ||||
|   extends LitElement | ||||
| @@ -35,90 +38,33 @@ export class HuiMarkdownCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _title(): string { | ||||
|     return this._config!.title || ""; | ||||
|   } | ||||
|  | ||||
|   get _content(): string { | ||||
|     return this._config!.content || ""; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.title" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._title} | ||||
|           .configValue=${"title"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <paper-textarea | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.markdown.content" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|           .value=${this._content} | ||||
|           .configValue=${"content"} | ||||
|           @keydown=${this._ignoreKeydown} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           autocapitalize="none" | ||||
|           autocomplete="off" | ||||
|           spellcheck="false" | ||||
|         ></paper-textarea> | ||||
|         <hui-theme-select-editor | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|         .data=${this._config} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         ></hui-theme-select-editor> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _ignoreKeydown(ev: KeyboardEvent) { | ||||
|     // Stop keyboard events from the paper-textarea from propagating to avoid accidentally closing the dialog when the user presses Enter. | ||||
|     ev.stopPropagation(); | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (target.configValue) { | ||||
|       if (target.value === "" && target.configValue !== "content") { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         this._config = { | ||||
|           ...this._config, | ||||
|           [target.configValue!]: target.value, | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   } | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => | ||||
|     this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ) || | ||||
|     this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.markdown.${schema.name}` | ||||
|     ); | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, object, optional, string, assign } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| @@ -63,7 +63,7 @@ export class HuiPictureCardEditor | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|         <ha-textfield | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.image" | ||||
|           )} (${this.hass.localize( | ||||
| @@ -71,8 +71,8 @@ export class HuiPictureCardEditor | ||||
|           )})" | ||||
|           .value=${this._image} | ||||
|           .configValue=${"image"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|           @input=${this._valueChanged} | ||||
|         ></ha-textfield> | ||||
|         <hui-theme-select-editor | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
| @@ -134,7 +134,15 @@ export class HuiPictureCardEditor | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|     return [ | ||||
|       configElementStyle, | ||||
|       css` | ||||
|         ha-textfield { | ||||
|           display: block; | ||||
|           margin-bottom: 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,24 +1,16 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, boolean, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import { computeRTLDirection } from "../../../../common/util/compute_rtl"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-select"; | ||||
| import "../../../../components/ha-switch"; | ||||
| import { ActionConfig } from "../../../../data/lovelace"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { PictureEntityCardConfig } from "../../cards/types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { ActionConfig } from "../../../../data/lovelace"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { PictureEntityCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-action-editor"; | ||||
| import "../../components/hui-entity-editor"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { EditorTarget } from "../types"; | ||||
| import type { EditorTarget } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
| @@ -38,7 +30,38 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const includeDomains = ["camera"]; | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { name: "entity", required: true, selector: { entity: {} } }, | ||||
|   { name: "name", selector: { text: {} } }, | ||||
|   { name: "image", selector: { text: {} } }, | ||||
|   { name: "camera_image", selector: { entity: { domain: "camera" } } }, | ||||
|   { | ||||
|     name: "", | ||||
|     type: "grid", | ||||
|     schema: [ | ||||
|       { | ||||
|         name: "camera_view", | ||||
|         selector: { select: { options: ["auto", "live"] } }, | ||||
|       }, | ||||
|       { name: "aspect_ratio", selector: { text: {} } }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     name: "", | ||||
|     type: "grid", | ||||
|     schema: [ | ||||
|       { | ||||
|         name: "show_name", | ||||
|         selector: { boolean: {} }, | ||||
|       }, | ||||
|       { | ||||
|         name: "show_state", | ||||
|         selector: { boolean: {} }, | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { name: "theme", selector: { theme: {} } }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-picture-entity-card-editor") | ||||
| export class HuiPictureEntityCardEditor | ||||
| @@ -54,30 +77,6 @@ export class HuiPictureEntityCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _name(): string { | ||||
|     return this._config!.name || ""; | ||||
|   } | ||||
|  | ||||
|   get _image(): string { | ||||
|     return this._config!.image || ""; | ||||
|   } | ||||
|  | ||||
|   get _camera_image(): string { | ||||
|     return this._config!.camera_image || ""; | ||||
|   } | ||||
|  | ||||
|   get _camera_view(): string { | ||||
|     return this._config!.camera_view || "auto"; | ||||
|   } | ||||
|  | ||||
|   get _aspect_ratio(): string { | ||||
|     return this._config!.aspect_ratio || ""; | ||||
|   } | ||||
|  | ||||
|   get _tap_action(): ActionConfig { | ||||
|     return this._config!.tap_action || { action: "more-info" }; | ||||
|   } | ||||
| @@ -86,140 +85,29 @@ export class HuiPictureEntityCardEditor | ||||
|     return this._config!.hold_action; | ||||
|   } | ||||
|  | ||||
|   get _show_name(): boolean { | ||||
|     return this._config!.show_name ?? true; | ||||
|   } | ||||
|  | ||||
|   get _show_state(): boolean { | ||||
|     return this._config!.show_state ?? true; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const actions = ["more-info", "toggle", "navigate", "call-service", "none"]; | ||||
|     const views = ["auto", "live"]; | ||||
|     const dir = computeRTLDirection(this.hass!); | ||||
|  | ||||
|     const data = { | ||||
|       show_state: true, | ||||
|       show_name: true, | ||||
|       camera_view: "auto", | ||||
|       ...this._config, | ||||
|     }; | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${data} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       ></ha-form> | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.entity" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.name" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._name} | ||||
|           .configValue=${"name"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.image" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._image} | ||||
|           .configValue=${"image"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.camera_image" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._camera_image} | ||||
|           .configValue=${"camera_image"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           .includeDomains=${includeDomains} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <div class="side-by-side"> | ||||
|           <ha-select | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.camera_view" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .configValue=${"camera_view"} | ||||
|             @selected=${this._valueChanged} | ||||
|             @closed=${stopPropagation} | ||||
|             fixedMenuPosition | ||||
|             naturalMenuWidth | ||||
|             .value=${views.indexOf(this._camera_view)} | ||||
|           > | ||||
|             ${views.map( | ||||
|               (view) => | ||||
|                 html`<mwc-list-item .value=${view}>${view}</mwc-list-item> ` | ||||
|             )} | ||||
|           </ha-select> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.aspect_ratio" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._aspect_ratio} | ||||
|             .configValue=${"aspect_ratio"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|         <div class="side-by-side"> | ||||
|           <div> | ||||
|             <ha-formfield | ||||
|               .label=${this.hass.localize( | ||||
|                 "ui.panel.lovelace.editor.card.generic.show_name" | ||||
|               )} | ||||
|               .dir=${dir} | ||||
|             > | ||||
|               <ha-switch | ||||
|                 .checked=${this._config!.show_name !== false} | ||||
|                 .configValue=${"show_name"} | ||||
|                 @change=${this._change} | ||||
|               ></ha-switch | ||||
|             ></ha-formfield> | ||||
|           </div> | ||||
|           <div> | ||||
|             <ha-formfield | ||||
|               .label=${this.hass.localize( | ||||
|                 "ui.panel.lovelace.editor.card.generic.show_state" | ||||
|               )} | ||||
|               .dir=${dir} | ||||
|             > | ||||
|               <ha-switch | ||||
|                 .checked=${this._config!.show_state !== false} | ||||
|                 .configValue=${"show_state"} | ||||
|                 @change=${this._change} | ||||
|               ></ha-switch | ||||
|             ></ha-formfield> | ||||
|           </div> | ||||
|         </div> | ||||
|         <hui-theme-select-editor | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></hui-theme-select-editor> | ||||
|         <div class="side-by-side"> | ||||
|           <hui-action-editor | ||||
|             .label="${this.hass.localize( | ||||
| @@ -231,7 +119,7 @@ export class HuiPictureEntityCardEditor | ||||
|             .config=${this._tap_action} | ||||
|             .actions=${actions} | ||||
|             .configValue=${"tap_action"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|             @value-changed=${this._changed} | ||||
|           ></hui-action-editor> | ||||
|           <hui-action-editor | ||||
|             .label="${this.hass.localize( | ||||
| @@ -243,32 +131,18 @@ export class HuiPictureEntityCardEditor | ||||
|             .config=${this._hold_action} | ||||
|             .actions=${actions} | ||||
|             .configValue=${"hold_action"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|             @value-changed=${this._changed} | ||||
|           ></hui-action-editor> | ||||
|         </div> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _change(ev: Event) { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|     const value = target.checked; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === value) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._config = { | ||||
|       ...this._config, | ||||
|       [target.configValue!]: value, | ||||
|     }; | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _changed(ev: CustomEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
| @@ -279,7 +153,6 @@ export class HuiPictureEntityCardEditor | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (target.configValue) { | ||||
|     if (value !== false && !value) { | ||||
|       this._config = { ...this._config }; | ||||
|       delete this._config[target.configValue!]; | ||||
| @@ -289,13 +162,27 @@ export class HuiPictureEntityCardEditor | ||||
|         [target.configValue!]: value, | ||||
|       }; | ||||
|     } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|       ) || | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.picture-entity.${schema.name}` | ||||
|       ) | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   static styles: CSSResultGroup = configElementStyle; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,25 +1,21 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "../../components/hui-action-editor"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { array, assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import "../../../../components/ha-select"; | ||||
| import { ActionConfig } from "../../../../data/lovelace"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { PictureGlanceCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-action-editor"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { ActionConfig } from "../../../../data/lovelace"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { PictureGlanceCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-entity-editor"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { EntityConfig } from "../../entity-rows/types"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { EntityConfig } from "../../entity-rows/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { processEditorEntities } from "../process-editor-entities"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entitiesConfigStruct } from "../structs/entities-struct"; | ||||
| import { EditorTarget } from "../types"; | ||||
| import type { EditorTarget } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
| @@ -38,7 +34,26 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const includeDomains = ["camera"]; | ||||
| const actions = ["more-info", "toggle", "navigate", "call-service", "none"]; | ||||
|  | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { name: "title", selector: { text: {} } }, | ||||
|   { name: "image", selector: { text: {} } }, | ||||
|   { name: "camera_image", selector: { entity: { domain: "camera" } } }, | ||||
|   { | ||||
|     name: "", | ||||
|     type: "grid", | ||||
|     schema: [ | ||||
|       { | ||||
|         name: "camera_view", | ||||
|         selector: { select: { options: ["auto", "live"] } }, | ||||
|       }, | ||||
|       { name: "aspect_ratio", selector: { text: {} } }, | ||||
|     ], | ||||
|   }, | ||||
|   { name: "entity", selector: { entity: {} } }, | ||||
|   { name: "theme", selector: { theme: {} } }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-picture-glance-card-editor") | ||||
| export class HuiPictureGlanceCardEditor | ||||
| @@ -57,39 +72,6 @@ export class HuiPictureGlanceCardEditor | ||||
|     this._configEntities = processEditorEntities(config.entities); | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _title(): string { | ||||
|     return this._config!.title || ""; | ||||
|   } | ||||
|  | ||||
|   get _image(): string { | ||||
|     return ( | ||||
|       this._config!.image || | ||||
|       (this._camera_image | ||||
|         ? "" | ||||
|         : "https://www.home-assistant.io/images/merchandise/shirt-frontpage.png") | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   get _camera_image(): string { | ||||
|     return this._config!.camera_image || ""; | ||||
|   } | ||||
|  | ||||
|   get _camera_view(): string { | ||||
|     return this._config!.camera_view || "auto"; | ||||
|   } | ||||
|  | ||||
|   get _state_image(): Record<string, unknown> { | ||||
|     return this._config!.state_image || {}; | ||||
|   } | ||||
|  | ||||
|   get _aspect_ratio(): string { | ||||
|     return this._config!.aspect_ratio || ""; | ||||
|   } | ||||
|  | ||||
|   get _tap_action(): ActionConfig { | ||||
|     return this._config!.tap_action || { action: "toggle" }; | ||||
|   } | ||||
| @@ -98,102 +80,27 @@ export class HuiPictureGlanceCardEditor | ||||
|     return this._config!.hold_action || { action: "more-info" }; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const actions = ["more-info", "toggle", "navigate", "call-service", "none"]; | ||||
|     const views = ["auto", "live"]; | ||||
|     const data = { camera_view: "auto", ...this._config }; | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${data} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       ></ha-form> | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.title" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._title} | ||||
|           .configValue=${"title"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.image" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._image} | ||||
|           .configValue=${"image"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.camera_image" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._camera_image} | ||||
|           .configValue=${"camera_image"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|           .includeDomains=${includeDomains} | ||||
|         ></ha-entity-picker> | ||||
|         <div class="side-by-side"> | ||||
|           <ha-select | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.camera_view" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .configValue=${"camera_view"} | ||||
|             @selected=${this._valueChanged} | ||||
|             @closed=${stopPropagation} | ||||
|             fixedMenuPosition | ||||
|             naturalMenuWidth | ||||
|             .value=${this._camera_view} | ||||
|           > | ||||
|             ${views.map( | ||||
|               (view) => | ||||
|                 html`<mwc-list-item .value=${view}>${view}</mwc-list-item> ` | ||||
|             )} | ||||
|           </ha-select> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.aspect_ratio" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._aspect_ratio} | ||||
|             .configValue=${"aspect_ratio"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.picture-glance.state_entity" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <div class="side-by-side"> | ||||
|           <hui-action-editor | ||||
|             .label="${this.hass.localize( | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.tap_action" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             )} | ||||
|             .hass=${this.hass} | ||||
|             .config=${this._tap_action} | ||||
|             .actions=${actions} | ||||
| @@ -201,11 +108,9 @@ export class HuiPictureGlanceCardEditor | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></hui-action-editor> | ||||
|           <hui-action-editor | ||||
|             .label="${this.hass.localize( | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.hold_action" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             )} | ||||
|             .hass=${this.hass} | ||||
|             .config=${this._hold_action} | ||||
|             .actions=${actions} | ||||
| @@ -216,19 +121,17 @@ export class HuiPictureGlanceCardEditor | ||||
|         <hui-entity-editor | ||||
|           .hass=${this.hass} | ||||
|           .entities=${this._configEntities} | ||||
|           @entities-changed=${this._valueChanged} | ||||
|           @entities-changed=${this._changed} | ||||
|         ></hui-entity-editor> | ||||
|         <hui-theme-select-editor | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></hui-theme-select-editor> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _changed(ev: CustomEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
| @@ -257,9 +160,24 @@ export class HuiPictureGlanceCardEditor | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.picture-glance.state_entity" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|       ) || | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.picture-glance.${schema.name}` | ||||
|       ) | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   static styles: CSSResultGroup = configElementStyle; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,16 +1,13 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { PlantStatusCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { PlantStatusCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -21,7 +18,11 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const includeDomains = ["plant"]; | ||||
| const SCHEMA: HaFormSchema[] = [ | ||||
|   { name: "entity", required: true, selector: { entity: { domain: "plant" } } }, | ||||
|   { name: "name", selector: { text: {} } }, | ||||
|   { name: "theme", selector: { theme: {} } }, | ||||
| ]; | ||||
|  | ||||
| @customElement("hui-plant-status-card-editor") | ||||
| export class HuiPlantStatusCardEditor | ||||
| @@ -37,83 +38,37 @@ export class HuiPlantStatusCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _name(): string { | ||||
|     return this._config!.name || ""; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.entity" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           .includeDomains=${includeDomains} | ||||
|           @change=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.name" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._name} | ||||
|           .configValue=${"name"} | ||||
|         .data=${this._config} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <hui-theme-select-editor | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></hui-theme-select-editor> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (target.configValue) { | ||||
|       if (target.value === "") { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         this._config = { | ||||
|           ...this._config, | ||||
|           [target.configValue!]: target.value, | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { | ||||
|   assert, | ||||
|   assign, | ||||
| @@ -13,20 +14,13 @@ import { | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import { domainIcon } from "../../../../common/entity/domain_icon"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-icon-picker"; | ||||
| import "../../../../components/ha-select"; | ||||
| import "../../../../components/ha-switch"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { SensorCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { SensorCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
| @@ -43,8 +37,6 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const includeDomains = ["counter", "input_number", "number", "sensor"]; | ||||
|  | ||||
| @customElement("hui-sensor-card-editor") | ||||
| export class HuiSensorCardEditor | ||||
|   extends LitElement | ||||
| @@ -59,201 +51,122 @@ export class HuiSensorCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _name(): string { | ||||
|     return this._config!.name || ""; | ||||
|   } | ||||
|  | ||||
|   get _icon(): string { | ||||
|     return this._config!.icon || ""; | ||||
|   } | ||||
|  | ||||
|   get _graph(): string { | ||||
|     return this._config!.graph || "none"; | ||||
|   } | ||||
|  | ||||
|   get _unit(): string { | ||||
|     return this._config!.unit || ""; | ||||
|   } | ||||
|  | ||||
|   get _detail(): number { | ||||
|     return this._config!.detail ?? 1; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   get _hours_to_show(): number | string { | ||||
|     return this._config!.hours_to_show || "24"; | ||||
|   } | ||||
|   private _schema = memoizeOne( | ||||
|     ( | ||||
|       entity: string, | ||||
|       icon: string | undefined, | ||||
|       entityState: HassEntity | ||||
|     ): HaFormSchema[] => [ | ||||
|       { | ||||
|         name: "entity", | ||||
|         selector: { | ||||
|           entity: { domain: ["counter", "input_number", "number", "sensor"] }, | ||||
|         }, | ||||
|       }, | ||||
|       { name: "name", selector: { text: {} } }, | ||||
|       { | ||||
|         type: "grid", | ||||
|         name: "", | ||||
|         schema: [ | ||||
|           { | ||||
|             name: "icon", | ||||
|             selector: { | ||||
|               icon: { | ||||
|                 placeholder: icon || entityState?.attributes.icon, | ||||
|                 fallbackPath: | ||||
|                   !icon && !entityState?.attributes.icon && entityState | ||||
|                     ? domainIcon(computeDomain(entity), entityState) | ||||
|                     : undefined, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|           { | ||||
|             name: "graph", | ||||
|             selector: { | ||||
|               select: { | ||||
|                 options: [ | ||||
|                   { | ||||
|                     value: "none", | ||||
|                     label: "None", | ||||
|                   }, | ||||
|                   { | ||||
|                     value: "line", | ||||
|                     label: "Line", | ||||
|                   }, | ||||
|                 ], | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|           { name: "unit", selector: { text: {} } }, | ||||
|           { name: "detail", selector: { boolean: {} } }, | ||||
|           { name: "theme", selector: { theme: {} } }, | ||||
|           { | ||||
|             name: "hours_to_show", | ||||
|             selector: { number: { min: 1, mode: "box" } }, | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ] | ||||
|   ); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const graphs = ["line", "none"]; | ||||
|     const entityState = this.hass.states[this._config.entity]; | ||||
|  | ||||
|     const entityState = this.hass.states[this._entity]; | ||||
|     const schema = this._schema( | ||||
|       this._config.entity, | ||||
|       this._config.icon, | ||||
|       entityState | ||||
|     ); | ||||
|  | ||||
|     const data = { | ||||
|       hours_to_show: 24, | ||||
|       graph: "none", | ||||
|       ...this._config, | ||||
|       detail: this._config!.detail === 2, | ||||
|     }; | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.entity" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           .includeDomains=${includeDomains} | ||||
|           @change=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.name" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._name} | ||||
|           .configValue=${"name"} | ||||
|         .data=${data} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <div class="side-by-side"> | ||||
|           <ha-icon-picker | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.icon" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._icon} | ||||
|             .placeholder=${this._icon || entityState?.attributes.icon} | ||||
|             .fallbackPath=${!this._icon && | ||||
|             !entityState?.attributes.icon && | ||||
|             entityState | ||||
|               ? domainIcon(computeDomain(entityState.entity_id), entityState) | ||||
|               : undefined} | ||||
|             .configValue=${"icon"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></ha-icon-picker> | ||||
|           <ha-select | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.sensor.graph_type" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .configValue=${"graph"} | ||||
|             @selected=${this._valueChanged} | ||||
|             @closed=${stopPropagation} | ||||
|             fixedMenuPosition | ||||
|             naturalMenuWidth | ||||
|             .value=${this._graph} | ||||
|           > | ||||
|             ${graphs.map( | ||||
|               (graph) => | ||||
|                 html`<mwc-list-item .value=${graph}>${graph}</mwc-list-item>` | ||||
|             )} | ||||
|           </ha-select> | ||||
|         </div> | ||||
|         <div class="side-by-side"> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.unit" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._unit} | ||||
|             .configValue=${"unit"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|           <ha-formfield | ||||
|             label=${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.sensor.show_more_detail" | ||||
|             )} | ||||
|           > | ||||
|             <ha-switch | ||||
|               .checked=${this._detail === 2} | ||||
|               .configValue=${"detail"} | ||||
|               @change=${this._change} | ||||
|             ></ha-switch> | ||||
|           </ha-formfield> | ||||
|         </div> | ||||
|         <div class="side-by-side"> | ||||
|           <hui-theme-select-editor | ||||
|             .hass=${this.hass} | ||||
|             .value=${this._theme} | ||||
|             .configValue=${"theme"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></hui-theme-select-editor> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.hours_to_show" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             type="number" | ||||
|             .value=${this._hours_to_show} | ||||
|             min="1" | ||||
|             .configValue=${"hours_to_show"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _change(ev: Event) { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     const config = ev.detail.value; | ||||
|     config.detail = config.detail ? 2 : 1; | ||||
|     fireEvent(this, "config-changed", { config }); | ||||
|   } | ||||
|  | ||||
|     const value = (ev.target! as EditorTarget).checked ? 2 : 1; | ||||
|  | ||||
|     if (this._detail === value) { | ||||
|       return; | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     this._config = { | ||||
|       ...this._config, | ||||
|       detail: value, | ||||
|     if (schema.name === "detail") { | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.sensor.show_more_detail" | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|       ) || | ||||
|       this.hass!.localize(`ui.panel.lovelace.editor.card.sensor.${schema.name}`) | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target! as EditorTarget; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (target.configValue) { | ||||
|       if ( | ||||
|         target.value === "" || | ||||
|         (target.type === "number" && isNaN(Number(target.value))) | ||||
|       ) { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         let value: any = target.value; | ||||
|         if (target.type === "number") { | ||||
|           value = Number(value); | ||||
|         } | ||||
|         this._config = { ...this._config, [target.configValue!]: value }; | ||||
|       } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "../../../../components/ha-textfield"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| @@ -57,7 +57,7 @@ export class HuiShoppingListEditor | ||||
|               </div> | ||||
|             ` | ||||
|           : ""} | ||||
|         <paper-input | ||||
|         <ha-textfield | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.title" | ||||
|           )} (${this.hass.localize( | ||||
| @@ -65,8 +65,8 @@ export class HuiShoppingListEditor | ||||
|           )})" | ||||
|           .value=${this._title} | ||||
|           .configValue=${"title"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|           @input=${this._valueChanged} | ||||
|         ></ha-textfield> | ||||
|         <hui-theme-select-editor | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { | ||||
|   array, | ||||
|   assert, | ||||
| @@ -14,22 +14,15 @@ import { | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/entity/ha-statistics-picker"; | ||||
| import "../../../../components/ha-checkbox"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-radio"; | ||||
| import type { HaRadio } from "../../../../components/ha-radio"; | ||||
| import "../../../../components/ha-select"; | ||||
| import { StatisticType } from "../../../../data/history"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { StatisticsGraphCardConfig } from "../../cards/types"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { StatisticsGraphCardConfig } from "../../cards/types"; | ||||
| import { processConfigEntities } from "../../common/process-config-entities"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entitiesConfigStruct } from "../structs/entities-struct"; | ||||
| import { EditorTarget } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const statTypeStruct = union([ | ||||
|   literal("sum"), | ||||
| @@ -78,133 +71,82 @@ export class HuiStatisticsGraphCardEditor | ||||
|       : []; | ||||
|   } | ||||
|  | ||||
|   get _title(): string { | ||||
|     return this._config!.title || ""; | ||||
|   } | ||||
|  | ||||
|   get _days_to_show(): number { | ||||
|     return this._config!.days_to_show || 30; | ||||
|   } | ||||
|  | ||||
|   get _period(): string { | ||||
|     return this._config!.period || "hour"; | ||||
|   } | ||||
|  | ||||
|   get _chart_type(): StatisticsGraphCardConfig["chart_type"] { | ||||
|     return this._config!.chart_type || "line"; | ||||
|   } | ||||
|  | ||||
|   get _stat_types(): StatisticType[] { | ||||
|     return this._config!.stat_types | ||||
|       ? Array.isArray(this._config!.stat_types) | ||||
|         ? this._config!.stat_types | ||||
|         : [this._config!.stat_types] | ||||
|       : ["mean", "min", "max", "sum"]; | ||||
|   } | ||||
|   private _schema = memoizeOne((localize: LocalizeFunc) => [ | ||||
|     { name: "title", selector: { text: {} } }, | ||||
|     { | ||||
|       name: "", | ||||
|       type: "grid", | ||||
|       schema: [ | ||||
|         { | ||||
|           name: "period", | ||||
|           required: true, | ||||
|           selector: { | ||||
|             select: { | ||||
|               options: periods.map((period) => ({ | ||||
|                 value: period, | ||||
|                 label: localize( | ||||
|                   `ui.panel.lovelace.editor.card.statistics-graph.periods.${period}` | ||||
|                 ), | ||||
|               })), | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           name: "days_to_show", | ||||
|           required: true, | ||||
|           selector: { number: { min: 1, mode: "box" } }, | ||||
|         }, | ||||
|         { | ||||
|           name: "stat_types", | ||||
|           required: true, | ||||
|           type: "multi_select", | ||||
|           options: [ | ||||
|             ["mean", "Mean"], | ||||
|             ["min", "Min"], | ||||
|             ["max", "Max"], | ||||
|             ["sum", "Sum"], | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           name: "chart_type", | ||||
|           required: true, | ||||
|           type: "select", | ||||
|           options: [ | ||||
|             ["line", "Line"], | ||||
|             ["bar", "Bar"], | ||||
|           ], | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   ]); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const schema = this._schema(this.hass.localize); | ||||
|     const stat_types = this._config!.stat_types | ||||
|       ? Array.isArray(this._config!.stat_types) | ||||
|         ? this._config!.stat_types | ||||
|         : [this._config!.stat_types] | ||||
|       : ["mean", "min", "max", "sum"]; | ||||
|     const data = { | ||||
|       chart_type: "line", | ||||
|       period: "hour", | ||||
|       days_to_show: 30, | ||||
|       ...this._config, | ||||
|       stat_types, | ||||
|     }; | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.title" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._title} | ||||
|           .configValue=${"title"} | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${data} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <div class="side-by-side"> | ||||
|           <ha-select | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.statistics-graph.period" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .configValue=${"period"} | ||||
|             @selected=${this._periodSelected} | ||||
|             @closed=${stopPropagation} | ||||
|             fixedMenuPosition | ||||
|             naturalMenuWidth | ||||
|             .value=${this._period} | ||||
|           > | ||||
|             ${periods.map( | ||||
|               (period) => | ||||
|                 html`<mwc-list-item .value=${period}> | ||||
|                   ${this.hass!.localize( | ||||
|                     `ui.panel.lovelace.editor.card.statistics-graph.periods.${period}` | ||||
|                   )} | ||||
|                 </mwc-list-item>` | ||||
|             )} | ||||
|           </ha-select> | ||||
|           <paper-input | ||||
|             type="number" | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.days_to_show" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._days_to_show} | ||||
|             min="1" | ||||
|             .configValue=${"days_to_show"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|         </div> | ||||
|         <p>Show stat types:</p> | ||||
|         <div class="side-by-side"> | ||||
|           <ha-formfield label="Sum"> | ||||
|             <ha-checkbox | ||||
|               .checked=${this._stat_types.includes("sum")} | ||||
|               name="sum" | ||||
|               @change=${this._statTypesChanged} | ||||
|             ></ha-checkbox> | ||||
|           </ha-formfield> | ||||
|           <ha-formfield label="Mean"> | ||||
|             <ha-checkbox | ||||
|               .checked=${this._stat_types.includes("mean")} | ||||
|               name="mean" | ||||
|               @change=${this._statTypesChanged} | ||||
|             ></ha-checkbox> | ||||
|           </ha-formfield> | ||||
|           <ha-formfield label="Min"> | ||||
|             <ha-checkbox | ||||
|               .checked=${this._stat_types.includes("min")} | ||||
|               name="min" | ||||
|               @change=${this._statTypesChanged} | ||||
|             ></ha-checkbox> | ||||
|           </ha-formfield> | ||||
|           <ha-formfield label="Max"> | ||||
|             <ha-checkbox | ||||
|               .checked=${this._stat_types.includes("max")} | ||||
|               name="max" | ||||
|               @change=${this._statTypesChanged} | ||||
|             ></ha-checkbox> | ||||
|           </ha-formfield> | ||||
|         </div> | ||||
|         <div class="side-by-side"> | ||||
|           <p>Chart type:</p> | ||||
|           <ha-formfield label="Line"> | ||||
|             <ha-radio | ||||
|               .checked=${this._chart_type === "line"} | ||||
|               value="line" | ||||
|               name="chart_type" | ||||
|               @change=${this._chartTypeChanged} | ||||
|             ></ha-radio> | ||||
|           </ha-formfield> | ||||
|           <ha-formfield label="Bar"> | ||||
|             <ha-radio | ||||
|               .checked=${this._chart_type === "bar"} | ||||
|               value="bar" | ||||
|               name="chart_type" | ||||
|               @change=${this._chartTypeChanged} | ||||
|             ></ha-radio> | ||||
|           </ha-formfield> | ||||
|         </div> | ||||
|       ></ha-form> | ||||
|         <ha-statistics-picker | ||||
|           .hass=${this.hass} | ||||
|           .pickStatisticLabel=${`Add a statistic`} | ||||
| @@ -217,82 +159,23 @@ export class HuiStatisticsGraphCardEditor | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _chartTypeChanged(ev: CustomEvent) { | ||||
|     const input = ev.currentTarget as HaRadio; | ||||
|     fireEvent(this, "config-changed", { | ||||
|       config: { ...this._config!, chart_type: input.value }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _statTypesChanged(ev) { | ||||
|     const name = ev.currentTarget.name; | ||||
|     const checked = ev.currentTarget.checked; | ||||
|     if (checked) { | ||||
|       fireEvent(this, "config-changed", { | ||||
|         config: { ...this._config!, stat_types: [...this._stat_types, name] }, | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     const statTypes = [...this._stat_types]; | ||||
|     fireEvent(this, "config-changed", { | ||||
|       config: { | ||||
|         ...this._config!, | ||||
|         stat_types: statTypes.filter((stat) => stat !== name), | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _periodSelected(ev) { | ||||
|     const newPeriod = ev.target.value | ||||
|       .period as StatisticsGraphCardConfig["period"]; | ||||
|     if (newPeriod === this._period) { | ||||
|       return; | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { | ||||
|       config: { ...this._config!, period: newPeriod }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|     const target = ev.target! as EditorTarget; | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => | ||||
|     this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ) || | ||||
|     this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.statistics-graph.${schema.name}` | ||||
|     ); | ||||
|  | ||||
|     const newValue = ev.detail?.value || target.value; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === newValue) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (newValue === "") { | ||||
|       this._config = { ...this._config }; | ||||
|       delete this._config[target.configValue!]; | ||||
|     } else { | ||||
|       let value: any = newValue; | ||||
|       if (target.type === "number") { | ||||
|         value = Number(value); | ||||
|       } | ||||
|       this._config = { | ||||
|         ...this._config, | ||||
|         [target.configValue!]: value, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       configElementStyle, | ||||
|       css` | ||||
|   static styles: CSSResultGroup = css` | ||||
|     ha-statistics-picker { | ||||
|       width: 100%; | ||||
|     } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   `; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -66,11 +66,9 @@ export class HuiThermostatCardEditor | ||||
|  | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return `${this.hass!.localize( | ||||
|       return this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       )} (${this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.config.required" | ||||
|       )})`; | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return this.hass!.localize( | ||||
|   | ||||
| @@ -1,22 +1,18 @@ | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, boolean, object, optional, string, assign } from "superstruct"; | ||||
| import { memoize } from "@fullcalendar/common"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { computeRTLDirection } from "../../../../common/util/compute_rtl"; | ||||
| import "../../../../components/entity/ha-entity-attribute-picker"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-radio"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { WeatherForecastCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import { LovelaceCardEditor } from "../../types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { WeatherForecastCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { UNAVAILABLE } from "../../../../data/entity"; | ||||
| import { WeatherEntity } from "../../../../data/weather"; | ||||
| import type { WeatherEntity } from "../../../../data/weather"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -33,8 +29,6 @@ const cardConfigStruct = assign( | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const includeDomains = ["weather"]; | ||||
|  | ||||
| @customElement("hui-weather-forecast-card-editor") | ||||
| export class HuiWeatherForecastCardEditor | ||||
|   extends LitElement | ||||
| @@ -61,30 +55,6 @@ export class HuiWeatherForecastCardEditor | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _name(): string { | ||||
|     return this._config!.name || ""; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   get _show_current(): boolean { | ||||
|     return this._config!.show_current ?? true; | ||||
|   } | ||||
|  | ||||
|   get _show_forecast(): boolean { | ||||
|     return this._config!.show_forecast ?? this._has_forecast === true; | ||||
|   } | ||||
|  | ||||
|   get _secondary_info_attribute(): string { | ||||
|     return this._config!.secondary_info_attribute || ""; | ||||
|   } | ||||
|  | ||||
|   get _has_forecast(): boolean | undefined { | ||||
|     if (this.hass && this._config) { | ||||
|       const stateObj = this.hass.states[this._config.entity] as WeatherEntity; | ||||
| @@ -95,146 +65,134 @@ export class HuiWeatherForecastCardEditor | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|   private _schema = memoize( | ||||
|     ( | ||||
|       entity: string, | ||||
|       localize: LocalizeFunc, | ||||
|       hasForecast?: boolean | ||||
|     ): HaFormSchema[] => { | ||||
|       const schema: HaFormSchema[] = [ | ||||
|         { | ||||
|           name: "entity", | ||||
|           required: true, | ||||
|           selector: { entity: { domain: "weather" } }, | ||||
|         }, | ||||
|         { name: "name", selector: { text: {} } }, | ||||
|         { | ||||
|           name: "", | ||||
|           type: "grid", | ||||
|           schema: [ | ||||
|             { | ||||
|               name: "secondary_info_attribute", | ||||
|               selector: { attribute: { entity_id: entity } }, | ||||
|             }, | ||||
|             { name: "theme", selector: { theme: {} } }, | ||||
|           ], | ||||
|         }, | ||||
|       ]; | ||||
|       if (hasForecast) { | ||||
|         schema.push({ | ||||
|           name: "forecast", | ||||
|           selector: { | ||||
|             select: { | ||||
|               options: [ | ||||
|                 { | ||||
|                   value: "show_both", | ||||
|                   label: localize( | ||||
|                     "ui.panel.lovelace.editor.card.weather-forecast.show_both" | ||||
|                   ), | ||||
|                 }, | ||||
|                 { | ||||
|                   value: "show_current", | ||||
|                   label: localize( | ||||
|                     "ui.panel.lovelace.editor.card.weather-forecast.show_only_current" | ||||
|                   ), | ||||
|                 }, | ||||
|                 { | ||||
|                   value: "show_forecast", | ||||
|                   label: localize( | ||||
|                     "ui.panel.lovelace.editor.card.weather-forecast.show_only_forecast" | ||||
|                   ), | ||||
|                 }, | ||||
|               ], | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|       } | ||||
|       return schema; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const schema = this._schema( | ||||
|       this._config.entity, | ||||
|       this.hass.localize, | ||||
|       this._has_forecast | ||||
|     ); | ||||
|  | ||||
|     const data: WeatherForecastCardConfig = { | ||||
|       show_current: true, | ||||
|       show_forecast: this._has_forecast, | ||||
|       ...this._config, | ||||
|     }; | ||||
|  | ||||
|     data.forecast = | ||||
|       data.show_current && data.show_forecast | ||||
|         ? "show_both" | ||||
|         : data.show_current | ||||
|         ? "show_current" | ||||
|         : "show_forecast"; | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.entity" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.required" | ||||
|           )})" | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           .includeDomains=${includeDomains} | ||||
|           @change=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <div class="side-by-side"> | ||||
|           <paper-input | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.name" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._name} | ||||
|             .configValue=${"name"} | ||||
|         .data=${data} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|           ></paper-input> | ||||
|           <hui-theme-select-editor | ||||
|             .hass=${this.hass} | ||||
|             .value=${this._theme} | ||||
|             .configValue=${"theme"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></hui-theme-select-editor> | ||||
|           <ha-entity-attribute-picker | ||||
|             .hass=${this.hass} | ||||
|             .entityId=${this._entity} | ||||
|             .label="${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.generic.secondary_info_attribute" | ||||
|             )} (${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.config.optional" | ||||
|             )})" | ||||
|             .value=${this._secondary_info_attribute} | ||||
|             .configValue=${"secondary_info_attribute"} | ||||
|             @value-changed=${this._valueChanged} | ||||
|           ></ha-entity-attribute-picker> | ||||
|         </div> | ||||
|         <div class="side-by-side"> | ||||
|           <ha-formfield | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.weather-forecast.show_both" | ||||
|             )} | ||||
|             .dir=${computeRTLDirection(this.hass)} | ||||
|           > | ||||
|             <ha-radio | ||||
|               .disabled=${this._has_forecast === false} | ||||
|               .checked=${this._has_forecast === true && | ||||
|               this._show_current && | ||||
|               this._show_forecast} | ||||
|               .configValue=${"show_both"} | ||||
|               @change=${this._valueChanged} | ||||
|             ></ha-radio | ||||
|           ></ha-formfield> | ||||
|           <ha-formfield | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.weather-forecast.show_only_current" | ||||
|             )} | ||||
|             .dir=${computeRTLDirection(this.hass)} | ||||
|           > | ||||
|             <ha-radio | ||||
|               .disabled=${this._has_forecast === false} | ||||
|               .checked=${this._has_forecast === false || | ||||
|               (this._show_current && !this._show_forecast)} | ||||
|               .configValue=${"show_current"} | ||||
|               @change=${this._valueChanged} | ||||
|             ></ha-radio | ||||
|           ></ha-formfield> | ||||
|           <ha-formfield | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.panel.lovelace.editor.card.weather-forecast.show_only_forecast" | ||||
|             )} | ||||
|             .dir=${computeRTLDirection(this.hass)} | ||||
|           > | ||||
|             <ha-radio | ||||
|               .disabled=${this._has_forecast === false} | ||||
|               .checked=${this._has_forecast === true && | ||||
|               !this._show_current && | ||||
|               this._show_forecast} | ||||
|               .configValue=${"show_forecast"} | ||||
|               @change=${this._valueChanged} | ||||
|             ></ha-radio | ||||
|           ></ha-formfield> | ||||
|         </div> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.currentTarget! as EditorTarget; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (target.configValue) { | ||||
|       if (target.configValue.startsWith("show_")) { | ||||
|         this._config = { ...this._config }; | ||||
|         if (target.configValue === "show_both") { | ||||
|           /* delete since true is default */ | ||||
|           delete this._config.show_current; | ||||
|           delete this._config.show_forecast; | ||||
|         } else if (target.configValue === "show_current") { | ||||
|           delete this._config.show_current; | ||||
|           this._config.show_forecast = false; | ||||
|         } else if (target.configValue === "show_forecast") { | ||||
|           delete this._config.show_forecast; | ||||
|           this._config.show_current = false; | ||||
|         } | ||||
|       } else if (target.value === "") { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     const config = ev.detail.value; | ||||
|     if (config.forecast === "show_both") { | ||||
|       config.show_current = true; | ||||
|       config.show_forecast = true; | ||||
|     } else if (config.forecast === "show_current") { | ||||
|       config.show_current = true; | ||||
|       config.show_forecast = false; | ||||
|     } else { | ||||
|         this._config = { | ||||
|           ...this._config, | ||||
|           [target.configValue!]: | ||||
|             target.checked !== undefined ? target.checked : target.value, | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|       config.show_current = false; | ||||
|       config.show_forecast = true; | ||||
|     } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|     delete config.forecast; | ||||
|     fireEvent(this, "config-changed", { config }); | ||||
|   } | ||||
|  | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "entity") { | ||||
|       return `${this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.entity" | ||||
|       )} (${this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.config.required" | ||||
|       )})`; | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|       ) || | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.weather_forecast.${schema.name}` | ||||
|       ) | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ export class HuiDialogEditLovelace extends LitElement { | ||||
|  | ||||
|   @state() private _lovelace?: Lovelace; | ||||
|  | ||||
|   private _config?: LovelaceConfig; | ||||
|   @state() private _config?: LovelaceConfig; | ||||
|  | ||||
|   private _saving = false; | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-textfield"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { LovelaceConfig } from "../../../../data/lovelace"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { configElementStyle } from "../config-elements/config-elements-style"; | ||||
| import { EditorTarget } from "../types"; | ||||
|  | ||||
| declare global { | ||||
| @@ -30,16 +29,14 @@ export class HuiLovelaceEditor extends LitElement { | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|       <ha-textfield | ||||
|         .label=${this.hass.localize( | ||||
|           "ui.panel.lovelace.editor.edit_lovelace.title" | ||||
|         )} | ||||
|         .value=${this._title} | ||||
|         .configValue=${"title"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|       </div> | ||||
|         @change=${this._valueChanged} | ||||
|       ></ha-textfield> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -66,9 +63,11 @@ export class HuiLovelaceEditor extends LitElement { | ||||
|     fireEvent(this, "lovelace-config-changed", { config: newConfig }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return configElementStyle; | ||||
|   static styles: CSSResultGroup = css` | ||||
|     ha-textfield { | ||||
|       display: block; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,24 +1,17 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import { html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||
| import { slugify } from "../../../../common/string/slugify"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-icon-picker"; | ||||
| import "../../../../components/ha-select"; | ||||
| import "../../../../components/ha-switch"; | ||||
| import { LovelaceViewConfig } from "../../../../data/lovelace"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import "../../components/hui-theme-select-editor"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { LovelaceViewConfig } from "../../../../data/lovelace"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { | ||||
|   DEFAULT_VIEW_LAYOUT, | ||||
|   PANEL_VIEW_LAYOUT, | ||||
|   SIDEBAR_VIEW_LAYOUT, | ||||
| } from "../../views/const"; | ||||
| import { configElementStyle } from "../config-elements/config-elements-style"; | ||||
| import { EditorTarget } from "../types"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -38,32 +31,35 @@ export class HuiViewEditor extends LitElement { | ||||
|  | ||||
|   private _suggestedPath = false; | ||||
|  | ||||
|   get _path(): string { | ||||
|     if (!this._config) { | ||||
|       return ""; | ||||
|     } | ||||
|     return this._config.path || ""; | ||||
|   } | ||||
|   private _schema = memoizeOne((localize): HaFormSchema[] => [ | ||||
|     { name: "title", selector: { text: {} } }, | ||||
|     { | ||||
|       name: "icon", | ||||
|       selector: { | ||||
|         icon: {}, | ||||
|       }, | ||||
|     }, | ||||
|     { name: "path", selector: { text: {} } }, | ||||
|     { name: "theme", selector: { theme: {} } }, | ||||
|     { | ||||
|       name: "type", | ||||
|       selector: { | ||||
|         select: { | ||||
|           options: [ | ||||
|             DEFAULT_VIEW_LAYOUT, | ||||
|             SIDEBAR_VIEW_LAYOUT, | ||||
|             PANEL_VIEW_LAYOUT, | ||||
|           ].map((type) => ({ | ||||
|             value: type, | ||||
|             label: localize(`ui.panel.lovelace.editor.edit_view.types.${type}`), | ||||
|           })), | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   ]); | ||||
|  | ||||
|   get _title(): string { | ||||
|     if (!this._config) { | ||||
|       return ""; | ||||
|     } | ||||
|     return this._config.title || ""; | ||||
|   } | ||||
|  | ||||
|   get _icon(): string { | ||||
|     if (!this._config) { | ||||
|       return ""; | ||||
|     } | ||||
|     return this._config.icon || ""; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     if (!this._config) { | ||||
|       return ""; | ||||
|     } | ||||
|     return this._config.theme || "Backend-selected"; | ||||
|   set config(config: LovelaceViewConfig) { | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _type(): string { | ||||
| @@ -75,142 +71,59 @@ export class HuiViewEditor extends LitElement { | ||||
|       : this._config.type || DEFAULT_VIEW_LAYOUT; | ||||
|   } | ||||
|  | ||||
|   set config(config: LovelaceViewConfig) { | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.hass) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const schema = this._schema(this.hass.localize); | ||||
|     const data = { | ||||
|       theme: "Backend-selected", | ||||
|       ...this._config, | ||||
|       type: this._type, | ||||
|     }; | ||||
|  | ||||
|     return html` | ||||
|       <div class="card-config"> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.title" | ||||
|           )}  (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._title} | ||||
|           .configValue=${"title"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @blur=${this._handleTitleBlur} | ||||
|         ></paper-input> | ||||
|         <ha-icon-picker | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.icon" | ||||
|           )} (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._icon} | ||||
|           .placeholder=${this._icon} | ||||
|           .configValue=${"icon"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></ha-icon-picker> | ||||
|         <paper-input | ||||
|           .label="${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.url" | ||||
|           )}  (${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})" | ||||
|           .value=${this._path} | ||||
|           .configValue=${"path"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></paper-input> | ||||
|         <hui-theme-select-editor | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|         .data=${data} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         ></hui-theme-select-editor> | ||||
|         <ha-select | ||||
|           .label=${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.edit_view.type" | ||||
|           )} | ||||
|           .value=${this._type} | ||||
|           @selected=${this._typeChanged} | ||||
|           @closed=${stopPropagation} | ||||
|           fixedMenuPosition | ||||
|           naturalMenuWidth | ||||
|         > | ||||
|           ${[DEFAULT_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT, PANEL_VIEW_LAYOUT].map( | ||||
|             (type) => html`<mwc-list-item .value=${type}> | ||||
|               ${this.hass.localize( | ||||
|                 `ui.panel.lovelace.editor.edit_view.types.${type}` | ||||
|               )} | ||||
|             </mwc-list-item>` | ||||
|           )} | ||||
|         </ha-select> | ||||
|       </div> | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: Event): void { | ||||
|     const target = ev.currentTarget! as EditorTarget; | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     const config = ev.detail.value; | ||||
|  | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     if (config.type === "masonry") { | ||||
|       delete config.type; | ||||
|     } | ||||
|  | ||||
|     let newConfig; | ||||
|  | ||||
|     if (target.configValue) { | ||||
|       newConfig = { | ||||
|         ...this._config, | ||||
|         [target.configValue!]: | ||||
|           target.checked !== undefined ? target.checked : target.value, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "view-config-changed", { config: newConfig }); | ||||
|   } | ||||
|  | ||||
|   private _typeChanged(ev): void { | ||||
|     const selected = ev.target.value; | ||||
|     if (selected === "") { | ||||
|       return; | ||||
|     } | ||||
|     const newConfig = { | ||||
|       ...this._config, | ||||
|     }; | ||||
|     delete newConfig.panel; | ||||
|     if (selected === "masonry") { | ||||
|       delete newConfig.type; | ||||
|     } else { | ||||
|       newConfig.type = selected; | ||||
|     } | ||||
|     fireEvent(this, "view-config-changed", { config: newConfig }); | ||||
|   } | ||||
|  | ||||
|   private _handleTitleBlur(ev) { | ||||
|     if ( | ||||
|       !this.isNew || | ||||
|       this._suggestedPath || | ||||
|       this._config.path || | ||||
|       !ev.currentTarget.value | ||||
|       this.isNew && | ||||
|       !this._suggestedPath && | ||||
|       config.title && | ||||
|       (!this._config.path || | ||||
|         config.path === slugify(this._config.title || "", "-")) | ||||
|     ) { | ||||
|       return; | ||||
|       config.path = slugify(config.title, "-"); | ||||
|     } | ||||
|  | ||||
|     const config = { | ||||
|       ...this._config, | ||||
|       path: slugify(ev.currentTarget.value, "-"), | ||||
|     }; | ||||
|     fireEvent(this, "view-config-changed", { config }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       configElementStyle, | ||||
|       css` | ||||
|         .panel { | ||||
|           color: var(--secondary-text-color); | ||||
|           display: block; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   private _computeLabelCallback = (schema: HaFormSchema) => { | ||||
|     if (schema.name === "path") { | ||||
|       return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.url`); | ||||
|     } | ||||
|     return ( | ||||
|       this.hass!.localize( | ||||
|         `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|       ) || this.hass.localize("ui.panel.lovelace.editor.edit_view.type") | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -9,13 +9,17 @@ import { | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "../../../components/ha-date-input"; | ||||
| import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; | ||||
| import { setInputDateTimeValue } from "../../../data/input_datetime"; | ||||
| import { | ||||
|   setInputDateTimeValue, | ||||
|   stateToIsoDateString, | ||||
| } from "../../../data/input_datetime"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import "../components/hui-generic-entity-row"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| import type { EntityConfig, LovelaceRow } from "./types"; | ||||
| import "../../../components/ha-time-input"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
|  | ||||
| @customElement("hui-input-datetime-entity-row") | ||||
| class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { | ||||
| @@ -49,14 +53,22 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     const name = this._config.name || computeStateName(stateObj); | ||||
|  | ||||
|     return html` | ||||
|       <hui-generic-entity-row .hass=${this.hass} .config=${this._config}> | ||||
|       <hui-generic-entity-row | ||||
|         .hass=${this.hass} | ||||
|         .config=${this._config} | ||||
|         .hideName=${stateObj.attributes.has_date && | ||||
|         stateObj.attributes.has_time} | ||||
|       > | ||||
|         ${stateObj.attributes.has_date | ||||
|           ? html` | ||||
|               <ha-date-input | ||||
|                 .label=${stateObj.attributes.has_time ? name : undefined} | ||||
|                 .locale=${this.hass.locale} | ||||
|                 .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)} | ||||
|                 .value=${`${stateObj.attributes.year}-${stateObj.attributes.month}-${stateObj.attributes.day}`} | ||||
|                 .value=${stateToIsoDateString(stateObj)} | ||||
|                 @value-changed=${this._dateChanged} | ||||
|               > | ||||
|               </ha-date-input> | ||||
|   | ||||
| @@ -1,12 +1,5 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
| } from "lit"; | ||||
| import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { stopPropagation } from "../../../common/dom/stop_propagation"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| @@ -87,8 +80,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|   static styles = css` | ||||
|     hui-generic-entity-row { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
| @@ -97,7 +89,6 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { | ||||
|       width: 100%; | ||||
|     } | ||||
|   `; | ||||
|   } | ||||
|  | ||||
|   private _selectedChanged(ev): void { | ||||
|     const stateObj = this.hass!.states[this._config!.entity]; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||
| import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity"; | ||||
| import { setValue } from "../../../data/input_text"; | ||||
| @@ -8,6 +8,7 @@ import "../components/hui-generic-entity-row"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| import { EntityConfig, LovelaceRow } from "./types"; | ||||
| import "../../../components/ha-textfield"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
|  | ||||
| @customElement("hui-input-text-entity-row") | ||||
| class HuiInputTextEntityRow extends LitElement implements LovelaceRow { | ||||
| @@ -42,8 +43,13 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow { | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <hui-generic-entity-row .hass=${this.hass} .config=${this._config}> | ||||
|       <hui-generic-entity-row | ||||
|         .hass=${this.hass} | ||||
|         .config=${this._config} | ||||
|         hideName | ||||
|       > | ||||
|         <ha-textfield | ||||
|           .label=${this._config.name || computeStateName(stateObj)} | ||||
|           .disabled=${stateObj.state === UNAVAILABLE} | ||||
|           .value=${stateObj.state} | ||||
|           .minlength=${stateObj.attributes.min} | ||||
| @@ -75,6 +81,16 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow { | ||||
|  | ||||
|     ev.target.blur(); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     hui-generic-entity-row { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|     } | ||||
|     ha-textfield { | ||||
|       width: 100%; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -23,6 +23,18 @@ class HuiTimerEntityRow extends LitElement { | ||||
|       throw new Error("Invalid configuration"); | ||||
|     } | ||||
|     this._config = config; | ||||
|  | ||||
|     if (!this.hass) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const stateObj = this.hass!.states[this._config.entity]; | ||||
|  | ||||
|     if (stateObj) { | ||||
|       this._startInterval(stateObj); | ||||
|     } else { | ||||
|       this._clearInterval(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public disconnectedCallback(): void { | ||||
| @@ -75,7 +87,9 @@ class HuiTimerEntityRow extends LitElement { | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|  | ||||
|     if (changedProps.has("hass")) { | ||||
|     if (!this._config || !changedProps.has("hass")) { | ||||
|       return; | ||||
|     } | ||||
|     const stateObj = this.hass!.states[this._config!.entity]; | ||||
|     const oldHass = changedProps.get("hass") as this["hass"]; | ||||
|     const oldStateObj = oldHass | ||||
| @@ -88,7 +102,6 @@ class HuiTimerEntityRow extends LitElement { | ||||
|       this._clearInterval(); | ||||
|     } | ||||
|   } | ||||
|   } | ||||
|  | ||||
|   private _clearInterval(): void { | ||||
|     if (this._interval) { | ||||
|   | ||||
| @@ -69,7 +69,10 @@ export class SideBarView extends LitElement implements LovelaceViewElement { | ||||
|       this._createCards(); | ||||
|     } | ||||
|  | ||||
|     if (!changedProperties.has("lovelace")) { | ||||
|     if ( | ||||
|       !changedProperties.has("lovelace") && | ||||
|       !changedProperties.has("_config") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,8 @@ import { | ||||
| import { ResolvedMediaSource } from "../../data/media_source"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
|  | ||||
| export const ERR_UNSUPPORTED_MEDIA = "Unsupported Media"; | ||||
|  | ||||
| export class BrowserMediaPlayer { | ||||
|   private player: HTMLAudioElement; | ||||
|  | ||||
| @@ -25,6 +27,10 @@ export class BrowserMediaPlayer { | ||||
|     private onChange: () => void | ||||
|   ) { | ||||
|     const player = new Audio(this.resolved.url); | ||||
|     if (player.canPlayType(resolved.mime_type) === "") { | ||||
|       throw new Error(ERR_UNSUPPORTED_MEDIA); | ||||
|     } | ||||
|     player.autoplay = true; | ||||
|     player.volume = volume; | ||||
|     player.addEventListener("play", this._handleChange); | ||||
|     player.addEventListener("playing", () => { | ||||
| @@ -33,15 +39,7 @@ export class BrowserMediaPlayer { | ||||
|     }); | ||||
|     player.addEventListener("pause", this._handleChange); | ||||
|     player.addEventListener("ended", this._handleChange); | ||||
|     player.addEventListener("canplaythrough", () => { | ||||
|       if (this._removed) { | ||||
|         return; | ||||
|       } | ||||
|       if (this.buffering) { | ||||
|         player.play(); | ||||
|       } | ||||
|       this.onChange(); | ||||
|     }); | ||||
|     player.addEventListener("canplaythrough", this._handleChange); | ||||
|     this.player = player; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -49,9 +49,13 @@ import { | ||||
|   SUPPORT_VOLUME_SET, | ||||
| } from "../../data/media-player"; | ||||
| import { ResolvedMediaSource } from "../../data/media_source"; | ||||
| import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import "../lovelace/components/hui-marquee"; | ||||
| import { BrowserMediaPlayer } from "./browser-media-player"; | ||||
| import { | ||||
|   BrowserMediaPlayer, | ||||
|   ERR_UNSUPPORTED_MEDIA, | ||||
| } from "./browser-media-player"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -125,6 +129,7 @@ export class BarMediaPlayer extends LitElement { | ||||
|       throw Error("Only browser supported"); | ||||
|     } | ||||
|     this._tearDownBrowserPlayer(); | ||||
|     try { | ||||
|       this._browserPlayer = new BrowserMediaPlayer( | ||||
|         this.hass, | ||||
|         item, | ||||
| @@ -132,6 +137,17 @@ export class BarMediaPlayer extends LitElement { | ||||
|         this._browserPlayerVolume, | ||||
|         () => this.requestUpdate("_browserPlayer") | ||||
|       ); | ||||
|     } catch (err: any) { | ||||
|       if (err.message === ERR_UNSUPPORTED_MEDIA) { | ||||
|         showAlertDialog(this, { | ||||
|           text: this.hass.localize( | ||||
|             "ui.components.media-browser.media_not_supported" | ||||
|           ), | ||||
|         }); | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
|     this._newMediaExpected = false; | ||||
|   } | ||||
|  | ||||
| @@ -264,7 +280,9 @@ export class BarMediaPlayer extends LitElement { | ||||
|     return html` | ||||
|     <div class="choose-player ${isBrowser ? "browser" : ""}"> | ||||
|       ${ | ||||
|         stateObj && supportsFeature(stateObj, SUPPORT_VOLUME_SET) | ||||
|         !this.narrow && | ||||
|         stateObj && | ||||
|         supportsFeature(stateObj, SUPPORT_VOLUME_SET) | ||||
|           ? html` | ||||
|               <ha-button-menu corner="BOTTOM_START" y="0" x="76"> | ||||
|                 <ha-icon-button | ||||
| @@ -601,6 +619,7 @@ export class BarMediaPlayer extends LitElement { | ||||
|  | ||||
|       img { | ||||
|         max-height: 100px; | ||||
|         max-width: 100px; | ||||
|       } | ||||
|  | ||||
|       .app img { | ||||
| @@ -613,8 +632,8 @@ export class BarMediaPlayer extends LitElement { | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) { | ||||
|         min-height: 80px; | ||||
|         max-height: 80px; | ||||
|         min-height: 56px; | ||||
|         max-height: 56px; | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) .controls-progress { | ||||
| @@ -622,13 +641,19 @@ export class BarMediaPlayer extends LitElement { | ||||
|         min-width: 48px; | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) .media-info { | ||||
|         padding-left: 8px; | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) .controls { | ||||
|         display: flex; | ||||
|         padding-bottom: 0; | ||||
|         --mdc-icon-size: 30px; | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) .choose-player { | ||||
|         padding-left: 0; | ||||
|         padding-right: 8px; | ||||
|         min-width: 48px; | ||||
|         flex: unset; | ||||
|         justify-content: center; | ||||
| @@ -636,16 +661,16 @@ export class BarMediaPlayer extends LitElement { | ||||
|  | ||||
|       :host([narrow]) .choose-player.browser { | ||||
|         justify-content: flex-end; | ||||
|         width: 100%; | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) img { | ||||
|         max-height: 80px; | ||||
|         max-height: 56px; | ||||
|         max-width: 56px; | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) .blank-image { | ||||
|         height: 80px; | ||||
|         width: 80px; | ||||
|         height: 56px; | ||||
|         width: 56px; | ||||
|       } | ||||
|  | ||||
|       :host([narrow]) mwc-linear-progress { | ||||
|   | ||||
| @@ -27,7 +27,10 @@ import { | ||||
|   MediaPickedEvent, | ||||
|   MediaPlayerItem, | ||||
| } from "../../data/media-player"; | ||||
| import { resolveMediaSource } from "../../data/media_source"; | ||||
| import { | ||||
|   ResolvedMediaSource, | ||||
|   resolveMediaSource, | ||||
| } from "../../data/media_source"; | ||||
| import "../../layouts/ha-app-layout"; | ||||
| import { haStyle } from "../../resources/styles"; | ||||
| import type { HomeAssistant, Route } from "../../types"; | ||||
| @@ -224,11 +227,19 @@ class PanelMediaBrowser extends LitElement { | ||||
|     } | ||||
|  | ||||
|     this._player.showResolvingNewMediaPicked(); | ||||
|  | ||||
|     const resolvedUrl = await resolveMediaSource( | ||||
|       this.hass, | ||||
|       item.media_content_id | ||||
|     ); | ||||
|     let resolvedUrl: ResolvedMediaSource; | ||||
|     try { | ||||
|       resolvedUrl = await resolveMediaSource(this.hass, item.media_content_id); | ||||
|     } catch (err: any) { | ||||
|       showAlertDialog(this, { | ||||
|         title: this.hass.localize( | ||||
|           "ui.components.media-browser.media_browsing_error" | ||||
|         ), | ||||
|         text: err.message, | ||||
|       }); | ||||
|       this._player.hideResolvingNewMediaPicked(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (resolvedUrl.mime_type.startsWith("audio/")) { | ||||
|       this._player.playItem(item, resolvedUrl); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user