From 868f24eb9fec21dfff0f3b48ade9e5fdd05ca265 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 4 Apr 2025 17:25:29 +0200 Subject: [PATCH] Add basic dialog --- src/components/ha-dialog.ts | 1 + src/data/lovelace.ts | 3 +- src/data/lovelace/config/action.ts | 7 + src/panels/lovelace/common/handle-action.ts | 14 ++ .../lovelace/components/hui-action-editor.ts | 32 +++ .../lovelace/editor/structs/action-struct.ts | 9 + src/panels/lovelace/types.ts | 5 + src/panels/lovelace/views/hui-masonry-view.ts | 10 +- src/panels/lovelace/views/hui-panel-view.ts | 9 +- .../lovelace/views/hui-sections-view.ts | 17 +- src/panels/lovelace/views/hui-view.ts | 24 +- .../views/view-popup/hui-dialog-view-popup.ts | 206 ++++++++++++++++++ .../view-popup/show-view-popup-dialog.ts | 17 ++ 13 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 src/panels/lovelace/views/view-popup/hui-dialog-view-popup.ts create mode 100644 src/panels/lovelace/views/view-popup/show-view-popup-dialog.ts diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index e326e6b19b..f36d93476b 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -139,6 +139,7 @@ export class HaDialog extends DialogBase { :host([flexContent]) .mdc-dialog .mdc-dialog__content { display: flex; flex-direction: column; + scrollbar-color: var(--scrollbar-thumb-color) transparent; } .header_title { display: flex; diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 98ed39e912..e64430832b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -3,7 +3,7 @@ import { getCollection } from "home-assistant-js-websocket"; import type { HuiBadge } from "../panels/lovelace/badges/hui-badge"; import type { HuiCard } from "../panels/lovelace/cards/hui-card"; import type { HuiSection } from "../panels/lovelace/sections/hui-section"; -import type { Lovelace } from "../panels/lovelace/types"; +import type { Lovelace, LovelaceDialogSize } from "../panels/lovelace/types"; import type { HomeAssistant } from "../types"; import type { LovelaceSectionConfig } from "./lovelace/config/section"; import type { LegacyLovelaceConfig } from "./lovelace/config/types"; @@ -24,6 +24,7 @@ export interface LovelaceViewElement extends HTMLElement { sections?: HuiSection[]; isStrategy: boolean; setConfig(config: LovelaceViewConfig): void; + getDialogSize?: () => LovelaceDialogSize; } export interface LovelaceSectionElement extends HTMLElement { diff --git a/src/data/lovelace/config/action.ts b/src/data/lovelace/config/action.ts index 6aa0a8ee88..0a836299df 100644 --- a/src/data/lovelace/config/action.ts +++ b/src/data/lovelace/config/action.ts @@ -45,6 +45,12 @@ export interface CustomActionConfig extends BaseActionConfig { action: "fire-dom-event"; } +export interface OpenDialogActionConfig extends BaseActionConfig { + action: "open-dialog"; + dashboard_path?: string; + view_path: string; +} + export interface BaseActionConfig { action: string; confirmation?: ConfirmationRestrictionConfig; @@ -60,6 +66,7 @@ export interface RestrictionConfig { } export type ActionConfig = + | OpenDialogActionConfig | ToggleActionConfig | CallServiceActionConfig | NavigateActionConfig diff --git a/src/panels/lovelace/common/handle-action.ts b/src/panels/lovelace/common/handle-action.ts index 7434f75f5d..e5c3b61f87 100644 --- a/src/panels/lovelace/common/handle-action.ts +++ b/src/panels/lovelace/common/handle-action.ts @@ -7,6 +7,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import type { HomeAssistant } from "../../../types"; import { showToast } from "../../../util/toast"; +import { showViewPopupDialog } from "../views/view-popup/show-view-popup-dialog"; import { toggleEntity } from "./entity/toggle-entity"; declare global { @@ -125,6 +126,19 @@ export const handleAction = async ( forwardHaptic("failure"); } break; + case "open-dialog": + if (actionConfig.view_path) { + showViewPopupDialog(node, { + dashboard_path: actionConfig.dashboard_path, + view_path: actionConfig.view_path, + }); + } else { + showToast(node, { + message: "No dashboard path and view path provided", + }); + forwardHaptic("failure"); + } + break; case "url": { if (actionConfig.url_path) { window.open(actionConfig.url_path); diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index cd1f466840..e038e49301 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -16,6 +16,7 @@ import type { ActionConfig, CallServiceActionConfig, NavigateActionConfig, + OpenDialogActionConfig, UrlActionConfig, } from "../../../data/lovelace/config/action"; import type { ServiceAction } from "../../../data/script"; @@ -30,6 +31,7 @@ const DEFAULT_ACTIONS: UiAction[] = [ "toggle", "navigate", "url", + "open-dialog", "perform-action", "assist", "none", @@ -93,6 +95,16 @@ export class HuiActionEditor extends LitElement { return config?.url_path || ""; } + get _view_path(): string { + const config = this.config as OpenDialogActionConfig | undefined; + return config?.view_path || ""; + } + + get _dashboard_path(): string { + const config = this.config as OpenDialogActionConfig | undefined; + return config?.dashboard_path || ""; + } + get _service(): string { const config = this.config as CallServiceActionConfig; return config?.perform_action || config?.service || ""; @@ -191,6 +203,26 @@ export class HuiActionEditor extends LitElement { > ` : nothing} + ${this.config?.action === "url" + ? html` + + + ` + : nothing} ${this.config?.action === "call-service" || this.config?.action === "perform-action" ? html` diff --git a/src/panels/lovelace/editor/structs/action-struct.ts b/src/panels/lovelace/editor/structs/action-struct.ts index b6b7c75a27..f10bd3edc2 100644 --- a/src/panels/lovelace/editor/structs/action-struct.ts +++ b/src/panels/lovelace/editor/structs/action-struct.ts @@ -55,6 +55,12 @@ const actionConfigStructNavigate = object({ confirmation: optional(actionConfigStructConfirmation), }); +const actionConfigStructOpenDialog = object({ + action: literal("open-dialog"), + dashboard_path: optional(string()), + view_path: optional(string()), +}); + const actionConfigStructAssist = type({ action: literal("assist"), pipeline_id: optional(string()), @@ -101,6 +107,9 @@ export const actionConfigStruct = dynamic((value) => { case "more-info": { return actionConfigStructMoreInfo; } + case "open-dialog": { + return actionConfigStructOpenDialog; + } } } diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index c3810265d9..dc1b397957 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -61,6 +61,11 @@ export interface LovelaceGridOptions { max_rows?: number; } +export interface LovelaceDialogSize { + width?: number | "full" | "auto"; + height?: number | "full" | "auto"; +} + export interface LovelaceCard extends HTMLElement { hass?: HomeAssistant; preview?: boolean; diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts index dc90245e1d..c5785ef5e0 100644 --- a/src/panels/lovelace/views/hui-masonry-view.ts +++ b/src/panels/lovelace/views/hui-masonry-view.ts @@ -13,7 +13,7 @@ import type { HuiBadge } from "../badges/hui-badge"; import "../badges/hui-view-badges"; import type { HuiCard } from "../cards/hui-card"; import { computeCardSize } from "../common/compute-card-size"; -import type { Lovelace } from "../types"; +import type { Lovelace, LovelaceDialogSize } from "../types"; // Find column with < 5 size, else smallest column const getColumnIndex = (columnSizes: number[], size: number) => { @@ -113,6 +113,14 @@ export class MasonryView extends LitElement implements LovelaceViewElement { }); } + public getDialogSize(): LovelaceDialogSize { + this._createColumns(); + + return { + width: "auto", + }; + } + private get mqls(): MediaQueryList[] { if (!this._mqls) { this._initMqls(); diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts index 17b091a3e3..0e32b3ea36 100644 --- a/src/panels/lovelace/views/hui-panel-view.ts +++ b/src/panels/lovelace/views/hui-panel-view.ts @@ -11,7 +11,7 @@ import type { HomeAssistant } from "../../../types"; import type { HuiCard } from "../cards/hui-card"; import type { HuiCardOptions } from "../components/hui-card-options"; import type { HuiWarning } from "../components/hui-warning"; -import type { Lovelace } from "../types"; +import type { Lovelace, LovelaceDialogSize } from "../types"; let editCodeLoaded = false; @@ -28,6 +28,13 @@ export class PanelView extends LitElement implements LovelaceViewElement { @state() private _card?: HuiCard | HuiWarning | HuiCardOptions; + public getDialogSize(): LovelaceDialogSize { + return { + height: "full", + width: "full", + }; + } + // eslint-disable-next-line @typescript-eslint/no-empty-function public setConfig(_config: LovelaceViewConfig): void {} diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index 58d45edba8..5de61775b6 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -43,7 +43,7 @@ import { } from "../editor/lovelace-path"; import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog"; import type { HuiSection } from "../sections/hui-section"; -import type { Lovelace } from "../types"; +import type { Lovelace, LovelaceDialogSize } from "../types"; export const DEFAULT_MAX_COLUMNS = 4; @@ -101,6 +101,21 @@ export class SectionsView extends LitElement implements LovelaceViewElement { this._config = config; } + public getDialogSize(): LovelaceDialogSize { + if (!this._config?.sections) { + return { + width: 400, + }; + } + const size = this._config.sections + .map((config) => config.column_span ?? 1) + .reduce((acc, val) => acc + val, 0); + + return { + width: Math.min(size, 3) * 400, + }; + } + private _sectionConfigKeys = new WeakMap(); private _getSectionKey(section: HuiSection) { diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 12d10e3214..354c1b767c 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -3,7 +3,7 @@ import type { PropertyValues } from "lit"; import { ReactiveElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { storage } from "../../../common/decorators/storage"; -import type { HASSDomEvent } from "../../../common/dom/fire_event"; +import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-state-label-badge"; import "../../../components/ha-svg-icon"; import type { LovelaceViewElement } from "../../../data/lovelace"; @@ -38,12 +38,13 @@ import { createErrorSectionConfig } from "../sections/hui-error-section"; import "../sections/hui-section"; import type { HuiSection } from "../sections/hui-section"; import { generateLovelaceViewStrategy } from "../strategies/get-strategy"; -import type { Lovelace } from "../types"; +import type { Lovelace, LovelaceDialogSize } from "../types"; import { getViewType } from "./get-view-type"; declare global { // for fire event interface HASSDomEvents { + "view-updated": undefined; "ll-create-card": { suggested?: string[] } | undefined; "ll-edit-card": { path: LovelaceCardPath }; "ll-delete-card": DeleteCardParams; @@ -54,6 +55,7 @@ declare global { "ll-delete-badge": DeleteBadgeParams; } interface HTMLElementEventMap { + "view-updated": HASSDomEvent; "ll-create-card": HASSDomEvent; "ll-edit-card": HASSDomEvent; "ll-delete-card": HASSDomEvent; @@ -93,6 +95,16 @@ export class HUIView extends ReactiveElement { }) protected _clipboard?: LovelaceCardConfig; + public getDialogSize(): LovelaceDialogSize | undefined { + if (!this._layoutElement) { + return undefined; + } + if (this._layoutElement.getDialogSize) { + return this._layoutElement.getDialogSize(); + } + return undefined; + } + private _createCardElement(cardConfig: LovelaceCardConfig) { const element = document.createElement("hui-card"); element.hass = this.hass; @@ -271,6 +283,14 @@ export class HUIView extends ReactiveElement { private _createLayoutElement(config: LovelaceViewConfig): void { this._layoutElement = createViewElement(config) as LovelaceViewElement; + this._layoutElement.addEventListener( + "ll-upgrade", + (ev: Event) => { + ev.stopPropagation(); + fireEvent(this, "view-updated"); + }, + { once: true } + ); this._layoutElementType = config.type; this._layoutElement.addEventListener("ll-create-card", (ev) => { showCreateCardDialog(this, { diff --git a/src/panels/lovelace/views/view-popup/hui-dialog-view-popup.ts b/src/panels/lovelace/views/view-popup/hui-dialog-view-popup.ts new file mode 100644 index 0000000000..25b755a6b9 --- /dev/null +++ b/src/panels/lovelace/views/view-popup/hui-dialog-view-popup.ts @@ -0,0 +1,206 @@ +import { mdiClose } from "@mdi/js"; +import type { PropertyValues } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-icon-button"; +import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; +import { + fetchConfig, + isStrategyDashboard, +} from "../../../../data/lovelace/config/types"; +import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { generateLovelaceDashboardStrategy } from "../../strategies/get-strategy"; +import type { Lovelace, LovelaceDialogSize } from "../../types"; +import "../hui-view"; +import type { HUIView } from "../hui-view"; +import type { ViewPopupDialogParams } from "./show-view-popup-dialog"; + +@customElement("hui-dialog-view-popup") +export class DialogViewPopup + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: ViewPopupDialogParams; + + @state() private _viewIndex?: number; + + @state() private _view?: HUIView; + + @state() private _viewSize?: LovelaceDialogSize; + + @state() private _viewConfig?: LovelaceViewConfig; + + protected updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if (changedProperties.has("hass") && this._view) { + this._view.hass = this.hass; + } + } + + public showDialog(params: ViewPopupDialogParams): void { + this._params = params; + this.fetchConfig(); + } + + public closeDialog() { + this._params = undefined; + this._view = undefined; + this._viewConfig = undefined; + this._viewSize = undefined; + this._viewIndex = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + return true; + } + + public async fetchConfig() { + if (!this._params) { + return; + } + + const dashboardPath = this._params.dashboard_path ?? null; + + const rawConfig = await fetchConfig( + this.hass.connection, + this._params?.dashboard_path ?? null, + false + ); + + let config: LovelaceConfig; + + if (isStrategyDashboard(rawConfig)) { + config = await generateLovelaceDashboardStrategy(rawConfig, this.hass); + } else { + config = rawConfig; + } + + const lovelace: Lovelace = { + config: config, + rawConfig: rawConfig, + editMode: false, + urlPath: dashboardPath, + enableFullEditMode: () => undefined, + mode: "storage", + locale: this.hass.locale, + saveConfig: async () => undefined, + deleteConfig: async () => undefined, + setEditMode: () => undefined, + showToast: () => undefined, + }; + + this._viewIndex = config.views.findIndex( + (v) => v.path === this._params?.view_path + ); + + const view = document.createElement("hui-view"); + view.lovelace = lovelace; + view.hass = this.hass; + view.index = this._viewIndex; + this._view = view; + this._view.addEventListener( + "view-updated", + (ev) => { + ev.stopPropagation(); + this._viewSize = this._view?.getDialogSize(); + }, + { once: true } + ); + this._viewConfig = config.views[this._viewIndex!]; + await this.updateComplete; + + this._viewSize = this._view.getDialogSize(); + } + + protected render() { + if (!this._params || !this._view) { + return nothing; + } + + const width = this._viewSize?.width ?? "auto"; + const height = this._viewSize?.height ?? "auto"; + const dialogMinWidth = + width === "full" + ? "100vw" + : typeof width === "number" + ? `${width}px` + : undefined; + const dialogMinHeight = + height === "full" + ? "100vh" + : typeof height === "number" + ? `${height}px` + : undefined; + + return html` + + + + ${this._viewConfig?.title} + +
${this._view}
+
+ `; + } + + static get styles() { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + --mdc-dialog-max-width: 90vw; + --mdc-dialog-max-height: 90vw; + --mdc-dialog-min-width: min(var(--dialog-width, none), 90vw); + --mdc-dialog-min-height: min(var(--dialog-height, none), 90vh); + } + + .content { + display: block; + flex: 1 1 0; + width: auto; + } + + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-dialog { + --mdc-dialog-min-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + --mdc-dialog-max-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + --mdc-dialog-min-height: 100%; + --mdc-dialog-max-height: 100%; + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-view-popup": DialogViewPopup; + } +} diff --git a/src/panels/lovelace/views/view-popup/show-view-popup-dialog.ts b/src/panels/lovelace/views/view-popup/show-view-popup-dialog.ts new file mode 100644 index 0000000000..99b8b88967 --- /dev/null +++ b/src/panels/lovelace/views/view-popup/show-view-popup-dialog.ts @@ -0,0 +1,17 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export interface ViewPopupDialogParams { + dashboard_path?: string; + view_path: string; +} + +export const showViewPopupDialog = ( + element: HTMLElement, + dialogParams: ViewPopupDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-view-popup", + dialogImport: () => import("./hui-dialog-view-popup"), + dialogParams, + }); +};