diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index 25c1d3bdb2..a63ad71787 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -29,6 +29,7 @@ import { } from "../../../../src/data/lovelace"; import "./hc-layout"; import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; +import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; @customElement("hc-cast") class HcCast extends LitElement { @@ -158,7 +159,8 @@ class HcCast extends LitElement { protected updated(changedProps) { super.updated(changedProps); - this.toggleAttribute( + toggleAttribute( + this, "hide-icons", this.lovelaceConfig ? !this.lovelaceConfig.views.some((view) => view.icon) diff --git a/package.json b/package.json index 8671285db2..7f06e4ca4a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@material/mwc-base": "^0.6.0", "@material/mwc-button": "^0.6.0", "@material/mwc-ripple": "^0.6.0", - "@mdi/svg": "3.9.97", + "@mdi/svg": "4.0.96", "@polymer/app-layout": "^3.0.2", "@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-route": "^3.0.2", @@ -176,7 +176,8 @@ "resolutions": { "@webcomponents/webcomponentsjs": "^2.2.10", "@vaadin/vaadin-lumo-styles": "^1.4.2", - "@polymer/polymer": "3.1.0" + "@polymer/polymer": "3.1.0", + "lit-html": "^1.1.2" }, "main": "src/home-assistant.js", "husky": { diff --git a/script/release b/script/release index 05131f5df2..0cb917e295 100755 --- a/script/release +++ b/script/release @@ -13,4 +13,4 @@ script/build_frontend rm -rf dist python3 setup.py sdist -python3 -m twine upload dist/* +python3 -m twine upload dist/* --skip-existing diff --git a/setup.py b/setup.py index b8a9b84269..c12595c8f9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190811.0", + version="20190815.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.ts b/src/auth/ha-auth-flow.ts index f6f9b6c829..902c2a8e07 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -11,7 +11,10 @@ import "../components/ha-form"; import "../components/ha-markdown"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { AuthProvider } from "../data/auth"; -import { ConfigFlowStep, ConfigFlowStepForm } from "../data/config_entries"; +import { + DataEntryFlowStep, + DataEntryFlowStepForm, +} from "../data/data_entry_flow"; type State = "loading" | "error" | "step"; @@ -22,7 +25,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { @property() public oauth2State?: string; @property() private _state: State = "loading"; @property() private _stepData: any = {}; - @property() private _step?: ConfigFlowStep; + @property() private _step?: DataEntryFlowStep; @property() private _errorMessage?: string; protected render() { @@ -87,7 +90,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { } } - private _renderStep(step: ConfigFlowStep) { + private _renderStep(step: DataEntryFlowStep) { switch (step.type) { case "abort": return html` @@ -192,7 +195,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { document.location.assign(url); } - private async _updateStep(step: ConfigFlowStep) { + private async _updateStep(step: DataEntryFlowStep) { let stepData: any = null; if ( this._step && @@ -219,7 +222,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { }, 100); } - private _computeStepDescription(step: ConfigFlowStepForm) { + private _computeStepDescription(step: DataEntryFlowStepForm) { const resourceKey = `ui.panel.page-authorize.form.providers.${ step.handler[0] }.step.${step.step_id}.description`; @@ -232,7 +235,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { return this.localize(resourceKey, ...args); } - private _computeLabelCallback(step: ConfigFlowStepForm) { + private _computeLabelCallback(step: DataEntryFlowStepForm) { // Returns a callback for ha-form to calculate labels per schema object return (schema) => this.localize( @@ -242,7 +245,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { ); } - private _computeErrorCallback(step: ConfigFlowStepForm) { + private _computeErrorCallback(step: DataEntryFlowStepForm) { // Returns a callback for ha-form to calculate error messages return (error) => this.localize( diff --git a/src/common/dom/toggle_attribute.ts b/src/common/dom/toggle_attribute.ts new file mode 100644 index 0000000000..c37be7a4f8 --- /dev/null +++ b/src/common/dom/toggle_attribute.ts @@ -0,0 +1,25 @@ +// Toggle Attribute Polyfill because it's too new for some browsers +export const toggleAttribute = ( + el: HTMLElement, + name: string, + force?: boolean +) => { + if (force !== undefined) { + force = !!force; + } + + if (el.hasAttribute(name)) { + if (force) { + return true; + } + + el.removeAttribute(name); + return false; + } + if (force === false) { + return false; + } + + el.setAttribute(name, ""); + return true; +}; diff --git a/src/common/entity/cover_icon.ts b/src/common/entity/cover_icon.ts index 88fe03f774..f75a2a1bf4 100644 --- a/src/common/entity/cover_icon.ts +++ b/src/common/entity/cover_icon.ts @@ -7,6 +7,10 @@ export default function coverIcon(state: HassEntity): string { switch (state.attributes.device_class) { case "garage": return open ? "hass:garage-open" : "hass:garage"; + case "door": + return open ? "hass:door-open" : "hass:door-closed"; + case "window": + return open ? "hass:window-open" : "hass:window-closed"; default: return domainIcon("cover", state.state); } diff --git a/src/components/ha-icon-next.ts b/src/components/ha-icon-next.ts index d6a16bc7bb..7a875d5e61 100644 --- a/src/components/ha-icon-next.ts +++ b/src/components/ha-icon-next.ts @@ -5,13 +5,15 @@ import { HaIcon } from "./ha-icon"; export class HaIconNext extends HaIcon { public connectedCallback() { - this.icon = - window.getComputedStyle(this).direction === "ltr" - ? "hass:chevron-right" - : "hass:chevron-left"; - - // calling super after setting icon to have it consistently show the icon (otherwise not always shown) super.connectedCallback(); + + // wait to check for direction since otherwise direction is wrong even though top level is RTL + setTimeout(() => { + this.icon = + window.getComputedStyle(this).direction === "ltr" + ? "hass:chevron-right" + : "hass:chevron-left"; + }, 100); } } diff --git a/src/components/ha-icon-prev.ts b/src/components/ha-icon-prev.ts index 8fa3c030e8..e8d58433e6 100644 --- a/src/components/ha-icon-prev.ts +++ b/src/components/ha-icon-prev.ts @@ -5,13 +5,15 @@ import { HaIcon } from "./ha-icon"; export class HaIconPrev extends HaIcon { public connectedCallback() { - this.icon = - window.getComputedStyle(this).direction === "ltr" - ? "hass:chevron-left" - : "hass:chevron-right"; - - // calling super after setting icon to have it consistently show the icon (otherwise not always shown) super.connectedCallback(); + + // wait to check for direction since otherwise direction is wrong even though top level is RTL + setTimeout(() => { + this.icon = + window.getComputedStyle(this).direction === "ltr" + ? "hass:chevron-left" + : "hass:chevron-right"; + }, 100); } } diff --git a/src/components/ha-paper-icon-button-arrow-next.ts b/src/components/ha-paper-icon-button-arrow-next.ts index 8d0914a0cb..6bf526ec2e 100644 --- a/src/components/ha-paper-icon-button-arrow-next.ts +++ b/src/components/ha-paper-icon-button-arrow-next.ts @@ -10,13 +10,15 @@ const paperIconButtonClass = customElements.get( export class HaPaperIconButtonArrowNext extends paperIconButtonClass { public connectedCallback() { - this.icon = - window.getComputedStyle(this).direction === "ltr" - ? "hass:arrow-right" - : "hass:arrow-left"; - - // calling super after setting icon to have it consistently show the icon (otherwise not always shown) super.connectedCallback(); + + // wait to check for direction since otherwise direction is wrong even though top level is RTL + setTimeout(() => { + this.icon = + window.getComputedStyle(this).direction === "ltr" + ? "hass:arrow-right" + : "hass:arrow-left"; + }, 100); } } diff --git a/src/components/ha-paper-icon-button-arrow-prev.ts b/src/components/ha-paper-icon-button-arrow-prev.ts index de512993f8..dd6378acad 100644 --- a/src/components/ha-paper-icon-button-arrow-prev.ts +++ b/src/components/ha-paper-icon-button-arrow-prev.ts @@ -12,17 +12,19 @@ export class HaPaperIconButtonArrowPrev extends paperIconButtonClass { public hassio?: boolean; public connectedCallback() { - this.icon = - window.getComputedStyle(this).direction === "ltr" - ? this.hassio - ? "hassio:arrow-left" - : "hass:arrow-left" - : this.hassio - ? "hassio:arrow-right" - : "hass:arrow-right"; - - // calling super after setting icon to have it consistently show the icon (otherwise not always shown) super.connectedCallback(); + + // wait to check for direction since otherwise direction is wrong even though top level is RTL + setTimeout(() => { + this.icon = + window.getComputedStyle(this).direction === "ltr" + ? this.hassio + ? "hassio:arrow-left" + : "hass:arrow-left" + : this.hassio + ? "hassio:arrow-right" + : "hass:arrow-right"; + }, 100); } } diff --git a/src/components/ha-paper-icon-button-next.ts b/src/components/ha-paper-icon-button-next.ts index 5a137fc2d3..e3038e5a00 100644 --- a/src/components/ha-paper-icon-button-next.ts +++ b/src/components/ha-paper-icon-button-next.ts @@ -10,13 +10,15 @@ const paperIconButtonClass = customElements.get( export class HaPaperIconButtonNext extends paperIconButtonClass { public connectedCallback() { - this.icon = - window.getComputedStyle(this).direction === "ltr" - ? "hass:chevron-right" - : "hass:chevron-left"; - - // calling super after setting icon to have it consistently show the icon (otherwise not always shown) super.connectedCallback(); + + // wait to check for direction since otherwise direction is wrong even though top level is RTL + setTimeout(() => { + this.icon = + window.getComputedStyle(this).direction === "ltr" + ? "hass:chevron-right" + : "hass:chevron-left"; + }, 100); } } diff --git a/src/components/ha-paper-icon-button-prev.ts b/src/components/ha-paper-icon-button-prev.ts index 06479fcd1f..396482be4b 100644 --- a/src/components/ha-paper-icon-button-prev.ts +++ b/src/components/ha-paper-icon-button-prev.ts @@ -10,13 +10,15 @@ const paperIconButtonClass = customElements.get( export class HaPaperIconButtonPrev extends paperIconButtonClass { public connectedCallback() { - this.icon = - window.getComputedStyle(this).direction === "ltr" - ? "hass:chevron-left" - : "hass:chevron-right"; - - // calling super after setting icon to have it consistently show the icon (otherwise not always shown) super.connectedCallback(); + + // wait to check for direction since otherwise direction is wrong even though top level is RTL + setTimeout(() => { + this.icon = + window.getComputedStyle(this).direction === "ltr" + ? "hass:chevron-left" + : "hass:chevron-right"; + }, 100); } } diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 1f031913e5..8385817ded 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -31,6 +31,7 @@ import computeDomain from "../common/entity/compute_domain"; import { classMap } from "lit-html/directives/class-map"; // tslint:disable-next-line: no-duplicate-imports import { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item"; +import { computeRTL } from "../common/util/compute_rtl"; const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"]; @@ -106,6 +107,9 @@ class HaSidebar extends LitElement { localStorage.defaultPage || DEFAULT_PANEL; @property() private _externalConfig?: ExternalConfig; @property() private _notifications?: PersistentNotification[]; + // property used only in css + // @ts-ignore + @property({ type: Boolean, reflect: true }) private _rtl = false; private _mouseLeaveTimeout?: number; private _tooltipHideTimeout?: number; @@ -299,7 +303,13 @@ class HaSidebar extends LitElement { if (changedProps.has("alwaysExpand")) { this.expanded = this.alwaysExpand; } - if (!SUPPORT_SCROLL_IF_NEEDED || !changedProps.has("hass")) { + if (!changedProps.has("hass")) { + return; + } + + this._rtl = computeRTL(this.hass); + + if (!SUPPORT_SCROLL_IF_NEEDED) { return; } const oldHass = changedProps.get("hass") as HomeAssistant | undefined; @@ -472,6 +482,10 @@ class HaSidebar extends LitElement { :host([expanded]) .menu paper-icon-button { margin-right: 23px; } + :host([expanded][_rtl]) .menu paper-icon-button { + margin-right: 0px; + margin-left: 23px; + } .title { display: none; @@ -521,6 +535,10 @@ class HaSidebar extends LitElement { :host([expanded]) paper-icon-item { width: 240px; } + :host([_rtl]) paper-icon-item { + padding-left: auto; + padding-right: 12px; + } ha-icon[slot="item-icon"] { color: var(--sidebar-icon-color); @@ -588,9 +606,16 @@ class HaSidebar extends LitElement { .profile paper-icon-item { padding-left: 4px; } + :host([_rtl]) .profile paper-icon-item { + padding-left: auto; + padding-right: 4px; + } .profile .item-text { margin-left: 8px; } + :host([_rtl]) .profile .item-text { + margin-right: 8px; + } .notification-badge { min-width: 20px; @@ -647,6 +672,11 @@ class HaSidebar extends LitElement { padding: 4px; font-weight: 500; } + + :host([_rtl]) .menu paper-icon-button { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + } `; } } diff --git a/src/components/user/ha-user-badge.ts b/src/components/user/ha-user-badge.ts index 586e6c4b26..842b89c4ad 100644 --- a/src/components/user/ha-user-badge.ts +++ b/src/components/user/ha-user-badge.ts @@ -9,6 +9,7 @@ import { } from "lit-element"; import { User } from "../../data/user"; import { CurrentUser } from "../../types"; +import { toggleAttribute } from "../../common/dom/toggle_attribute"; const computeInitials = (name: string) => { if (!name) { @@ -40,7 +41,8 @@ class StateBadge extends LitElement { protected updated(changedProps) { super.updated(changedProps); - this.toggleAttribute( + toggleAttribute( + this, "long", (this.user ? computeInitials(this.user.name) : "?").length > 2 ); diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index e0b773df70..c8119f1983 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -2,15 +2,7 @@ import { HomeAssistant } from "../types"; import { createCollection } from "home-assistant-js-websocket"; import { debounce } from "../common/util/debounce"; import { LocalizeFunc } from "../common/translations/localize"; - -export interface DataEntryFlowProgressedEvent { - type: "data_entry_flow_progressed"; - data: { - handler: string; - flow_id: string; - refresh: boolean; - }; -} +import { DataEntryFlowStep, DataEntryFlowProgress } from "./data_entry_flow"; export interface ConfigEntry { entry_id: string; @@ -22,80 +14,23 @@ export interface ConfigEntry { supports_options: boolean; } -export interface FieldSchema { - name: string; - default?: any; - optional: boolean; -} - -export interface ConfigFlowProgress { - flow_id: string; - handler: string; - context: { - title_placeholders: { [key: string]: string }; - [key: string]: any; - }; -} - -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 ConfigFlowStepExternal { - type: "external"; - flow_id: string; - handler: string; - step_id: string; - url: string; - description_placeholders: { [key: string]: string }; -} - -export interface ConfigFlowStepCreateEntry { - type: "create_entry"; - version: number; - flow_id: string; - handler: string; - title: string; - // Config entry ID - result: string; - 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 - | ConfigFlowStepExternal - | ConfigFlowStepCreateEntry - | ConfigFlowStepAbort; - export const createConfigFlow = (hass: HomeAssistant, handler: string) => - hass.callApi("POST", "config/config_entries/flow", { + hass.callApi("POST", "config/config_entries/flow", { handler, }); export const fetchConfigFlow = (hass: HomeAssistant, flowId: string) => - hass.callApi("GET", `config/config_entries/flow/${flowId}`); + hass.callApi( + "GET", + `config/config_entries/flow/${flowId}` + ); export const handleConfigFlowStep = ( hass: HomeAssistant, flowId: string, data: { [key: string]: any } ) => - hass.callApi( + hass.callApi( "POST", `config/config_entries/flow/${flowId}`, data @@ -105,7 +40,7 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); export const getConfigFlowsInProgress = (hass: HomeAssistant) => - hass.callApi("GET", "config/config_entries/flow"); + hass.callApi("GET", "config/config_entries/flow"); export const getConfigFlowHandlers = (hass: HomeAssistant) => hass.callApi("GET", "config/config_entries/flow_handlers"); @@ -130,9 +65,9 @@ const subscribeConfigFlowInProgressUpdates = (conn, store) => export const subscribeConfigFlowInProgress = ( hass: HomeAssistant, - onChange: (flows: ConfigFlowProgress[]) => void + onChange: (flows: DataEntryFlowProgress[]) => void ) => - createCollection( + createCollection( "_configFlowProgress", fetchConfigFlowInProgress, subscribeConfigFlowInProgressUpdates, @@ -145,7 +80,7 @@ export const getConfigEntries = (hass: HomeAssistant) => export const localizeConfigFlowTitle = ( localize: LocalizeFunc, - flow: ConfigFlowProgress + flow: DataEntryFlowProgress ) => { const placeholders = flow.context.title_placeholders || {}; const placeholderKeys = Object.keys(placeholders); @@ -159,3 +94,34 @@ export const localizeConfigFlowTitle = ( }); return localize(`component.${flow.handler}.config.flow_title`, ...args); }; + +// Options flow + +export const createOptionsFlow = (hass: HomeAssistant, handler: string) => + hass.callApi( + "POST", + "config/config_entries/options/flow", + { + handler, + } + ); + +export const fetchOptionsFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi( + "GET", + `config/config_entries/options/flow/${flowId}` + ); + +export const handleOptionsFlowStep = ( + hass: HomeAssistant, + flowId: string, + data: { [key: string]: any } +) => + hass.callApi( + "POST", + `config/config_entries/options/flow/${flowId}`, + data + ); + +export const deleteOptionsFlow = (hass: HomeAssistant, flowId: string) => + hass.callApi("DELETE", `config/config_entries/options/flow/${flowId}`); diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts new file mode 100644 index 0000000000..7f2e8ab894 --- /dev/null +++ b/src/data/data_entry_flow.ts @@ -0,0 +1,68 @@ +export interface DataEntryFlowProgressedEvent { + type: "data_entry_flow_progressed"; + data: { + handler: string; + flow_id: string; + refresh: boolean; + }; +} + +export interface FieldSchema { + name: string; + default?: any; + optional: boolean; +} + +export interface DataEntryFlowProgress { + flow_id: string; + handler: string; + context: { + title_placeholders: { [key: string]: string }; + [key: string]: any; + }; +} + +export interface DataEntryFlowStepForm { + type: "form"; + flow_id: string; + handler: string; + step_id: string; + data_schema: FieldSchema[]; + errors: { [key: string]: string }; + description_placeholders: { [key: string]: string }; +} + +export interface DataEntryFlowStepExternal { + type: "external"; + flow_id: string; + handler: string; + step_id: string; + url: string; + description_placeholders: { [key: string]: string }; +} + +export interface DataEntryFlowStepCreateEntry { + type: "create_entry"; + version: number; + flow_id: string; + handler: string; + title: string; + // Config entry ID + result: string; + description: string; + description_placeholders: { [key: string]: string }; +} + +export interface DataEntryFlowStepAbort { + type: "abort"; + flow_id: string; + handler: string; + reason: string; + description_placeholders: { [key: string]: string }; +} + +export type DataEntryFlowStep = + | DataEntryFlowStepForm + | DataEntryFlowStepExternal + | DataEntryFlowStepCreateEntry + | DataEntryFlowStepAbort; diff --git a/src/dialogs/config-flow/dialog-config-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts similarity index 81% rename from src/dialogs/config-flow/dialog-config-flow.ts rename to src/dialogs/config-flow/dialog-data-entry-flow.ts index 1ca5932d34..c10900e340 100644 --- a/src/dialogs/config-flow/dialog-config-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -22,14 +22,8 @@ import "../../components/dialog/ha-paper-dialog"; // tslint:disable-next-line import { HaPaperDialog } from "../../components/dialog/ha-paper-dialog"; import { haStyleDialog } from "../../resources/styles"; -import { - fetchConfigFlow, - ConfigFlowStep, - deleteConfigFlow, - getConfigFlowHandlers, -} from "../../data/config_entries"; import { PolymerChangedEvent } from "../../polymer-types"; -import { HaConfigFlowParams } from "./show-dialog-config-flow"; +import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; import "./step-flow-pick-handler"; import "./step-flow-loading"; @@ -46,7 +40,7 @@ import { subscribeAreaRegistry, } from "../../data/area_registry"; import { HomeAssistant } from "../../types"; -import { caseInsensitiveCompare } from "../../common/string/compare"; +import { DataEntryFlowStep } from "../../data/data_entry_flow"; let instance = 0; @@ -54,20 +48,20 @@ declare global { // for fire event interface HASSDomEvents { "flow-update": { - step?: ConfigFlowStep; - stepPromise?: Promise; + step?: DataEntryFlowStep; + stepPromise?: Promise; }; } } -@customElement("dialog-config-flow") -class ConfigFlowDialog extends LitElement { +@customElement("dialog-data-entry-flow") +class DataEntryFlowDialog extends LitElement { public hass!: HomeAssistant; - @property() private _params?: HaConfigFlowParams; + @property() private _params?: DataEntryFlowDialogParams; @property() private _loading = true; private _instance = instance; @property() private _step: - | ConfigFlowStep + | DataEntryFlowStep | undefined // Null means we need to pick a config flow | null; @@ -77,12 +71,15 @@ class ConfigFlowDialog extends LitElement { private _unsubAreas?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc; - public async showDialog(params: HaConfigFlowParams): Promise { + public async showDialog(params: DataEntryFlowDialogParams): Promise { this._params = params; this._instance = instance++; // Create a new config flow. Show picker - if (!params.continueFlowId) { + if (!params.continueFlowId && !params.startFlowHandler) { + if (!params.flowConfig.getFlowHandlers) { + throw new Error("No getFlowHandlers defined in flow config"); + } this._step = null; // We only load the handlers once @@ -90,13 +87,7 @@ class ConfigFlowDialog extends LitElement { this._loading = true; this.updateComplete.then(() => this._scheduleCenterDialog()); try { - this._handlers = (await getConfigFlowHandlers(this.hass)).sort( - (handlerA, handlerB) => - caseInsensitiveCompare( - this.hass.localize(`component.${handlerA}.config.title`), - this.hass.localize(`component.${handlerB}.config.title`) - ) - ); + this._handlers = await params.flowConfig.getFlowHandlers(this.hass); } finally { this._loading = false; } @@ -108,7 +99,9 @@ class ConfigFlowDialog extends LitElement { this._loading = true; const curInstance = this._instance; - const step = await fetchConfigFlow(this.hass, params.continueFlowId); + const step = await (params.continueFlowId + ? params.flowConfig.fetchFlow(this.hass, params.continueFlowId) + : params.flowConfig.createFlow(this.hass, params.startFlowHandler!)); // Happens if second showDialog called if (curInstance !== this._instance) { @@ -145,6 +138,7 @@ class ConfigFlowDialog extends LitElement { ? // Show handler picker html` @@ -152,6 +146,7 @@ class ConfigFlowDialog extends LitElement { : this._step.type === "form" ? html` @@ -159,6 +154,7 @@ class ConfigFlowDialog extends LitElement { : this._step.type === "external" ? html` @@ -166,6 +162,7 @@ class ConfigFlowDialog extends LitElement { : this._step.type === "abort" ? html` @@ -177,6 +174,7 @@ class ConfigFlowDialog extends LitElement { ` : html` + step: DataEntryFlowStep | undefined | Promise ): Promise { if (step instanceof Promise) { this._loading = true; @@ -267,12 +270,14 @@ class ConfigFlowDialog extends LitElement { // If we created this flow, delete it now. if (this._step && !flowFinished && !this._params.continueFlowId) { - deleteConfigFlow(this.hass, this._step.flow_id); + this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id); } - this._params.dialogClosedCallback({ - flowFinished, - }); + if (this._params.dialogClosedCallback) { + this._params.dialogClosedCallback({ + flowFinished, + }); + } this._step = undefined; this._params = undefined; @@ -319,6 +324,6 @@ class ConfigFlowDialog extends LitElement { declare global { interface HTMLElementTagNameMap { - "dialog-config-flow": ConfigFlowDialog; + "dialog-data-entry-flow": DataEntryFlowDialog; } } diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index a38db25f36..f8718393f4 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -1,20 +1,128 @@ -import { fireEvent } from "../../common/dom/fire_event"; +import { + getConfigFlowHandlers, + fetchConfigFlow, + handleConfigFlowStep, + deleteConfigFlow, + createConfigFlow, +} from "../../data/config_entries"; +import { html } from "lit-element"; +import { localizeKey } from "../../common/translations/localize"; +import { + showFlowDialog, + DataEntryFlowDialogParams, + loadDataEntryFlowDialog, +} from "./show-dialog-data-entry-flow"; +import { caseInsensitiveCompare } from "../../common/string/compare"; -export interface HaConfigFlowParams { - continueFlowId?: string; - dialogClosedCallback: (params: { flowFinished: boolean }) => void; -} - -export const loadConfigFlowDialog = () => - import(/* webpackChunkName: "dialog-config-flow" */ "./dialog-config-flow"); +export const loadConfigFlowDialog = loadDataEntryFlowDialog; export const showConfigFlowDialog = ( element: HTMLElement, - dialogParams: HaConfigFlowParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-config-flow", - dialogImport: loadConfigFlowDialog, - dialogParams, + dialogParams: Omit +): void => + showFlowDialog(element, dialogParams, { + loadDevicesAndAreas: true, + getFlowHandlers: (hass) => + getConfigFlowHandlers(hass).then((handlers) => + handlers.sort((handlerA, handlerB) => + caseInsensitiveCompare( + hass.localize(`component.${handlerA}.config.title`), + hass.localize(`component.${handlerB}.config.title`) + ) + ) + ), + createFlow: createConfigFlow, + fetchFlow: fetchConfigFlow, + handleFlowStep: handleConfigFlowStep, + deleteFlow: deleteConfigFlow, + + renderAbortDescription(hass, step) { + const description = localizeKey( + hass.localize, + `component.${step.handler}.config.abort.${step.reason}`, + step.description_placeholders + ); + + return description + ? html` + + ` + : ""; + }, + + renderShowFormStepHeader(hass, step) { + return hass.localize( + `component.${step.handler}.config.step.${step.step_id}.title` + ); + }, + + renderShowFormStepDescription(hass, step) { + const description = localizeKey( + hass.localize, + `component.${step.handler}.config.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; + }, + + renderShowFormStepFieldLabel(hass, step, field) { + return hass.localize( + `component.${step.handler}.config.step.${step.step_id}.data.${ + field.name + }` + ); + }, + + renderShowFormStepFieldError(hass, step, error) { + return hass.localize(`component.${step.handler}.config.error.${error}`); + }, + + renderExternalStepHeader(hass, step) { + return hass.localize( + `component.${step.handler}.config.step.${step.step_id}.title` + ); + }, + + renderExternalStepDescription(hass, step) { + const description = localizeKey( + hass.localize, + `component.${step.handler}.config.${step.step_id}.description`, + step.description_placeholders + ); + + return html` +

+ ${hass.localize( + "ui.panel.config.integrations.config_flow.external_step.description" + )} +

+ ${description + ? html` + + ` + : ""} + `; + }, + + renderCreateEntryDescription(hass, step) { + const description = localizeKey( + hass.localize, + `component.${step.handler}.config.create_entry.${step.description || + "default"}`, + step.description_placeholders + ); + + return html` + ${description + ? html` + + ` + : ""} +

Created config for ${step.title}.

+ `; + }, }); -}; diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts new file mode 100644 index 0000000000..a3609a733f --- /dev/null +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -0,0 +1,96 @@ +import { TemplateResult } from "lit-html"; +import { fireEvent } from "../../common/dom/fire_event"; +import { HomeAssistant } from "../../types"; +import { + DataEntryFlowStepCreateEntry, + DataEntryFlowStepExternal, + DataEntryFlowStepForm, + DataEntryFlowStep, + DataEntryFlowStepAbort, + FieldSchema, +} from "../../data/data_entry_flow"; + +export interface FlowConfig { + loadDevicesAndAreas: boolean; + + getFlowHandlers?: (hass: HomeAssistant) => Promise; + + createFlow(hass: HomeAssistant, handler: string): Promise; + + fetchFlow(hass: HomeAssistant, flowId: string): Promise; + + handleFlowStep( + hass: HomeAssistant, + flowId: string, + data: { [key: string]: any } + ): Promise; + + deleteFlow(hass: HomeAssistant, flowId: string): Promise; + + renderAbortDescription( + hass: HomeAssistant, + step: DataEntryFlowStepAbort + ): TemplateResult | ""; + + renderShowFormStepHeader( + hass: HomeAssistant, + step: DataEntryFlowStepForm + ): string; + + renderShowFormStepDescription( + hass: HomeAssistant, + step: DataEntryFlowStepForm + ): TemplateResult | ""; + + renderShowFormStepFieldLabel( + hass: HomeAssistant, + step: DataEntryFlowStepForm, + field: FieldSchema + ): string; + + renderShowFormStepFieldError( + hass: HomeAssistant, + step: DataEntryFlowStepForm, + error: string + ): string; + + renderExternalStepHeader( + hass: HomeAssistant, + step: DataEntryFlowStepExternal + ): string; + + renderExternalStepDescription( + hass: HomeAssistant, + step: DataEntryFlowStepExternal + ): TemplateResult | ""; + + renderCreateEntryDescription( + hass: HomeAssistant, + step: DataEntryFlowStepCreateEntry + ): TemplateResult | ""; +} + +export interface DataEntryFlowDialogParams { + startFlowHandler?: string; + continueFlowId?: string; + dialogClosedCallback?: (params: { flowFinished: boolean }) => void; + flowConfig: FlowConfig; +} + +export const loadDataEntryFlowDialog = () => + import(/* webpackChunkName: "dialog-config-flow" */ "./dialog-data-entry-flow"); + +export const showFlowDialog = ( + element: HTMLElement, + dialogParams: Omit, + flowConfig: FlowConfig +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-data-entry-flow", + dialogImport: loadDataEntryFlowDialog, + dialogParams: { + ...dialogParams, + flowConfig, + }, + }); +}; diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts new file mode 100644 index 0000000000..a4f8f5126c --- /dev/null +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -0,0 +1,83 @@ +import { + fetchOptionsFlow, + handleOptionsFlowStep, + deleteOptionsFlow, + createOptionsFlow, + ConfigEntry, +} from "../../data/config_entries"; +import { html } from "lit-element"; +import { localizeKey } from "../../common/translations/localize"; +import { + showFlowDialog, + loadDataEntryFlowDialog, +} from "./show-dialog-data-entry-flow"; + +export const loadOptionsFlowDialog = loadDataEntryFlowDialog; + +export const showOptionsFlowDialog = ( + element: HTMLElement, + configEntry: ConfigEntry +): void => + showFlowDialog( + element, + { + startFlowHandler: configEntry.entry_id, + }, + { + loadDevicesAndAreas: false, + createFlow: createOptionsFlow, + fetchFlow: fetchOptionsFlow, + handleFlowStep: handleOptionsFlowStep, + deleteFlow: deleteOptionsFlow, + + renderAbortDescription(hass, step) { + const description = localizeKey( + hass.localize, + `component.${configEntry.domain}.options.abort.${step.reason}`, + step.description_placeholders + ); + + return description + ? html` + + ` + : ""; + }, + + renderShowFormStepHeader(hass, _step) { + return hass.localize(`ui.dialogs.options_flow.form.header`); + }, + + renderShowFormStepDescription(_hass, _step) { + return ""; + }, + + renderShowFormStepFieldLabel(hass, step, field) { + return hass.localize( + `component.${configEntry.domain}.options.step.${step.step_id}.data.${ + field.name + }` + ); + }, + + renderShowFormStepFieldError(hass, _step, error) { + return hass.localize( + `component.${configEntry.domain}.options.error.${error}` + ); + }, + + renderExternalStepHeader(_hass, _step) { + return ""; + }, + + renderExternalStepDescription(_hass, _step) { + return ""; + }, + + renderCreateEntryDescription(hass, _step) { + return html` +

${hass.localize(`ui.dialogs.options_flow.success.description`)}

+ `; + }, + } + ); diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index e1dfdbe577..3af1b63d92 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -8,38 +8,27 @@ import { } from "lit-element"; import "@material/mwc-button"; -import { ConfigFlowStepAbort } from "../../data/config_entries"; +import { DataEntryFlowStepAbort } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; -import { localizeKey } from "../../common/translations/localize"; import { fireEvent } from "../../common/dom/fire_event"; import { configFlowContentStyles } from "./styles"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; @customElement("step-flow-abort") class StepFlowAbort extends LitElement { + public flowConfig!: FlowConfig; + @property() public hass!: HomeAssistant; @property() - private step!: ConfigFlowStepAbort; + private step!: DataEntryFlowStepAbort; protected render(): TemplateResult | void { - const localize = this.hass.localize; - const step = this.step; - - const description = localizeKey( - localize, - `component.${step.handler}.config.abort.${step.reason}`, - step.description_placeholders - ); - return html`

Aborted

- ${description - ? html` - - ` - : ""} + ${this.flowConfig.renderAbortDescription(this.hass, this.step)}
Close diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 801854ff49..cd1183ec16 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -12,9 +12,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; -import { ConfigFlowStepCreateEntry } from "../../data/config_entries"; import { HomeAssistant } from "../../types"; -import { localizeKey } from "../../common/translations/localize"; import { fireEvent } from "../../common/dom/fire_event"; import { configFlowContentStyles } from "./styles"; import { @@ -25,14 +23,18 @@ import { AreaRegistryEntry, createAreaRegistryEntry, } from "../../data/area_registry"; +import { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; @customElement("step-flow-create-entry") class StepFlowCreateEntry extends LitElement { + public flowConfig!: FlowConfig; + @property() public hass!: HomeAssistant; @property() - public step!: ConfigFlowStepCreateEntry; + public step!: DataEntryFlowStepCreateEntry; @property() public devices!: DeviceRegistryEntry[]; @@ -42,24 +44,11 @@ class StepFlowCreateEntry extends LitElement { protected render(): TemplateResult | void { const localize = this.hass.localize; - const step = this.step; - - const description = localizeKey( - localize, - `component.${step.handler}.config.create_entry.${step.description || - "default"}`, - step.description_placeholders - ); return html`

Success!

- ${description - ? html` - - ` - : ""} -

Created config for ${step.title}.

+ ${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)} ${this.devices.length === 0 ? "" : html` diff --git a/src/dialogs/config-flow/step-flow-external.ts b/src/dialogs/config-flow/step-flow-external.ts index 51175ce755..47cd149705 100644 --- a/src/dialogs/config-flow/step-flow-external.ts +++ b/src/dialogs/config-flow/step-flow-external.ts @@ -9,51 +9,34 @@ import { } from "lit-element"; import "@material/mwc-button"; -import { - ConfigFlowStepExternal, - DataEntryFlowProgressedEvent, - fetchConfigFlow, -} from "../../data/config_entries"; import { HomeAssistant } from "../../types"; -import { localizeKey } from "../../common/translations/localize"; import { fireEvent } from "../../common/dom/fire_event"; import { configFlowContentStyles } from "./styles"; +import { + DataEntryFlowStepExternal, + DataEntryFlowProgressedEvent, +} from "../../data/data_entry_flow"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; @customElement("step-flow-external") class StepFlowExternal extends LitElement { + public flowConfig!: FlowConfig; + @property() public hass!: HomeAssistant; @property() - private step!: ConfigFlowStepExternal; + private step!: DataEntryFlowStepExternal; protected render(): TemplateResult | void { const localize = this.hass.localize; - const step = this.step; - - const description = localizeKey( - localize, - `component.${step.handler}.config.${step.step_id}.description`, - step.description_placeholders - ); return html`

- ${localize( - `component.${step.handler}.config.step.${step.step_id}.title` - )} + ${this.flowConfig.renderExternalStepHeader(this.hass, this.step)}

-

- ${localize( - "ui.panel.config.integrations.config_flow.external_step.description" - )} -

- ${description - ? html` - - ` - : ""} + ${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
@@ -76,7 +59,7 @@ class StepFlowExternal extends LitElement { } fireEvent(this, "flow-update", { - stepPromise: fetchConfigFlow(this.hass, this.step.flow_id), + stepPromise: this.flowConfig.fetchFlow(this.hass, this.step.flow_id), }); }, "data_entry_flow_progressed" diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 9e24395f33..e492125650 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -15,21 +15,19 @@ import "@polymer/paper-spinner/paper-spinner"; import "../../components/ha-form"; import "../../components/ha-markdown"; import "../../resources/ha-style"; -import { - handleConfigFlowStep, - FieldSchema, - ConfigFlowStepForm, -} from "../../data/config_entries"; import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import { fireEvent } from "../../common/dom/fire_event"; -import { localizeKey } from "../../common/translations/localize"; import { configFlowContentStyles } from "./styles"; +import { DataEntryFlowStepForm, FieldSchema } from "../../data/data_entry_flow"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; @customElement("step-flow-form") class StepFlowForm extends LitElement { + public flowConfig!: FlowConfig; + @property() - public step!: ConfigFlowStepForm; + public step!: DataEntryFlowStepForm; @property() public hass!: HomeAssistant; @@ -44,7 +42,6 @@ class StepFlowForm extends LitElement { private _errorMsg?: string; protected render(): TemplateResult | void { - const localize = this.hass.localize; const step = this.step; const allRequiredInfoFilledIn = @@ -59,17 +56,9 @@ class StepFlowForm extends LitElement { !["", undefined].includes(this._stepData![field.name]) ); - const description = localizeKey( - localize, - `component.${step.handler}.config.step.${step.step_id}.description`, - step.description_placeholders - ); - return html`

- ${localize( - `component.${step.handler}.config.step.${step.step_id}.title` - )} + ${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}

${this._errorMsg @@ -77,11 +66,7 @@ class StepFlowForm extends LitElement {
${this._errorMsg}
` : ""} - ${description - ? html` - - ` - : ""} + ${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} { - const step = this.step as ConfigFlowStepForm; - - return this.hass.localize( - `component.${step.handler}.config.step.${step.step_id}.data.${ - schema.name - }` - ); - }; + private _labelCallback = (field: FieldSchema): string => + this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field); private _errorCallback = (error: string) => - this.hass.localize(`component.${this.step.handler}.config.error.${error}`); + this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error); static get styles(): CSSResultArray { return [ diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 7edc2b1f35..ef60141ba2 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -11,7 +11,6 @@ import "@polymer/paper-spinner/paper-spinner-lite"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; import { HomeAssistant } from "../../types"; -import { createConfigFlow } from "../../data/config_entries"; import { fireEvent } from "../../common/dom/fire_event"; import memoizeOne from "memoize-one"; import * as Fuse from "fuse.js"; @@ -19,6 +18,7 @@ import * as Fuse from "fuse.js"; import "../../components/ha-icon-next"; import "../../common/search/search-input"; import { styleMap } from "lit-html/directives/style-map"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; interface HandlerObj { name: string; @@ -27,6 +27,8 @@ interface HandlerObj { @customElement("step-flow-pick-handler") class StepFlowPickHandler extends LitElement { + public flowConfig!: FlowConfig; + @property() public hass!: HomeAssistant; @property() public handlers!: string[]; @property() private filter?: string; @@ -97,7 +99,10 @@ class StepFlowPickHandler extends LitElement { private async _handlerPicked(ev) { fireEvent(this, "flow-update", { - stepPromise: createConfigFlow(this.hass, ev.currentTarget.handler.slug), + stepPromise: this.flowConfig.createFlow( + this.hass, + ev.currentTarget.handler.slug + ), }); } diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index a63f247035..70b99e022f 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -21,6 +21,7 @@ import { PolymerChangedEvent } from "../polymer-types"; // tslint:disable-next-line: no-duplicate-imports import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/app-drawer-layout"; import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer"; +import { toggleAttribute } from "../common/dom/toggle_attribute"; const NON_SWIPABLE_PANELS = ["kiosk", "map"]; @@ -114,7 +115,8 @@ class HomeAssistantMain extends LitElement { protected updated(changedProps: PropertyValues) { super.updated(changedProps); - this.toggleAttribute( + toggleAttribute( + this, "expanded", this.narrow || this.hass.dockedSidebar !== "auto" ); diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts index c6a9e1b332..b8de3d44bb 100644 --- a/src/onboarding/onboarding-integrations.ts +++ b/src/onboarding/onboarding-integrations.ts @@ -18,7 +18,6 @@ import { getConfigFlowsInProgress, getConfigEntries, ConfigEntry, - ConfigFlowProgress, localizeConfigFlowTitle, } from "../data/config_entries"; import { compare } from "../common/string/compare"; @@ -28,13 +27,14 @@ import { debounce } from "../common/util/debounce"; import { fireEvent } from "../common/dom/fire_event"; import { onboardIntegrationStep } from "../data/onboarding"; import { genClientId } from "home-assistant-js-websocket"; +import { DataEntryFlowProgress } from "../data/data_entry_flow"; @customElement("onboarding-integrations") class OnboardingIntegrations extends LitElement { @property() public hass!: HomeAssistant; @property() public onboardingLocalize!: LocalizeFunc; @property() private _entries?: ConfigEntry[]; - @property() private _discovered?: ConfigFlowProgress[]; + @property() private _discovered?: DataEntryFlowProgress[]; private _unsubEvents?: () => void; public connectedCallback() { diff --git a/src/panels/config/integrations/ha-config-entry-page.js b/src/panels/config/integrations/ha-config-entry-page.js index dcad335b18..59cb34878c 100644 --- a/src/panels/config/integrations/ha-config-entry-page.js +++ b/src/panels/config/integrations/ha-config-entry-page.js @@ -11,6 +11,7 @@ import "./ha-ce-entities-card"; import { EventsMixin } from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; import NavigateMixin from "../../../mixins/navigate-mixin"; +import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; class HaConfigEntryPage extends NavigateMixin( EventsMixin(LocalizeMixin(PolymerElement)) @@ -34,6 +35,13 @@ class HaConfigEntryPage extends NavigateMixin( } +