diff --git a/src/common/util/uid.ts b/src/common/util/uid.ts new file mode 100644 index 0000000000..f52a03e0cd --- /dev/null +++ b/src/common/util/uid.ts @@ -0,0 +1,9 @@ +function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +} + +export function uid() { + return s4() + s4() + s4() + s4() + s4(); +} diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index f7b3a461bb..cfb4489bb6 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -33,12 +33,12 @@ export const migrateConfig = (hass: HomeAssistant): Promise => export const saveConfig = ( hass: HomeAssistant, config: LovelaceConfig | string, - configFormat: "json" | "yaml" + format: "json" | "yaml" ): Promise => hass.callWS({ type: "lovelace/config/save", config, - format: configFormat, + format, }); export const getCardConfig = ( @@ -54,13 +54,13 @@ export const updateCardConfig = ( hass: HomeAssistant, cardId: string, config: LovelaceCardConfig | string, - configFormat: "json" | "yaml" + format: "json" | "yaml" ): Promise => hass.callWS({ type: "lovelace/config/card/update", card_id: cardId, card_config: config, - format: configFormat, + format, }); export const deleteCard = ( @@ -71,3 +71,16 @@ export const deleteCard = ( type: "lovelace/config/card/delete", card_id: cardId, }); + +export const addCard = ( + hass: HomeAssistant, + viewId: string, + config: LovelaceCardConfig | string, + format: "json" | "yaml" +): Promise => + hass.callWS({ + type: "lovelace/config/card/add", + view_id: viewId, + card_config: config, + format, + }); diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts index 182c26aac5..2afd44ad37 100644 --- a/src/panels/lovelace/cards/hui-entities-card.ts +++ b/src/panels/lovelace/cards/hui-entities-card.ts @@ -43,6 +43,11 @@ class HuiEntitiesCard extends hassLocalizeLitMixin(LitElement) await import("../editor/config-elements/hui-entities-card-editor"); return document.createElement("hui-entities-card-editor"); } + + public static getStubConfig(): object { + return { entities: [] }; + } + protected _hass?: HomeAssistant; protected _config?: Config; protected _configEntities?: ConfigEntity[]; diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index 0fefb11001..51c8edea87 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -47,6 +47,9 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement) await import("../editor/config-elements/hui-glance-card-editor"); return document.createElement("hui-glance-card-editor"); } + public static getStubConfig(): object { + return { entities: [] }; + } public hass?: HomeAssistant; private _config?: Config; diff --git a/src/panels/lovelace/common/get-card-element-tag.ts b/src/panels/lovelace/common/get-card-element-tag.ts new file mode 100644 index 0000000000..ee80d760a1 --- /dev/null +++ b/src/panels/lovelace/common/get-card-element-tag.ts @@ -0,0 +1,7 @@ +const CUSTOM_TYPE_PREFIX = "custom:"; + +export function getCardElementTag(type: string): string { + return type.startsWith(CUSTOM_TYPE_PREFIX) + ? type.substr(CUSTOM_TYPE_PREFIX.length) + : `hui-${type}-card`; +} diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 9f8ced06c4..d64ed72ea0 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -1,10 +1,7 @@ -import "@polymer/paper-button/paper-button"; import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; +import "@polymer/paper-button/paper-button"; import { fireEvent } from "../../../common/dom/fire_event"; -import { - showEditCardDialog, - registerEditCardDialog, -} from "../editor/hui-dialog-edit-card"; +import { showEditCardDialog } from "../editor/hui-dialog-edit-card"; import { confDeleteCard } from "../editor/delete-card"; import { HomeAssistant } from "../../../types"; @@ -14,14 +11,14 @@ declare global { // for fire event interface HASSDomEvents { "show-edit-card": { - cardConfig: LovelaceCardConfig; + cardConfig?: LovelaceCardConfig; + viewId?: string | number; + add: boolean; reloadLovelace: () => void; }; } } -let registeredDialog = false; - export class HuiCardOptions extends LitElement { public cardConfig?: LovelaceCardConfig; protected hass?: HomeAssistant; @@ -30,14 +27,6 @@ export class HuiCardOptions extends LitElement { return { hass: {} }; } - public connectedCallback() { - super.connectedCallback(); - if (!registeredDialog) { - registeredDialog = true; - registerEditCardDialog(this); - } - } - protected render() { return html` + `; + } + + private _cardPicked(ev: Event): void { + const type = (ev.currentTarget! as CardPickTarget).type; + const tag = getCardElementTag(type); + + const elClass = customElements.get(tag); + let config: LovelaceCardConfig = { type, id: uid() }; + + if (elClass && elClass.getStubConfig) { + const cardConfig = elClass.getStubConfig(this.hass); + config = { ...config, ...cardConfig }; + } + + fireEvent(this, "card-picked", { + config, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-card-picker": HuiCardPicker; + } +} + +customElements.define("hui-card-picker", HuiCardPicker); diff --git a/src/panels/lovelace/editor/hui-card-preview.ts b/src/panels/lovelace/editor/hui-card-preview.ts index 4cedeec537..4abeeb21a6 100644 --- a/src/panels/lovelace/editor/hui-card-preview.ts +++ b/src/panels/lovelace/editor/hui-card-preview.ts @@ -6,8 +6,7 @@ import { HomeAssistant } from "../../../types"; import { LovelaceCardConfig } from "../../../data/lovelace"; import { LovelaceCard } from "../types"; import { ConfigError } from "./types"; - -const CUSTOM_TYPE_PREFIX = "custom:"; +import { getCardElementTag } from "../common/get-card-element-tag"; export class HuiCardPreview extends HTMLElement { private _hass?: HomeAssistant; @@ -39,9 +38,7 @@ export class HuiCardPreview extends HTMLElement { return; } - const tag = configValue.type.startsWith(CUSTOM_TYPE_PREFIX) - ? configValue.type.substr(CUSTOM_TYPE_PREFIX.length) - : `hui-${configValue.type}-card`; + const tag = getCardElementTag(configValue.type); if (tag.toUpperCase() === this._element.tagName) { this._element.setConfig(configValue); diff --git a/src/panels/lovelace/editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/hui-dialog-edit-card.ts index 306fe59f18..0516753e64 100644 --- a/src/panels/lovelace/editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/hui-dialog-edit-card.ts @@ -19,15 +19,18 @@ declare global { } } +let registeredDialog = false; const dialogShowEvent = "show-edit-card"; const dialogTag = "hui-dialog-edit-card"; export interface EditCardDialogParams { - cardConfig: LovelaceCardConfig; + cardConfig?: LovelaceCardConfig; + viewId?: string | number; + add: boolean; reloadLovelace: () => void; } -export const registerEditCardDialog = (element: HTMLElement) => +const registerEditCardDialog = (element: HTMLElement) => fireEvent(element, "register-dialog", { dialogShowEvent, dialogTag, @@ -37,7 +40,13 @@ export const registerEditCardDialog = (element: HTMLElement) => export const showEditCardDialog = ( element: HTMLElement, editCardDialogParams: EditCardDialogParams -) => fireEvent(element, dialogShowEvent, editCardDialogParams); +) => { + if (!registeredDialog) { + registeredDialog = true; + registerEditCardDialog(element); + } + fireEvent(element, dialogShowEvent, editCardDialogParams); +}; export class HuiDialogEditCard extends LitElement { protected hass?: HomeAssistant; @@ -60,7 +69,12 @@ export class HuiDialogEditCard extends LitElement { if (!this._params) { return html``; } - if (!this._params.cardConfig.id) { + if ( + (!this._params.add && + this._params.cardConfig && + !this._params.cardConfig.id) || + (this._params.add && !this._params.viewId) + ) { return html` `; } + + private _cancel() { + this._params = { + add: false, + reloadLovelace: () => { + return; + }, + }; + } } declare global { diff --git a/src/panels/lovelace/editor/hui-edit-card.ts b/src/panels/lovelace/editor/hui-edit-card.ts index 92e9a23962..f8fe186f28 100644 --- a/src/panels/lovelace/editor/hui-edit-card.ts +++ b/src/panels/lovelace/editor/hui-edit-card.ts @@ -1,4 +1,9 @@ -import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; +import { + html, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "@polymer/lit-element"; import { classMap } from "lit-html/directives/classMap"; import { TemplateResult } from "lit-html"; import yaml from "js-yaml"; @@ -12,19 +17,30 @@ import "@polymer/paper-button/paper-button"; import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import { HomeAssistant } from "../../../types"; -import { updateCardConfig, LovelaceCardConfig } from "../../../data/lovelace"; +import { + addCard, + updateCardConfig, + LovelaceCardConfig, +} from "../../../data/lovelace"; import { fireEvent } from "../../../common/dom/fire_event"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import "./hui-yaml-editor"; +import "./hui-card-picker"; import "./hui-card-preview"; // This is not a duplicate import, one is for types, one is for element. // tslint:disable-next-line import { HuiCardPreview } from "./hui-card-preview"; import { LovelaceCardEditor } from "../types"; -import { YamlChangedEvent, ConfigValue, ConfigError } from "./types"; +import { + YamlChangedEvent, + CardPickedEvent, + ConfigValue, + ConfigError, +} from "./types"; import { extYamlSchema } from "./yaml-ext-schema"; import { EntityConfig } from "../entity-rows/types"; +import { getCardElementTag } from "../common/get-card-element-tag"; declare global { interface HASSDomEvents { @@ -37,30 +53,17 @@ declare global { "config-changed": { config: LovelaceCardConfig; }; + "cancel-edit-card": {}; } } -const CUSTOM_TYPE_PREFIX = "custom:"; - export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { - protected hass?: HomeAssistant; - private _cardId?: string; - private _originalConfig?: LovelaceCardConfig; - private _configElement?: LovelaceCardEditor | null; - private _uiEditor?: boolean; - private _configValue?: ConfigValue; - private _configState?: string; - private _loading?: boolean; - private _isToggleAvailable?: boolean; - private _saving: boolean; - private _errorMsg?: TemplateResult; - private _cardType?: string; - static get properties(): PropertyDeclarations { return { - _hass: {}, + hass: {}, + cardConfig: {}, + viewId: {}, _cardId: {}, - _originalConfig: {}, _configElement: {}, _configValue: {}, _configState: {}, @@ -68,34 +71,9 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { _uiEditor: {}, _saving: {}, _loading: {}, - _isToggleAvailable: {}, }; } - protected constructor() { - super(); - this._saving = false; - } - - set cardConfig(cardConfig: LovelaceCardConfig) { - this._originalConfig = cardConfig; - if (String(cardConfig.id) !== this._cardId) { - this._configValue = { format: "yaml", value: undefined }; - this._configState = "OK"; - this._uiEditor = true; - this._cardId = String(cardConfig.id); - this._loadConfigElement(cardConfig); - } - } - - public async showDialog(): Promise { - // Wait till dialog is rendered. - if (this._dialog == null) { - await this.updateComplete; - } - this._dialog.open(); - } - private get _dialog(): PaperDialogElement { return this.shadowRoot!.querySelector("paper-dialog")!; } @@ -104,9 +82,64 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { return this.shadowRoot!.querySelector("hui-card-preview")!; } + public cardConfig?: LovelaceCardConfig; + public viewId?: string | number; + protected hass?: HomeAssistant; + private _cardId?: string; + private _configElement?: LovelaceCardEditor | null; + private _uiEditor?: boolean; + private _configValue?: ConfigValue; + private _configState?: string; + private _loading?: boolean; + private _saving: boolean; + private _errorMsg?: TemplateResult; + private _cardType?: string; + + protected constructor() { + super(); + this._saving = false; + } + + public async showDialog(): Promise { + // Wait till dialog is rendered. + if (this._dialog == null) { + await this.updateComplete; + } + this._dialog.open(); + } + + protected async updated(changedProperties: PropertyValues): Promise { + super.updated(changedProperties); + if ( + !changedProperties.has("cardConfig") && + !changedProperties.has("viewId") + ) { + return; + } + + this._configValue = { format: "yaml", value: undefined }; + this._configState = "OK"; + this._uiEditor = true; + this._errorMsg = undefined; + this._configElement = undefined; + + if (this.cardConfig && String(this.cardConfig.id) !== this._cardId) { + this._loading = true; + this._cardId = String(this.cardConfig.id); + this._loadConfigElement(this.cardConfig); + } else { + this._cardId = undefined; + } + + if (this.viewId && !this.cardConfig) { + this._resizeDialog(); + } + } + protected render(): TemplateResult { let content; - if (!this._configElement !== undefined) { + let preview; + if (this._configElement !== undefined) { if (this._uiEditor) { content = html`
${this._configElement}
@@ -121,6 +154,17 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { > `; } + preview = html` +
+ + `; + } else if (this.viewId && !this.cardConfig) { + content = html` + + `; } return html` @@ -142,16 +186,17 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { ` : "" } - ${content} -
- + ${content} ${preview} ${ !this._loading ? html`
${ this.localize( @@ -163,7 +208,8 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { >${this.localize("ui.common.cancel")} { - if (!this._isToggleAvailable) { - alert("You can't switch editor."); - return; - } - if (this._uiEditor && this._configValue!.format === "json") { - if (this._isConfigChanged()) { - this._configValue = { - format: "yaml", - value: yaml.safeDump(this._configValue!.value), - }; - } else { - this._configValue = { format: "yaml", value: undefined }; - } - this._uiEditor = !this._uiEditor; - } else if (this._configElement && this._configValue!.format === "yaml") { - this._configValue = { - format: "json", - value: yaml.safeLoad(this._configValue!.value, { - schema: extYamlSchema, - }), - }; - 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(); - } - private _save(): void { this._saving = true; this._updateConfigInBackend(); @@ -283,7 +296,9 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { } private _closeDialog(): void { - this._cardId = undefined; + this.cardConfig = undefined; + this.viewId = undefined; + fireEvent(this, "cancel-edit-card"); this._dialog.close(); } @@ -301,21 +316,39 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { } try { - await updateCardConfig( - this.hass!, - this._cardId!, - this._configValue!.value!, - this._configValue!.format - ); + if (this.viewId) { + await addCard( + this.hass!, + String(this.viewId), + this._configValue!.value!, + this._configValue!.format + ); + } else { + await updateCardConfig( + this.hass!, + this._cardId!, + this._configValue!.value!, + this._configValue!.format + ); + } this._closeDialog(); this._saveDone(); - fireEvent(this, "reload-lovelace"); } catch (err) { alert(`Saving failed: ${err.message}`); this._saveDone(); } } + private async _handleCardPicked(ev: CardPickedEvent): Promise { + const succes = await this._loadConfigElement(ev.detail.config); + if (!succes) { + this._configValue = { + format: "yaml", + value: yaml.safeDump(ev.detail.config), + }; + } + } + private _handleYamlChanged(ev: YamlChangedEvent): void { this._configValue = { format: "yaml", value: ev.detail.yaml }; try { @@ -324,11 +357,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { }) as LovelaceCardConfig; this._updatePreview(config); this._configState = "OK"; - if (!this._isToggleAvailable && this._configElement !== null) { - this._isToggleAvailable = true; - } } catch (err) { - this._isToggleAvailable = false; this._configState = "YAML_ERROR"; this._setPreviewError({ type: "YAML Error", @@ -365,8 +394,42 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { this._resizeDialog(); } + private async _toggleEditor(): Promise { + if (this._uiEditor && this._configValue!.format === "json") { + if (this._isConfigChanged()) { + this._configValue = { + format: "yaml", + value: yaml.safeDump(this._configValue!.value), + }; + } else { + this._configValue = { format: "yaml", value: undefined }; + } + this._uiEditor = !this._uiEditor; + } else if (this._configElement && this._configValue!.format === "yaml") { + const yamlConfig = this._configValue!.value; + const cardConfig = yaml.safeLoad(yamlConfig, { + schema: extYamlSchema, + }) as LovelaceCardConfig; + this._uiEditor = !this._uiEditor; + if (cardConfig.type !== this._cardType) { + const succes = await this._loadConfigElement(cardConfig); + if (!succes) { + this._loadedDialog(); + } + this._cardType = cardConfig.type; + } else { + this._configValue = { + format: "json", + value: cardConfig, + }; + this._configElement.setConfig(cardConfig); + } + } + this._resizeDialog(); + } + private _isConfigValid() { - if (!this._cardId || !this._configValue || !this._configValue.value) { + if (!this._configValue || !this._configValue.value) { return false; } if (this._configState === "OK") { @@ -377,36 +440,36 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { } private _isConfigChanged(): boolean { + if (this.viewId) { + return true; + } const configValue = this._configValue!.format === "yaml" ? yaml.safeDump(this._configValue!.value) : this._configValue!.value; - return JSON.stringify(configValue) !== JSON.stringify(this._originalConfig); + return JSON.stringify(configValue) !== JSON.stringify(this.cardConfig); } - private async _loadConfigElement(conf: LovelaceCardConfig): Promise { + private async _loadConfigElement(conf: LovelaceCardConfig): Promise { if (!conf) { - return; + return false; } 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`; + const tag = getCardElementTag(conf.type); const elClass = customElements.get(tag); let configElement; - try { + if (elClass && elClass.getConfigElement) { configElement = await elClass.getConfigElement(); - } catch (err) { + } else { this._uiEditor = false; this._configElement = null; - return; + return false; } try { @@ -418,7 +481,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { `; this._uiEditor = false; this._configElement = null; - return; + return false; } configElement.hass = this.hass; @@ -427,8 +490,9 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { ); this._configValue = { format: "json", value: conf }; this._configElement = configElement; - this._isToggleAvailable = true; + await this.updateComplete; this._updatePreview(conf); + return true; } } diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index 8c929f37a5..c814cc8ded 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -7,6 +7,12 @@ export interface YamlChangedEvent extends Event { }; } +export interface CardPickedEvent extends Event { + detail: { + config: LovelaceCardConfig; + }; +} + export interface ConfigValue { format: "json" | "yaml"; value?: string | LovelaceCardConfig; @@ -30,3 +36,7 @@ export interface EditorTarget extends EventTarget { checked?: boolean; configValue?: string; } + +export interface CardPickTarget extends EventTarget { + type: string; +} diff --git a/src/panels/lovelace/hui-view.js b/src/panels/lovelace/hui-view.js index 5c717968f5..59af019449 100644 --- a/src/panels/lovelace/hui-view.js +++ b/src/panels/lovelace/hui-view.js @@ -1,15 +1,18 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; +import "@polymer/paper-fab/paper-fab"; import "../../components/entity/ha-state-label-badge"; import "./components/hui-card-options"; import applyThemesOnElement from "../../common/dom/apply_themes_on_element"; +import EventsMixin from "../../mixins/events-mixin"; import createCardElement from "./common/create-card-element"; import { computeCardSize } from "./common/compute-card-size"; +import { showEditCardDialog } from "./editor/hui-dialog-edit-card"; -class HUIView extends PolymerElement { +class HUIView extends EventsMixin(PolymerElement) { static get template() { return html`
+ `; } @@ -93,6 +115,16 @@ class HUIView extends PolymerElement { this._badges = []; } + _addCard() { + showEditCardDialog(this, { + viewId: this.config.id, + add: true, + reloadLovelace: () => { + this.fire("config-refresh"); + }, + }); + } + _createBadges(config) { const root = this.$.badges; while (root.lastChild) {