From 2f36304f069eb971550763690f621eb99611d16d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Apr 2019 12:51:41 -0700 Subject: [PATCH] Move picking new integration into dialog (#3110) --- src/data/config_entries.ts | 12 +++ src/dialogs/config-flow/dialog-config-flow.ts | 102 +++++++++++++----- .../config-flow/show-dialog-config-flow.ts | 3 - src/dialogs/config-flow/step-flow-form.ts | 2 + .../config-flow/step-flow-pick-handler.ts | 67 ++++++++++++ .../ha-config-entries-dashboard.js | 63 +++++++---- 6 files changed, 199 insertions(+), 50 deletions(-) create mode 100644 src/dialogs/config-flow/step-flow-pick-handler.ts diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 48618bdc80..3741fea239 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -6,6 +6,12 @@ export interface FieldSchema { optional: boolean; } +export interface ConfigFlowProgress { + flow_id: string; + handler: string; + context: { [key: string]: any }; +} + export interface ConfigFlowStepForm { type: "form"; flow_id: string; @@ -62,3 +68,9 @@ export const handleConfigFlowStep = ( 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"); + +export const getConfigFlowHandlers = (hass: HomeAssistant) => + hass.callApi("GET", "config/config_entries/flow_handlers"); diff --git a/src/dialogs/config-flow/dialog-config-flow.ts b/src/dialogs/config-flow/dialog-config-flow.ts index 37c1b1b7d6..3b495419d8 100644 --- a/src/dialogs/config-flow/dialog-config-flow.ts +++ b/src/dialogs/config-flow/dialog-config-flow.ts @@ -23,13 +23,14 @@ import { HaPaperDialog } from "../../components/dialog/ha-paper-dialog"; import { haStyleDialog } from "../../resources/styles"; import { fetchConfigFlow, - createConfigFlow, ConfigFlowStep, deleteConfigFlow, + getConfigFlowHandlers, } from "../../data/config_entries"; import { PolymerChangedEvent } from "../../polymer-types"; import { HaConfigFlowParams } from "./show-dialog-config-flow"; +import "./step-flow-pick-handler"; import "./step-flow-loading"; import "./step-flow-form"; import "./step-flow-abort"; @@ -39,6 +40,7 @@ import { fetchDeviceRegistry, } from "../../data/device_registry"; import { AreaRegistryEntry, fetchAreaRegistry } from "../../data/area_registry"; +import { HomeAssistant } from "../../types"; let instance = 0; @@ -47,12 +49,15 @@ declare global { interface HASSDomEvents { "flow-update": { step?: ConfigFlowStep; + stepPromise?: Promise; }; } } @customElement("dialog-config-flow") class ConfigFlowDialog extends LitElement { + public hass!: HomeAssistant; + @property() private _params?: HaConfigFlowParams; @@ -62,7 +67,11 @@ class ConfigFlowDialog extends LitElement { private _instance = instance; @property() - private _step?: ConfigFlowStep; + private _step: + | ConfigFlowStep + | undefined + // Null means we need to pick a config flow + | null; @property() private _devices?: DeviceRegistryEntry[]; @@ -70,25 +79,35 @@ class ConfigFlowDialog extends LitElement { @property() private _areas?: AreaRegistryEntry[]; + @property() + private _handlers?: string[]; + public async showDialog(params: HaConfigFlowParams): Promise { this._params = params; - this._loading = true; this._instance = instance++; - const fetchStep = params.continueFlowId - ? fetchConfigFlow(params.hass, params.continueFlowId) - : params.newFlowForHandler - ? createConfigFlow(params.hass, params.newFlowForHandler) - : undefined; + // Create a new config flow. Show picker + if (!params.continueFlowId) { + this._step = null; - if (!fetchStep) { - throw new Error(`Pass in either continueFlowId or newFlorForHandler`); + // We only load the handlers once + if (this._handlers === undefined) { + this._loading = true; + this.updateComplete.then(() => this._scheduleCenterDialog()); + try { + this._handlers = await getConfigFlowHandlers(this.hass); + } finally { + this._loading = false; + } + } + await this.updateComplete; + this._scheduleCenterDialog(); + return; } + this._loading = true; const curInstance = this._instance; - - await this.updateComplete; - const step = await fetchStep; + const step = await fetchConfigFlow(this.hass, params.continueFlowId); // Happens if second showDialog called if (curInstance !== this._instance) { @@ -99,7 +118,7 @@ class ConfigFlowDialog extends LitElement { 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); + this._scheduleCenterDialog(); } protected render(): TemplateResult | void { @@ -113,7 +132,7 @@ class ConfigFlowDialog extends LitElement { opened @opened-changed=${this._openedChanged} > - ${this._loading + ${this._loading || (this._step === null && this._handlers === undefined) ? html` ` @@ -121,18 +140,26 @@ class ConfigFlowDialog extends LitElement { ? // When we are going to next step, we render 1 round of empty // to reset the element. "" + : this._step === null + ? // Show handler picker + html` + + ` : this._step.type === "form" ? html` ` : this._step.type === "abort" ? html` ` : this._devices === undefined || this._areas === undefined @@ -143,7 +170,7 @@ class ConfigFlowDialog extends LitElement { : html` @@ -155,7 +182,8 @@ class ConfigFlowDialog extends LitElement { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this.addEventListener("flow-update", (ev) => { - this._processStep((ev as any).detail.step); + const { step, stepPromise } = (ev as any).detail; + this._processStep(step || stepPromise); }); } @@ -170,6 +198,10 @@ class ConfigFlowDialog extends LitElement { } } + private _scheduleCenterDialog() { + setTimeout(() => this._dialog.center(), 0); + } + private get _dialog(): HaPaperDialog { return this.shadowRoot!.querySelector("ha-paper-dialog")!; } @@ -177,17 +209,29 @@ class ConfigFlowDialog extends LitElement { private async _fetchDevices(configEntryId) { // Wait 5 seconds to give integrations time to find devices await new Promise((resolve) => setTimeout(resolve, 5000)); - const devices = await fetchDeviceRegistry(this._params!.hass); + const devices = await fetchDeviceRegistry(this.hass); this._devices = devices.filter((device) => device.config_entries.includes(configEntryId) ); } private async _fetchAreas() { - this._areas = await fetchAreaRegistry(this._params!.hass); + this._areas = await fetchAreaRegistry(this.hass); } - private async _processStep(step: ConfigFlowStep): Promise { + private async _processStep( + step: ConfigFlowStep | undefined | Promise + ): Promise { + if (step instanceof Promise) { + this._loading = true; + try { + this._step = await step; + } finally { + this._loading = false; + } + return; + } + if (step === undefined) { this._flowDone(); return; @@ -206,8 +250,8 @@ class ConfigFlowDialog extends LitElement { ); // If we created this flow, delete it now. - if (this._step && !flowFinished && this._params.newFlowForHandler) { - deleteConfigFlow(this._params.hass, this._step.flow_id); + if (this._step && !flowFinished && !this._params.continueFlowId) { + deleteConfigFlow(this.hass, this._step.flow_id); } this._params.dialogClosedCallback({ @@ -221,8 +265,14 @@ class ConfigFlowDialog extends LitElement { private _openedChanged(ev: PolymerChangedEvent): void { // Closed dialog by clicking on the overlay - if (this._step && !ev.detail.value) { - this._flowDone(); + if (!ev.detail.value) { + if (this._step) { + this._flowDone(); + } else if (this._step === null) { + // Flow aborted during picking flow + this._step = undefined; + this._params = undefined; + } } } diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 35f86f8c3e..a38db25f36 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -1,10 +1,7 @@ -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; } diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 0a2df43164..3f5b93825b 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -167,6 +167,8 @@ class StepFlowForm extends LitElement { toSendData ); + // make sure we're still showing the same step as when we + // fired off request. if (!this.step || flowId !== this.step.flow_id) { return; } diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts new file mode 100644 index 0000000000..ef64b0798a --- /dev/null +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -0,0 +1,67 @@ +import { + LitElement, + TemplateResult, + html, + css, + customElement, + CSSResult, +} from "lit-element"; +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 "../../components/ha-icon-next"; + +@customElement("step-flow-pick-handler") +class StepFlowPickHandler extends LitElement { + public hass!: HomeAssistant; + public handlers!: string[]; + + protected render(): TemplateResult | void { + return html` +

${this.hass.localize("ui.panel.config.integrations.new")}

+
+ ${this.handlers.map( + (handler) => + html` + + + ${this.hass.localize(`component.${handler}.config.title`)} + + + + ` + )} +
+ `; + } + + private async _handlerPicked(ev) { + fireEvent(this, "flow-update", { + stepPromise: createConfigFlow(this.hass, ev.currentTarget.handler), + }); + } + + static get styles(): CSSResult { + return css` + h2 { + padding-left: 16px; + } + div { + overflow: auto; + max-height: 600px; + } + paper-item { + cursor: pointer; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "step-flow-pick-handler": StepFlowPickHandler; + } +} diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.js b/src/panels/config/integrations/ha-config-entries-dashboard.js index c2720962df..e7582b6c91 100644 --- a/src/panels/config/integrations/ha-config-entries-dashboard.js +++ b/src/panels/config/integrations/ha-config-entries-dashboard.js @@ -1,6 +1,7 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@polymer/paper-tooltip/paper-tooltip"; import "@material/mwc-button"; +import "@polymer/paper-fab/paper-fab"; import "@polymer/paper-card/paper-card"; import "@polymer/iron-icon/iron-icon"; import "@polymer/paper-item/paper-item"; @@ -13,6 +14,7 @@ import "../../../layouts/hass-subpage"; import "../../../resources/ha-style"; import "../../../components/ha-icon-next"; +import { computeRTL } from "../../../common/util/compute_rtl"; import "../ha-config-section"; import EventsMixin from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; @@ -50,6 +52,28 @@ class HaConfigManagerDashboard extends LocalizeMixin( color: var(--primary-text-color); text-decoration: none; } + paper-fab { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 1; + } + + paper-fab[is-wide] { + bottom: 24px; + right: 24px; + } + + paper-fab[rtl] { + right: auto; + left: 16px; + } + + paper-fab[rtl][is-wide] { + bottom: 24px; + right: auto; + left: 24px; + } - - [[localize('ui.panel.config.integrations.new')]] - - - - + `; } @@ -162,6 +176,12 @@ class HaConfigManagerDashboard extends LocalizeMixin( progress: Array, handlers: Array, + + rtl: { + type: Boolean, + reflectToAttribute: true, + computed: "_computeRTL(hass)", + }, }; } @@ -170,17 +190,14 @@ class HaConfigManagerDashboard extends LocalizeMixin( loadConfigFlowDialog(); } - _createFlow(ev) { + _createFlow() { showConfigFlowDialog(this, { - hass: this.hass, - newFlowForHandler: ev.model.item, dialogClosedCallback: () => this.fire("hass-reload-entries"), }); } _continueFlow(ev) { showConfigFlowDialog(this, { - hass: this.hass, continueFlowId: ev.model.item.flow_id, dialogClosedCallback: () => this.fire("hass-reload-entries"), }); @@ -230,6 +247,10 @@ class HaConfigManagerDashboard extends LocalizeMixin( _handleMoreInfo(ev) { this.fire("hass-more-info", { entityId: ev.model.item.entity_id }); } + + _computeRTL(hass) { + return computeRTL(hass); + } } customElements.define("ha-config-entries-dashboard", HaConfigManagerDashboard);