diff --git a/package.json b/package.json index 4577c34aed..1d604cb278 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "@gfx/zopfli": "^1.0.9", "@types/chai": "^4.1.7", "@types/codemirror": "^0.0.71", + "@types/leaflet": "^1.4.3", "@types/memoize-one": "^4.1.0", "@types/mocha": "^5.2.5", "babel-eslint": "^10", diff --git a/setup.py b/setup.py index 2a9f4977d5..cfa3788d19 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190220.0", + version="20190227.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/auth/ha-auth-flow.js b/src/auth/ha-auth-flow.js index 100b76beab..e07774c90b 100644 --- a/src/auth/ha-auth-flow.js +++ b/src/auth/ha-auth-flow.js @@ -94,7 +94,7 @@ class HaAuthFlow extends localizeLiteMixin(PolymerElement) { this.addEventListener("keypress", (ev) => { if (ev.keyCode === 13) { - this._handleSubmit(); + this._handleSubmit(ev); } }); } @@ -205,7 +205,8 @@ class HaAuthFlow extends localizeLiteMixin(PolymerElement) { ); } - async _handleSubmit() { + async _handleSubmit(ev) { + ev.preventDefault(); if (this._step.type !== "form") { this._providerChanged(this.authProvider, null); return; diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index b8f7922114..75d7311e3c 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -1,8 +1,13 @@ +import { Map } from "leaflet"; + // Sets up a Leaflet map on the provided DOM element -export const setupLeafletMap = async (mapElement) => { +export type LeafletModuleType = typeof import("leaflet"); + +export const setupLeafletMap = async ( + mapElement +): Promise<[Map, LeafletModuleType]> => { // tslint:disable-next-line - const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) - .default; + const Leaflet = (await import(/* webpackChunkName: "leaflet" */ "leaflet")) as LeafletModuleType; Leaflet.Icon.Default.imagePath = "/static/images/leaflet"; const map = Leaflet.map(mapElement); diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index b8cf388d5c..5222f79adf 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -143,6 +143,7 @@ export class HaStateLabelBadge extends LitElement { case "binary_sensor": case "device_tracker": case "updater": + case "person": return stateIcon(state); case "sun": return state.state === "above_horizon" @@ -158,11 +159,11 @@ export class HaStateLabelBadge extends LitElement { private _computeLabel(domain, state, _timerTimeRemaining) { if ( state.state === "unavailable" || - ["device_tracker", "alarm_control_panel"].includes(domain) + ["device_tracker", "alarm_control_panel", "person"].includes(domain) ) { // Localize the state with a special state_badge namespace, which has variations of // the state translations that are truncated to fit within the badge label. Translations - // are only added for device_tracker and alarm_control_panel. + // are only added for device_tracker, alarm_control_panel and person. return ( this.hass!.localize(`state_badge.${domain}.${state.state}`) || this.hass!.localize(`state_badge.default.${state.state}`) || diff --git a/src/components/ha-card.ts b/src/components/ha-card.ts index 917ec69f8d..3cc329a87f 100644 --- a/src/components/ha-card.ts +++ b/src/components/ha-card.ts @@ -18,8 +18,12 @@ class HaCard extends LitElement { var(--paper-card-background-color, white) ); border-radius: var(--ha-card-border-radius, 2px); - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), - 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + box-shadow: var( + --ha-card-box-shadow, + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12), + 0 3px 1px -2px rgba(0, 0, 0, 0.2) + ); color: var(--primary-text-color); display: block; transition: all 0.3s ease-out; diff --git a/src/components/ha-cards.js b/src/components/ha-cards.js index c5ecbac486..1e94047d80 100644 --- a/src/components/ha-cards.js +++ b/src/components/ha-cards.js @@ -36,12 +36,13 @@ const PRIORITY = { // badges have priority >= 0 updater: 0, sun: 1, - device_tracker: 2, - alarm_control_panel: 3, - timer: 4, - sensor: 5, - binary_sensor: 6, - mailbox: 7, + person: 2, + device_tracker: 3, + alarm_control_panel: 4, + timer: 5, + sensor: 6, + binary_sensor: 7, + mailbox: 8, }; const getPriority = (domain) => (domain in PRIORITY ? PRIORITY[domain] : 100); diff --git a/src/components/ha-color-picker.js b/src/components/ha-color-picker.js index 04f05866a8..0fd47626f8 100644 --- a/src/components/ha-color-picker.js +++ b/src/components/ha-color-picker.js @@ -140,6 +140,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) { hueSegments: { type: Number, value: 0, + observer: "segmentationChange", }, // the amount segments for the hue @@ -149,6 +150,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) { saturationSegments: { type: Number, value: 0, + observer: "segmentationChange", }, // set to true to make the segments purely esthetical @@ -590,5 +592,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) { this.tooltip = svgElement.tooltip; svgElement.appendChild(svgElement.tooltip); } + + segmentationChange() { + if (this.backgroundLayer) { + this.drawColorWheel(); + } + } } customElements.define("ha-color-picker", HaColorPicker); diff --git a/src/components/ha-menu-button.js b/src/components/ha-menu-button.js deleted file mode 100644 index 80c959f541..0000000000 --- a/src/components/ha-menu-button.js +++ /dev/null @@ -1,50 +0,0 @@ -import "@polymer/paper-icon-button/paper-icon-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import EventsMixin from "../mixins/events-mixin"; - -/* - * @appliesMixin EventsMixin - */ -class HaMenuButton extends EventsMixin(PolymerElement) { - static get template() { - return html` - - `; - } - - static get properties() { - return { - narrow: { - type: Boolean, - value: false, - }, - - showMenu: { - type: Boolean, - value: false, - }, - - hassio: { - type: Boolean, - value: false, - }, - }; - } - - toggleMenu(ev) { - ev.stopPropagation(); - this.fire(this.showMenu ? "hass-close-menu" : "hass-open-menu"); - } - - _getIcon(hassio) { - // hass:menu - return `${hassio ? "hassio" : "hass"}:menu`; - } -} - -customElements.define("ha-menu-button", HaMenuButton); diff --git a/src/components/ha-menu-button.ts b/src/components/ha-menu-button.ts new file mode 100644 index 0000000000..199d5228ac --- /dev/null +++ b/src/components/ha-menu-button.ts @@ -0,0 +1,44 @@ +import "@polymer/paper-icon-button/paper-icon-button"; +import { + property, + TemplateResult, + LitElement, + html, + customElement, +} from "lit-element"; + +import { fireEvent } from "../common/dom/fire_event"; + +@customElement("ha-menu-button") +class HaMenuButton extends LitElement { + @property({ type: Boolean }) + public showMenu = false; + + @property({ type: Boolean }) + public hassio = false; + + protected render(): TemplateResult | void { + return html` + + `; + } + + // We are not going to use ShadowDOM as we're rendering a single element + // without any CSS used. + protected createRenderRoot(): Element | ShadowRoot { + return this; + } + + private _toggleMenu(): void { + fireEvent(this, this.showMenu ? "hass-close-menu" : "hass-open-menu"); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-menu-button": HaMenuButton; + } +} diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts new file mode 100644 index 0000000000..2fb4f019a3 --- /dev/null +++ b/src/data/config_entries.ts @@ -0,0 +1,63 @@ +import { HomeAssistant } from "../types"; + +export interface FieldSchema { + name: string; + default?: any; + optional: boolean; +} + +export interface ConfigFlowStepForm { + type: "form"; + flow_id: string; + handler: string; + step_id: string; + data_schema: FieldSchema[]; + errors: { [key: string]: string }; + description_placeholders: { [key: string]: string }; +} + +export interface ConfigFlowStepCreateEntry { + type: "create_entry"; + version: number; + flow_id: string; + handler: string; + title: string; + data: any; + description: string; + description_placeholders: { [key: string]: string }; +} + +export interface ConfigFlowStepAbort { + type: "abort"; + flow_id: string; + handler: string; + reason: string; + description_placeholders: { [key: string]: string }; +} + +export type ConfigFlowStep = + | ConfigFlowStepForm + | ConfigFlowStepCreateEntry + | ConfigFlowStepAbort; + +export const createConfigFlow = (hass: HomeAssistant, handler: string) => + hass.callApi("POST", "config/config_entries/flow", { + handler, + }); + +export const fetchConfigFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi("GET", `config/config_entries/flow/${flowId}`); + +export const handleConfigFlowStep = ( + hass: HomeAssistant, + flowId: string, + data: { [key: string]: any } +) => + hass.callApi( + "POST", + `config/config_entries/flow/${flowId}`, + data + ); + +export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); diff --git a/src/data/input_text.ts b/src/data/input_text.ts index a8ed653111..04ee6c334c 100644 --- a/src/data/input_text.ts +++ b/src/data/input_text.ts @@ -1,7 +1,7 @@ import { HomeAssistant } from "../types"; export const setValue = (hass: HomeAssistant, entity: string, value: string) => - hass.callService("input_text", "set_value", { + hass.callService(entity.split(".", 1)[0], "set_value", { value, entity_id: entity, }); diff --git a/src/dialogs/config-flow/dialog-config-flow.ts b/src/dialogs/config-flow/dialog-config-flow.ts new file mode 100644 index 0000000000..c72abf0949 --- /dev/null +++ b/src/dialogs/config-flow/dialog-config-flow.ts @@ -0,0 +1,378 @@ +import { + LitElement, + TemplateResult, + html, + CSSResultArray, + css, + customElement, + property, + PropertyValues, +} from "lit-element"; +import "@material/mwc-button"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import "@polymer/paper-tooltip/paper-tooltip"; +import "@polymer/paper-spinner/paper-spinner"; +import "@polymer/paper-dialog/paper-dialog"; +// Not duplicate, is for typing +// tslint:disable-next-line +import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog"; + +import "../../components/ha-form"; +import "../../components/ha-markdown"; +import "../../resources/ha-style"; +import { haStyleDialog } from "../../resources/styles"; +import { + fetchConfigFlow, + createConfigFlow, + ConfigFlowStep, + handleConfigFlowStep, + deleteConfigFlow, + FieldSchema, + ConfigFlowStepForm, +} from "../../data/config_entries"; +import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types"; +import { HaConfigFlowParams } from "./show-dialog-config-flow"; + +let instance = 0; + +@customElement("dialog-config-flow") +class ConfigFlowDialog extends LitElement { + @property() + private _params?: HaConfigFlowParams; + + @property() + private _loading = true; + + private _instance = instance; + + @property() + private _step?: ConfigFlowStep; + + @property() + private _stepData?: { [key: string]: any }; + + @property() + private _errorMsg?: string; + + public async showDialog(params: HaConfigFlowParams): Promise { + this._params = params; + this._loading = true; + this._instance = instance++; + this._step = undefined; + this._stepData = {}; + this._errorMsg = undefined; + + const fetchStep = params.continueFlowId + ? fetchConfigFlow(params.hass, params.continueFlowId) + : params.newFlowForHandler + ? createConfigFlow(params.hass, params.newFlowForHandler) + : undefined; + + if (!fetchStep) { + throw new Error(`Pass in either continueFlowId or newFlorForHandler`); + } + + const curInstance = this._instance; + + await this.updateComplete; + const step = await fetchStep; + + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; + } + + this._processStep(step); + this._loading = false; + // When the flow changes, center the dialog. + // Don't do it on each step or else the dialog keeps bouncing. + setTimeout(() => this._dialog.center(), 0); + } + + protected render(): TemplateResult | void { + if (!this._params) { + return html``; + } + const localize = this._params.hass.localize; + + const step = this._step; + let headerContent: string | undefined; + let bodyContent: TemplateResult | undefined; + let buttonContent: TemplateResult | undefined; + let descriptionKey: string | undefined; + + if (!step) { + bodyContent = html` +
+ +
+ `; + } else if (step.type === "abort") { + descriptionKey = `component.${step.handler}.config.abort.${step.reason}`; + headerContent = "Aborted"; + bodyContent = html``; + buttonContent = html` + Close + `; + } else if (step.type === "create_entry") { + descriptionKey = `component.${ + step.handler + }.config.create_entry.${step.description || "default"}`; + headerContent = "Success!"; + bodyContent = html` +

Created config for ${step.title}

+ `; + buttonContent = html` + Close + `; + } else { + // form + descriptionKey = `component.${step.handler}.config.step.${ + step.step_id + }.description`; + headerContent = localize( + `component.${step.handler}.config.step.${step.step_id}.title` + ); + bodyContent = html` + + `; + + const allRequiredInfoFilledIn = + this._stepData && + step.data_schema.every( + (field) => + field.optional || + !["", undefined].includes(this._stepData![field.name]) + ); + + buttonContent = this._loading + ? html` +
+ +
+ ` + : html` +
+ + Submit + + + ${!allRequiredInfoFilledIn + ? html` + + Not all required fields are filled in. + + ` + : html``} +
+ `; + } + + let description: string | undefined; + + if (step && descriptionKey) { + const args: [string, ...string[]] = [descriptionKey]; + const placeholders = step.description_placeholders || {}; + Object.keys(placeholders).forEach((key) => { + args.push(key); + args.push(placeholders[key]); + }); + description = localize(...args); + } + + return html` + +

+ ${headerContent} +

+ + ${this._errorMsg + ? html` +
${this._errorMsg}
+ ` + : ""} + ${description + ? html` + + ` + : ""} + ${bodyContent} +
+
+ ${buttonContent} +
+
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.addEventListener("keypress", (ev) => { + if (ev.keyCode === 13) { + this._submitStep(); + } + }); + } + + private get _dialog(): PaperDialogElement { + return this.shadowRoot!.querySelector("paper-dialog")!; + } + + private async _submitStep(): Promise { + this._loading = true; + this._errorMsg = undefined; + + const curInstance = this._instance; + const stepData = this._stepData || {}; + + const toSendData = {}; + Object.keys(stepData).forEach((key) => { + const value = stepData[key]; + const isEmpty = [undefined, ""].includes(value); + + if (!isEmpty) { + toSendData[key] = value; + } + }); + + try { + const step = await handleConfigFlowStep( + this._params!.hass, + this._step!.flow_id, + toSendData + ); + + if (curInstance !== this._instance) { + return; + } + + this._processStep(step); + } catch (err) { + this._errorMsg = + (err && err.body && err.body.message) || "Unknown error occurred"; + } finally { + this._loading = false; + } + } + + private _processStep(step: ConfigFlowStep): void { + this._step = step; + + // We got a new form if there are no errors. + if (step.type === "form") { + if (!step.errors) { + step.errors = {}; + } + + if (Object.keys(step.errors).length === 0) { + const data = {}; + step.data_schema.forEach((field) => { + if ("default" in field) { + data[field.name] = field.default; + } + }); + this._stepData = data; + } + } + } + + private _flowDone(): void { + if (!this._params) { + return; + } + const flowFinished = Boolean( + this._step && ["success", "abort"].includes(this._step.type) + ); + + // If we created this flow, delete it now. + if (this._step && !flowFinished && this._params.newFlowForHandler) { + deleteConfigFlow(this._params.hass, this._step.flow_id); + } + + this._params.dialogClosedCallback({ + flowFinished, + }); + + this._errorMsg = undefined; + this._step = undefined; + this._stepData = {}; + this._params = undefined; + } + + private _openedChanged(ev: PolymerChangedEvent): void { + // Closed dialog by clicking on the overlay + if (this._step && !ev.detail.value) { + this._flowDone(); + } + } + + private _stepDataChanged(ev: PolymerChangedEvent): void { + this._stepData = applyPolymerEvent(ev, this._stepData); + } + + private _labelCallback = (schema: FieldSchema): string => { + const step = this._step as ConfigFlowStepForm; + + return this._params!.hass.localize( + `component.${step.handler}.config.step.${step.step_id}.data.${ + schema.name + }` + ); + }; + + private _errorCallback = (error: string) => + this._params!.hass.localize( + `component.${this._step!.handler}.config.error.${error}` + ); + + static get styles(): CSSResultArray { + return [ + haStyleDialog, + css` + .error { + color: red; + } + paper-dialog { + max-width: 500px; + } + ha-markdown { + word-break: break-word; + } + ha-markdown a { + color: var(--primary-color); + } + ha-markdown img:first-child:last-child { + display: block; + margin: 0 auto; + } + .init-spinner { + padding: 10px 100px 34px; + text-align: center; + } + .submit-spinner { + margin-right: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-config-flow": ConfigFlowDialog; + } +} diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts new file mode 100644 index 0000000000..35f86f8c3e --- /dev/null +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -0,0 +1,23 @@ +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; + +export interface HaConfigFlowParams { + hass: HomeAssistant; + continueFlowId?: string; + newFlowForHandler?: string; + dialogClosedCallback: (params: { flowFinished: boolean }) => void; +} + +export const loadConfigFlowDialog = () => + import(/* webpackChunkName: "dialog-config-flow" */ "./dialog-config-flow"); + +export const showConfigFlowDialog = ( + element: HTMLElement, + dialogParams: HaConfigFlowParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-config-flow", + dialogImport: loadConfigFlowDialog, + dialogParams, + }); +}; diff --git a/src/dialogs/more-info/controls/more-info-light.js b/src/dialogs/more-info/controls/more-info-light.js index 8fefea9d6c..99c1bf2c53 100644 --- a/src/dialogs/more-info/controls/more-info-light.js +++ b/src/dialogs/more-info/controls/more-info-light.js @@ -48,6 +48,11 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) { --paper-slider-knob-start-border-color: var(--primary-color); } + .segmentationContainer { + position: relative; + width: 100%; + } + ha-color-picker { display: block; width: 100%; @@ -57,6 +62,29 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) { transition: max-height 0.5s ease-in; } + .segmentationButton { + position: absolute; + top: 11%; + transform: translate(0%, 0%); + padding: 0px; + max-height: 0px; + width: 23px; + height: 23px; + opacity: var(--dark-secondary-opacity); + overflow: hidden; + transition: max-height 0.5s ease-in; + } + + .has-color.is-on .segmentationContainer .segmentationButton { + position: absolute; + top: 11%; + transform: translate(0%, 0%); + width: 23px; + height: 23px; + padding: 0px; + opacity: var(--dark-secondary-opacity); + } + .has-effect_list.is-on .effect_list, .has-brightness .brightness, .has-color_temp.is-on .color_temp, @@ -75,6 +103,11 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) { padding-top: 16px; } + .has-color.is-on .segmentationButton { + max-height: 100px; + overflow: visible; + } + .has-color.is-on ha-color-picker { max-height: 500px; overflow: visible; @@ -126,16 +159,22 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) { on-change="wvSliderChanged" > - - - +
+ + + +
@@ -65,44 +64,6 @@ export default (superClass) => } try { await callService(conn, domain, service, serviceData); - - const entityIds = Array.isArray(serviceData.entity_id) - ? serviceData.entity_id - : [serviceData.entity_id]; - - const names = []; - for (const entityId of entityIds) { - const stateObj = this.hass.states[entityId]; - if (stateObj) { - names.push(computeStateName(stateObj)); - } - } - if (names.length === 0) { - names.push(entityIds[0]); - } - - let message; - const name = names.join(", "); - if (service === "turn_on" && serviceData.entity_id) { - message = this.hass.localize( - "ui.notification_toast.entity_turned_on", - "entity", - name - ); - } else if (service === "turn_off" && serviceData.entity_id) { - message = this.hass.localize( - "ui.notification_toast.entity_turned_off", - "entity", - name - ); - } else { - message = this.hass.localize( - "ui.notification_toast.service_called", - "service", - `${domain}/${service}` - ); - } - this.fire("hass-notification", { message }); } catch (err) { if (__DEV__) { // eslint-disable-next-line diff --git a/src/layouts/app/home-assistant.ts b/src/layouts/app/home-assistant.ts index e7141e3b46..62529ef38b 100644 --- a/src/layouts/app/home-assistant.ts +++ b/src/layouts/app/home-assistant.ts @@ -17,6 +17,8 @@ import { dialogManagerMixin } from "./dialog-manager-mixin"; import ConnectionMixin from "./connection-mixin"; import NotificationMixin from "./notification-mixin"; import DisconnectToastMixin from "./disconnect-toast-mixin"; +import { urlSyncMixin } from "./url-sync-mixin"; + import { Route, HomeAssistant } from "../../types"; import { navigate } from "../../common/navigate"; @@ -36,6 +38,7 @@ export class HomeAssistantAppEl extends ext(HassBaseMixin(LitElement), [ ConnectionMixin, NotificationMixin, dialogManagerMixin, + urlSyncMixin, ]) { @property() private _route?: Route; @property() private _error?: boolean; diff --git a/src/layouts/app/more-info-mixin.ts b/src/layouts/app/more-info-mixin.ts index c4a3d8fed5..66c442cf90 100644 --- a/src/layouts/app/more-info-mixin.ts +++ b/src/layouts/app/more-info-mixin.ts @@ -6,7 +6,7 @@ declare global { // for fire event interface HASSDomEvents { "hass-more-info": { - entityId: string; + entityId: string | null; }; } } diff --git a/src/layouts/app/url-sync-mixin.ts b/src/layouts/app/url-sync-mixin.ts new file mode 100644 index 0000000000..c6817760a6 --- /dev/null +++ b/src/layouts/app/url-sync-mixin.ts @@ -0,0 +1,90 @@ +import { Constructor, LitElement } from "lit-element"; +import { HassBaseEl } from "./hass-base-mixin"; +import { fireEvent } from "../../common/dom/fire_event"; + +/* tslint:disable:no-console */ +const DEBUG = false; + +export const urlSyncMixin = ( + superClass: Constructor +) => + // Disable this functionality in the demo. + __DEMO__ + ? superClass + : class extends superClass { + private _ignoreNextHassChange = false; + private _ignoreNextPopstate = false; + private _moreInfoOpenedFromPath?: string; + + public connectedCallback(): void { + super.connectedCallback(); + window.addEventListener("popstate", this._popstateChangeListener); + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener("popstate", this._popstateChangeListener); + } + + protected hassChanged(newHass, oldHass): void { + super.hassChanged(newHass, oldHass); + + if (this._ignoreNextHassChange) { + if (DEBUG) { + console.log("ignore hasschange"); + } + this._ignoreNextHassChange = false; + return; + } + if ( + !oldHass || + oldHass.moreInfoEntityId === newHass.moreInfoEntityId + ) { + if (DEBUG) { + console.log("ignoring hass change"); + } + return; + } + + if (newHass.moreInfoEntityId) { + if (DEBUG) { + console.log("pushing state"); + } + // We keep track of where we opened moreInfo from so that we don't + // pop the state when we close the modal if the modal has navigated + // us away. + this._moreInfoOpenedFromPath = window.location.pathname; + history.pushState(null, "", window.location.pathname); + } else if ( + window.location.pathname === this._moreInfoOpenedFromPath + ) { + if (DEBUG) { + console.log("history back"); + } + this._ignoreNextPopstate = true; + history.back(); + } + } + + private _popstateChangeListener = (ev) => { + if (this._ignoreNextPopstate) { + if (DEBUG) { + console.log("ignore popstate"); + } + this._ignoreNextPopstate = false; + return; + } + + if (DEBUG) { + console.log("popstate", ev); + } + + if (this.hass && this.hass.moreInfoEntityId) { + if (DEBUG) { + console.log("deselect entity"); + } + this._ignoreNextHassChange = true; + fireEvent(this, "hass-more-info", { entityId: null }); + } + }; + }; diff --git a/src/layouts/hass-subpage.js b/src/layouts/hass-subpage.js deleted file mode 100644 index 692a3a50b0..0000000000 --- a/src/layouts/hass-subpage.js +++ /dev/null @@ -1,41 +0,0 @@ -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-icon-button/paper-icon-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../components/ha-paper-icon-button-arrow-prev"; - -class HassSubpage extends PolymerElement { - static get template() { - return html` - - - - - -
[[header]]
- -
-
- - -
- `; - } - - static get properties() { - return { - header: String, - }; - } - - _backTapped() { - history.back(); - } -} - -customElements.define("hass-subpage", HassSubpage); diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 8895df0e74..f318a7b47f 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -22,10 +22,9 @@ class HassSubpage extends LitElement { - + >
${this.header}
diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index 58679ff86a..656064920f 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -15,8 +15,6 @@ import { AppDrawerElement } from "@polymer/app-layout/app-drawer/app-drawer"; import "@polymer/app-route/app-route"; import "@polymer/iron-media-query/iron-media-query"; -import "../util/ha-url-sync"; - import "./partial-panel-resolver"; import { HomeAssistant, Route } from "../types"; import { fireEvent } from "../common/dom/fire_event"; @@ -47,7 +45,6 @@ class HomeAssistantMain extends LitElement { const disableSwipe = NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1; return html` - - + + ${this.hass.localize( + "ui.panel.config.area_registry.picker.integrations_page" + )} + ${this._items.map((entry) => { diff --git a/src/panels/config/automation/ha-automation-picker.js b/src/panels/config/automation/ha-automation-picker.js index 348e7d4fef..a699599bbf 100644 --- a/src/panels/config/automation/ha-automation-picker.js +++ b/src/panels/config/automation/ha-automation-picker.js @@ -81,9 +81,11 @@ class HaAutomationPicker extends LocalizeMixin(NavigateMixin(PolymerElement)) {
[[localize('ui.panel.config.automation.picker.introduction')]] - +

+ + [[localize('ui.panel.config.automation.picker.learn_more')]] + +

- import(/* webpackChunkName: "ha-config-flow" */ "./ha-config-flow"), - }); - } + loadConfigFlowDialog(); } _createFlow(ev) { - this.fire("show-config-flow", { + showConfigFlowDialog(this, { hass: this.hass, newFlowForHandler: ev.model.item, dialogClosedCallback: () => this.fire("hass-reload-entries"), @@ -186,7 +179,7 @@ class HaConfigManagerDashboard extends LocalizeMixin( } _continueFlow(ev) { - this.fire("show-config-flow", { + showConfigFlowDialog(this, { hass: this.hass, continueFlowId: ev.model.item.flow_id, dialogClosedCallback: () => this.fire("hass-reload-entries"), diff --git a/src/panels/config/config-entries/ha-config-flow.js b/src/panels/config/config-entries/ha-config-flow.js deleted file mode 100644 index fdb37c66ef..0000000000 --- a/src/panels/config/config-entries/ha-config-flow.js +++ /dev/null @@ -1,365 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "@polymer/paper-dialog/paper-dialog"; -import "@polymer/paper-tooltip/paper-tooltip"; -import "@polymer/paper-spinner/paper-spinner"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../components/ha-form"; -import "../../../components/ha-markdown"; -import "../../../resources/ha-style"; - -import EventsMixin from "../../../mixins/events-mixin"; -import LocalizeMixin from "../../../mixins/localize-mixin"; - -let instance = 0; - -/* - * @appliesMixin LocalizeMixin - * @appliesMixin EventsMixin - */ -class HaConfigFlow extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - -

- - - -

- - - - - -
- - - -
-
- `; - } - - static get properties() { - return { - _hass: Object, - _dialogClosedCallback: Function, - _instance: Number, - - _loading: { - type: Boolean, - value: false, - }, - - // Error message when can't talk to server etc - _errorMsg: String, - - _canSubmit: { - type: Boolean, - computed: "_computeCanSubmit(_step, _stepData, _counter)", - }, - - // Bogus counter because observing of `_stepData` doesn't seem to work - _counter: { - type: Number, - value: 0, - }, - - _opened: { - type: Boolean, - value: false, - }, - - _step: { - type: Object, - value: null, - }, - - /* - * Store user entered data. - */ - _stepData: { - type: Object, - value: null, - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("keypress", (ev) => { - if (ev.keyCode === 13) { - this._submitStep(); - } - }); - } - - showDialog({ - hass, - continueFlowId, - newFlowForHandler, - dialogClosedCallback, - }) { - this.hass = hass; - this._instance = instance++; - this._dialogClosedCallback = dialogClosedCallback; - this._createdFromHandler = !!newFlowForHandler; - this._loading = true; - this._opened = true; - - const fetchStep = continueFlowId - ? this.hass.callApi("get", `config/config_entries/flow/${continueFlowId}`) - : this.hass.callApi("post", "config/config_entries/flow", { - handler: newFlowForHandler, - }); - - const curInstance = this._instance; - - fetchStep.then((step) => { - if (curInstance !== this._instance) return; - - this._processStep(step); - this._loading = false; - // When the flow changes, center the dialog. - // Don't do it on each step or else the dialog keeps bouncing. - setTimeout(() => this.$.dialog.center(), 0); - }); - } - - _submitStep() { - this._loading = true; - this._errorMsg = null; - - const curInstance = this._instance; - - const data = {}; - Object.keys(this._stepData).forEach((key) => { - const value = this._stepData[key]; - const isEmpty = [undefined, ""].includes(value); - - if (!isEmpty) { - data[key] = value; - } - }); - - this.hass - .callApi("post", `config/config_entries/flow/${this._step.flow_id}`, data) - .then( - (step) => { - if (curInstance !== this._instance) return; - - this._processStep(step); - this._loading = false; - }, - (err) => { - this._errorMsg = - (err && err.body && err.body.message) || "Unknown error occurred"; - this._loading = false; - } - ); - } - - _processStep(step) { - if (!step.errors) step.errors = {}; - this._step = step; - // We got a new form if there are no errors. - if (step.type === "form" && Object.keys(step.errors).length === 0) { - const data = {}; - step.data_schema.forEach((field) => { - if ("default" in field) { - data[field.name] = field.default; - } - }); - this._stepData = data; - } - } - - _flowDone() { - this._opened = false; - const flowFinished = - this._step && ["success", "abort"].includes(this._step.type); - - if (this._step && !flowFinished && this._createdFromHandler) { - this.hass.callApi( - "delete", - `config/config_entries/flow/${this._step.flow_id}` - ); - } - - this._dialogClosedCallback({ - flowFinished, - }); - - this._errorMsg = null; - this._step = null; - this._stepData = {}; - this._dialogClosedCallback = null; - } - - _equals(a, b) { - return a === b; - } - - _openedChanged(ev) { - // Closed dialog by clicking on the overlay - if (this._step && !ev.detail.value) { - this._flowDone(); - } - } - - _computeStepTitle(localize, step) { - return localize( - `component.${step.handler}.config.step.${step.step_id}.title` - ); - } - - _computeStepDescription(localize, step) { - const args = []; - if (step.type === "form") { - args.push( - `component.${step.handler}.config.step.${step.step_id}.description` - ); - } else if (step.type === "abort") { - args.push(`component.${step.handler}.config.abort.${step.reason}`); - } else if (step.type === "create_entry") { - args.push( - `component.${step.handler}.config.create_entry.${step.description || - "default"}` - ); - } - - const placeholders = step.description_placeholders || {}; - Object.keys(placeholders).forEach((key) => { - args.push(key); - args.push(placeholders[key]); - }); - - return localize(...args); - } - - _computeLabelCallback(localize, step) { - // Returns a callback for ha-form to calculate labels per schema object - return (schema) => - localize( - `component.${step.handler}.config.step.${step.step_id}.data.${ - schema.name - }` - ); - } - - _computeErrorCallback(localize, step) { - // Returns a callback for ha-form to calculate error messages - return (error) => - localize(`component.${step.handler}.config.error.${error}`); - } - - _computeCanSubmit(step, stepData) { - // We can submit if all required fields are filled in - return ( - step !== null && - step.type === "form" && - stepData !== null && - step.data_schema.every( - (field) => - field.optional || !["", undefined].includes(stepData[field.name]) - ) - ); - } - - _increaseCounter() { - this._counter += 1; - } -} - -customElements.define("ha-config-flow", HaConfigFlow); diff --git a/src/panels/config/entity_registry/ha-config-entity-registry.ts b/src/panels/config/entity_registry/ha-config-entity-registry.ts index 016f03e995..8e5bd8b375 100644 --- a/src/panels/config/entity_registry/ha-config-entity-registry.ts +++ b/src/panels/config/entity_registry/ha-config-entity-registry.ts @@ -70,10 +70,12 @@ class HaConfigEntityRegistry extends LitElement { ${this.hass.localize( "ui.panel.config.entity_registry.picker.introduction2" )} -

+ + ${this.hass.localize( + "ui.panel.config.entity_registry.picker.integrations_page" + )} + ${this._items.map((entry) => { diff --git a/src/panels/config/js/automation.js b/src/panels/config/js/automation.js index 5c7304c8f6..4f90040444 100644 --- a/src/panels/config/js/automation.js +++ b/src/panels/config/js/automation.js @@ -73,7 +73,9 @@ export default class Automation extends Component { )}

- + {localize( + "ui.panel.config.automation.editor.triggers.learn_more" + )} - + {localize( + "ui.panel.config.automation.editor.conditions.learn_more" + )} - + {localize("ui.panel.config.automation.editor.actions.learn_more")}