diff --git a/setup.py b/setup.py index ec88258062..a0ff0ca994 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20181207.0", + version="20181210.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/common/const.ts b/src/common/const.ts index aa25c5a259..c9dcab53b9 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -7,6 +7,9 @@ /** Icon to use when no icon specified for domain. */ export const DEFAULT_DOMAIN_ICON = "hass:bookmark"; +/** Panel to show when no panel is picked. */ +export const DEFAULT_PANEL = "states"; + /** Domains that have a state card. */ export const DOMAINS_WITH_CARD = [ "climate", diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 054a79ce26..d3ea76c430 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -1,22 +1,23 @@ import { HomeAssistant } from "../types"; export interface LovelaceConfig { - _frontendAuto: boolean; title?: string; views: LovelaceViewConfig[]; } export interface LovelaceViewConfig { + index?: number; title?: string; badges?: string[]; cards?: LovelaceCardConfig[]; - id?: string; + path?: string; icon?: string; theme?: string; } export interface LovelaceCardConfig { - id?: string; + index?: number; + view_index?: number; type: string; [key: string]: any; } @@ -60,95 +61,11 @@ export const fetchConfig = ( force, }); -export const migrateConfig = (hass: HomeAssistant): Promise => - hass.callWS({ - type: "lovelace/config/migrate", - }); - export const saveConfig = ( hass: HomeAssistant, - config: LovelaceConfig | string, - format: "json" | "yaml" + config: LovelaceConfig ): Promise => hass.callWS({ type: "lovelace/config/save", config, - format, - }); - -export const getCardConfig = ( - hass: HomeAssistant, - cardId: string -): Promise => - hass.callWS({ - type: "lovelace/config/card/get", - card_id: cardId, - }); - -export const updateCardConfig = ( - hass: HomeAssistant, - cardId: string, - config: LovelaceCardConfig | string, - format: "json" | "yaml" -): Promise => - hass.callWS({ - type: "lovelace/config/card/update", - card_id: cardId, - card_config: config, - format, - }); - -export const deleteCard = ( - hass: HomeAssistant, - cardId: string -): Promise => - hass.callWS({ - 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, - }); - -export const updateViewConfig = ( - hass: HomeAssistant, - viewId: string, - config: LovelaceViewConfig | string, - format: "json" | "yaml" -): Promise => - hass.callWS({ - type: "lovelace/config/view/update", - view_id: viewId, - view_config: config, - format, - }); - -export const deleteView = ( - hass: HomeAssistant, - viewId: string -): Promise => - hass.callWS({ - type: "lovelace/config/view/delete", - view_id: viewId, - }); - -export const addView = ( - hass: HomeAssistant, - config: LovelaceViewConfig | string, - format: "json" | "yaml" -): Promise => - hass.callWS({ - type: "lovelace/config/view/add", - view_config: config, - format, }); diff --git a/src/layouts/app/home-assistant.js b/src/layouts/app/home-assistant.js index 75f0722d49..2cd729b534 100644 --- a/src/layouts/app/home-assistant.js +++ b/src/layouts/app/home-assistant.js @@ -10,6 +10,7 @@ import "../home-assistant-main"; import "../ha-init-page"; import "../../resources/ha-style"; import registerServiceWorker from "../../util/register-service-worker"; +import { DEFAULT_PANEL } from "../../common/const"; import HassBaseMixin from "./hass-base-mixin"; import AuthMixin from "./auth-mixin"; @@ -94,7 +95,7 @@ class HomeAssistant extends ext(PolymerElement, [ } computePanelUrl(routeData) { - return (routeData && routeData.panel) || "lovelace"; + return (routeData && routeData.panel) || DEFAULT_PANEL; } panelUrlChanged(newPanelUrl) { diff --git a/src/layouts/home-assistant-main.js b/src/layouts/home-assistant-main.js index 06eeaf113c..10104fc42a 100644 --- a/src/layouts/home-assistant-main.js +++ b/src/layouts/home-assistant-main.js @@ -11,6 +11,7 @@ import "./partial-panel-resolver"; import EventsMixin from "../mixins/events-mixin"; import NavigateMixin from "../mixins/navigate-mixin"; import { computeRTL } from "../common/util/compute_rtl"; +import { DEFAULT_PANEL } from "../common/const"; import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar"); import(/* webpackChunkName: "voice-command-dialog" */ "../dialogs/ha-voice-command-dialog"); @@ -98,7 +99,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) { ready() { super.ready(); - this._defaultPage = localStorage.defaultPage || "lovelace"; + this._defaultPage = localStorage.defaultPage || DEFAULT_PANEL; this.addEventListener("hass-open-menu", () => this.handleOpenMenu()); this.addEventListener("hass-close-menu", () => this.handleCloseMenu()); this.addEventListener("hass-start-voice", (ev) => @@ -135,7 +136,7 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) { connectedCallback() { super.connectedCallback(); if (document.location.pathname === "/") { - this.navigate(`/${localStorage.defaultPage || "lovelace"}`, true); + this.navigate(`/${localStorage.defaultPage || DEFAULT_PANEL}`, true); } } diff --git a/src/panels/dev-info/ha-panel-dev-info.js b/src/panels/dev-info/ha-panel-dev-info.js index 8ac30304dd..1252aaa84b 100644 --- a/src/panels/dev-info/ha-panel-dev-info.js +++ b/src/panels/dev-info/ha-panel-dev-info.js @@ -20,6 +20,7 @@ import formatTime from "../../common/datetime/format_time"; import EventsMixin from "../../mixins/events-mixin"; import LocalizeMixin from "../../mixins/localize-mixin"; +const OPT_IN_PANEL = "lovelace"; let registeredDialog = false; class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) { @@ -164,7 +165,7 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) {

- Go back to the old states page + Try out the new Lovelace UI

[[_defaultPageText()]]
@@ -364,15 +365,15 @@ class HaPanelDevInfo extends EventsMixin(LocalizeMixin(PolymerElement)) { _defaultPageText() { return `>> ${ - localStorage.defaultPage === "states" ? "Remove" : "Set" - } the old states as default page on this device <<`; + localStorage.defaultPage === OPT_IN_PANEL ? "Remove" : "Set" + } ${OPT_IN_PANEL} as default page on this device <<`; } _toggleDefaultPage() { - if (localStorage.defaultPage === "states") { + if (localStorage.defaultPage === OPT_IN_PANEL) { delete localStorage.defaultPage; } else { - localStorage.defaultPage = "states"; + localStorage.defaultPage = OPT_IN_PANEL; } this.$.love.innerText = this._defaultPageText(); } diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index b161d8bb59..4dbdbc40c1 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -134,8 +134,14 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement) this._roundSliderStyle = loaded.roundSliderStyle; this._jQuery = loaded.jQuery; - const brightness = this.hass!.states[this._config!.entity].attributes - .brightness; + const stateObj = this.hass!.states[this._config!.entity] as LightEntity; + + if (!stateObj) { + return; + } + + const brightness = stateObj.attributes.brightness || 0; + this._jQuery("#light", this.shadowRoot).roundSlider({ ...lightConfig, change: (value) => this._setBrightness(value), @@ -152,7 +158,13 @@ export class HuiLightCard extends hassLocalizeLitMixin(LitElement) return; } - const attrs = this.hass!.states[this._config!.entity].attributes; + const stateObj = this.hass!.states[this._config!.entity]; + + if (!stateObj) { + return; + } + + const attrs = stateObj.attributes; this._jQuery("#light", this.shadowRoot).roundSlider({ value: Math.round((attrs.brightness / 254) * 100) || 0, diff --git a/src/panels/lovelace/common/directives/long-press-directive.ts b/src/panels/lovelace/common/directives/long-press-directive.ts index 718ccbafab..879a0f3089 100644 --- a/src/panels/lovelace/common/directives/long-press-directive.ts +++ b/src/panels/lovelace/common/directives/long-press-directive.ts @@ -110,7 +110,8 @@ class LongPress extends HTMLElement implements LongPress { const clickEnd = (ev: Event) => { if ( this.cooldownEnd || - (ev instanceof TouchEvent && this.timer === undefined) + (["touchend", "touchcancel"].includes(ev.type) && + this.timer === undefined) ) { return; } diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 1be6e0bfba..448f4507f3 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -94,7 +94,7 @@ const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => { const generateViewConfig = ( localize: LocalizeFunc, - id: string, + path: string, title: string | undefined, icon: string | undefined, entities: HassEntities, @@ -158,7 +158,7 @@ const generateViewConfig = ( }); return { - id, + path, title, icon, badges, @@ -228,7 +228,6 @@ export const generateLovelaceConfig = ( } return { - _frontendAuto: true, title, views, }; diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index d05333bb1b..53a8ee0ea9 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -1,31 +1,23 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import "@polymer/paper-button/paper-button"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { showEditCardDialog } from "../editor/show-edit-card-dialog"; +import "@polymer/paper-icon-button/paper-icon-button"; +import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; import { confDeleteCard } from "../editor/delete-card"; import { HomeAssistant } from "../../../types"; import { LovelaceCardConfig } from "../../../data/lovelace"; - -declare global { - // for fire event - interface HASSDomEvents { - "show-edit-card": { - cardConfig?: LovelaceCardConfig; - viewId?: string | number; - add: boolean; - reloadLovelace: () => void; - }; - } -} +import { Lovelace } from "../types"; +import { swapCard } from "../editor/config-util"; export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) { public cardConfig?: LovelaceCardConfig; protected hass?: HomeAssistant; + protected lovelace?: Lovelace; + protected path?: [number, number]; static get properties(): PropertyDeclarations { - return { hass: {} }; + return { hass: {}, lovelace: {}, path: {} }; } protected render() { @@ -38,14 +30,14 @@ export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) { box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px, rgba(0, 0, 0, 0.2) 0px 3px 1px -2px; - text-align: right; } paper-button { color: var(--primary-color); font-weight: 500; } - paper-button.warning:not([disabled]) { - color: var(--google-red-500); + paper-icon-button.delete { + color: var(--secondary-text-color); + float: right; } @@ -55,36 +47,55 @@ export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) { this.localize("ui.panel.lovelace.editor.edit_card.edit") } - ${ - this.localize("ui.panel.lovelace.editor.edit_card.delete") - } + + + `; } + private _editCard(): void { - if (!this.cardConfig) { - return; - } showEditCardDialog(this, { - cardConfig: this.cardConfig, - add: false, - reloadLovelace: () => fireEvent(this, "config-refresh"), + lovelace: this.lovelace!, + path: this.path!, }); } - private _deleteCard(): void { - if (!this.cardConfig) { - return; - } - if (!this.cardConfig.id) { - this._editCard(); - return; - } - confDeleteCard(this.hass!, this.cardConfig.id, () => - fireEvent(this, "config-refresh") + + private _cardUp(): void { + const lovelace = this.lovelace!; + const path = this.path!; + lovelace.saveConfig( + swapCard(lovelace.config, path, [path[0], path[1] - 1]) ); } + + private _cardDown(): void { + const lovelace = this.lovelace!; + const path = this.path!; + lovelace.saveConfig( + swapCard(lovelace.config, path, [path[0], path[1] + 1]) + ); + } + + private _deleteCard(): void { + confDeleteCard(this.lovelace!, this.path!); + } } declare global { diff --git a/src/panels/lovelace/editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts similarity index 78% rename from src/panels/lovelace/editor/hui-card-picker.ts rename to src/panels/lovelace/editor/card-editor/hui-card-picker.ts index af8950750c..c9f06aaac6 100644 --- a/src/panels/lovelace/editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -2,22 +2,11 @@ import { html, LitElement } from "@polymer/lit-element"; import { TemplateResult } from "lit-html"; import "@polymer/paper-button/paper-button"; -import { HomeAssistant } from "../../../types"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { LovelaceCardConfig } from "../../../data/lovelace"; -import { getCardElementTag } from "../common/get-card-element-tag"; -import { CardPickTarget } from "./types"; -import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; - -import { uid } from "../../../common/util/uid"; - -declare global { - interface HASSDomEvents { - "card-picked": { - config: LovelaceCardConfig; - }; - } -} +import { HomeAssistant } from "../../../../types"; +import { LovelaceCardConfig } from "../../../../data/lovelace"; +import { getCardElementTag } from "../../common/get-card-element-tag"; +import { CardPickTarget } from "../types"; +import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin"; const cards = [ { name: "Alarm panel", type: "alarm-panel" }, @@ -28,7 +17,7 @@ const cards = [ { name: "Gauge", type: "gauge" }, { name: "Glance", type: "glance" }, { name: "History Graph", type: "history-graph" }, - { name: "Horizontal Stack", type: "horizontal-graph" }, + { name: "Horizontal Stack", type: "horizontal-stack" }, { name: "iFrame", type: "iframe" }, { name: "Light", type: "light" }, { name: "Map", type: "map" }, @@ -47,7 +36,8 @@ const cards = [ ]; export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) { - protected hass?: HomeAssistant; + public hass?: HomeAssistant; + public cardPicked?: (cardConf: LovelaceCardConfig) => void; protected render(): TemplateResult { return html` @@ -90,16 +80,14 @@ export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) { const tag = getCardElementTag(type); const elClass = customElements.get(tag); - let config: LovelaceCardConfig = { type, id: uid() }; + let config: LovelaceCardConfig = { type }; if (elClass && elClass.getStubConfig) { const cardConfig = elClass.getStubConfig(this.hass); config = { ...config, ...cardConfig }; } - fireEvent(this, "card-picked", { - config, - }); + this.cardPicked!(config); } } diff --git a/src/panels/lovelace/editor/hui-card-preview.ts b/src/panels/lovelace/editor/card-editor/hui-card-preview.ts similarity index 77% rename from src/panels/lovelace/editor/hui-card-preview.ts rename to src/panels/lovelace/editor/card-editor/hui-card-preview.ts index 08933f84f5..26552fc705 100644 --- a/src/panels/lovelace/editor/hui-card-preview.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-preview.ts @@ -1,12 +1,12 @@ import "@polymer/paper-input/paper-textarea"; -import createCardElement from "../common/create-card-element"; -import createErrorCardConfig from "../common/create-error-card-config"; -import { HomeAssistant } from "../../../types"; -import { LovelaceCardConfig } from "../../../data/lovelace"; -import { LovelaceCard } from "../types"; -import { ConfigError } from "./types"; -import { getCardElementTag } from "../common/get-card-element-tag"; +import createCardElement from "../../common/create-card-element"; +import createErrorCardConfig from "../../common/create-error-card-config"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCardConfig } from "../../../../data/lovelace"; +import { LovelaceCard } from "../../types"; +import { ConfigError } from "../types"; +import { getCardElementTag } from "../../common/get-card-element-tag"; export class HuiCardPreview extends HTMLElement { private _hass?: HomeAssistant; diff --git a/src/panels/lovelace/editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts similarity index 50% rename from src/panels/lovelace/editor/hui-dialog-edit-card.ts rename to src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index 0b5c3f7112..3046a4d5f4 100644 --- a/src/panels/lovelace/editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -1,11 +1,12 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { TemplateResult } from "lit-html"; -import { HomeAssistant } from "../../../types"; -import { HASSDomEvent } from "../../../common/dom/fire_event"; -import { LovelaceCardConfig } from "../../../data/lovelace"; +import { HomeAssistant } from "../../../../types"; +import { HASSDomEvent } from "../../../../common/dom/fire_event"; +import { LovelaceCardConfig } from "../../../../data/lovelace"; import "./hui-edit-card"; -import "./hui-migrate-config"; +import "./hui-dialog-pick-card"; +import { EditCardDialogParams } from "./show-edit-card-dialog"; declare global { // for fire event @@ -18,66 +19,67 @@ declare global { } } -export interface EditCardDialogParams { - cardConfig?: LovelaceCardConfig; - viewId?: string | number; - add: boolean; - reloadLovelace: () => void; -} - export class HuiDialogEditCard extends LitElement { protected hass?: HomeAssistant; private _params?: EditCardDialogParams; + private _cardConfig?: LovelaceCardConfig; static get properties(): PropertyDeclarations { return { hass: {}, _params: {}, + _cardConfig: {}, }; } + constructor() { + super(); + this._cardPicked = this._cardPicked.bind(this); + this._cancel = this._cancel.bind(this); + } + public async showDialog(params: EditCardDialogParams): Promise { this._params = params; - await this.updateComplete; - (this.shadowRoot!.children[0] as any).showDialog(); + this._cardConfig = + params.path.length === 2 + ? (this._cardConfig = params.lovelace.config.views[ + params.path[0] + ].cards![params.path[1]]) + : undefined; } protected render(): TemplateResult { if (!this._params) { return html``; } - if ( - (!this._params.add && - this._params.cardConfig && - !this._params.cardConfig.id) || - (this._params.add && !this._params.viewId) - ) { + if (!this._cardConfig) { + // Card picker return html` - + .cardPicked="${this._cardPicked}" + > `; } return html` `; } + private _cardPicked(cardConf: LovelaceCardConfig) { + this._cardConfig = cardConf; + } + private _cancel() { - this._params = { - add: false, - reloadLovelace: () => { - return; - }, - }; + this._params = undefined; + this._cardConfig = undefined; } } diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-pick-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-pick-card.ts new file mode 100644 index 0000000000..75553235ad --- /dev/null +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-pick-card.ts @@ -0,0 +1,47 @@ +import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; +import { TemplateResult } from "lit-html"; +import "@polymer/paper-dialog/paper-dialog"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; + +import "./hui-card-picker"; +import { HomeAssistant } from "../../../../types"; +import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin"; +import { LovelaceCardConfig } from "../../../../data/lovelace"; + +export class HuiDialogPickCard extends hassLocalizeLitMixin(LitElement) { + public hass?: HomeAssistant; + public cardPicked?: (cardConf: LovelaceCardConfig) => void; + + static get properties(): PropertyDeclarations { + return {}; + } + + protected render(): TemplateResult { + return html` + +

${this.localize("ui.panel.lovelace.editor.edit_card.header")}

+ + + +
+ SKIP +
+
+ `; + } + + private _skipPick() { + this.cardPicked!({ type: "" }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-pick-card": HuiDialogPickCard; + } +} + +customElements.define("hui-dialog-pick-card", HuiDialogPickCard); diff --git a/src/panels/lovelace/editor/hui-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-edit-card.ts similarity index 76% rename from src/panels/lovelace/editor/hui-edit-card.ts rename to src/panels/lovelace/editor/card-editor/hui-edit-card.ts index 5a523617c9..ef346233d3 100644 --- a/src/panels/lovelace/editor/hui-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-edit-card.ts @@ -15,31 +15,22 @@ import "@polymer/paper-dialog/paper-dialog"; import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-button/paper-button"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import { HomeAssistant } from "../../../types"; -import { - addCard, - updateCardConfig, - LovelaceCardConfig, -} from "../../../data/lovelace"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; +import { HomeAssistant } from "../../../../types"; +import { 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, - 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"; +import { LovelaceCardEditor, Lovelace } from "../../types"; +import { YamlChangedEvent, ConfigValue, ConfigError } from "../types"; +import { extYamlSchema } from "../yaml-ext-schema"; +import { EntityConfig } from "../../entity-rows/types"; +import { getCardElementTag } from "../../common/get-card-element-tag"; +import { addCard, replaceCard } from "../config-util"; declare global { interface HASSDomEvents { @@ -52,17 +43,30 @@ declare global { "config-changed": { config: LovelaceCardConfig; }; - "cancel-edit-card": {}; } } export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { + public hass?: HomeAssistant; + public lovelace?: Lovelace; + public path?: [number] | [number, number]; + public cardConfig?: LovelaceCardConfig; + public closeDialog?: () => void; + 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; + static get properties(): PropertyDeclarations { return { hass: {}, cardConfig: {}, - viewId: {}, - _cardId: {}, + viewIndex: {}, + _cardIndex: {}, _configElement: {}, _configValue: {}, _configState: {}, @@ -81,38 +85,15 @@ 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 updated(changedProperties: PropertyValues): void { super.updated(changedProperties); - if ( - !changedProperties.has("cardConfig") && - !changedProperties.has("viewId") - ) { + + if (!changedProperties.has("cardConfig")) { return; } @@ -122,17 +103,8 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { 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(); - } + this._loading = true; + this._loadConfigElement(this.cardConfig!); } protected render(): TemplateResult { @@ -147,7 +119,6 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { content = html` @@ -157,18 +128,11 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
`; - } else if (this.viewId && !this.cardConfig) { - content = html` - - `; } return html` ${this.renderStyle()} - +

${this.localize("ui.panel.lovelace.editor.edit_card.header")}

- ${this.localize("ui.common.cancel")} { await this.updateComplete; this._loading = false; @@ -292,58 +247,42 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { fireEvent(this._dialog, "iron-resize"); } - private _closeDialog(): void { - this.cardConfig = undefined; - this.viewId = undefined; - fireEvent(this, "cancel-edit-card"); - this._dialog.close(); - } - - private async _updateConfigInBackend(): Promise { + private async _save(): Promise { if (!this._isConfigValid()) { alert("Your config is not valid, please fix your config before saving."); - this._saveDone(); return; } if (!this._isConfigChanged()) { - this._closeDialog(); - this._saveDone(); + this.closeDialog!(); return; } + this._saving = true; + + const cardConf: LovelaceCardConfig = + this._configValue!.format === "yaml" + ? yaml.safeLoad(this._configValue!.value!, { + schema: extYamlSchema, + }) + : this._configValue!.value!; + try { - 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 - ); - } - fireEvent(this, "reload-lovelace"); - this._closeDialog(); - this._saveDone(); + const lovelace = this.lovelace!; + await lovelace.saveConfig( + this._creatingCard + ? addCard(lovelace.config, this.path as [number], cardConf) + : replaceCard( + lovelace.config, + this.path as [number, number], + cardConf + ) + ); + this.closeDialog!(); } 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), - }; + } finally { + this._saving = false; } } @@ -394,14 +333,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { 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._configValue = { + format: "yaml", + value: yaml.safeDump(this._configValue!.value), + }; this._uiEditor = !this._uiEditor; } else if (this._configElement && this._configValue!.format === "yaml") { const yamlConfig = this._configValue!.value; @@ -438,12 +373,12 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { } private _isConfigChanged(): boolean { - if (this.viewId) { + if (this._creatingCard) { return true; } const configValue = this._configValue!.format === "yaml" - ? yaml.safeDump(this._configValue!.value) + ? yaml.safeLoad(this._configValue!.value) : this._configValue!.value; return JSON.stringify(configValue) !== JSON.stringify(this.cardConfig); } @@ -465,6 +400,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { if (elClass && elClass.getConfigElement) { configElement = await elClass.getConfigElement(); } else { + this._configValue = { format: "yaml", value: yaml.safeDump(conf) }; this._uiEditor = false; this._configElement = null; return false; @@ -477,6 +413,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { Your config is not supported by the UI editor:
${err.message}
Falling back to YAML editor. `; + this._configValue = { + format: "yaml", + value: yaml.safeDump(conf), + }; this._uiEditor = false; this._configElement = null; return false; @@ -492,6 +432,10 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) { this._updatePreview(conf); return true; } + + private get _creatingCard(): boolean { + return this.path!.length === 1; + } } declare global { diff --git a/src/panels/lovelace/editor/hui-yaml-editor.ts b/src/panels/lovelace/editor/card-editor/hui-yaml-editor.ts similarity index 56% rename from src/panels/lovelace/editor/hui-yaml-editor.ts rename to src/panels/lovelace/editor/card-editor/hui-yaml-editor.ts index 6dcb47b70c..906df6f121 100644 --- a/src/panels/lovelace/editor/hui-yaml-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-yaml-editor.ts @@ -1,42 +1,29 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { TemplateResult } from "lit-html"; import "@polymer/paper-input/paper-textarea"; -import "@polymer/paper-spinner/paper-spinner"; -import { HomeAssistant } from "../../../types"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { getCardConfig } from "../../../data/lovelace"; +import { HomeAssistant } from "../../../../types"; +import { fireEvent } from "../../../../common/dom/fire_event"; export class HuiYAMLEditor extends LitElement { - public cardId?: string; protected hass?: HomeAssistant; private _yaml?: string; - private _loading?: boolean; static get properties(): PropertyDeclarations { - return { _yaml: {}, cardId: {} }; + return { _yaml: {} }; } set yaml(yaml: string) { if (yaml === undefined) { - this._loading = true; - this._loadConfig(); + return; } else { this._yaml = yaml; - if (this._loading) { - this._loading = false; - } } } protected render(): TemplateResult { return html` ${this.renderStyle()} - `; } - private async _loadConfig(): Promise { - if (!this.hass || !this.cardId) { - return; - } - - this._yaml = await getCardConfig(this.hass, this.cardId); - if (this._loading) { - this._loading = false; - } - } - private _valueChanged(ev: Event): void { const target = ev.target! as any; this._yaml = target.value; diff --git a/src/panels/lovelace/editor/show-edit-card-dialog.ts b/src/panels/lovelace/editor/card-editor/show-edit-card-dialog.ts similarity index 76% rename from src/panels/lovelace/editor/show-edit-card-dialog.ts rename to src/panels/lovelace/editor/card-editor/show-edit-card-dialog.ts index dfd8ee379a..8fb12d0f01 100644 --- a/src/panels/lovelace/editor/show-edit-card-dialog.ts +++ b/src/panels/lovelace/editor/card-editor/show-edit-card-dialog.ts @@ -1,5 +1,5 @@ -import { LovelaceCardConfig } from "../../../data/lovelace"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { Lovelace } from "../../types"; declare global { // for fire event @@ -13,10 +13,8 @@ const dialogShowEvent = "show-edit-card"; const dialogTag = "hui-dialog-edit-card"; export interface EditCardDialogParams { - cardConfig?: LovelaceCardConfig; - viewId?: string | number; - add: boolean; - reloadLovelace: () => void; + lovelace: Lovelace; + path: [number] | [number, number]; } const registerEditCardDialog = (element: HTMLElement) => 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 16614a117f..233c9e3ba5 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 @@ -33,7 +33,6 @@ const entitiesConfigStruct = struct.union([ const cardConfigStruct = struct({ type: "string", - id: "string|number", title: "string|number?", theme: "string?", show_header_toggle: "boolean?", diff --git a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts index 672b8b6775..dadb82f702 100644 --- a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts @@ -32,7 +32,6 @@ const entitiesConfigStruct = struct.union([ const cardConfigStruct = struct({ type: "string", - id: "string|number", title: "string|number?", theme: "string?", columns: "number?", @@ -93,6 +92,7 @@ export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement) > { + const [viewIndex] = path; + const views: LovelaceViewConfig[] = []; + + config.views.forEach((viewConf, index) => { + if (index !== viewIndex) { + views.push(config.views[index]); + return; + } + + const cards = viewConf.cards + ? [...viewConf.cards, cardConfig] + : [cardConfig]; + + views.push({ + ...viewConf, + cards, + }); + }); + + return { + ...config, + views, + }; +}; + +export const replaceCard = ( + config: LovelaceConfig, + path: [number, number], + cardConfig: LovelaceCardConfig +): LovelaceConfig => { + const [viewIndex, cardIndex] = path; + const views: LovelaceViewConfig[] = []; + + config.views.forEach((viewConf, index) => { + if (index !== viewIndex) { + views.push(config.views[index]); + return; + } + + views.push({ + ...viewConf, + cards: (viewConf.cards || []).map((origConf, ind) => + ind === cardIndex ? cardConfig : origConf + ), + }); + }); + + return { + ...config, + views, + }; +}; + +export const deleteCard = ( + config: LovelaceConfig, + path: [number, number] +): LovelaceConfig => { + const [viewIndex, cardIndex] = path; + const views: LovelaceViewConfig[] = []; + + config.views.forEach((viewConf, index) => { + if (index !== viewIndex) { + views.push(config.views[index]); + return; + } + + views.push({ + ...viewConf, + cards: (viewConf.cards || []).filter( + (_origConf, ind) => ind !== cardIndex + ), + }); + }); + + return { + ...config, + views, + }; +}; + +export const swapCard = ( + config: LovelaceConfig, + path1: [number, number], + path2: [number, number] +): LovelaceConfig => { + const card1 = config.views[path1[0]].cards![path1[1]]; + const card2 = config.views[path2[0]].cards![path2[1]]; + + const origView1 = config.views[path1[0]]; + const newView1 = { + ...origView1, + cards: origView1.cards!.map((origCard, index) => + index === path1[1] ? card2 : origCard + ), + }; + + const origView2 = path1[0] === path2[0] ? newView1 : config.views[path2[0]]; + const newView2 = { + ...origView2, + cards: origView2.cards!.map((origCard, index) => + index === path2[1] ? card1 : origCard + ), + }; + + return { + ...config, + views: config.views.map((origView, index) => + index === path2[0] ? newView2 : index === path1[0] ? newView1 : origView + ), + }; +}; + +export const addView = ( + config: LovelaceConfig, + viewConfig: LovelaceViewConfig +): LovelaceConfig => ({ + ...config, + views: config.views.concat(viewConfig), +}); + +export const replaceView = ( + config: LovelaceConfig, + viewIndex: number, + viewConfig: LovelaceViewConfig +): LovelaceConfig => ({ + ...config, + views: config.views.map((origView, index) => + index === viewIndex ? viewConfig : origView + ), +}); + +export const deleteView = ( + config: LovelaceConfig, + viewIndex: number +): LovelaceConfig => ({ + ...config, + views: config.views.filter((_origView, index) => index !== viewIndex), +}); diff --git a/src/panels/lovelace/editor/delete-card.ts b/src/panels/lovelace/editor/delete-card.ts index 11646605cc..c4afc7e863 100644 --- a/src/panels/lovelace/editor/delete-card.ts +++ b/src/panels/lovelace/editor/delete-card.ts @@ -1,17 +1,15 @@ -import { deleteCard } from "../../../data/lovelace"; -import { HomeAssistant } from "../../../types"; +import { Lovelace } from "../types"; +import { deleteCard } from "./config-util"; export async function confDeleteCard( - hass: HomeAssistant, - cardId: string, - reloadLovelace: () => void + lovelace: Lovelace, + path: [number, number] ): Promise { if (!confirm("Are you sure you want to delete this card?")) { return; } try { - await deleteCard(hass, String(cardId)); - reloadLovelace(); + await lovelace.saveConfig(deleteCard(lovelace.config, path)); } catch (err) { alert(`Deleting failed: ${err.message}`); } diff --git a/src/panels/lovelace/editor/delete-view.ts b/src/panels/lovelace/editor/delete-view.ts deleted file mode 100644 index 740c00ede6..0000000000 --- a/src/panels/lovelace/editor/delete-view.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { deleteView } from "../../../data/lovelace"; -import { HomeAssistant } from "../../../types"; - -export async function confDeleteView( - hass: HomeAssistant, - viewId: string, - reloadLovelace: () => void -): Promise { - if (!confirm("Are you sure you want to delete this view?")) { - return; - } - try { - await deleteView(hass, String(viewId)); - reloadLovelace(); - } catch (err) { - alert(`Deleting failed: ${err.message}`); - } -} diff --git a/src/panels/lovelace/editor/hui-dialog-save-config.ts b/src/panels/lovelace/editor/hui-dialog-save-config.ts index 9b7bc343ff..7e00454cfa 100644 --- a/src/panels/lovelace/editor/hui-dialog-save-config.ts +++ b/src/panels/lovelace/editor/hui-dialog-save-config.ts @@ -10,40 +10,8 @@ import "@polymer/paper-button/paper-button"; import { HomeAssistant } from "../../../types"; -import { - saveConfig, - migrateConfig, - LovelaceConfig, -} from "../../../data/lovelace"; -import { fireEvent } from "../../../common/dom/fire_event"; import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; - -declare global { - // for fire event - interface HASSDomEvents { - "show-save-config": SaveDialogParams; - } -} - -const dialogShowEvent = "show-save-config"; -const dialogTag = "hui-dialog-save-config"; - -export interface SaveDialogParams { - config: LovelaceConfig; - reloadLovelace: () => void; -} - -export const registerSaveDialog = (element: HTMLElement) => - fireEvent(element, "register-dialog", { - dialogShowEvent, - dialogTag, - dialogImport: () => import("./hui-dialog-save-config"), - }); - -export const showSaveDialog = ( - element: HTMLElement, - saveDialogParams: SaveDialogParams -) => fireEvent(element, dialogShowEvent, saveDialogParams); +import { SaveDialogParams } from "./show-save-config-dialog"; export class HuiSaveConfig extends hassLocalizeLitMixin(LitElement) { protected hass?: HomeAssistant; @@ -137,13 +105,11 @@ export class HuiSaveConfig extends hassLocalizeLitMixin(LitElement) { return; } this._saving = true; - delete this._params.config._frontendAuto; try { - await saveConfig(this.hass, this._params.config, "json"); - await migrateConfig(this.hass); + const lovelace = this._params!.lovelace; + await lovelace.saveConfig(lovelace.config); this._saving = false; this._closeDialog(); - this._params.reloadLovelace!(); } catch (err) { alert(`Saving failed: ${err.message}`); this._saving = false; @@ -157,4 +123,4 @@ declare global { } } -customElements.define(dialogTag, HuiSaveConfig); +customElements.define("hui-dialog-save-config", HuiSaveConfig); diff --git a/src/panels/lovelace/editor/hui-migrate-config.ts b/src/panels/lovelace/editor/hui-migrate-config.ts deleted file mode 100644 index e9f32f52e1..0000000000 --- a/src/panels/lovelace/editor/hui-migrate-config.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; -import { TemplateResult } from "lit-html"; - -import "@polymer/paper-spinner/paper-spinner"; -import "@polymer/paper-dialog/paper-dialog"; -// This is not a duplicate import, one is for types, one is for element. -// tslint:disable-next-line -import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog"; -import "@polymer/paper-button/paper-button"; -import { fireEvent } from "../../../common/dom/fire_event"; - -import { HomeAssistant } from "../../../types"; -import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; -import { migrateConfig } from "../../../data/lovelace"; - -export class HuiMigrateConfig extends hassLocalizeLitMixin(LitElement) { - protected hass?: HomeAssistant; - private _migrating?: boolean; - - static get properties(): PropertyDeclarations { - return { _hass: {}, _migrating: {} }; - } - - private get _dialog(): PaperDialogElement { - return this.shadowRoot!.querySelector("paper-dialog")!; - } - - public async showDialog(): Promise { - // Wait till dialog is rendered. - if (this._dialog == null) { - await this.updateComplete; - } - this._dialog.open(); - } - - protected render(): TemplateResult { - return html` - ${this.renderStyle()} - -

${this.localize("ui.panel.lovelace.editor.migrate.header")}

- -

${this.localize("ui.panel.lovelace.editor.migrate.para_no_id")}

-

- ${this.localize("ui.panel.lovelace.editor.migrate.para_migrate")} -

-
-
- ${this.localize("ui.common.cancel")} - - - ${ - this.localize("ui.panel.lovelace.editor.migrate.migrate") - } -
-
- `; - } - - private renderStyle(): TemplateResult { - return html` - - `; - } - - private _closeDialog(): void { - this._dialog.close(); - } - - private async _migrateConfig(): Promise { - this._migrating = true; - try { - await migrateConfig(this.hass!); - this._closeDialog(); - this._migrating = false; - fireEvent(this, "reload-lovelace"); - } catch (err) { - alert(`Migration failed: ${err.message}`); - this._migrating = false; - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-migrate-config": HuiMigrateConfig; - } -} - -customElements.define("hui-migrate-config", HuiMigrateConfig); diff --git a/src/panels/lovelace/editor/show-save-config-dialog.ts b/src/panels/lovelace/editor/show-save-config-dialog.ts new file mode 100644 index 0000000000..f5207c8241 --- /dev/null +++ b/src/panels/lovelace/editor/show-save-config-dialog.ts @@ -0,0 +1,33 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { Lovelace } from "../types"; + +declare global { + // for fire event + interface HASSDomEvents { + "show-save-config": SaveDialogParams; + } +} + +const dialogShowEvent = "show-save-config"; +const dialogTag = "hui-dialog-save-config"; + +export interface SaveDialogParams { + lovelace: Lovelace; +} + +let registeredDialog = false; + +export const showSaveDialog = ( + element: HTMLElement, + saveDialogParams: SaveDialogParams +) => { + if (!registeredDialog) { + registeredDialog = true; + fireEvent(element, "register-dialog", { + dialogShowEvent, + dialogTag, + dialogImport: () => import("./hui-dialog-save-config"), + }); + } + fireEvent(element, dialogShowEvent, saveDialogParams); +}; diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index fe76d20ece..b91e381632 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -1,5 +1,6 @@ import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace"; import { EntityConfig } from "../entity-rows/types"; +import { InputType } from "zlib"; export interface YamlChangedEvent extends Event { detail: { @@ -7,12 +8,6 @@ export interface YamlChangedEvent extends Event { }; } -export interface CardPickedEvent extends Event { - detail: { - config: LovelaceCardConfig; - }; -} - export interface ViewEditEvent extends Event { detail: { config: LovelaceViewConfig; @@ -41,6 +36,7 @@ export interface EditorTarget extends EventTarget { index?: number; checked?: boolean; configValue?: string; + type?: InputType; } export interface CardPickTarget extends EventTarget { diff --git a/src/panels/lovelace/editor/hui-dialog-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts similarity index 67% rename from src/panels/lovelace/editor/hui-dialog-edit-view.ts rename to src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts index 3f4d13129f..087ed20ce2 100644 --- a/src/panels/lovelace/editor/hui-dialog-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts @@ -1,10 +1,9 @@ import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { TemplateResult } from "lit-html"; -import { HomeAssistant } from "../../../types"; -import { HASSDomEvent } from "../../../common/dom/fire_event"; +import { HomeAssistant } from "../../../../types"; +import { HASSDomEvent } from "../../../../common/dom/fire_event"; import "./hui-edit-view"; -import "./hui-migrate-config"; import { EditViewDialogParams } from "./show-edit-view-dialog"; declare global { @@ -39,24 +38,11 @@ export class HuiDialogEditView extends LitElement { if (!this._params) { return html``; } - if ( - !this._params.add && - this._params.viewConfig && - !("id" in this._params.viewConfig) - ) { - return html` - - `; - } return html` `; diff --git a/src/panels/lovelace/editor/hui-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-edit-view.ts similarity index 65% rename from src/panels/lovelace/editor/hui-edit-view.ts rename to src/panels/lovelace/editor/view-editor/hui-edit-view.ts index 18d7b65186..733ec46a39 100644 --- a/src/panels/lovelace/editor/hui-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-edit-view.ts @@ -1,9 +1,4 @@ -import { - html, - LitElement, - PropertyDeclarations, - PropertyValues, -} from "@polymer/lit-element"; +import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; import { TemplateResult } from "lit-html"; import "@polymer/paper-spinner/paper-spinner"; @@ -11,53 +6,51 @@ import "@polymer/paper-tabs/paper-tab"; import "@polymer/paper-tabs/paper-tabs"; import "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-icon-button/paper-icon-button.js"; -import "@polymer/paper-item/paper-item.js"; -import "@polymer/paper-listbox/paper-listbox.js"; -import "@polymer/paper-menu-button/paper-menu-button.js"; // This is not a duplicate import, one is for types, one is for element. // tslint:disable-next-line import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog"; import "@polymer/paper-button/paper-button"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "../components/hui-entity-editor"; -import "./config-elements/hui-view-editor"; -import { HomeAssistant } from "../../../types"; +import "../../components/hui-entity-editor"; +import "./hui-view-editor"; +import { HomeAssistant } from "../../../../types"; import { - addView, - updateViewConfig, LovelaceViewConfig, -} from "../../../data/lovelace"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; -import { EntitiesEditorEvent, ViewEditEvent } from "./types"; -import { processEditorEntities } from "./process-editor-entities"; -import { EntityConfig } from "../entity-rows/types"; -import { confDeleteView } from "./delete-view"; -import { navigate } from "../../../common/navigate"; + LovelaceCardConfig, +} from "../../../../data/lovelace"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin"; +import { EntitiesEditorEvent, ViewEditEvent } from "../types"; +import { processEditorEntities } from "../process-editor-entities"; +import { EntityConfig } from "../../entity-rows/types"; +import { navigate } from "../../../../common/navigate"; +import { Lovelace } from "../../types"; +import { deleteView, addView, replaceView } from "../config-util"; export class HuiEditView extends hassLocalizeLitMixin(LitElement) { + public lovelace?: Lovelace; + public viewIndex?: number; + protected hass?: HomeAssistant; + private _config?: LovelaceViewConfig; + private _badges?: EntityConfig[]; + private _cards?: LovelaceCardConfig[]; + private _saving: boolean; + private _curTabIndex: number; + private _curTab?: string; + static get properties(): PropertyDeclarations { return { hass: {}, - viewConfig: {}, - add: {}, + lovelace: {}, + viewIndex: {}, _config: {}, _badges: {}, + _cards: {}, _saving: {}, _curTab: {}, }; } - public viewConfig?: LovelaceViewConfig; - public add?: boolean; - public reloadLovelace?: () => {}; - protected hass?: HomeAssistant; - private _config?: LovelaceViewConfig; - private _badges?: EntityConfig[]; - private _saving: boolean; - private _curTabIndex: number; - private _curTab?: string; - protected constructor() { super(); this._saving = false; @@ -69,28 +62,21 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { if (this._dialog == null) { await this.updateComplete; } - this._dialog.open(); - } - protected updated(changedProperties: PropertyValues): void { - super.updated(changedProperties); - if (!changedProperties.has("viewConfig") && !changedProperties.has("add")) { - return; - } - if ( - this.viewConfig && - (!changedProperties.get("viewConfig") || - this.viewConfig.id !== - (changedProperties.get("viewConfig") as LovelaceViewConfig).id) - ) { - const { cards, badges, ...viewConfig } = this.viewConfig; - this._config = viewConfig; - this._badges = badges ? processEditorEntities(badges) : []; - } else if (changedProperties.has("add")) { + if (this.viewIndex === undefined) { this._config = {}; this._badges = []; + this._cards = []; + } else { + const { cards, badges, ...viewConfig } = this.lovelace!.config.views[ + this.viewIndex + ]; + this._config = viewConfig; + this._badges = badges ? processEditorEntities(badges) : []; + this._cards = cards; } - this._resizeDialog(); + + this._dialog.open(); } private get _dialog(): PaperDialogElement { @@ -139,6 +125,18 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { ${content}
+ ${ + this.viewIndex !== undefined + ? html` + + ` + : "" + } ${this.localize("ui.common.cancel")} @@ -152,15 +150,6 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { > ${this.localize("ui.common.save")} - - - - Delete - -
`; @@ -182,6 +171,10 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { height: 14px; margin-right: 20px; } + paper-icon-button.delete { + margin-right: auto; + color: var(--secondary-text-color); + } paper-spinner { display: none; } @@ -199,23 +192,27 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { `; } - private _save(): void { - this._saving = true; - this._updateConfigInBackend(); - } - - private _delete() { - if (this._config!.cards && this._config!.cards!.length > 0) { + private async _delete() { + if (this._cards && this._cards.length > 0) { alert( - "You can't delete a view that has card in them. Remove the cards first." + "You can't delete a view that has cards in it. Remove the cards first." ); return; } - confDeleteView(this.hass!, this._config!.id!, () => { + + if (!confirm("Are you sure you want to delete this view?")) { + return; + } + + try { + await this.lovelace!.saveConfig( + deleteView(this.lovelace!.config, this.viewIndex!) + ); this._closeDialog(); - this.reloadLovelace!(); navigate(this, `/lovelace/0`); - }); + } catch (err) { + alert(`Deleting failed: ${err.message}`); + } } private async _resizeDialog(): Promise { @@ -225,9 +222,9 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { private _closeDialog(): void { this._curTabIndex = 0; + this.lovelace = undefined; this._config = {}; this._badges = []; - this.viewConfig = undefined; this._dialog.close(); } @@ -239,39 +236,35 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { this._resizeDialog(); } - private async _updateConfigInBackend(): Promise { + private async _save(): Promise { if (!this._config) { return; } if (!this._isConfigChanged()) { this._closeDialog(); - this._saving = false; return; } - if (this._badges) { - this._config.badges = this._badges.map((entityConf) => { - return entityConf.entity; - }); - } + this._saving = true; + + const viewConf: LovelaceViewConfig = { + cards: this._cards, + badges: this._badges!.map((entityConf) => entityConf.entity), + ...this._config, + }; + + const lovelace = this.lovelace!; try { - if (this.add) { - this._config.cards = []; - await addView(this.hass!, this._config, "json"); - } else { - await updateViewConfig( - this.hass!, - String(this.viewConfig!.id!), - this._config, - "json" - ); - } - this.reloadLovelace!(); + await lovelace.saveConfig( + this._creatingView + ? addView(lovelace.config, viewConf) + : replaceView(lovelace.config, this.viewIndex!, viewConf) + ); this._closeDialog(); - this._saving = false; } catch (err) { alert(`Saving failed: ${err.message}`); + } finally { this._saving = false; } } @@ -290,10 +283,15 @@ export class HuiEditView extends hassLocalizeLitMixin(LitElement) { } private _isConfigChanged(): boolean { - if (!this.add) { - return true; - } - return JSON.stringify(this._config) !== JSON.stringify(this.viewConfig); + return ( + this._creatingView || + JSON.stringify(this._config) !== + JSON.stringify(this.lovelace!.config.views[this.viewIndex!]) + ); + } + + private get _creatingView(): boolean { + return this.viewIndex === undefined; } } diff --git a/src/panels/lovelace/editor/config-elements/hui-view-editor.ts b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts similarity index 92% rename from src/panels/lovelace/editor/config-elements/hui-view-editor.ts rename to src/panels/lovelace/editor/view-editor/hui-view-editor.ts index 64ee490639..f9f95d7ad2 100644 --- a/src/panels/lovelace/editor/config-elements/hui-view-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts @@ -6,7 +6,7 @@ import { EditorTarget } from "../types"; import { hassLocalizeLitMixin } from "../../../../mixins/lit-localize-mixin"; import { HomeAssistant } from "../../../../types"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { configElementStyle } from "./config-elements-style"; +import { configElementStyle } from "../config-elements/config-elements-style"; import "../../components/hui-theme-select-editor"; import { LovelaceViewConfig } from "../../../../data/lovelace"; @@ -24,12 +24,11 @@ export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) { return { hass: {}, _config: {} }; } - get _id(): string { + get _path(): string { if (!this._config) { return ""; } - - return "id" in this._config ? this._config.id! : ""; + return this._config.path || ""; } get _title(): string { @@ -68,12 +67,6 @@ export class HuiViewEditor extends hassLocalizeLitMixin(LitElement) { return html` ${configElementStyle}
- + void; + lovelace: Lovelace; + viewIndex?: number; } const registerEditViewDialog = (element: HTMLElement) => diff --git a/src/panels/lovelace/ha-panel-lovelace.js b/src/panels/lovelace/ha-panel-lovelace.js deleted file mode 100644 index 936b0e5d1a..0000000000 --- a/src/panels/lovelace/ha-panel-lovelace.js +++ /dev/null @@ -1,154 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "@polymer/paper-button/paper-button"; - -import { registerSaveDialog } from "./editor/hui-dialog-save-config"; -import { fetchConfig } from "../../data/lovelace"; -import "../../layouts/hass-loading-screen"; -import "../../layouts/hass-error-screen"; -import "./hui-root"; -import localizeMixin from "../../mixins/localize-mixin"; - -let registeredDialog = false; - -class Lovelace extends localizeMixin(PolymerElement) { - static get template() { - return html` - - - - - `; - } - - static get properties() { - return { - hass: Object, - - narrow: { - type: Boolean, - value: false, - }, - - showMenu: { - type: Boolean, - value: false, - }, - - route: Object, - - _columns: { - type: Number, - value: 1, - }, - - _state: { - type: String, - value: "loading", - }, - - _errorMsg: String, - - _config: { - type: Object, - value: null, - }, - }; - } - - static get observers() { - return ["_updateColumns(narrow, showMenu)"]; - } - - ready() { - this._fetchConfig(false); - this._updateColumns = this._updateColumns.bind(this); - this.mqls = [300, 600, 900, 1200].map((width) => { - const mql = matchMedia(`(min-width: ${width}px)`); - mql.addListener(this._updateColumns); - return mql; - }); - this._updateColumns(); - super.ready(); - } - - _updateColumns() { - const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0); - // Do -1 column if the menu is docked and open - this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu)); - } - - _forceFetchConfig() { - this._fetchConfig(true); - } - - async _fetchConfig(force) { - try { - const conf = await fetchConfig(this.hass, force); - this.setProperties({ - _config: conf, - _state: "loaded", - }); - } catch (err) { - if (err.code === "file_not_found") { - const { - generateLovelaceConfig, - } = await import("./common/generate-lovelace-config"); - this.setProperties({ - _config: generateLovelaceConfig(this.hass, this.localize), - _state: "loaded", - }); - if (!registeredDialog) { - registeredDialog = true; - registerSaveDialog(this); - } - } else { - this.setProperties({ - _state: "error", - _errorMsg: err.message, - }); - } - } - } - - _equal(a, b) { - return a === b; - } -} - -customElements.define("ha-panel-lovelace", Lovelace); diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts new file mode 100644 index 0000000000..6bc94a67c0 --- /dev/null +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -0,0 +1,185 @@ +import "@polymer/paper-button/paper-button"; + +import { fetchConfig, LovelaceConfig, saveConfig } from "../../data/lovelace"; +import "../../layouts/hass-loading-screen"; +import "../../layouts/hass-error-screen"; +import "./hui-root"; +import { HomeAssistant, PanelInfo } from "../../types"; +import { Lovelace } from "./types"; +import { LitElement, html, PropertyValues } from "@polymer/lit-element"; +import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin"; +import { TemplateResult } from "lit-html"; +import { showSaveDialog } from "./editor/show-save-config-dialog"; + +interface LovelacePanelConfig { + mode: "yaml" | "storage"; +} + +class LovelacePanel extends hassLocalizeLitMixin(LitElement) { + public panel?: PanelInfo; + public hass?: HomeAssistant; + public narrow?: boolean; + public showMenu?: boolean; + public route?: object; + private _columns?: number; + private _state?: "loading" | "loaded" | "error"; + private _errorMsg?: string; + private lovelace?: Lovelace; + private mqls?: MediaQueryList[]; + + static get properties() { + return { + hass: {}, + lovelace: {}, + narrow: { type: Boolean, value: false }, + showMenu: { type: Boolean, value: false }, + route: {}, + _columns: { type: Number, value: 1 }, + _state: { type: String, value: "loading" }, + _errorMsg: String, + _config: { type: {}, value: null }, + }; + } + + public render(): TemplateResult { + const state = this._state!; + + if (state === "loaded") { + return html` + + `; + } + + if (state === "error") { + return html` + + + Reload ui-lovelace.yaml + + `; + } + + return html` + + `; + } + + public updated(changedProps: PropertyValues): void { + if (changedProps.has("narrow") || changedProps.has("showMenu")) { + this._updateColumns(); + } + } + + public firstUpdated() { + this._fetchConfig(false); + this._updateColumns = this._updateColumns.bind(this); + this.mqls = [300, 600, 900, 1200].map((width) => { + const mql = matchMedia(`(min-width: ${width}px)`); + mql.addListener(this._updateColumns); + return mql; + }); + this._updateColumns(); + } + + private _updateColumns() { + const matchColumns = this.mqls!.reduce( + (cols, mql) => cols + Number(mql.matches), + 0 + ); + // Do -1 column if the menu is docked and open + this._columns = Math.max( + 1, + matchColumns - Number(!this.narrow && this.showMenu) + ); + } + + private _forceFetchConfig() { + this._fetchConfig(true); + } + + private async _fetchConfig(force) { + let conf; + let gen: boolean; + + try { + conf = await fetchConfig(this.hass!, force); + gen = false; + } catch (err) { + if (err.code !== "config_not_found") { + // tslint:disable-next-line + console.log(err); + this._state = "error"; + this._errorMsg = err.message; + return; + } + const { + generateLovelaceConfig, + } = await import("./common/generate-lovelace-config"); + conf = generateLovelaceConfig(this.hass!, this.localize); + gen = true; + } + + this._state = "loaded"; + this.lovelace = { + config: conf, + autoGen: gen, + editMode: this.lovelace ? this.lovelace.editMode : false, + mode: this.panel!.config.mode, + setEditMode: (editMode: boolean) => { + if (!editMode || !this.lovelace!.autoGen) { + this._updateLovelace({ editMode }); + return; + } + showSaveDialog(this, { + lovelace: this.lovelace!, + }); + }, + saveConfig: async (newConfig: LovelaceConfig): Promise => { + const { config, autoGen } = this.lovelace!; + try { + // Optimistic update + this._updateLovelace({ config: newConfig, autoGen: false }); + await saveConfig(this.hass!, newConfig); + } catch (err) { + // tslint:disable-next-line + console.error(err); + // Rollback the optimistic update + this._updateLovelace({ config, autoGen }); + throw err; + } + }, + }; + } + + private _updateLovelace(props: Partial) { + this.lovelace = { + ...this.lovelace!, + ...props, + }; + } +} + +customElements.define("ha-panel-lovelace", LovelacePanel); diff --git a/src/panels/lovelace/hui-root.js b/src/panels/lovelace/hui-root.js index 2dab751fa9..614e833de0 100644 --- a/src/panels/lovelace/hui-root.js +++ b/src/panels/lovelace/hui-root.js @@ -32,8 +32,7 @@ import "./hui-unused-entities"; import "./hui-view"; import debounce from "../../common/util/debounce"; import createCardElement from "./common/create-card-element"; -import { showSaveDialog } from "./editor/hui-dialog-save-config"; -import { showEditViewDialog } from "./editor/show-edit-view-dialog"; +import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog"; // CSS and JS should only be imported once. Modules and HTML are safe. const CSS_CACHE = {}; @@ -59,20 +58,28 @@ class HUIRoot extends NavigateMixin( --paper-tabs-selection-bar-color: var(--text-primary-color, #FFF); text-transform: uppercase; } + paper-tab.iron-selected .edit-view-icon{ + display: inline-flex; + } + .edit-view-icon { + padding-left: 8px; + display: none; + } #add-view { position: absolute; height: 44px; } + #add-view ha-icon { + background-color: var(--accent-color); + border-radius: 5px; + margin-top: 4px; + } app-toolbar a { color: var(--text-primary-color, white); } paper-button.warning:not([disabled]) { color: var(--google-red-500); } - #add-view ha-icon { - background-color: var(--accent-color); - border-radius: 5px; - } #view { min-height: calc(100vh - 112px); /** @@ -90,9 +97,6 @@ class HUIRoot extends NavigateMixin( paper-item { cursor: pointer; } - .edit-view-icon { - padding-left: 8px; - } [[_computeTabTitle(item.title)]] -