diff --git a/setup.py b/setup.py index a0ff0ca994..42f74be931 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20181210.0", + version="20181210.1", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index 6bc94a67c0..4edf80b331 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -10,11 +10,14 @@ 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"; +import { generateLovelaceConfig } from "./common/generate-lovelace-config"; interface LovelacePanelConfig { mode: "yaml" | "storage"; } +let editorLoaded = false; + class LovelacePanel extends hassLocalizeLitMixin(LitElement) { public panel?: PanelInfo; public hass?: HomeAssistant; @@ -22,7 +25,7 @@ class LovelacePanel extends hassLocalizeLitMixin(LitElement) { public showMenu?: boolean; public route?: object; private _columns?: number; - private _state?: "loading" | "loaded" | "error"; + private _state?: "loading" | "loaded" | "error" | "yaml-editor"; private _errorMsg?: string; private lovelace?: Lovelace; private mqls?: MediaQueryList[]; @@ -41,6 +44,11 @@ class LovelacePanel extends hassLocalizeLitMixin(LitElement) { }; } + constructor() { + super(); + this._closeEditor = this._closeEditor.bind(this); + } + public render(): TemplateResult { const state = this._state!; @@ -79,6 +87,15 @@ class LovelacePanel extends hassLocalizeLitMixin(LitElement) { `; } + if (state === "yaml-editor") { + return html` + + `; + } + return html` cols + Number(mql.matches), @@ -121,12 +142,11 @@ class LovelacePanel extends hassLocalizeLitMixin(LitElement) { } private async _fetchConfig(force) { - let conf; - let gen: boolean; + let conf: LovelaceConfig; + let confMode: Lovelace["mode"] = this.panel!.config.mode; try { conf = await fetchConfig(this.hass!, force); - gen = false; } catch (err) { if (err.code !== "config_not_found") { // tslint:disable-next-line @@ -135,21 +155,24 @@ class LovelacePanel extends hassLocalizeLitMixin(LitElement) { this._errorMsg = err.message; return; } - const { - generateLovelaceConfig, - } = await import("./common/generate-lovelace-config"); conf = generateLovelaceConfig(this.hass!, this.localize); - gen = true; + confMode = "generated"; } this._state = "loaded"; this.lovelace = { config: conf, - autoGen: gen, editMode: this.lovelace ? this.lovelace.editMode : false, - mode: this.panel!.config.mode, + mode: confMode, + enableFullEditMode: () => { + if (!editorLoaded) { + editorLoaded = true; + import(/* webpackChunkName: "lovelace-yaml-editor" */ "./hui-editor"); + } + this._state = "yaml-editor"; + }, setEditMode: (editMode: boolean) => { - if (!editMode || !this.lovelace!.autoGen) { + if (!editMode || this.lovelace!.mode !== "generated") { this._updateLovelace({ editMode }); return; } @@ -158,16 +181,16 @@ class LovelacePanel extends hassLocalizeLitMixin(LitElement) { }); }, saveConfig: async (newConfig: LovelaceConfig): Promise => { - const { config, autoGen } = this.lovelace!; + const { config, mode } = this.lovelace!; try { // Optimistic update - this._updateLovelace({ config: newConfig, autoGen: false }); + this._updateLovelace({ config: newConfig, mode: "storage" }); await saveConfig(this.hass!, newConfig); } catch (err) { // tslint:disable-next-line console.error(err); // Rollback the optimistic update - this._updateLovelace({ config, autoGen }); + this._updateLovelace({ config, mode }); throw err; } }, diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts new file mode 100644 index 0000000000..b1e7ace684 --- /dev/null +++ b/src/panels/lovelace/hui-editor.ts @@ -0,0 +1,128 @@ +import { LitElement, html } from "@polymer/lit-element"; +import { TemplateResult } from "lit-html"; +import yaml from "js-yaml"; + +import "@polymer/app-layout/app-header-layout/app-header-layout"; +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import "@polymer/paper-button/paper-button"; +import "@polymer/paper-icon-button/paper-icon-button"; + +import { Lovelace } from "./types"; +import { hassLocalizeLitMixin } from "../../mixins/lit-localize-mixin"; + +const TAB_INSERT = " "; + +class LovelaceFullConfigEditor extends hassLocalizeLitMixin(LitElement) { + public lovelace?: Lovelace; + public closeEditor?: () => void; + private _haStyle?: DocumentFragment; + + static get properties() { + return { + lovelace: {}, + }; + } + + public render(): TemplateResult { + return html` + ${this.renderStyle()} + + + + +
Edit Config
+ Save +
+
+
+ +
+
+ `; + } + + protected firstUpdated() { + const textArea = this.textArea; + textArea.value = yaml.safeDump(this.lovelace!.config); + textArea.addEventListener("keydown", (e) => { + if (e.keyCode !== 9) { + return; + } + + e.preventDefault(); + + // tab was pressed, get caret position/selection + const val = textArea.value; + const start = textArea.selectionStart; + const end = textArea.selectionEnd; + + // set textarea value to: text before caret + tab + text after caret + textArea.value = + val.substring(0, start) + TAB_INSERT + val.substring(end); + + // put caret at right position again + textArea.selectionStart = textArea.selectionEnd = + start + TAB_INSERT.length; + }); + } + + protected renderStyle() { + if (!this._haStyle) { + this._haStyle = document.importNode( + (document.getElementById("ha-style")! + .children[0] as HTMLTemplateElement).content, + true + ); + } + + return html` + ${this._haStyle} + + `; + } + + private _handleSave() { + let value; + try { + value = yaml.safeLoad(this.textArea.value); + } catch (err) { + alert(`Unable to parse YAML: ${err}`); + return; + } + + this.lovelace!.saveConfig(value); + } + + private get textArea(): HTMLTextAreaElement { + return this.shadowRoot!.querySelector("textarea")!; + } +} + +customElements.define("hui-editor", LovelaceFullConfigEditor); diff --git a/src/panels/lovelace/hui-root.js b/src/panels/lovelace/hui-root.js index 614e833de0..415855e555 100644 --- a/src/panels/lovelace/hui-root.js +++ b/src/panels/lovelace/hui-root.js @@ -124,9 +124,14 @@ class HUIRoot extends NavigateMixin( > - Refresh + Unused entities - [[localize("ui.panel.lovelace.editor.configure_ui")]] (alpha) + [[localize("ui.panel.lovelace.editor.configure_ui")]] + Help @@ -174,55 +179,36 @@ class HUIRoot extends NavigateMixin( return { narrow: Boolean, showMenu: Boolean, - hass: { - type: Object, - observer: "_hassChanged", - }, + hass: { type: Object, observer: "_hassChanged" }, config: { type: Object, computed: "_computeConfig(lovelace)", observer: "_configChanged", }, - lovelace: { - type: Object, - }, - columns: { - type: Number, - observer: "_columnsChanged", - }, - - _curView: { - type: Number, - value: 0, - }, - - route: { - type: Object, - observer: "_routeChanged", - }, - - notificationsOpen: { - type: Boolean, - value: false, - }, - - _persistentNotifications: { - type: Array, - value: [], - }, - + lovelace: { type: Object }, + columns: { type: Number, observer: "_columnsChanged" }, + _curView: { type: Number, value: 0 }, + route: { type: Object, observer: "_routeChanged" }, + notificationsOpen: { type: Boolean, value: false }, + _persistentNotifications: { type: Array, value: [] }, _notifications: { type: Array, computed: "_updateNotifications(hass.states, _persistentNotifications)", }, - + _yamlMode: { + type: Boolean, + computed: "_computeYamlMode(lovelace)", + }, + _storageMode: { + type: Boolean, + computed: "_computeStorageMode(lovelace)", + }, _editMode: { type: Boolean, value: false, computed: "_computeEditMode(lovelace)", observer: "_editModeChanged", }, - routeData: Object, }; } @@ -308,7 +294,15 @@ class HUIRoot extends NavigateMixin( window.open("https://www.home-assistant.io/lovelace/", "_blank"); } + _handleFullEditor() { + this.lovelace.enableFullEditMode(); + } + _editModeEnable() { + if (this._yamlMode) { + window.alert("The edit UI is not available when in YAML mode."); + return; + } this.lovelace.setEditMode(true); if (this.config.views.length < 2) { this.$.view.classList.remove("tabs-hidden"); @@ -445,6 +439,14 @@ class HUIRoot extends NavigateMixin( return lovelace ? lovelace.config : null; } + _computeYamlMode(lovelace) { + return lovelace ? lovelace.mode === "yaml" : false; + } + + _computeStorageMode(lovelace) { + return lovelace ? lovelace.mode === "storage" : false; + } + _computeEditMode(lovelace) { return lovelace ? lovelace.editMode : false; } diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 52a7a6bb31..db3f7af72b 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -4,8 +4,8 @@ import { LovelaceCardConfig, LovelaceConfig } from "../../data/lovelace"; export interface Lovelace { config: LovelaceConfig; editMode: boolean; - autoGen: boolean; - mode: "yaml" | "storage"; + mode: "generated" | "yaml" | "storage"; + enableFullEditMode: () => void; setEditMode: (editMode: boolean) => void; saveConfig: (newConfig: LovelaceConfig) => Promise; }