diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts index 14821f8b77..7a830e7d88 100644 --- a/src/data/data_entry_flow.ts +++ b/src/data/data_entry_flow.ts @@ -2,6 +2,8 @@ import { Connection } from "home-assistant-js-websocket"; import type { HaFormSchema } from "../components/ha-form/types"; import { ConfigEntry } from "./config_entries"; +export type FlowType = "config_flow" | "options_flow" | "repair_flow"; + export interface DataEntryFlowProgressedEvent { type: "data_entry_flow_progressed"; data: { @@ -30,6 +32,7 @@ export interface DataEntryFlowStepForm { errors: Record; description_placeholders?: Record; last_step: boolean | null; + preview?: string; } export interface DataEntryFlowStepExternal { diff --git a/src/data/group.ts b/src/data/group.ts index cfae3ed858..e6a004b1a8 100644 --- a/src/data/group.ts +++ b/src/data/group.ts @@ -1,8 +1,10 @@ import { HassEntityAttributeBase, HassEntityBase, + UnsubscribeFunc, } from "home-assistant-js-websocket"; import { computeDomain } from "../common/entity/compute_domain"; +import { HomeAssistant } from "../types"; interface GroupEntityAttributes extends HassEntityAttributeBase { entity_id: string[]; @@ -24,3 +26,20 @@ export const computeGroupDomain = ( ]; return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined; }; + +export const subscribePreviewGroupSensor = ( + hass: HomeAssistant, + flow_id: string, + flow_type: "config_flow" | "options_flow", + user_input: Record, + callback: (preview: { + state: string; + attributes: Record; + }) => void +): Promise => + hass.connection.subscribeMessage(callback, { + type: "group/sensor/start_preview", + flow_id, + flow_type, + user_input, + }); diff --git a/src/dialogs/config-flow/previews/entity-preview-row.ts b/src/dialogs/config-flow/previews/entity-preview-row.ts new file mode 100644 index 0000000000..1ca2b10c13 --- /dev/null +++ b/src/dialogs/config-flow/previews/entity-preview-row.ts @@ -0,0 +1,71 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import { isUnavailableState } from "../../../data/entity"; +import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; +import { HomeAssistant } from "../../../types"; + +@customElement("entity-preview-row") +class EntityPreviewRow extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private stateObj?: HassEntity; + + protected render() { + if (!this.stateObj) { + return nothing; + } + const stateObj = this.stateObj; + return html` +
+ ${computeStateName(stateObj)} +
+
+ ${stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP && + !isUnavailableState(stateObj.state) + ? html` + + ` + : computeStateDisplay( + this.hass!.localize, + stateObj, + this.hass.locale, + this.hass.config, + this.hass.entities + )} +
`; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + align-items: center; + flex-direction: row; + } + .name { + margin-left: 16px; + margin-right: 8px; + flex: 1 1 30%; + } + .value { + direction: ltr; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "entity-preview-row": EntityPreviewRow; + } +} diff --git a/src/dialogs/config-flow/previews/flow-preview-group_sensor.ts b/src/dialogs/config-flow/previews/flow-preview-group_sensor.ts new file mode 100644 index 0000000000..4da52df762 --- /dev/null +++ b/src/dialogs/config-flow/previews/flow-preview-group_sensor.ts @@ -0,0 +1,92 @@ +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { LitElement, html } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { FlowType } from "../../../data/data_entry_flow"; +import { subscribePreviewGroupSensor } from "../../../data/group"; +import { HomeAssistant } from "../../../types"; +import "./entity-preview-row"; + +@customElement("flow-preview-group_sensor") +class FlowPreviewGroupSensor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public flowType!: FlowType; + + public handler!: string; + + public stepId!: string; + + @property() public flowId!: string; + + @property() public stepData!: Record; + + @state() private _preview?: HassEntity; + + 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._subscribePreview(); + } + } + + protected render() { + return html``; + } + + private _setPreview = (preview: { + state: string; + attributes: Record; + }) => { + const now = new Date().toISOString(); + this._preview = { + entity_id: "sensor.flow_preview", + last_changed: now, + last_updated: now, + context: { id: "", parent_id: null, user_id: null }, + ...preview, + }; + }; + + private async _subscribePreview() { + if (this._unsub) { + (await this._unsub)(); + this._unsub = undefined; + } + if (this.flowType === "repair_flow") { + return; + } + if (!this.stepData.type) { + this._preview = undefined; + return; + } + try { + this._unsub = subscribePreviewGroupSensor( + this.hass, + this.flowId, + this.flowType, + this.stepData, + this._setPreview + ); + } catch (err) { + this._preview = undefined; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "flow-preview-group_sensor": FlowPreviewGroupSensor; + } +} diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index f7b3fc7e37..23c37f1797 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -19,6 +19,7 @@ export const showConfigFlowDialog = ( dialogParams: Omit ): void => showFlowDialog(element, dialogParams, { + flowType: "config_flow", loadDevicesAndAreas: true, createFlow: async (hass, handler) => { const [step] = await Promise.all([ diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 265a5cd5f6..5dbd48e7b3 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -9,11 +9,14 @@ import { DataEntryFlowStepForm, DataEntryFlowStepMenu, DataEntryFlowStepProgress, + FlowType, } from "../../data/data_entry_flow"; import type { IntegrationManifest } from "../../data/integration"; import type { HomeAssistant } from "../../types"; export interface FlowConfig { + flowType: FlowType; + loadDevicesAndAreas: boolean; createFlow(hass: HomeAssistant, handler: string): Promise; diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index d95a38c29a..012f0c580e 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -27,6 +27,7 @@ export const showOptionsFlowDialog = ( manifest, }, { + flowType: "options_flow", loadDevicesAndAreas: false, createFlow: async (hass, handler) => { const [step] = await Promise.all([ diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index f951dda0d2..eaf09bef98 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -1,15 +1,18 @@ -import "@material/mwc-button"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; +import "@material/mwc-button"; import { css, CSSResultGroup, html, LitElement, + nothing, PropertyValues, TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../common/dom/fire_event"; +import { isNavigationClick } from "../../common/dom/is-navigation-click"; import "../../components/ha-alert"; import "../../components/ha-circular-progress"; import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data"; @@ -21,7 +24,7 @@ import type { DataEntryFlowStepForm } from "../../data/data_entry_flow"; import type { HomeAssistant } from "../../types"; import type { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; -import { isNavigationClick } from "../../common/dom/is-navigation-click"; +import { haStyle } from "../../resources/styles"; @customElement("step-flow-form") class StepFlowForm extends LitElement { @@ -66,6 +69,23 @@ class StepFlowForm extends LitElement { .localizeValue=${this._localizeValueCallback} > + ${step.preview + ? html`
+

+ ${this.hass.localize( + "ui.panel.config.integrations.config_flow.preview" + )}: +

+ ${dynamicElement(`flow-preview-${this.step.preview}`, { + hass: this.hass, + flowType: this.flowConfig.flowType, + handler: step.handler, + stepId: step.step_id, + flowId: step.flow_id, + stepData, + })} +
` + : nothing}
${this._loading ? html` @@ -93,6 +113,13 @@ class StepFlowForm extends LitElement { this.addEventListener("keydown", this._handleKeyDown); } + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("step") && this.step?.preview) { + import(`./previews/flow-preview-${this.step.preview}`); + } + } + private _clickHandler(ev: MouseEvent) { if (isNavigationClick(ev, false)) { fireEvent(this, "flow-update", { @@ -199,6 +226,7 @@ class StepFlowForm extends LitElement { static get styles(): CSSResultGroup { return [ + haStyle, configFlowContentStyles, css` .error { diff --git a/src/dialogs/config-flow/styles.ts b/src/dialogs/config-flow/styles.ts index 3d93386476..daaa9342af 100644 --- a/src/dialogs/config-flow/styles.ts +++ b/src/dialogs/config-flow/styles.ts @@ -23,7 +23,8 @@ export const configFlowContentStyles = css` box-sizing: border-box; } - .content { + .content, + .preview { margin-top: 20px; padding: 0 24px; } diff --git a/src/panels/config/repairs/show-dialog-repair-flow.ts b/src/panels/config/repairs/show-dialog-repair-flow.ts index b723382b80..c4730ab86c 100644 --- a/src/panels/config/repairs/show-dialog-repair-flow.ts +++ b/src/panels/config/repairs/show-dialog-repair-flow.ts @@ -27,6 +27,7 @@ export const showRepairsFlowDialog = ( dialogClosedCallback, }, { + flowType: "repair_flow", loadDevicesAndAreas: false, createFlow: async (hass, handler) => { const [step] = await Promise.all([ diff --git a/src/translations/en.json b/src/translations/en.json index 3bf9d6582a..f9f71e30c1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3572,6 +3572,7 @@ "finish": "Finish", "submit": "Submit", "next": "Next", + "preview": "Preview", "found_following_devices": "We found the following devices", "yaml_only_title": "This device cannot be added from the UI", "yaml_only": "You can add this device by adding it to your ''configuration.yaml''. See the documentation for more information.",