diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index fa638acb85..32bebb98ff 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -1,4 +1,5 @@ import { HomeAssistant } from "../types"; +import { Connection } from "home-assistant-js-websocket"; export interface LovelaceConfig { title?: string; @@ -76,3 +77,8 @@ export const saveConfig = ( type: "lovelace/config/save", config, }); + +export const subscribeLovelaceUpdates = ( + conn: Connection, + onChange: () => void +) => conn.subscribeEvents(onChange, "lovelace_updated"); diff --git a/src/managers/notification-manager.ts b/src/managers/notification-manager.ts index 8ff0afb521..0cc3dda81d 100644 --- a/src/managers/notification-manager.ts +++ b/src/managers/notification-manager.ts @@ -1,30 +1,95 @@ +import { + LitElement, + query, + property, + TemplateResult, + html, + css, + CSSResult, +} from "lit-element"; import { computeRTL } from "../common/util/compute_rtl"; -import "../components/ha-toast"; -import { LitElement, query, property, TemplateResult, html } from "lit-element"; import { HomeAssistant } from "../types"; +import "@material/mwc-button"; +import "../components/ha-toast"; // Typing // tslint:disable-next-line: no-duplicate-imports import { HaToast } from "../components/ha-toast"; export interface ShowToastParams { message: string; + action?: ToastActionParams; + duration?: number; + dismissable?: boolean; +} + +export interface ToastActionParams { + action: () => void; + text: string; } class NotificationManager extends LitElement { @property() public hass!: HomeAssistant; + + @property() private _action?: ToastActionParams; + @property() private _noCancelOnOutsideClick: boolean = false; + @query("ha-toast") private _toast!: HaToast; - public showDialog({ message }: ShowToastParams) { + public showDialog({ + message, + action, + duration, + dismissable, + }: ShowToastParams) { const toast = this._toast; toast.setAttribute("dir", computeRTL(this.hass) ? "rtl" : "ltr"); - toast.show(message); + this._action = action || undefined; + this._noCancelOnOutsideClick = + dismissable === undefined ? false : !dismissable; + toast.hide(); + toast.show({ + text: message, + duration: duration === undefined ? 3000 : duration, + }); } protected render(): TemplateResult | void { return html` - + + ${this._action + ? html` + + ` + : ""} + + `; + } + + private buttonClicked() { + this._toast.hide(); + if (this._action) { + this._action.action(); + } + } + + static get styles(): CSSResult { + return css` + mwc-button { + color: var(--primary-color); + font-weight: bold; + } `; } } customElements.define("notification-manager", NotificationManager); + +declare global { + // for fire event + interface HASSDomEvents { + "hass-notification": ShowToastParams; + } +} diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index 8cf00fd008..e8765cfcc3 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -1,6 +1,11 @@ import "@material/mwc-button"; -import { fetchConfig, LovelaceConfig, saveConfig } from "../../data/lovelace"; +import { + fetchConfig, + LovelaceConfig, + saveConfig, + subscribeLovelaceUpdates, +} from "../../data/lovelace"; import "../../layouts/hass-loading-screen"; import "../../layouts/hass-error-screen"; import "./hui-root"; @@ -15,6 +20,7 @@ import { } from "lit-element"; import { showSaveDialog } from "./editor/show-save-config-dialog"; import { generateLovelaceConfig } from "./common/generate-lovelace-config"; +import { showToast } from "../../util/toast"; interface LovelacePanelConfig { mode: "yaml" | "storage"; @@ -42,6 +48,8 @@ class LovelacePanel extends LitElement { private mqls?: MediaQueryList[]; + private _saving: boolean = false; + constructor() { super(); this._closeEditor = this._closeEditor.bind(this); @@ -66,7 +74,11 @@ class LovelacePanel extends LitElement { if (state === "error") { return html` - Reload Lovelace + ${this.hass!.localize( + "ui.panel.lovelace.reload_lovelace" + )} `; } @@ -107,6 +119,16 @@ class LovelacePanel extends LitElement { public firstUpdated() { this._fetchConfig(false); + // we don't want to unsub as we want to stay informed of updates + subscribeLovelaceUpdates(this.hass!.connection, () => + this._lovelaceChanged() + ); + // reload lovelace on reconnect so we are sure we have the latest config + window.addEventListener("connection-status", (ev) => { + if (ev.detail === "connected") { + this._fetchConfig(false); + } + }); this._updateColumns = this._updateColumns.bind(this); this.mqls = [300, 600, 900, 1200].map((width) => { const mql = matchMedia(`(min-width: ${width}px)`); @@ -155,6 +177,22 @@ class LovelacePanel extends LitElement { ); } + private _lovelaceChanged() { + if (this._saving) { + this._saving = false; + } else { + showToast(this, { + message: this.hass!.localize("ui.panel.lovelace.changed_toast.message"), + action: { + action: () => this._fetchConfig(false), + text: this.hass!.localize("ui.panel.lovelace.changed_toast.refresh"), + }, + duration: 0, + dismissable: false, + }); + } + } + private _forceFetchConfig() { this._fetchConfig(true); } @@ -211,6 +249,7 @@ class LovelacePanel extends LitElement { config: newConfig, mode: "storage", }); + this._saving = true; await saveConfig(this.hass!, newConfig); } catch (err) { // tslint:disable-next-line diff --git a/src/polymer-types.ts b/src/polymer-types.ts index 4ffbf77df6..b00de62941 100644 --- a/src/polymer-types.ts +++ b/src/polymer-types.ts @@ -31,9 +31,6 @@ declare global { "iron-resize": undefined; "config-refresh": undefined; "ha-refresh-cloud-status": undefined; - "hass-notification": { - message: string; - }; "hass-api-called": { success: boolean; response: unknown; diff --git a/src/state/disconnect-toast-mixin.ts b/src/state/disconnect-toast-mixin.ts index 853fa3f392..a36ec359ff 100644 --- a/src/state/disconnect-toast-mixin.ts +++ b/src/state/disconnect-toast-mixin.ts @@ -1,37 +1,31 @@ import { Constructor, LitElement } from "lit-element"; import { HassBaseEl } from "./hass-base-mixin"; -import { HaToast } from "../components/ha-toast"; -import { computeRTL } from "../common/util/compute_rtl"; +import { showToast } from "../util/toast"; export default (superClass: Constructor) => class extends superClass { - private _discToast?: HaToast; - protected firstUpdated(changedProps) { super.firstUpdated(changedProps); // Need to load in advance because when disconnected, can't dynamically load code. - import(/* webpackChunkName: "ha-toast" */ "../components/ha-toast"); + import(/* webpackChunkName: "notification-manager" */ "../managers/notification-manager"); } protected hassReconnected() { super.hassReconnected(); - if (this._discToast) { - this._discToast.opened = false; - } + + showToast(this, { + message: "", + duration: 1, + }); } protected hassDisconnected() { super.hassDisconnected(); - if (!this._discToast) { - const el = document.createElement("ha-toast"); - el.duration = 0; - this._discToast = el; - this.shadowRoot!.appendChild(el as any); - } - this._discToast.dir = computeRTL(this.hass!); - this._discToast.text = this.hass!.localize( - "ui.notification_toast.connection_lost" - ); - this._discToast.opened = true; + + showToast(this, { + message: this.hass!.localize("ui.notification_toast.connection_lost"), + duration: 0, + dismissable: false, + }); } }; diff --git a/src/translations/en.json b/src/translations/en.json index e8fddbcaaa..1c160b0482 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -999,7 +999,12 @@ "warning": { "entity_not_found": "Entity not available: {entity}", "entity_non_numeric": "Entity is non-numeric: {entity}" - } + }, + "changed_toast": { + "message": "The Lovelace config was updated, would you like to refresh?", + "refresh": "Refresh" + }, + "reload_lovelace": "Reload Lovelace" }, "mailbox": { "empty": "You do not have any messages", diff --git a/src/util/register-service-worker.ts b/src/util/register-service-worker.ts index e48c2dc437..1d9870019d 100644 --- a/src/util/register-service-worker.ts +++ b/src/util/register-service-worker.ts @@ -1,3 +1,6 @@ +import { HassElement } from "../state/hass-element"; +import { showToast } from "./toast"; + export const registerServiceWorker = (notifyUpdate = true) => { if ( !("serviceWorker" in navigator) || @@ -20,9 +23,19 @@ export const registerServiceWorker = (notifyUpdate = true) => { !__DEMO__ ) { // Notify users here of a new frontend being available. - import(/* webpackChunkName: "show-new-frontend-toast" */ "./show-new-frontend-toast").then( - (mod) => mod.default(installingWorker) - ); + const haElement = window.document.querySelector( + "home-assistant, ha-onboarding" + )! as HassElement; + showToast(haElement, { + message: "A new version of the frontend is available.", + action: { + action: () => + installingWorker.postMessage({ type: "skipWaiting" }), + text: "reload", + }, + duration: 0, + dismissable: false, + }); } }); }); diff --git a/src/util/show-new-frontend-toast.js b/src/util/show-new-frontend-toast.js deleted file mode 100644 index a089245bda..0000000000 --- a/src/util/show-new-frontend-toast.js +++ /dev/null @@ -1,20 +0,0 @@ -import "@material/mwc-button"; -import "../components/ha-toast"; - -export default (installingWorker) => { - const toast = document.createElement("ha-toast"); - toast.opened = true; - toast.text = "A new version of the frontend is available."; - toast.duration = 0; - - const button = document.createElement("mwc-button"); - button.addEventListener("click", () => - installingWorker.postMessage({ type: "skipWaiting" }) - ); - button.style.color = "var(--primary-color)"; - button.style.fontWeight = "bold"; - button.label = "reload"; - toast.appendChild(button); - - document.body.appendChild(toast); -};