Add overview dialog

This commit is contained in:
Wendelin
2025-09-18 11:08:52 +02:00
parent 89ef753309
commit c68d73b1ef
4 changed files with 331 additions and 142 deletions

View File

@@ -0,0 +1,118 @@
import { ContextProvider } from "@lit/context";
import { mdiClose } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { labelsContext } from "../../../data/context";
import { subscribeLabelRegistry } from "../../../data/label_registry";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-dialog";
import type { HaMdDialog } from "../../ha-md-dialog";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../ha-target-picker-item-row";
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
@customElement("ha-dialog-target-details")
class DialogTargetDetails
extends SubscribeMixin(LitElement)
implements HassDialog
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: TargetDetailsDialogParams;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
public showDialog(params: TargetDetailsDialogParams): void {
this._params = params;
}
public closeDialog() {
this._dialog?.close();
return true;
}
private _dialogClosed() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._params = undefined;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
];
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">${this._params.title}</span>
<span slot="subtitle"
>${this.hass.localize("ui.components.target-picker.overview")}</span
>
</ha-dialog-header>
<div slot="content">
<ha-target-picker-item-row
.hass=${this.hass}
.type=${this._params.type}
.itemId=${this._params.itemId}
.deviceFilter=${this._params.deviceFilter}
.entityFilter=${this._params.entityFilter}
.includeDomains=${this._params.includeDomains}
.includeDeviceClasses=${this._params.includeDeviceClasses}
expand
></ha-target-picker-item-row>
</div>
</ha-md-dialog>
`;
}
static styles = css`
ha-md-dialog {
min-width: 400px;
max-height: 90%;
--dialog-content-padding: 8px 24px
max(var(--safe-area-inset-bottom, 0px), 32px);
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
min-width: 100%;
min-height: 100%;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-target-details": DialogTargetDetails;
}
}

View File

@@ -0,0 +1,28 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../entity/ha-entity-picker";
import type { TargetType } from "../ha-target-picker-item-row";
export type NewBackupType = "automatic" | "manual";
export interface TargetDetailsDialogParams {
title: string;
type: TargetType;
itemId: string;
deviceFilter?: HaDevicePickerDeviceFilterFunc;
entityFilter?: HaEntityPickerEntityFilterFunc;
includeDomains?: string[];
includeDeviceClasses?: string[];
}
export const loadTargetDetailsDialog = () => import("./dialog-target-details");
export const showTargetDetailsDialog = (
element: HTMLElement,
params: TargetDetailsDialogParams
) =>
fireEvent(element, "show-dialog", {
dialogTag: "ha-dialog-target-details",
dialogImport: loadTargetDetailsDialog,
dialogParams: params,
});

View File

