From d350c35c4e555df581bc46709b8fd7fc1ee80427 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Aug 2023 12:56:53 +0200 Subject: [PATCH] Add preview for template (#17699) --- src/data/ws-templates.ts | 26 ++++ .../previews/flow-preview-template.ts | 114 ++++++++++++++++++ src/dialogs/config-flow/step-flow-form.ts | 9 +- 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/dialogs/config-flow/previews/flow-preview-template.ts diff --git a/src/data/ws-templates.ts b/src/data/ws-templates.ts index 57bfde655b..3b4256be27 100644 --- a/src/data/ws-templates.ts +++ b/src/data/ws-templates.ts @@ -1,4 +1,5 @@ import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { HomeAssistant } from "../types"; export interface RenderTemplateResult { result: string; @@ -12,6 +13,17 @@ interface TemplateListeners { time: boolean; } +export type TemplatePreview = TemplatePreviewState | TemplatePreviewError; + +interface TemplatePreviewState { + state: string; + attributes: Record; +} + +interface TemplatePreviewError { + error: string; +} + export const subscribeRenderTemplate = ( conn: Connection, onChange: (result: RenderTemplateResult) => void, @@ -27,3 +39,17 @@ export const subscribeRenderTemplate = ( type: "render_template", ...params, }); + +export const subscribePreviewTemplate = ( + hass: HomeAssistant, + flow_id: string, + flow_type: "config_flow" | "options_flow", + user_input: Record, + callback: (preview: TemplatePreview) => void +): Promise => + hass.connection.subscribeMessage(callback, { + type: "template/start_preview", + flow_id, + flow_type, + user_input, + }); diff --git a/src/dialogs/config-flow/previews/flow-preview-template.ts b/src/dialogs/config-flow/previews/flow-preview-template.ts new file mode 100644 index 0000000000..44fbe03ff5 --- /dev/null +++ b/src/dialogs/config-flow/previews/flow-preview-template.ts @@ -0,0 +1,114 @@ +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { LitElement, html } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { debounce } from "../../../common/util/debounce"; +import { FlowType } from "../../../data/data_entry_flow"; +import { + TemplatePreview, + subscribePreviewTemplate, +} from "../../../data/ws-templates"; +import { HomeAssistant } from "../../../types"; +import "./entity-preview-row"; +import { fireEvent } from "../../../common/dom/fire_event"; + +@customElement("flow-preview-template") +class FlowPreviewTemplate extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public flowType!: FlowType; + + public handler!: string; + + @property() public stepId!: string; + + @property() public flowId!: string; + + @property() public stepData!: Record; + + @state() private _preview?: HassEntity; + + @state() private _error?: string; + + private _unsub?: Promise; + + disconnectedCallback(): void { + super.disconnectedCallback(); + if (this._unsub) { + this._unsub.then((unsub) => unsub()); + this._unsub = undefined; + } + } + + willUpdate(changedProps) { + if (changedProps.has("stepData")) { + this._debouncedSubscribePreview(); + } + } + + protected render() { + if (this._error) { + return html`${this._error}`; + } + return html``; + } + + private _setPreview = (preview: TemplatePreview) => { + if ("error" in preview) { + this._error = preview.error; + this._preview = undefined; + return; + } + this._error = undefined; + const now = new Date().toISOString(); + this._preview = { + entity_id: `${this.stepId}.flow_preview`, + last_changed: now, + last_updated: now, + context: { id: "", parent_id: null, user_id: null }, + ...preview, + }; + }; + + private _debouncedSubscribePreview = debounce(() => { + this._subscribePreview(); + }, 250); + + private async _subscribePreview() { + if (this._unsub) { + (await this._unsub)(); + this._unsub = undefined; + } + if (this.flowType === "repair_flow") { + return; + } + try { + this._unsub = subscribePreviewTemplate( + this.hass, + this.flowId, + this.flowType, + this.stepData, + this._setPreview + ); + await this._unsub; + fireEvent(this, "set-flow-errors", { errors: {} }); + } catch (err: any) { + if (typeof err.message === "string") { + this._error = err.message; + } else { + this._error = undefined; + fireEvent(this, "set-flow-errors", err.message); + } + this._unsub = undefined; + this._preview = undefined; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "flow-preview-template": FlowPreviewTemplate; + } +} diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index eaf09bef98..bffd423ef9 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -70,7 +70,7 @@ class StepFlowForm extends LitElement { > ${step.preview - ? html`
+ ? html`

${this.hass.localize( "ui.panel.config.integrations.config_flow.preview" @@ -107,6 +107,10 @@ class StepFlowForm extends LitElement { `; } + private _setError(ev: CustomEvent) { + this.step = { ...this.step, errors: ev.detail }; + } + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0); @@ -253,6 +257,9 @@ class StepFlowForm extends LitElement { } declare global { + interface HASSDomEvents { + "set-flow-errors": { errors: DataEntryFlowStepForm["errors"] }; + } interface HTMLElementTagNameMap { "step-flow-form": StepFlowForm; }