mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-30 12:46:35 +00:00
Convert config flow to Lit/TS (#2814)
* Convert config flow to Lit/TS * Apply suggestions from code review Co-Authored-By: balloob <paulus@home-assistant.io> * Add missing import * Apply suggestions from code review Co-Authored-By: balloob <paulus@home-assistant.io> * Address comments
This commit is contained in:
parent
e406a50b50
commit
534b18ee30
63
src/data/config_entries.ts
Normal file
63
src/data/config_entries.ts
Normal file
@ -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<ConfigFlowStep>("POST", "config/config_entries/flow", {
|
||||||
|
handler,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||||
|
hass.callApi<ConfigFlowStep>("GET", `config/config_entries/flow/${flowId}`);
|
||||||
|
|
||||||
|
export const handleConfigFlowStep = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
flowId: string,
|
||||||
|
data: { [key: string]: any }
|
||||||
|
) =>
|
||||||
|
hass.callApi<ConfigFlowStep>(
|
||||||
|
"POST",
|
||||||
|
`config/config_entries/flow/${flowId}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||||
|
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
|
378
src/dialogs/config-flow/dialog-config-flow.ts
Normal file
378
src/dialogs/config-flow/dialog-config-flow.ts
Normal file
@ -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<void> {
|
||||||
|
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`
|
||||||
|
<div class="init-spinner">
|
||||||
|
<paper-spinner active></paper-spinner>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (step.type === "abort") {
|
||||||
|
descriptionKey = `component.${step.handler}.config.abort.${step.reason}`;
|
||||||
|
headerContent = "Aborted";
|
||||||
|
bodyContent = html``;
|
||||||
|
buttonContent = html`
|
||||||
|
<mwc-button @click="${this._flowDone}">Close</mwc-button>
|
||||||
|
`;
|
||||||
|
} else if (step.type === "create_entry") {
|
||||||
|
descriptionKey = `component.${
|
||||||
|
step.handler
|
||||||
|
}.config.create_entry.${step.description || "default"}`;
|
||||||
|
headerContent = "Success!";
|
||||||
|
bodyContent = html`
|
||||||
|
<p>Created config for ${step.title}</p>
|
||||||
|
`;
|
||||||
|
buttonContent = html`
|
||||||
|
<mwc-button @click="${this._flowDone}">Close</mwc-button>
|
||||||
|
`;
|
||||||
|
} 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`
|
||||||
|
<ha-form
|
||||||
|
.data=${this._stepData}
|
||||||
|
@data-changed=${this._stepDataChanged}
|
||||||
|
.schema=${step.data_schema}
|
||||||
|
.error=${step.errors}
|
||||||
|
.computeLabel=${this._labelCallback}
|
||||||
|
.computeError=${this._errorCallback}
|
||||||
|
></ha-form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const allRequiredInfoFilledIn =
|
||||||
|
this._stepData &&
|
||||||
|
step.data_schema.every(
|
||||||
|
(field) =>
|
||||||
|
field.optional ||
|
||||||
|
!["", undefined].includes(this._stepData![field.name])
|
||||||
|
);
|
||||||
|
|
||||||
|
buttonContent = this._loading
|
||||||
|
? html`
|
||||||
|
<div class="submit-spinner">
|
||||||
|
<paper-spinner active></paper-spinner>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<div>
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._submitStep}
|
||||||
|
.disabled=${!allRequiredInfoFilledIn}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</mwc-button>
|
||||||
|
|
||||||
|
${!allRequiredInfoFilledIn
|
||||||
|
? html`
|
||||||
|
<paper-tooltip position="left">
|
||||||
|
Not all required fields are filled in.
|
||||||
|
</paper-tooltip>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
<paper-dialog
|
||||||
|
with-backdrop
|
||||||
|
.opened=${true}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
${headerContent}
|
||||||
|
</h2>
|
||||||
|
<paper-dialog-scrollable>
|
||||||
|
${this._errorMsg
|
||||||
|
? html`
|
||||||
|
<div class="error">${this._errorMsg}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${description
|
||||||
|
? html`
|
||||||
|
<ha-markdown .content=${description} allow-svg></ha-markdown>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${bodyContent}
|
||||||
|
</paper-dialog-scrollable>
|
||||||
|
<div class="buttons">
|
||||||
|
${buttonContent}
|
||||||
|
</div>
|
||||||
|
</paper-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
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<boolean>): void {
|
||||||
|
// Closed dialog by clicking on the overlay
|
||||||
|
if (this._step && !ev.detail.value) {
|
||||||
|
this._flowDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stepDataChanged(ev: PolymerChangedEvent<any>): 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;
|
||||||
|
}
|
||||||
|
}
|
23
src/dialogs/config-flow/show-dialog-config-flow.ts
Normal file
23
src/dialogs/config-flow/show-dialog-config-flow.ts
Normal file
@ -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,
|
||||||
|
});
|
||||||
|
};
|
@ -17,8 +17,10 @@ import "../ha-config-section";
|
|||||||
import EventsMixin from "../../../mixins/events-mixin";
|
import EventsMixin from "../../../mixins/events-mixin";
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||||
import computeStateName from "../../../common/entity/compute_state_name";
|
import computeStateName from "../../../common/entity/compute_state_name";
|
||||||
|
import {
|
||||||
let registeredDialog = false;
|
loadConfigFlowDialog,
|
||||||
|
showConfigFlowDialog,
|
||||||
|
} from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @appliesMixin LocalizeMixin
|
* @appliesMixin LocalizeMixin
|
||||||
@ -165,20 +167,11 @@ class HaConfigManagerDashboard extends LocalizeMixin(
|
|||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
loadConfigFlowDialog();
|
||||||
if (!registeredDialog) {
|
|
||||||
registeredDialog = true;
|
|
||||||
this.fire("register-dialog", {
|
|
||||||
dialogShowEvent: "show-config-flow",
|
|
||||||
dialogTag: "ha-config-flow",
|
|
||||||
dialogImport: () =>
|
|
||||||
import(/* webpackChunkName: "ha-config-flow" */ "./ha-config-flow"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_createFlow(ev) {
|
_createFlow(ev) {
|
||||||
this.fire("show-config-flow", {
|
showConfigFlowDialog(this, {
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
newFlowForHandler: ev.model.item,
|
newFlowForHandler: ev.model.item,
|
||||||
dialogClosedCallback: () => this.fire("hass-reload-entries"),
|
dialogClosedCallback: () => this.fire("hass-reload-entries"),
|
||||||
@ -186,7 +179,7 @@ class HaConfigManagerDashboard extends LocalizeMixin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
_continueFlow(ev) {
|
_continueFlow(ev) {
|
||||||
this.fire("show-config-flow", {
|
showConfigFlowDialog(this, {
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
continueFlowId: ev.model.item.flow_id,
|
continueFlowId: ev.model.item.flow_id,
|
||||||
dialogClosedCallback: () => this.fire("hass-reload-entries"),
|
dialogClosedCallback: () => this.fire("hass-reload-entries"),
|
||||||
|
@ -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`
|
|
||||||
<style include="ha-style-dialog">
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<paper-dialog
|
|
||||||
id="dialog"
|
|
||||||
with-backdrop=""
|
|
||||||
opened="{{_opened}}"
|
|
||||||
on-opened-changed="_openedChanged"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
|
||||||
Aborted
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
|
||||||
Success!
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
|
||||||
[[_computeStepTitle(localize, _step)]]
|
|
||||||
</template>
|
|
||||||
</h2>
|
|
||||||
<paper-dialog-scrollable>
|
|
||||||
<template is="dom-if" if="[[_errorMsg]]">
|
|
||||||
<div class="error">[[_errorMsg]]</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!_step]]">
|
|
||||||
<div class="init-spinner">
|
|
||||||
<paper-spinner active></paper-spinner>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_step]]">
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
|
||||||
<p>Created config for [[_step.title]]</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
is="dom-if"
|
|
||||||
if="[[_computeStepDescription(localize, _step)]]"
|
|
||||||
>
|
|
||||||
<ha-markdown
|
|
||||||
content="[[_computeStepDescription(localize, _step)]]"
|
|
||||||
allow-svg
|
|
||||||
></ha-markdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
|
||||||
<ha-form
|
|
||||||
data="{{_stepData}}"
|
|
||||||
on-data-changed="_increaseCounter"
|
|
||||||
schema="[[_step.data_schema]]"
|
|
||||||
error="[[_step.errors]]"
|
|
||||||
compute-label="[[_computeLabelCallback(localize, _step)]]"
|
|
||||||
compute-error="[[_computeErrorCallback(localize, _step)]]"
|
|
||||||
></ha-form>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</paper-dialog-scrollable>
|
|
||||||
<div class="buttons">
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
|
||||||
<mwc-button on-click="_flowDone">Close</mwc-button>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
|
||||||
<mwc-button on-click="_flowDone">Close</mwc-button>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
|
||||||
<template is="dom-if" if="[[_loading]]">
|
|
||||||
<div class="submit-spinner">
|
|
||||||
<paper-spinner active></paper-spinner>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[!_loading]]">
|
|
||||||
<div>
|
|
||||||
<mwc-button on-click="_submitStep" disabled="[[!_canSubmit]]"
|
|
||||||
>Submit</mwc-button
|
|
||||||
>
|
|
||||||
<template is="dom-if" if="[[!_canSubmit]]">
|
|
||||||
<paper-tooltip position="left">
|
|
||||||
Not all required fields are filled in.
|
|
||||||
</paper-tooltip>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</paper-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
@ -1,9 +1,20 @@
|
|||||||
// Force file to be a module to augment global scope.
|
export const applyPolymerEvent = <T>(
|
||||||
export {};
|
ev: PolymerChangedEvent<T>,
|
||||||
|
curValue: T
|
||||||
|
): T => {
|
||||||
|
const { path, value } = ev.detail;
|
||||||
|
if (!path) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const propName = path.split(".")[1];
|
||||||
|
return { ...curValue, [propName]: value };
|
||||||
|
};
|
||||||
|
|
||||||
export interface PolymerChangedEvent<T> extends Event {
|
export interface PolymerChangedEvent<T> extends Event {
|
||||||
detail: {
|
detail: {
|
||||||
value: T;
|
value: T;
|
||||||
|
path?: string;
|
||||||
|
queueProperty: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user