diff --git a/package.json b/package.json index 6baea313b5..e42662bcc7 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "react-big-calendar": "^0.19.2", "regenerator-runtime": "^0.12.1", "round-slider": "^1.3.2", + "superstruct": "^0.6.0", "unfetch": "^4.0.1", "web-animations-js": "^2.3.1", "xss": "^1.0.3" diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts index eebdd38494..e51dd1b526 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts @@ -1,11 +1,13 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { TemplateResult } from "lit-html"; +import { struct } from "superstruct"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-toggle-button/paper-toggle-button"; import { processEditorEntities } from "../process-editor-entities"; + import { EntitiesEditorEvent, EditorTarget } from "../types"; import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin"; import { HomeAssistant } from "../../../../types"; @@ -20,6 +22,24 @@ import "../../components/hui-entity-editor"; import "../../../../components/ha-card"; import "../../../../components/ha-icon"; +const entitiesConfigStruct = struct.union([ + { + entity: "string", + name: "string?", + icon: "string?", + }, + "string", +]); + +const cardConfigStruct = struct({ + type: "string", + id: "string|number", + title: "string|number?", + theme: "string?", + show_header_toggle: "boolean?", + entities: [entitiesConfigStruct], +}); + export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement) implements LovelaceCardEditor { public hass?: HomeAssistant; @@ -39,6 +59,8 @@ export class HuiEntitiesCardEditor extends hassLocalizeLitMixin(LitElement) } public setConfig(config: Config): void { + config = cardConfigStruct(config); + this._config = { type: "entities", ...config }; this._configEntities = processEditorEntities(config.entities); } diff --git a/src/panels/lovelace/editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/hui-dialog-edit-card.ts index a686e91b82..306fe59f18 100644 --- a/src/panels/lovelace/editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/hui-dialog-edit-card.ts @@ -20,7 +20,7 @@ declare global { } const dialogShowEvent = "show-edit-card"; -const dialogTag = "hui-dialog-edit-config"; +const dialogTag = "hui-dialog-edit-card"; export interface EditCardDialogParams { cardConfig: LovelaceCardConfig; @@ -81,7 +81,7 @@ export class HuiDialogEditCard extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-dialog-edit-config": HuiDialogEditCard; + "hui-dialog-edit-card": HuiDialogEditCard; } } diff --git a/src/panels/lovelace/editor/hui-edit-card.ts b/src/panels/lovelace/editor/hui-edit-card.ts index 5e69084d92..92e9a23962 100644 --- a/src/panels/lovelace/editor/hui-edit-card.ts +++ b/src/panels/lovelace/editor/hui-edit-card.ts @@ -53,6 +53,8 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { private _loading?: boolean; private _isToggleAvailable?: boolean; private _saving: boolean; + private _errorMsg?: TemplateResult; + private _cardType?: string; static get properties(): PropertyDeclarations { return { @@ -62,6 +64,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { _configElement: {}, _configValue: {}, _configState: {}, + _errorMsg: {}, _uiEditor: {}, _saving: {}, _loading: {}, @@ -77,14 +80,11 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { set cardConfig(cardConfig: LovelaceCardConfig) { this._originalConfig = cardConfig; if (String(cardConfig.id) !== this._cardId) { - this._loading = true; - this._uiEditor = true; - this._configElement = undefined; this._configValue = { format: "yaml", value: undefined }; this._configState = "OK"; - this._isToggleAvailable = false; + this._uiEditor = true; this._cardId = String(cardConfig.id); - this._loadConfigElement(); + this._loadConfigElement(cardConfig); } } @@ -105,6 +105,24 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { } protected render(): TemplateResult { + let content; + if (!this._configElement !== undefined) { + if (this._uiEditor) { + content = html` +
${this._configElement}
+ `; + } else { + content = html` + + `; + } + } + return html` ${this.renderStyle()} @@ -118,19 +136,13 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { class="${classMap({ hidden: this._loading! })}" > ${ - this._uiEditor && this._configElement !== null + this._errorMsg ? html` -
${this._configElement}
- ` - : html` - +
${this._errorMsg}
` + : "" } + ${content}
@@ -164,7 +176,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { > ` - : html`` + : "" }
`; @@ -200,6 +212,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { .element-editor { margin-bottom: 8px; } + .error { + color: #ef5350; + border-bottom: 1px solid #ef5350; + } hr { color: #000; opacity: 0.12; @@ -213,7 +229,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { `; } - private _toggleEditor(): void { + private async _toggleEditor(): Promise { if (!this._isToggleAvailable) { alert("You can't switch editor."); return; @@ -235,9 +251,13 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { schema: extYamlSchema, }), }; - this._configElement.setConfig(this._configValue! - .value as LovelaceCardConfig); this._uiEditor = !this._uiEditor; + const cardConfig = this._configValue!.value! as LovelaceCardConfig; + if (cardConfig.type !== this._cardType) { + await this._loadConfigElement(cardConfig); + this._cardType = cardConfig.type; + } + this._configElement.setConfig(cardConfig); } this._resizeDialog(); } @@ -263,6 +283,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { } private _closeDialog(): void { + this._cardId = undefined; this._dialog.close(); } @@ -363,11 +384,16 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { return JSON.stringify(configValue) !== JSON.stringify(this._originalConfig); } - private async _loadConfigElement(): Promise { - if (!this._originalConfig) { + private async _loadConfigElement(conf: LovelaceCardConfig): Promise { + if (!conf) { return; } - const conf = this._originalConfig; + + this._errorMsg = undefined; + this._loading = true; + this._configElement = undefined; + this._isToggleAvailable = false; + const tag = conf.type.startsWith(CUSTOM_TYPE_PREFIX) ? conf!.type.substr(CUSTOM_TYPE_PREFIX.length) : `hui-${conf!.type}-card`; @@ -378,20 +404,30 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { try { configElement = await elClass.getConfigElement(); } catch (err) { - this._configElement = null; this._uiEditor = false; + this._configElement = null; return; } - this._isToggleAvailable = true; + try { + configElement.setConfig(conf); + } catch (err) { + this._errorMsg = html` + Your config is not supported by the UI editor:
${err.message}
Falling back to YAML editor. + `; + this._uiEditor = false; + this._configElement = null; + return; + } - configElement.setConfig(conf); configElement.hass = this.hass; configElement.addEventListener("config-changed", (ev) => this._handleUIConfigChanged(ev.detail.config) ); this._configValue = { format: "json", value: conf }; this._configElement = configElement; + this._isToggleAvailable = true; this._updatePreview(conf); } } diff --git a/yarn.lock b/yarn.lock index c2efe9bb1e..645bdfbaf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4226,6 +4226,16 @@ clone-buffer@^1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= +clone-deep@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" + integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.4" + kind-of "^6.0.0" + shallow-clone "^1.0.0" + clone-stats@^0.0.1, clone-stats@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" @@ -6452,6 +6462,11 @@ follow-redirects@^1.0.0: dependencies: debug "=3.1.0" +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -8556,7 +8571,7 @@ kind-of@^5.0.0: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.1, kind-of@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== @@ -9716,6 +9731,14 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -12267,6 +12290,15 @@ shady-css-parser@^0.1.0: resolved "https://registry.yarnpkg.com/shady-css-parser/-/shady-css-parser-0.1.0.tgz#534dc79c8ca5884c5ed92a4e5a13d6d863bca428" integrity sha512-irfJUUkEuDlNHKZNAp2r7zOyMlmbfVJ+kWSfjlCYYUx/7dJnANLCyTzQZsuxy5NJkvtNwSxY5Gj8MOlqXUQPyA== +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" + integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== + dependencies: + is-extendable "^0.1.1" + kind-of "^5.0.0" + mixin-object "^2.0.1" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -12903,6 +12935,14 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +superstruct@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.6.0.tgz#20d2073526cf683a57f258695e009c4a19134ad0" + integrity sha512-6Y+bh5oFXCMUmGGzcdwd8M2qXMWn9aH3Qu2wV8Cg/Lxu+3fTxJ0dTx54nKd/Sm3lSz3i901xVatzev7c/xN8Lg== + dependencies: + clone-deep "^2.0.1" + kind-of "^6.0.1" + supports-color@3.1.2, supports-color@5.4.0, supports-color@^0.2.0, supports-color@^2.0.0, supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: version "3.1.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5"