Add basic dialog

This commit is contained in:
Paul Bottein 2025-04-04 17:25:29 +02:00
parent e271989cee
commit 868f24eb9f
No known key found for this signature in database
13 changed files with 348 additions and 6 deletions

View File

@ -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;

View File

@ -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 {

View File

@ -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

View File

@ -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);

View File

@ -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 {
></ha-textfield>
`
: nothing}
${this.config?.action === "url"
? html`
<ha-textfield
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.action-editor.url_path"
)}
.value=${this._dashboard_path}
.configValue=${"dashboard_path"}
@input=${this._valueChanged}
></ha-textfield>
<ha-textfield
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.action-editor.url_path"
)}
.value=${this._view_path}
.configValue=${"view_path"}
@input=${this._valueChanged}
></ha-textfield>
`
: nothing}
${this.config?.action === "call-service" ||
this.config?.action === "perform-action"
? html`

View File

@ -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<any>((value) => {
case "more-info": {
return actionConfigStructMoreInfo;
}
case "open-dialog": {
return actionConfigStructOpenDialog;
}
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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 {}

View File

@ -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<HuiSection, string>();
private _getSectionKey(section: HuiSection) {

View File

@ -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<HASSDomEvents["view-updated"]>;
"ll-create-card": HASSDomEvent<HASSDomEvents["ll-create-card"]>;
"ll-edit-card": HASSDomEvent<HASSDomEvents["ll-edit-card"]>;
"ll-delete-card": HASSDomEvent<HASSDomEvents["ll-delete-card"]>;
@ -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, {

View File

@ -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<ViewPopupDialogParams>
{
@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`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this._viewConfig?.title ?? " "}
hideActions
flexContent
style=${styleMap({
"--dialog-width": dialogMinWidth,
"--dialog-height": dialogMinHeight,
})}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">${this._viewConfig?.title}</span>
</ha-dialog-header>
<div class="content">${this._view}</div>
</ha-dialog>
`;
}
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;
}
}

View File

@ -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,
});
};