@@ -1,6 +1,5 @@
import { consume } from "@lit/context";
import {
mdiChevronDown,
mdiClose,
mdiDevices,
mdiHome,
@@ -31,6 +30,7 @@ import {
extractFromTarget,
type ExtractFromTargetResult,
} from "../../data/target";
import { buttonLinkStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
@@ -43,6 +43,7 @@ import "../ha-md-list-item";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-state-icon";
import "../ha-svg-icon";
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
@@ -54,6 +55,10 @@ export class HaTargetPickerItemRow extends LitElement {
@property({ attribute: "item-id" }) public itemId!: string;
@property({ type: Boolean }) public expand = false;
@property({ type: Boolean, attribute: "last" }) public lastItem = false;
@property({ type: Boolean, attribute: "sub-entry", reflect: true })
public subEntry = false;
@@ -85,8 +90,6 @@ export class HaTargetPickerItemRow extends LitElement {
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@state() private _expanded = false;
@state() private _iconImg?: string;
@state() private _domainName?: string;
@@ -106,7 +109,6 @@ export class HaTargetPickerItemRow extends LitElement {
protected willUpdate(changedProps: PropertyValues) {
if (!this.subEntry && changedProps.has("itemId")) {
this._updateItemData();
this._expanded = false;
}
}
@@ -129,63 +131,68 @@ export class HaTargetPickerItemRow extends LitElement {
}
return html`
<ha-md-list-item
.type=${this.type === "entity" ? "text" : "button"}
@click=${this._toggleExpand}
>
${this.type !== "entity"
? html`<ha-svg-icon
class="expand-button ${entries?.referenced_entities.length &&
this._expanded
? "expanded"
: ""}"
.path=${mdiChevronDown}
slot="start"
></ha-svg-icon>`
: nothing}
${iconPath
? html`<ha-icon slot="start" .icon=${iconPath}></ha-icon>`
: this._iconImg
? html`<img
slot="start"
alt=${this._domainName || ""}
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${this._iconImg}
/>`
: fallbackIconPath
? html`<ha-svg-icon
slot="start"
.path=${fallbackIconPath}
></ha-svg-icon>`
: stateObject
? html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObject}
slot="start"
>
</ha-state-icon>
`
: nothing}
<ha-md-list-item type="text">
<div slot="start">
${this.subEntry
? html`
<div class="horizontal-line-wrapper">
<div class="horizontal-line"></div>
</div>
`
: nothing}
${iconPath
? html`<ha-icon .icon=${iconPath}></ha-icon>`
: this._iconImg
? html`<img
alt=${this._domainName || ""}
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${this._iconImg}
/>`
: fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: stateObject
? html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObject}
>
</ha-state-icon>
`
: nothing}
</div>
<div slot="headline">${name}</div>
${context && !this.hideContext
? html`<span slot="supporting-text">${context}</span>`
: nothing}
: this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain"
>${this._domainName}</span
>`
: nothing}
${!this.subEntry &&
((entries && (showEntities || showDevices)) || this._domainName)
? html`
<div slot="end" class="summary">
${showEntities
? html`<span class="main"
>${this.hass.localize(
${showEntities && !this.expand
? html`<button class="main link" @click=${this._openDetails}>
${this.hass.localize(
"ui.components.target-picker.entities_count",
{
count: entries?.referenced_entities.length,
}
)}</span
>`
: nothing}
)}
</button>`
: showEntities
? html`<span class="main">
${this.hass.localize(
"ui.components.target-picker.entities_count",
{
count: entries?.referenced_entities.length,
}
)}
</span>`
: nothing}
${showDevices
? html`<span class="secondary"
>${this.hass.localize(
@@ -204,7 +211,7 @@ export class HaTargetPickerItemRow extends LitElement {
</div>
`
: nothing}
${!this.subEntry
${!this.expand && !this.subEntry
? html`
<ha-icon-button
.path=${mdiClose}
@@ -214,7 +221,7 @@ export class HaTargetPickerItemRow extends LitElement {
`
: nothing}
</ha-md-list-item>
${this._expanded && entries && entries.referenced_entities
${this.expand && entries && entries.referenced_entities
? this._renderEntries()
: nothing}
`;
@@ -305,31 +312,39 @@ export class HaTargetPickerItemRow extends LitElement {
: [];
return html`
<ha-md-list class="entries">
${rows1.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
.type=${nextType}
.itemId=${itemId}
.parentEntries=${rows1Entries?.[index]}
.hideContext=${this.hideContext || this.type !== "label"}
></ha-target-picker-item-row>
`
)}
${rows2.map(
(itemId) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
type="entity"
.itemId=${itemId}
.hideContext=${this.hideContext || this.type !== "label"}
></ha-target-picker-item-row>
`
)}
</ha-md-list>
<div class="entries-tree">
<div class="line-wrapper">
<div class="line"></div>
</div>
<ha-md-list class="entries">
${rows1.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
.type=${nextType}
.itemId=${itemId}
.parentEntries=${rows1Entries?.[index]}
.hideContext=${this.hideContext || this.type !== "label"}
expand
.lastItem=${rows2.length === 0 && index === rows1.length - 1}
></ha-target-picker-item-row>
`
)}
${rows2.map(
(itemId, index) => html`
<ha-target-picker-item-row
sub-entry
.hass=${this.hass}
type="entity"
.itemId=${itemId}
.hideContext=${this.hideContext || this.type !== "label"}
.lastItem=${index === rows2.length - 1}
></ha-target-picker-item-row>
`
)}
</ha-md-list>
</div>
`;
}
@@ -528,76 +543,103 @@ export class HaTargetPickerItemRow extends LitElement {
}
}
private _toggleExpand() {
const entries = this.parentEntries || this._entries;
if (
this.type === "entity" ||
!entries ||
entries.referenced_entities.length === 0
) {
return;
}
this._expanded = !this._expanded;
private _openDetails() {
showTargetDetailsDialog(this, {
title: this._itemData(this.type, this.itemId).name,
type: this.type,
itemId: this.itemId,
deviceFilter: this.deviceFilter,
entityFilter: this.entityFilter,
includeDomains: this.includeDomains,
includeDeviceClasses: this.includeDeviceClasses,
});
}
static styles = css`
:host {
--md-list-item-top-space: 0;
--md-list-item-bottom-space: 0;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--md-list-item-two-line-container-height: 56px;
}
static styles = [
buttonLinkStyle,
css`
:host {
--md-list-item-top-space: 0;
--md-list-item-bottom-space: 0;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--md-list-item-two-line-container-height: 56px;
}
state-badge {
color: var(--ha-color-on-neutral-quiet);
}
img {
width: 24px;
height: 24px;
}
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
.expand-button.expanded {
transform: rotate(180deg);
}
ha-icon-button {
--mdc-icon-button-size: 32px;
}
.summary {
display: flex;
flex-direction: column;
align-items: flex-end;
line-height: var(--ha-line-height-condensed);
}
:host([sub-entry]) .summary {
margin-right: 48px;
}
.summary .main {
font-weight: var(--ha-font-weight-medium);
}
.summary .secondary {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
.summary .secondary.domain {
font-family: var(--ha-font-family-code);
}
:host([expand]:not([sub-entry])) ha-md-list-item {
border: 2px solid var(--ha-color-border-neutral-loud);
background-color: var(--ha-color-fill-neutral-quiet-resting);
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
}
.entries {
padding: 0;
padding-left: 40px;
overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
state-badge {
color: var(--ha-color-on-neutral-quiet);
}
img {
width: 24px;
height: 24px;
}
ha-icon-button {
--mdc-icon-button-size: 32px;
}
.summary {
display: flex;
flex-direction: column;
align-items: flex-end;
line-height: var(--ha-line-height-condensed);
}
:host([sub-entry]) .summary {
margin-right: 48px;
}
.summary .main {
font-weight: var(--ha-font-weight-medium);
}
.summary .secondary {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
.domain {
font-family: var(--ha-font-family-code);
}
:host([sub-entry]) .entries {
border-bottom: none;
}
`;
.entries-tree {
display: flex;
position: relative;
}
.entries-tree .line-wrapper {
padding: 20px;
}
.entries-tree .line-wrapper .line {
border-left: 2px dashed var(--divider-color);
height: 100%;
position: absolute;
top: 0;
}
:host([sub-entry]) .entries-tree .line-wrapper .line {
height: calc(100% + 18px);
top: -18px;
}
.entries {
padding: 0;
--md-item-overflow: visible;
}
.horizontal-line-wrapper {
position: relative;
}
.horizontal-line-wrapper .horizontal-line {
position: absolute;
top: 11px;
margin-inline-start: -28px;
width: 29px;
border-top: 2px dashed var(--divider-color);
}
`,
];
}
declare global {

View File

@@ -683,6 +683,7 @@
"add_label_id": "Choose label",
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"overview": "Overview",
"selected": {
"entity": "Entities: {count}",
"device": "Devices: {count}",