mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-29 20:26:39 +00:00
Cleanup config flow (#2932)
* Break up config flow dialog * Allow picking devices when config flow finishes * Lint * Tweaks
This commit is contained in:
parent
2aec877310
commit
915c441a94
@ -79,3 +79,25 @@ export const computeLocalize = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Silly helper function that converts an object of placeholders to array so we
|
||||||
|
* can convert it back to an object again inside the localize func.
|
||||||
|
* @param localize
|
||||||
|
* @param key
|
||||||
|
* @param placeholders
|
||||||
|
*/
|
||||||
|
export const localizeKey = (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
key: string,
|
||||||
|
placeholders?: { [key: string]: string }
|
||||||
|
) => {
|
||||||
|
const args: [string, ...string[]] = [key];
|
||||||
|
if (placeholders) {
|
||||||
|
Object.keys(placeholders).forEach((placeholderKey) => {
|
||||||
|
args.push(placeholderKey);
|
||||||
|
args.push(placeholders[placeholderKey]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return localize(...args);
|
||||||
|
};
|
||||||
|
@ -39,7 +39,7 @@ class StateBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (!changedProps.has("stateObj")) {
|
if (!changedProps.has("stateObj") || !this.stateObj) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const stateObj = this.stateObj;
|
const stateObj = this.stateObj;
|
||||||
|
@ -22,7 +22,8 @@ export interface ConfigFlowStepCreateEntry {
|
|||||||
flow_id: string;
|
flow_id: string;
|
||||||
handler: string;
|
handler: string;
|
||||||
title: string;
|
title: string;
|
||||||
data: any;
|
// Config entry ID
|
||||||
|
result: string;
|
||||||
description: string;
|
description: string;
|
||||||
description_placeholders: { [key: string]: string };
|
description_placeholders: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
@ -25,16 +25,32 @@ import {
|
|||||||
fetchConfigFlow,
|
fetchConfigFlow,
|
||||||
createConfigFlow,
|
createConfigFlow,
|
||||||
ConfigFlowStep,
|
ConfigFlowStep,
|
||||||
handleConfigFlowStep,
|
|
||||||
deleteConfigFlow,
|
deleteConfigFlow,
|
||||||
FieldSchema,
|
|
||||||
ConfigFlowStepForm,
|
|
||||||
} from "../../data/config_entries";
|
} from "../../data/config_entries";
|
||||||
import { PolymerChangedEvent, applyPolymerEvent } from "../../polymer-types";
|
import { PolymerChangedEvent } from "../../polymer-types";
|
||||||
import { HaConfigFlowParams } from "./show-dialog-config-flow";
|
import { HaConfigFlowParams } from "./show-dialog-config-flow";
|
||||||
|
|
||||||
|
import "./step-flow-loading";
|
||||||
|
import "./step-flow-form";
|
||||||
|
import "./step-flow-abort";
|
||||||
|
import "./step-flow-create-entry";
|
||||||
|
import {
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
fetchDeviceRegistry,
|
||||||
|
} from "../../data/device_registry";
|
||||||
|
import { AreaRegistryEntry, fetchAreaRegistry } from "../../data/area_registry";
|
||||||
|
|
||||||
let instance = 0;
|
let instance = 0;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// for fire event
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"flow-update": {
|
||||||
|
step?: ConfigFlowStep;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("dialog-config-flow")
|
@customElement("dialog-config-flow")
|
||||||
class ConfigFlowDialog extends LitElement {
|
class ConfigFlowDialog extends LitElement {
|
||||||
@property()
|
@property()
|
||||||
@ -49,18 +65,15 @@ class ConfigFlowDialog extends LitElement {
|
|||||||
private _step?: ConfigFlowStep;
|
private _step?: ConfigFlowStep;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
private _stepData?: { [key: string]: any };
|
private _devices?: DeviceRegistryEntry[];
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
private _errorMsg?: string;
|
private _areas?: AreaRegistryEntry[];
|
||||||
|
|
||||||
public async showDialog(params: HaConfigFlowParams): Promise<void> {
|
public async showDialog(params: HaConfigFlowParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._loading = true;
|
this._loading = true;
|
||||||
this._instance = instance++;
|
this._instance = instance++;
|
||||||
this._step = undefined;
|
|
||||||
this._stepData = {};
|
|
||||||
this._errorMsg = undefined;
|
|
||||||
|
|
||||||
const fetchStep = params.continueFlowId
|
const fetchStep = params.continueFlowId
|
||||||
? fetchConfigFlow(params.hass, params.continueFlowId)
|
? fetchConfigFlow(params.hass, params.continueFlowId)
|
||||||
@ -93,201 +106,91 @@ class ConfigFlowDialog extends LitElement {
|
|||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return html``;
|
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`
|
return html`
|
||||||
<paper-dialog
|
<paper-dialog with-backdrop opened @opened-changed=${this._openedChanged}>
|
||||||
with-backdrop
|
${this._loading
|
||||||
.opened=${true}
|
? html`
|
||||||
@opened-changed=${this._openedChanged}
|
<step-flow-loading></step-flow-loading>
|
||||||
>
|
`
|
||||||
<h2>
|
: this._step === undefined
|
||||||
${headerContent}
|
? // When we are going to next step, we render 1 round of empty
|
||||||
</h2>
|
// to reset the element.
|
||||||
<paper-dialog-scrollable>
|
""
|
||||||
${this._errorMsg
|
: this._step.type === "form"
|
||||||
? html`
|
? html`
|
||||||
<div class="error">${this._errorMsg}</div>
|
<step-flow-form
|
||||||
`
|
.step=${this._step}
|
||||||
: ""}
|
.hass=${this._params.hass}
|
||||||
${description
|
></step-flow-form>
|
||||||
? html`
|
`
|
||||||
<ha-markdown .content=${description} allow-svg></ha-markdown>
|
: this._step.type === "abort"
|
||||||
`
|
? html`
|
||||||
: ""}
|
<step-flow-abort
|
||||||
${bodyContent}
|
.step=${this._step}
|
||||||
</paper-dialog-scrollable>
|
.hass=${this._params.hass}
|
||||||
<div class="buttons">
|
></step-flow-abort>
|
||||||
${buttonContent}
|
`
|
||||||
</div>
|
: this._devices === undefined || this._areas === undefined
|
||||||
|
? // When it's a create entry result, we will fetch device & area registry
|
||||||
|
html`
|
||||||
|
<step-flow-loading></step-flow-loading>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<step-flow-create-entry
|
||||||
|
.step=${this._step}
|
||||||
|
.hass=${this._params.hass}
|
||||||
|
.devices=${this._devices}
|
||||||
|
.areas=${this._areas}
|
||||||
|
></step-flow-create-entry>
|
||||||
|
`}
|
||||||
</paper-dialog>
|
</paper-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this.addEventListener("keypress", (ev) => {
|
this.addEventListener("flow-update", (ev) => {
|
||||||
if (ev.keyCode === 13) {
|
this._processStep((ev as any).detail.step);
|
||||||
this._submitStep();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
changedProps.has("_step") &&
|
||||||
|
this._step &&
|
||||||
|
this._step.type === "create_entry"
|
||||||
|
) {
|
||||||
|
this._fetchDevices(this._step.result);
|
||||||
|
this._fetchAreas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private get _dialog(): PaperDialogElement {
|
private get _dialog(): PaperDialogElement {
|
||||||
return this.shadowRoot!.querySelector("paper-dialog")!;
|
return this.shadowRoot!.querySelector("paper-dialog")!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _submitStep(): Promise<void> {
|
private async _fetchDevices(configEntryId) {
|
||||||
this._loading = true;
|
// Wait 5 seconds to give integrations time to find devices
|
||||||
this._errorMsg = undefined;
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
const devices = await fetchDeviceRegistry(this._params!.hass);
|
||||||
const curInstance = this._instance;
|
this._devices = devices.filter((device) =>
|
||||||
const stepData = this._stepData || {};
|
device.config_entries.includes(configEntryId)
|
||||||
|
);
|
||||||
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 {
|
private async _fetchAreas() {
|
||||||
this._step = step;
|
this._areas = await fetchAreaRegistry(this._params!.hass);
|
||||||
|
}
|
||||||
|
|
||||||
// We got a new form if there are no errors.
|
private async _processStep(step: ConfigFlowStep): Promise<void> {
|
||||||
if (step.type === "form") {
|
if (step === undefined) {
|
||||||
if (!step.errors) {
|
this._flowDone();
|
||||||
step.errors = {};
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this._step = undefined;
|
||||||
|
await this.updateComplete;
|
||||||
|
this._step = step;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _flowDone(): void {
|
private _flowDone(): void {
|
||||||
@ -307,10 +210,9 @@ class ConfigFlowDialog extends LitElement {
|
|||||||
flowFinished,
|
flowFinished,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._errorMsg = undefined;
|
|
||||||
this._step = undefined;
|
this._step = undefined;
|
||||||
this._stepData = {};
|
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
|
this._devices = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||||
@ -320,51 +222,17 @@ class ConfigFlowDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
static get styles(): CSSResultArray {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
paper-dialog {
|
paper-dialog {
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
ha-markdown {
|
paper-dialog > * {
|
||||||
word-break: break-word;
|
margin: 0;
|
||||||
}
|
|
||||||
ha-markdown a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
ha-markdown img:first-child:last-child {
|
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
padding: 0;
|
||||||
}
|
|
||||||
.init-spinner {
|
|
||||||
padding: 10px 100px 34px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.submit-spinner {
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
66
src/dialogs/config-flow/step-flow-abort.ts
Normal file
66
src/dialogs/config-flow/step-flow-abort.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
property,
|
||||||
|
CSSResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@material/mwc-button";
|
||||||
|
|
||||||
|
import { ConfigFlowStepAbort } 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";
|
||||||
|
|
||||||
|
@customElement("step-flow-abort")
|
||||||
|
class StepFlowAbort extends LitElement {
|
||||||
|
@property()
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
private step!: ConfigFlowStepAbort;
|
||||||
|
|
||||||
|
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`
|
||||||
|
<h2>Aborted</h2>
|
||||||
|
<div class="content">
|
||||||
|
${
|
||||||
|
description
|
||||||
|
? html`
|
||||||
|
<ha-markdown .content=${description} allow-svg></ha-markdown>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<mwc-button @click="${this._flowDone}">Close</mwc-button>
|
||||||
|
</div>
|
||||||
|
</paper-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _flowDone(): void {
|
||||||
|
fireEvent(this, "flow-update", { step: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return configFlowContentStyles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"step-flow-abort": StepFlowAbort;
|
||||||
|
}
|
||||||
|
}
|
191
src/dialogs/config-flow/step-flow-create-entry.ts
Normal file
191
src/dialogs/config-flow/step-flow-create-entry.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
property,
|
||||||
|
CSSResultArray,
|
||||||
|
css,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@material/mwc-button";
|
||||||
|
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 {
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
updateDeviceRegistryEntry,
|
||||||
|
} from "../../data/device_registry";
|
||||||
|
import {
|
||||||
|
AreaRegistryEntry,
|
||||||
|
createAreaRegistryEntry,
|
||||||
|
} from "../../data/area_registry";
|
||||||
|
|
||||||
|
@customElement("step-flow-create-entry")
|
||||||
|
class StepFlowCreateEntry extends LitElement {
|
||||||
|
@property()
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public step!: ConfigFlowStepCreateEntry;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public devices!: DeviceRegistryEntry[];
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public areas!: AreaRegistryEntry[];
|
||||||
|
|
||||||
|
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`
|
||||||
|
<h2>Success!</h2>
|
||||||
|
<div class="content">
|
||||||
|
${
|
||||||
|
description
|
||||||
|
? html`
|
||||||
|
<ha-markdown .content=${description} allow-svg></ha-markdown>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<p>Created config for ${step.title}.</p>
|
||||||
|
${
|
||||||
|
this.devices.length === 0
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<p>We found the following devices:</p>
|
||||||
|
<div class="devices">
|
||||||
|
${this.devices.map(
|
||||||
|
(device) =>
|
||||||
|
html`
|
||||||
|
<div class="device">
|
||||||
|
<b>${device.name}</b><br />
|
||||||
|
${device.model} (${device.manufacturer})
|
||||||
|
|
||||||
|
<paper-dropdown-menu-light
|
||||||
|
label="Area"
|
||||||
|
.device=${device.id}
|
||||||
|
@selected-item-changed=${this._handleAreaChanged}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
selected="0"
|
||||||
|
>
|
||||||
|
<paper-item>
|
||||||
|
${localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.no_area"
|
||||||
|
)}
|
||||||
|
</paper-item>
|
||||||
|
${this.areas.map(
|
||||||
|
(area) => html`
|
||||||
|
<paper-item .area=${area.area_id}>
|
||||||
|
${area.name}
|
||||||
|
</paper-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu-light>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
${
|
||||||
|
this.devices.length > 0
|
||||||
|
? html`
|
||||||
|
<mwc-button @click="${this._addArea}">Add Area</mwc-button>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
<mwc-button @click="${this._flowDone}">Finish</mwc-button>
|
||||||
|
</div>
|
||||||
|
</paper-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _flowDone(): void {
|
||||||
|
fireEvent(this, "flow-update", { step: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addArea() {
|
||||||
|
const name = prompt("Name of the new area?");
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const area = await createAreaRegistryEntry(this.hass, {
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
this.areas = [...this.areas, area];
|
||||||
|
} catch (err) {
|
||||||
|
alert("Failed to create area.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleAreaChanged(ev: Event) {
|
||||||
|
const dropdown = ev.currentTarget as any;
|
||||||
|
const device = dropdown.device;
|
||||||
|
|
||||||
|
// Item first becomes null, then new item.
|
||||||
|
if (!dropdown.selectedItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const area = dropdown.selectedItem.area;
|
||||||
|
try {
|
||||||
|
await updateDeviceRegistryEntry(this.hass, device, {
|
||||||
|
area_id: area,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Error saving area: ${err.message}`);
|
||||||
|
dropdown.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultArray {
|
||||||
|
return [
|
||||||
|
configFlowContentStyles,
|
||||||
|
css`
|
||||||
|
.devices {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: -4px;
|
||||||
|
}
|
||||||
|
.device {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.buttons > *:last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"step-flow-create-entry": StepFlowCreateEntry;
|
||||||
|
}
|
||||||
|
}
|
222
src/dialogs/config-flow/step-flow-form.ts
Normal file
222
src/dialogs/config-flow/step-flow-form.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
CSSResultArray,
|
||||||
|
css,
|
||||||
|
customElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@material/mwc-button";
|
||||||
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
|
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";
|
||||||
|
|
||||||
|
@customElement("step-flow-form")
|
||||||
|
class StepFlowForm extends LitElement {
|
||||||
|
@property()
|
||||||
|
public step!: ConfigFlowStepForm;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
private _loading = false;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
private _stepData?: { [key: string]: any };
|
||||||
|
|
||||||
|
@property()
|
||||||
|
private _errorMsg?: string;
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
const localize = this.hass.localize;
|
||||||
|
const step = this.step;
|
||||||
|
|
||||||
|
const allRequiredInfoFilledIn =
|
||||||
|
this._stepData === undefined
|
||||||
|
? // If no data filled in, just check that any field is required
|
||||||
|
step.data_schema.find((field) => !field.optional) === undefined
|
||||||
|
: // If data is filled in, make sure all required fields are
|
||||||
|
this._stepData &&
|
||||||
|
step.data_schema.every(
|
||||||
|
(field) =>
|
||||||
|
field.optional ||
|
||||||
|
!["", undefined].includes(this._stepData![field.name])
|
||||||
|
);
|
||||||
|
|
||||||
|
const description = localizeKey(
|
||||||
|
localize,
|
||||||
|
`component.${step.handler}.config.step.${step.step_id}.description`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<h2>
|
||||||
|
${localize(
|
||||||
|
`component.${step.handler}.config.step.${step.step_id}.title`
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<div class="content">
|
||||||
|
${this._errorMsg
|
||||||
|
? html`
|
||||||
|
<div class="error">${this._errorMsg}</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${description
|
||||||
|
? html`
|
||||||
|
<ha-markdown .content=${description} allow-svg></ha-markdown>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<ha-form
|
||||||
|
.data=${this._stepDataProcessed}
|
||||||
|
@data-changed=${this._stepDataChanged}
|
||||||
|
.schema=${step.data_schema}
|
||||||
|
.error=${step.errors}
|
||||||
|
.computeLabel=${this._labelCallback}
|
||||||
|
.computeError=${this._errorCallback}
|
||||||
|
></ha-form>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
${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>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this.addEventListener("keypress", (ev) => {
|
||||||
|
if (ev.keyCode === 13) {
|
||||||
|
this._submitStep();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _stepDataProcessed() {
|
||||||
|
if (this._stepData !== undefined) {
|
||||||
|
return this._stepData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
this.step.data_schema.forEach((field) => {
|
||||||
|
if ("default" in field) {
|
||||||
|
data[field.name] = field.default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _submitStep(): Promise<void> {
|
||||||
|
this._loading = true;
|
||||||
|
this._errorMsg = undefined;
|
||||||
|
|
||||||
|
const flowId = this.step.flow_id;
|
||||||
|
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.hass,
|
||||||
|
this.step.flow_id,
|
||||||
|
toSendData
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.step || flowId !== this.step.flow_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "flow-update", {
|
||||||
|
step,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._errorMsg =
|
||||||
|
(err && err.body && err.body.message) || "Unknown error occurred";
|
||||||
|
} finally {
|
||||||
|
this._loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.hass.localize(
|
||||||
|
`component.${step.handler}.config.step.${step.step_id}.data.${
|
||||||
|
schema.name
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _errorCallback = (error: string) =>
|
||||||
|
this.hass.localize(`component.${this.step.handler}.config.error.${error}`);
|
||||||
|
|
||||||
|
static get styles(): CSSResultArray {
|
||||||
|
return [
|
||||||
|
configFlowContentStyles,
|
||||||
|
css`
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-spinner {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"step-flow-form": StepFlowForm;
|
||||||
|
}
|
||||||
|
}
|
35
src/dialogs/config-flow/step-flow-loading.ts
Normal file
35
src/dialogs/config-flow/step-flow-loading.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
customElement,
|
||||||
|
CSSResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@polymer/paper-spinner/paper-spinner-lite";
|
||||||
|
|
||||||
|
@customElement("step-flow-loading")
|
||||||
|
class StepFlowLoading extends LitElement {
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html`
|
||||||
|
<div class="init-spinner">
|
||||||
|
<paper-spinner-lite active></paper-spinner-lite>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
.init-spinner {
|
||||||
|
padding: 50px 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"step-flow-loading": StepFlowLoading;
|
||||||
|
}
|
||||||
|
}
|
33
src/dialogs/config-flow/styles.ts
Normal file
33
src/dialogs/config-flow/styles.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { css } from "lit-element";
|
||||||
|
|
||||||
|
export const configFlowContentStyles = css`
|
||||||
|
h2 {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 8px 8px 24px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--primary-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`;
|
Loading…
x
Reference in New Issue
Block a user