import "@material/mwc-button"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, PropertyValues, TemplateResult, } from "lit"; import { customElement, state } from "lit/decorators"; import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import { computeRTL } from "../../common/util/compute_rtl"; import "../../components/ha-circular-progress"; import "../../components/ha-dialog"; import "../../components/ha-form/ha-form"; import "../../components/ha-icon-button"; import "../../components/ha-markdown"; import { AreaRegistryEntry, subscribeAreaRegistry, } from "../../data/area_registry"; import { fetchConfigFlowInProgress } from "../../data/config_flow"; import { DataEntryFlowProgress, DataEntryFlowStep, subscribeDataEntryFlowProgressed, } from "../../data/data_entry_flow"; import { DeviceRegistryEntry, subscribeDeviceRegistry, } from "../../data/device_registry"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { showAlertDialog } from "../generic/show-dialog-box"; import { DataEntryFlowDialogParams, LoadingReason, } from "./show-dialog-data-entry-flow"; import "./step-flow-abort"; import "./step-flow-create-entry"; import "./step-flow-external"; import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-pick-flow"; import "./step-flow-pick-handler"; import "./step-flow-progress"; let instance = 0; interface FlowUpdateEvent { step?: DataEntryFlowStep; stepPromise?: Promise; } declare global { // for fire event interface HASSDomEvents { "flow-update": FlowUpdateEvent; } // for add event listener interface HTMLElementEventMap { "flow-update": HASSDomEvent; } } @customElement("dialog-data-entry-flow") class DataEntryFlowDialog extends LitElement { public hass!: HomeAssistant; @state() private _params?: DataEntryFlowDialogParams; @state() private _loading?: LoadingReason; private _instance = instance; @state() private _step: | DataEntryFlowStep | undefined // Null means we need to pick a config flow | null; @state() private _devices?: DeviceRegistryEntry[]; @state() private _areas?: AreaRegistryEntry[]; @state() private _handlers?: string[]; @state() private _handler?: string; @state() private _flowsInProgress?: DataEntryFlowProgress[]; private _unsubAreas?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc; private _unsubDataEntryFlowProgressed?: Promise; public async showDialog(params: DataEntryFlowDialogParams): Promise { this._params = params; this._instance = instance++; if (params.startFlowHandler) { this._checkFlowsInProgress(params.startFlowHandler); return; } if (params.continueFlowId) { this._loading = "loading_flow"; const curInstance = this._instance; let step: DataEntryFlowStep; try { step = await params.flowConfig.fetchFlow( this.hass, params.continueFlowId ); } catch (err: any) { this._step = undefined; this._params = undefined; showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.integrations.config_flow.error" ), text: this.hass.localize( "ui.panel.config.integrations.config_flow.could_not_load" ), }); return; } // Happens if second showDialog called if (curInstance !== this._instance) { return; } this._processStep(step); this._loading = undefined; return; } // Create a new config flow. Show picker if (!params.flowConfig.getFlowHandlers) { throw new Error("No getFlowHandlers defined in flow config"); } this._step = null; // We only load the handlers once if (this._handlers === undefined) { this._loading = "loading_handlers"; try { this._handlers = await params.flowConfig.getFlowHandlers(this.hass); } finally { this._loading = undefined; } } } public closeDialog() { if (!this._params) { return; } const flowFinished = Boolean( this._step && ["create_entry", "abort"].includes(this._step.type) ); // If we created this flow, delete it now. if (this._step && !flowFinished && !this._params.continueFlowId) { this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id); } if (this._step && this._params.dialogClosedCallback) { this._params.dialogClosedCallback({ flowFinished, entryId: "result" in this._step ? this._step.result?.entry_id : undefined, }); } this._step = undefined; this._params = undefined; this._devices = undefined; this._flowsInProgress = undefined; this._handler = undefined; if (this._unsubAreas) { this._unsubAreas(); this._unsubAreas = undefined; } if (this._unsubDevices) { this._unsubDevices(); this._unsubDevices = undefined; } if (this._unsubDataEntryFlowProgressed) { this._unsubDataEntryFlowProgressed.then((unsub) => { unsub(); }); this._unsubDataEntryFlowProgressed = undefined; } fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { if (!this._params) { return html``; } return html`
${this._loading || (this._step === null && this._handlers === undefined && this._handler === undefined) ? html` ` : this._step === undefined ? // When we are going to next step, we render 1 round of empty // to reset the element. "" : html` ${this._step === null ? this._handler ? html`` : // Show handler picker html` ` : this._step.type === "form" ? html` ` : this._step.type === "external" ? html` ` : this._step.type === "abort" ? html` ` : this._step.type === "progress" ? html` ` : this._devices === undefined || this._areas === undefined ? // When it's a create entry result, we will fetch device & area registry html` ` : html` `} `}
`; } protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); this.addEventListener("flow-update", (ev) => { const { step, stepPromise } = ev.detail; this._processStep(step || stepPromise); }); } public willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); if (!changedProps.has("_step") || !this._step) { return; } if (["external", "progress"].includes(this._step.type)) { // external and progress step will send update event from the backend, so we should subscribe to them this._subscribeDataEntryFlowProgressed(); } if (this._step.type === "create_entry") { if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) { this._fetchDevices(this._step.result.entry_id); this._fetchAreas(); } else { this._devices = []; this._areas = []; } } } private async _fetchDevices(configEntryId) { this._unsubDevices = subscribeDeviceRegistry( this.hass.connection, (devices) => { this._devices = devices.filter((device) => device.config_entries.includes(configEntryId) ); } ); } private async _fetchAreas() { this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => { this._areas = areas; }); } private async _checkFlowsInProgress(handler: string) { this._loading = "loading_handlers"; this._handler = handler; const flowsInProgress = ( await fetchConfigFlowInProgress(this.hass.connection) ).filter((flow) => flow.handler === handler); if (!flowsInProgress.length) { // No flows in progress, create a new flow this._loading = "loading_flow"; let step: DataEntryFlowStep; try { step = await this._params!.flowConfig.createFlow(this.hass, handler); } catch (err: any) { this._step = undefined; this._params = undefined; showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.integrations.config_flow.error" ), text: this.hass.localize( "ui.panel.config.integrations.config_flow.could_not_load" ), }); return; } finally { this._handler = undefined; } this._processStep(step); } else { this._step = null; this._flowsInProgress = flowsInProgress; } this._loading = undefined; } private _handlerPicked(ev) { this._checkFlowsInProgress(ev.detail.handler); } private async _processStep( step: DataEntryFlowStep | undefined | Promise ): Promise { if (step instanceof Promise) { this._loading = "loading_step"; try { this._step = await step; } finally { this._loading = undefined; } return; } if (step === undefined) { this.closeDialog(); return; } this._step = undefined; await this.updateComplete; this._step = step; } private _subscribeDataEntryFlowProgressed() { if (this._unsubDataEntryFlowProgressed) { return; } this._unsubDataEntryFlowProgressed = subscribeDataEntryFlowProgressed( this.hass.connection, async (ev) => { if (ev.data.flow_id !== this._step?.flow_id) { return; } this._processStep( this._params!.flowConfig.fetchFlow(this.hass, this._step?.flow_id) ); } ); } static get styles(): CSSResultGroup { return [ haStyleDialog, css` ha-dialog { --dialog-content-padding: 0; } ha-icon-button { padding: 16px; position: absolute; top: 0; right: 0; } ha-icon-button[rtl] { right: auto; left: 0; } `, ]; } } declare global { interface HTMLElementTagNameMap { "dialog-data-entry-flow": DataEntryFlowDialog; } }