mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-01 14:49:47 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc5d9f48bb | ||
|
|
3d3cdabfe7 | ||
|
|
4b1f12c0ef |
+267
-313
@@ -1,16 +1,17 @@
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiDevices,
|
||||
mdiPaletteSwatch,
|
||||
mdiTextureBox,
|
||||
mdiTransitConnectionVariant,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { Blueprints } from "../data/blueprint";
|
||||
import { fetchBlueprints } from "../data/blueprint";
|
||||
@@ -18,14 +19,15 @@ import type { ConfigEntry } from "../data/config_entries";
|
||||
import { getConfigEntries } from "../data/config_entries";
|
||||
import type { ItemType, RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-list-item";
|
||||
import "./ha-state-icon";
|
||||
import "./ha-switch";
|
||||
import "./ha-list";
|
||||
import "./ha-svg-icon";
|
||||
import "./item/ha-list-item-button";
|
||||
import type { HaListItemButton } from "./item/ha-list-item-button";
|
||||
import "./list/ha-grouped-list";
|
||||
|
||||
@customElement("ha-related-items")
|
||||
export class HaRelatedItems extends LitElement {
|
||||
@@ -35,16 +37,16 @@ export class HaRelatedItems extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public itemId!: string;
|
||||
|
||||
@property({ attribute: false }) public exclude?: (keyof RelatedResult)[];
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public empty = true;
|
||||
|
||||
@state() private _entries?: ConfigEntry[];
|
||||
|
||||
@state() private _blueprints?: Record<"automation" | "script", Blueprints>;
|
||||
|
||||
@state() private _related?: RelatedResult;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
}
|
||||
|
||||
private async _fetchConfigEntries() {
|
||||
if (this._entries) {
|
||||
return;
|
||||
@@ -64,6 +66,19 @@ export class HaRelatedItems extends LitElement {
|
||||
this._blueprints = { automation, script };
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("_related") || changedProps.has("exclude")) {
|
||||
this.empty =
|
||||
!this._related ||
|
||||
!Object.entries(this._related).some(
|
||||
([section, items]) =>
|
||||
items?.length &&
|
||||
!this.exclude?.includes(section as keyof RelatedResult)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
@@ -127,25 +142,14 @@ export class HaRelatedItems extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _isExcluded(section: keyof RelatedResult) {
|
||||
return this.exclude?.includes(section) ?? false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._related) {
|
||||
return nothing;
|
||||
}
|
||||
if (Object.keys(this._related).length === 0) {
|
||||
return html`
|
||||
<ha-list>
|
||||
<ha-list-item hasMeta graphic="icon" noninteractive>
|
||||
<ha-svg-icon
|
||||
.path=${mdiAlertCircleOutline}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.related-items.no_related_found"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`;
|
||||
}
|
||||
|
||||
const { configEntries, configEntryDomains } = this._getConfigEntries(
|
||||
this._related.config_entry,
|
||||
@@ -154,97 +158,77 @@ export class HaRelatedItems extends LitElement {
|
||||
|
||||
return html`
|
||||
${
|
||||
this._related.entity
|
||||
this._related.entity && !this._isExcluded("entity")
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.entity")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._relatedEntities(this._related.entity).map(
|
||||
(entity) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entityId=${entity.entity_id}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.stateObj=${entity}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
${entity.attributes.friendly_name || entity.entity_id}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.related-items.entity"
|
||||
)}
|
||||
</ha-list>
|
||||
>
|
||||
${this._relatedEntities(this._related.entity).map((entity) =>
|
||||
this._renderEntityRow(entity)
|
||||
)}
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.device
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.components.related-items.device")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
this._related.device && !this._isExcluded("device")
|
||||
? html`
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.related-items.device"
|
||||
)}
|
||||
>
|
||||
${this._related.device.map((relatedDeviceId) => {
|
||||
const device = this.hass.devices[relatedDeviceId];
|
||||
if (!device) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<a href="/config/devices/device/${relatedDeviceId}">
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${
|
||||
device.entry_type === "service"
|
||||
? mdiTransitConnectionVariant
|
||||
: mdiDevices
|
||||
}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${device.name_by_user || device.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<ha-list-item-button
|
||||
.href=${`/config/devices/device/${relatedDeviceId}`}
|
||||
.headline=${device.name_by_user || device.name || ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${
|
||||
device.entry_type === "service"
|
||||
? mdiTransitConnectionVariant
|
||||
: mdiDevices
|
||||
}
|
||||
></ha-svg-icon>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
})}
|
||||
</ha-list>`
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
configEntries || this._related.integration
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.components.related-items.integration")}
|
||||
</h3>
|
||||
<ha-list
|
||||
>${configEntries?.map((entry) => {
|
||||
(configEntries || this._related.integration) &&
|
||||
!this._isExcluded("integration")
|
||||
? html`
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.related-items.integration"
|
||||
)}
|
||||
>
|
||||
${configEntries?.map((entry) => {
|
||||
if (!entry) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<a
|
||||
href=${`/config/integrations/integration/${entry.domain}#config_entry=${entry.entry_id}`}
|
||||
<ha-list-item-button
|
||||
.href=${`/config/integrations/integration/${entry.domain}#config_entry=${entry.entry_id}`}
|
||||
.headline=${`${this.hass.localize(
|
||||
`component.${entry.domain}.title`
|
||||
)}: ${entry.title}`}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img
|
||||
.src=${brandsUrl(
|
||||
{
|
||||
domain: entry.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${entry.domain}
|
||||
slot="graphic"
|
||||
/>
|
||||
${this.hass.localize(`component.${entry.domain}.title`)}:
|
||||
${entry.title} <ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
${this._renderBrandIcon(entry.domain)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
})}
|
||||
${this._related.integration
|
||||
@@ -252,259 +236,216 @@ export class HaRelatedItems extends LitElement {
|
||||
(integration) => !configEntryDomains.has(integration)
|
||||
)
|
||||
.map(
|
||||
(integration) =>
|
||||
html`<a
|
||||
href=${`/config/integrations/integration/${integration}`}
|
||||
(integration) => html`
|
||||
<ha-list-item-button
|
||||
.href=${`/config/integrations/integration/${integration}`}
|
||||
.headline=${this.hass.localize(
|
||||
`component.${integration}.title`
|
||||
)}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img
|
||||
.src=${brandsUrl(
|
||||
{
|
||||
domain: integration,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${integration}
|
||||
slot="graphic"
|
||||
/>
|
||||
${this.hass.localize(`component.${integration}.title`)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
${this._renderBrandIcon(integration)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>`
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.area
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.components.related-items.area")}
|
||||
</h3>
|
||||
<ha-list
|
||||
>${this._related.area.map((relatedAreaId) => {
|
||||
this._related.area && !this._isExcluded("area")
|
||||
? html`
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize("ui.components.related-items.area")}
|
||||
>
|
||||
${this._related.area.map((relatedAreaId) => {
|
||||
const area = this.hass.areas[relatedAreaId];
|
||||
if (!area) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<a href="/config/areas/area/${relatedAreaId}">
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
.graphic=${area.picture ? "avatar" : "icon"}
|
||||
>
|
||||
${
|
||||
area.picture
|
||||
? html` <div
|
||||
<ha-list-item-button
|
||||
.href=${`/config/areas/area/${relatedAreaId}`}
|
||||
.headline=${area.name}
|
||||
>
|
||||
${
|
||||
area.picture
|
||||
? html`
|
||||
<div
|
||||
class="avatar"
|
||||
style=${styleMap({
|
||||
backgroundImage: `url(${area.picture})`,
|
||||
})}
|
||||
slot="graphic"
|
||||
></div>`
|
||||
: area.icon
|
||||
? html`<ha-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
></div>
|
||||
`
|
||||
: area.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
slot="start"
|
||||
.icon=${area.icon}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="graphic"
|
||||
></ha-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`
|
||||
}
|
||||
${area.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
></ha-svg-icon>
|
||||
`
|
||||
}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
})}
|
||||
</ha-list>`
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.group
|
||||
this._related.group && !this._isExcluded("group")
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.group")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._relatedGroups(this._related.group).map(
|
||||
(group) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entityId=${group.entity_id}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.stateObj=${group}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
${group.attributes.friendly_name || group.entity_id}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize("ui.components.related-items.group")}
|
||||
>
|
||||
${this._relatedGroups(this._related.group).map((group) =>
|
||||
this._renderEntityRow(group)
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.scene
|
||||
this._related.scene && !this._isExcluded("scene")
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.scene")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._relatedScenes(this._related.scene).map(
|
||||
(scene) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entityId=${scene.entity_id}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.stateObj=${scene}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
${scene.attributes.friendly_name || scene.entity_id}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize("ui.components.related-items.scene")}
|
||||
>
|
||||
${this._relatedScenes(this._related.scene).map((scene) =>
|
||||
this._renderEntityRow(scene)
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.automation_blueprint
|
||||
this._related.automation_blueprint &&
|
||||
!this._isExcluded("automation_blueprint")
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.blueprint")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._related.automation_blueprint.map((path) => {
|
||||
const blueprintMeta = this._blueprints
|
||||
? this._blueprints.automation[path]
|
||||
: undefined;
|
||||
return html`<a href="/config/blueprint/dashboard">
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiPaletteSwatch}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${
|
||||
!blueprintMeta || "error" in blueprintMeta
|
||||
? path
|
||||
: blueprintMeta.metadata.name || path
|
||||
}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`;
|
||||
})}
|
||||
</ha-list>
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.related-items.blueprint"
|
||||
)}
|
||||
>
|
||||
${this._related.automation_blueprint.map((path) =>
|
||||
this._renderBlueprintRow(path, "automation")
|
||||
)}
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.automation
|
||||
this._related.automation && !this._isExcluded("automation")
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.automation")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.related-items.automation"
|
||||
)}
|
||||
>
|
||||
${this._relatedAutomations(this._related.automation).map(
|
||||
(automation) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entityId=${automation.entity_id}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.stateObj=${automation}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
${
|
||||
automation.attributes.friendly_name ||
|
||||
automation.entity_id
|
||||
}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
(automation) => this._renderEntityRow(automation)
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.script_blueprint
|
||||
this._related.script_blueprint && !this._isExcluded("script_blueprint")
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.blueprint")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._related.script_blueprint.map((path) => {
|
||||
const blueprintMeta = this._blueprints
|
||||
? this._blueprints.script[path]
|
||||
: undefined;
|
||||
return html`<a href="/config/blueprint/dashboard">
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiPaletteSwatch}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${
|
||||
!blueprintMeta || "error" in blueprintMeta
|
||||
? path
|
||||
: blueprintMeta.metadata.name || path
|
||||
}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`;
|
||||
})}
|
||||
</ha-list>
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.related-items.blueprint"
|
||||
)}
|
||||
>
|
||||
${this._related.script_blueprint.map((path) =>
|
||||
this._renderBlueprintRow(path, "script")
|
||||
)}
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._related.script
|
||||
this._related.script && !this._isExcluded("script")
|
||||
? html`
|
||||
<h3>
|
||||
${this.hass.localize("ui.components.related-items.script")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._relatedScripts(this._related.script).map(
|
||||
(script) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openMoreInfo}
|
||||
.entityId=${script.entity_id}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.stateObj=${script}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
${script.attributes.friendly_name || script.entity_id}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.related-items.script"
|
||||
)}
|
||||
</ha-list>
|
||||
>
|
||||
${this._relatedScripts(this._related.script).map((script) =>
|
||||
this._renderEntityRow(script)
|
||||
)}
|
||||
</ha-grouped-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEntityRow(entity: HassEntity) {
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.headline=${entity.attributes.friendly_name || entity.entity_id}
|
||||
data-entity-id=${entity.entity_id}
|
||||
@click=${this._openMoreInfo}
|
||||
>
|
||||
<ha-state-icon slot="start" .stateObj=${entity}></ha-state-icon>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderBrandIcon(domain: string) {
|
||||
return html`
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderBlueprintRow(path: string, type: "automation" | "script") {
|
||||
const blueprintMeta = this._blueprints
|
||||
? this._blueprints[type][path]
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
href="/config/blueprint/dashboard"
|
||||
.headline=${
|
||||
!blueprintMeta || "error" in blueprintMeta
|
||||
? path
|
||||
: blueprintMeta.metadata.name || path
|
||||
}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPaletteSwatch}></ha-svg-icon>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _findRelated() {
|
||||
this._related = await findRelated(this.hass, this.itemType, this.itemId);
|
||||
if (this._related.config_entry) {
|
||||
if (this._related.config_entry && !this._isExcluded("integration")) {
|
||||
this._fetchConfigEntries();
|
||||
}
|
||||
if (this._related.script_blueprint || this._related.automation_blueprint) {
|
||||
@@ -512,36 +453,49 @@ export class HaRelatedItems extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _openMoreInfo(ev: CustomEvent) {
|
||||
const entityId = (ev.target as any).entityId;
|
||||
private _openMoreInfo(ev: HASSDomCurrentTargetEvent<HaListItemButton>) {
|
||||
const entityId = ev.currentTarget.dataset.entityId;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 24px;
|
||||
}
|
||||
h3 {
|
||||
padding: 0 24px;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.avatar {
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-6);
|
||||
}
|
||||
|
||||
:host([empty]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
--ha-row-item-min-height: 40px;
|
||||
--ha-row-item-gap: var(--ha-space-3);
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
img[slot="start"],
|
||||
.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -15,7 +15,7 @@ import "../../components/ha-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import "../../components/list/ha-grouped-list";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
|
||||
export interface AddToActionListItem {
|
||||
@@ -76,20 +76,19 @@ class HaAddToActionList extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._localizeValue(section.title, section.titleKey)}
|
||||
</h3>
|
||||
${
|
||||
section.actions.length
|
||||
? html`<ha-list-base>
|
||||
${section.actions.map((action, actionIndex) =>
|
||||
<ha-grouped-list
|
||||
.header=${this._localizeValue(section.title, section.titleKey)}
|
||||
>
|
||||
${
|
||||
section.actions.length
|
||||
? section.actions.map((action, actionIndex) =>
|
||||
this._renderActionItem(action, sectionIndex, actionIndex)
|
||||
)}
|
||||
</ha-list-base>`
|
||||
: html`<h4 class="empty">
|
||||
${this._localizeValue(section.empty, section.emptyKey)}
|
||||
</h4>`
|
||||
}
|
||||
)
|
||||
: html`<div class="empty">
|
||||
${this._localizeValue(section.empty, section.emptyKey)}
|
||||
</div>`
|
||||
}
|
||||
</ha-grouped-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -161,28 +160,19 @@ class HaAddToActionList extends LitElement {
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 0 var(--ha-space-6);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
ha-grouped-list + ha-grouped-list {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
|
||||
margin: 0;
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-inline: var(--ha-space-5);
|
||||
}
|
||||
|
||||
ha-icon,
|
||||
ha-svg-icon {
|
||||
display: flex;
|
||||
@@ -194,7 +184,7 @@ class HaAddToActionList extends LitElement {
|
||||
}
|
||||
|
||||
.plus {
|
||||
color: var(--primary-color);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-list-item-button[disabled] .start-icon,
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { mdiCheck, mdiContentCopy, mdiDevices, mdiTextureBox } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiCheck, mdiContentCopy } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||
import { computeFloorName } from "../../common/entity/compute_floor_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import checkValidDate from "../../common/datetime/check_valid_date";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import "../../components/ha-attribute-value";
|
||||
import "../../components/ha-floor-icon";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/ha-label";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
@@ -24,9 +15,7 @@ import "../../components/item/ha-list-item-value";
|
||||
import "../../components/list/ha-grouped-list";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-yaml-editor";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
@@ -35,7 +24,6 @@ import { getFeatures } from "../../common/entity/get_domain_features";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { titleCase } from "../../common/string/title-case";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
interface DetailsViewParams {
|
||||
@@ -45,9 +33,6 @@ interface DetailsViewParams {
|
||||
interface DetailEntry {
|
||||
translationKey: LocalizeKeys;
|
||||
value: string;
|
||||
displayValue?: TemplateResult;
|
||||
href?: string;
|
||||
icon?: TemplateResult;
|
||||
copyable?: boolean;
|
||||
}
|
||||
|
||||
@@ -63,10 +48,6 @@ class HaMoreInfoDetails extends LitElement {
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
|
||||
@state() private _copiedValue?: string;
|
||||
|
||||
private _copyFeedbackTimeout?: number;
|
||||
@@ -80,9 +61,6 @@ class HaMoreInfoDetails extends LitElement {
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("entry") && this.entry) {
|
||||
this.hass.loadBackendTranslation("title", [this.entry.platform]);
|
||||
}
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
@@ -103,123 +81,17 @@ class HaMoreInfoDetails extends LitElement {
|
||||
this._stateObj,
|
||||
this.hass.formatEntityAttributeName
|
||||
);
|
||||
const { floor, area, device } = getEntityContext(
|
||||
this._stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = area ? (computeAreaName(area) ?? area.area_id) : undefined;
|
||||
const deviceName = device
|
||||
? computeDeviceNameDisplay(device, this.hass.localize, this.hass.states)
|
||||
: undefined;
|
||||
const integrationName = this.entry?.platform
|
||||
? this.hass.localize(`component.${this.entry.platform}.title`) ||
|
||||
this.entry.platform
|
||||
: undefined;
|
||||
const labels =
|
||||
this.entry?.labels.map((labelId) => ({
|
||||
id: labelId,
|
||||
entry: this._labels?.find((label) => label.label_id === labelId),
|
||||
})) ?? [];
|
||||
const labelNames = labels.map(({ id, entry }) => entry?.name ?? id);
|
||||
const contextEntries: DetailEntry[] = [];
|
||||
|
||||
if (floor && floorName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.dialogs.more_info_control.floor",
|
||||
value: floorName,
|
||||
href: "/config/areas/dashboard",
|
||||
icon: html`<ha-floor-icon slot="end" .floor=${floor}></ha-floor-icon>`,
|
||||
});
|
||||
}
|
||||
if (area && areaName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.components.related-items.area",
|
||||
value: areaName,
|
||||
href: `/config/areas/area/${area.area_id}`,
|
||||
icon: area.icon
|
||||
? html`<ha-icon slot="end" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="end" .path=${mdiTextureBox}></ha-svg-icon>`,
|
||||
});
|
||||
}
|
||||
if (device && deviceName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.components.related-items.device",
|
||||
value: deviceName,
|
||||
href: `/config/devices/device/${device.id}`,
|
||||
icon: html`<ha-svg-icon slot="end" .path=${mdiDevices}></ha-svg-icon>`,
|
||||
});
|
||||
}
|
||||
if (this.entry?.platform && integrationName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.components.related-items.integration",
|
||||
value: integrationName,
|
||||
href: this.entry.config_entry_id
|
||||
? `/config/integrations/integration/${this.entry.platform}#config_entry=${this.entry.config_entry_id}`
|
||||
: undefined,
|
||||
icon: html`<img
|
||||
slot="end"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: this.entry.platform,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>`,
|
||||
});
|
||||
}
|
||||
|
||||
contextEntries.push(
|
||||
const entityEntries: DetailEntry[] = [
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.entity_id",
|
||||
value: this.params.entityId,
|
||||
copyable: true,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.labels",
|
||||
value: labelNames.join(", ") || this.hass.localize("ui.common.none"),
|
||||
displayValue: labels.length
|
||||
? html`<div class="labels">
|
||||
${labels.map(
|
||||
({ id, entry }) => html`
|
||||
<ha-label
|
||||
class="text-ellipsis"
|
||||
.color=${entry?.color ?? undefined}
|
||||
.description=${entry?.description ?? undefined}
|
||||
>
|
||||
${
|
||||
entry?.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${entry.icon}
|
||||
></ha-icon>`
|
||||
: nothing
|
||||
}
|
||||
${entry?.name ?? id}
|
||||
</ha-label>
|
||||
`
|
||||
)}
|
||||
</div>`
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
];
|
||||
|
||||
const yamlData = {
|
||||
context: {
|
||||
...(floorName ? { floor: floorName } : {}),
|
||||
...(areaName ? { area: areaName } : {}),
|
||||
...(deviceName ? { device: deviceName } : {}),
|
||||
...(integrationName ? { integration: integrationName } : {}),
|
||||
entity_id: this.params.entityId,
|
||||
labels: labelNames,
|
||||
},
|
||||
entity_id: this.params.entityId,
|
||||
...stateYamlData,
|
||||
};
|
||||
|
||||
@@ -234,15 +106,12 @@ class HaMoreInfoDetails extends LitElement {
|
||||
in-dialog
|
||||
></ha-yaml-editor>`
|
||||
: html`
|
||||
<ha-grouped-list>
|
||||
${this._renderEntries(contextEntries)}
|
||||
</ha-grouped-list>
|
||||
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
"ui.dialogs.more_info_control.entity"
|
||||
)}
|
||||
>
|
||||
${this._renderEntries(entityEntries)}
|
||||
${this._renderEntries(stateEntries)}
|
||||
</ha-grouped-list>
|
||||
|
||||
@@ -269,17 +138,12 @@ class HaMoreInfoDetails extends LitElement {
|
||||
stateEntries: DetailEntry[];
|
||||
attributes: { name: string; label: string }[];
|
||||
yamlData: {
|
||||
state: {
|
||||
translated: string;
|
||||
raw: string;
|
||||
last_changed: string;
|
||||
last_updated: string;
|
||||
};
|
||||
state: string;
|
||||
last_changed: string;
|
||||
last_updated: string;
|
||||
attributes: Record<string, string>;
|
||||
};
|
||||
} => {
|
||||
const translatedState = this.hass.formatEntityState(stateObj);
|
||||
|
||||
const attributes = Object.keys(stateObj.attributes)
|
||||
.map((a) => ({
|
||||
name: a,
|
||||
@@ -292,12 +156,8 @@ class HaMoreInfoDetails extends LitElement {
|
||||
return {
|
||||
stateEntries: [
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.translated",
|
||||
value: translatedState,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.raw",
|
||||
value: stateObj.state,
|
||||
translationKey: "ui.dialogs.more_info_control.state",
|
||||
value: this.hass.formatEntityState(stateObj),
|
||||
},
|
||||
{
|
||||
translationKey: "ui.dialogs.more_info_control.last_changed",
|
||||
@@ -310,12 +170,9 @@ class HaMoreInfoDetails extends LitElement {
|
||||
],
|
||||
attributes,
|
||||
yamlData: {
|
||||
state: {
|
||||
translated: translatedState,
|
||||
raw: stateObj.state,
|
||||
last_changed: stateObj.last_changed,
|
||||
last_updated: stateObj.last_updated,
|
||||
},
|
||||
state: stateObj.state,
|
||||
last_changed: stateObj.last_changed,
|
||||
last_updated: stateObj.last_updated,
|
||||
attributes: stateObj.attributes,
|
||||
},
|
||||
};
|
||||
@@ -334,47 +191,34 @@ class HaMoreInfoDetails extends LitElement {
|
||||
return entries.map((entry) => {
|
||||
const label = this.hass.localize(entry.translationKey);
|
||||
|
||||
if (!entry.href && !entry.copyable) {
|
||||
if (!entry.copyable) {
|
||||
return html`
|
||||
<ha-list-item-value .label=${label}
|
||||
>${entry.displayValue ?? entry.value}</ha-list-item-value
|
||||
>${entry.value}</ha-list-item-value
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
if (entry.copyable) {
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.copy_value",
|
||||
{ label, value: entry.value }
|
||||
)}
|
||||
data-value=${entry.value}
|
||||
@click=${this._copyValue}
|
||||
>
|
||||
<div class="link-row" slot="content">
|
||||
<div class="label">${label}</div>
|
||||
<div class="value">${entry.value}</div>
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
class=${this._copiedValue === entry.value ? "copy-success" : ""}
|
||||
slot="end"
|
||||
.path=${
|
||||
this._copiedValue === entry.value ? mdiCheck : mdiContentCopy
|
||||
}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list-item-button .href=${entry.href}>
|
||||
<ha-list-item-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.copy_value",
|
||||
{ label, value: entry.value }
|
||||
)}
|
||||
data-value=${entry.value}
|
||||
@click=${this._copyValue}
|
||||
>
|
||||
<div class="link-row" slot="content">
|
||||
<div class="label">${label}</div>
|
||||
<div class="value">${entry.value}</div>
|
||||
</div>
|
||||
${entry.icon ?? nothing}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
<ha-svg-icon
|
||||
class=${this._copiedValue === entry.value ? "copy-success" : ""}
|
||||
slot="end"
|
||||
.path=${
|
||||
this._copiedValue === entry.value ? mdiCheck : mdiContentCopy
|
||||
}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
});
|
||||
@@ -439,7 +283,7 @@ class HaMoreInfoDetails extends LitElement {
|
||||
.filter(([_key, value]) => typeof value === "number")
|
||||
.map(([key, value]) =>
|
||||
supportsFeature(stateObj, value as number)
|
||||
? titleCase(key.replaceAll("_", "\u00A0").toLowerCase())
|
||||
? titleCase(key.replaceAll("_", " ").toLowerCase())
|
||||
: undefined
|
||||
)
|
||||
.filter(Boolean)
|
||||
@@ -470,16 +314,6 @@ class HaMoreInfoDetails extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
ha-list-item-button::part(end) {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-list-item-button img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.link-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -499,22 +333,6 @@ class HaMoreInfoDetails extends LitElement {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.labels {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.labels ha-label {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-svg-icon.copy-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
mdiContentDuplicate,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
mdiFormatListBulletedSquare,
|
||||
mdiInformationOutline,
|
||||
mdiLinkVariant,
|
||||
mdiPencil,
|
||||
mdiPencilOff,
|
||||
mdiPencilOutline,
|
||||
@@ -60,7 +60,7 @@ import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-prev";
|
||||
import "../../components/ha-related-items";
|
||||
import "./ha-more-info-related";
|
||||
import type {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
@@ -69,8 +69,6 @@ import {
|
||||
getExtendedEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { ItemType } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import type { EntitySettingsState } from "../../panels/config/entities/entity-registry-settings-editor";
|
||||
import type { Helper } from "../../panels/config/helpers/const";
|
||||
@@ -598,20 +596,22 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const defaultTitle = breadcrumb.pop() || entityId;
|
||||
const addToTitle = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title",
|
||||
{ target: defaultTitle }
|
||||
);
|
||||
const addToMenuItem = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
);
|
||||
const title =
|
||||
const viewTitle =
|
||||
this._currView === "details"
|
||||
? this.hass.localize("ui.dialogs.more_info_control.details")
|
||||
: this._currView === "add_to"
|
||||
? addToTitle
|
||||
: this._childView?.viewTitle || defaultTitle;
|
||||
: this._currView === "related"
|
||||
? this.hass.localize("ui.dialogs.more_info_control.related")
|
||||
: this._currView === "add_to"
|
||||
? addToMenuItem
|
||||
: this._childView?.viewTitle;
|
||||
const defaultTitle = breadcrumb[breadcrumb.length - 1] || entityId;
|
||||
if (!viewTitle) {
|
||||
breadcrumb.pop();
|
||||
}
|
||||
const title = viewTitle || defaultTitle;
|
||||
|
||||
const favoritesContext =
|
||||
this._entry && stateObj
|
||||
@@ -856,7 +856,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
<ha-dropdown-item value="related">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiInformationOutline}
|
||||
.path=${mdiLinkVariant}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.related"
|
||||
@@ -865,7 +865,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
<ha-dropdown-item value="details">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListBulletedSquare}
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.details"
|
||||
@@ -954,15 +954,11 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
`
|
||||
: this._currView === "related"
|
||||
? html`
|
||||
<ha-related-items
|
||||
<ha-more-info-related
|
||||
.hass=${this.hass}
|
||||
.itemId=${entityId}
|
||||
.itemType=${
|
||||
SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity"
|
||||
}
|
||||
></ha-related-items>
|
||||
.entry=${this._entry}
|
||||
.params=${{ entityId }}
|
||||
></ha-more-info-related>
|
||||
`
|
||||
: this._currView === "add_to"
|
||||
? html`
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
import { mdiDevices, mdiTextureBox } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../../common/entity/compute_floor_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-floor-icon";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/ha-label";
|
||||
import "../../components/ha-related-items";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/item/ha-list-item-value";
|
||||
import "../../components/list/ha-grouped-list";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
|
||||
interface RelatedViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
interface ContextEntry {
|
||||
translationKey: LocalizeKeys;
|
||||
value: string;
|
||||
displayValue?: TemplateResult;
|
||||
href?: string;
|
||||
icon?: TemplateResult;
|
||||
}
|
||||
|
||||
const CONTEXT_SECTIONS: (keyof RelatedResult)[] = [
|
||||
"device",
|
||||
"area",
|
||||
"integration",
|
||||
"config_entry",
|
||||
];
|
||||
|
||||
@customElement("ha-more-info-related")
|
||||
class HaMoreInfoRelated extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public params?: RelatedViewParams;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("entry") && this.entry) {
|
||||
this.hass.loadBackendTranslation("title", [this.entry.platform]);
|
||||
}
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const domain = computeDomain(this.params.entityId);
|
||||
const itemType = SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity";
|
||||
|
||||
const { floor, area, device } = getEntityContext(
|
||||
this._stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = area ? (computeAreaName(area) ?? area.area_id) : undefined;
|
||||
const deviceName = device
|
||||
? computeDeviceNameDisplay(device, this.hass.localize, this.hass.states)
|
||||
: undefined;
|
||||
const integrationName = this.entry?.platform
|
||||
? this.hass.localize(`component.${this.entry.platform}.title`) ||
|
||||
this.entry.platform
|
||||
: undefined;
|
||||
const labels =
|
||||
this.entry?.labels.map((labelId) => ({
|
||||
id: labelId,
|
||||
entry: this._labels?.find((label) => label.label_id === labelId),
|
||||
})) ?? [];
|
||||
|
||||
const contextEntries: ContextEntry[] = [];
|
||||
|
||||
if (floor && floorName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.dialogs.more_info_control.floor",
|
||||
value: floorName,
|
||||
href: "/config/areas/dashboard",
|
||||
icon: html`<ha-floor-icon slot="end" .floor=${floor}></ha-floor-icon>`,
|
||||
});
|
||||
}
|
||||
if (area && areaName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.components.related-items.area",
|
||||
value: areaName,
|
||||
href: `/config/areas/area/${area.area_id}`,
|
||||
icon: area.icon
|
||||
? html`<ha-icon slot="end" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="end" .path=${mdiTextureBox}></ha-svg-icon>`,
|
||||
});
|
||||
}
|
||||
if (device && deviceName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.components.related-items.device",
|
||||
value: deviceName,
|
||||
href: `/config/devices/device/${device.id}`,
|
||||
icon: html`<ha-svg-icon slot="end" .path=${mdiDevices}></ha-svg-icon>`,
|
||||
});
|
||||
}
|
||||
if (this.entry?.platform && integrationName) {
|
||||
contextEntries.push({
|
||||
translationKey: "ui.components.related-items.integration",
|
||||
value: integrationName,
|
||||
href: this.entry.config_entry_id
|
||||
? `/config/integrations/integration/${this.entry.platform}#config_entry=${this.entry.config_entry_id}`
|
||||
: undefined,
|
||||
icon: html`<img
|
||||
slot="end"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: this.entry.platform,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>`,
|
||||
});
|
||||
}
|
||||
contextEntries.push({
|
||||
translationKey: "ui.dialogs.more_info_control.labels",
|
||||
value:
|
||||
labels.map(({ id, entry }) => entry?.name ?? id).join(", ") ||
|
||||
this.hass.localize("ui.dialogs.more_info_control.no_labels"),
|
||||
displayValue: labels.length
|
||||
? html`<div class="labels">
|
||||
${labels.map(
|
||||
({ id, entry }) => html`
|
||||
<ha-label
|
||||
class="text-ellipsis"
|
||||
.color=${entry?.color ?? undefined}
|
||||
.description=${entry?.description ?? undefined}
|
||||
>
|
||||
${
|
||||
entry?.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${entry.icon}
|
||||
></ha-icon>`
|
||||
: nothing
|
||||
}
|
||||
${entry?.name ?? id}
|
||||
</ha-label>
|
||||
`
|
||||
)}
|
||||
</div>`
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-grouped-list
|
||||
.header=${this.hass.localize("ui.dialogs.more_info_control.context")}
|
||||
>
|
||||
${this._renderEntries(contextEntries)}
|
||||
</ha-grouped-list>
|
||||
<ha-related-items
|
||||
.hass=${this.hass}
|
||||
.itemId=${this.params.entityId}
|
||||
.itemType=${itemType}
|
||||
.exclude=${itemType === "entity" ? CONTEXT_SECTIONS : undefined}
|
||||
></ha-related-items>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEntries(entries: ContextEntry[]) {
|
||||
return entries.map((entry) => {
|
||||
const label = this.hass.localize(entry.translationKey);
|
||||
|
||||
if (!entry.href) {
|
||||
return html`
|
||||
<ha-list-item-value .label=${label}
|
||||
>${entry.displayValue ?? entry.value}</ha-list-item-value
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list-item-button .href=${entry.href}>
|
||||
<div class="link-row" slot="content">
|
||||
<div class="label">${label}</div>
|
||||
<div class="value">${entry.value}</div>
|
||||
</div>
|
||||
${entry.icon ?? nothing}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
ha-related-items {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
--ha-row-item-min-height: 40px;
|
||||
--ha-row-item-gap: var(--ha-space-3);
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
ha-list-item-button::part(end) {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-list-item-button img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.link-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.link-row .label {
|
||||
flex: 1;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.link-row .value {
|
||||
max-width: 60%;
|
||||
min-width: 0;
|
||||
text-align: end;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.labels {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.labels ha-label {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-related": HaMoreInfoRelated;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
import type { ResolvedMediaSource } from "../../data/media_source";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export const ERR_UNSUPPORTED_MEDIA = "Unsupported Media";
|
||||
|
||||
export class BrowserMediaPlayer {
|
||||
private player: HTMLAudioElement;
|
||||
|
||||
@@ -24,12 +22,10 @@ export class BrowserMediaPlayer {
|
||||
public item: MediaPlayerItem,
|
||||
public resolved: ResolvedMediaSource,
|
||||
volume: number,
|
||||
private onChange: () => void
|
||||
private onChange: () => void,
|
||||
private onError: () => void
|
||||
) {
|
||||
const player = new Audio(this.resolved.url);
|
||||
if (player.canPlayType(resolved.mime_type) === "") {
|
||||
throw new Error(ERR_UNSUPPORTED_MEDIA);
|
||||
}
|
||||
player.autoplay = true;
|
||||
player.volume = volume;
|
||||
player.addEventListener("play", this._handleChange);
|
||||
@@ -40,6 +36,8 @@ export class BrowserMediaPlayer {
|
||||
player.addEventListener("pause", this._handleChange);
|
||||
player.addEventListener("ended", this._handleChange);
|
||||
player.addEventListener("canplaythrough", this._handleChange);
|
||||
player.addEventListener("seeked", this._handleChange);
|
||||
player.addEventListener("error", this._handleError);
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@@ -49,6 +47,12 @@ export class BrowserMediaPlayer {
|
||||
}
|
||||
};
|
||||
|
||||
private _handleError = () => {
|
||||
if (!this._removed) {
|
||||
this.onError();
|
||||
}
|
||||
};
|
||||
|
||||
public pause() {
|
||||
this.buffering = false;
|
||||
this.player.pause();
|
||||
@@ -63,6 +67,10 @@ export class BrowserMediaPlayer {
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
public seek(position: number) {
|
||||
this.player.currentTime = position;
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this._removed = true;
|
||||
// @ts-ignore
|
||||
@@ -100,7 +108,10 @@ export class BrowserMediaPlayer {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
MediaPlayerEntityFeature.PLAY |
|
||||
MediaPlayerEntityFeature.PAUSE |
|
||||
MediaPlayerEntityFeature.VOLUME_SET,
|
||||
MediaPlayerEntityFeature.VOLUME_SET |
|
||||
(Number.isFinite(this.player.duration)
|
||||
? MediaPlayerEntityFeature.SEEK
|
||||
: 0),
|
||||
};
|
||||
|
||||
if (this.player.duration) {
|
||||
|
||||
@@ -58,10 +58,7 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../lovelace/components/hui-marquee";
|
||||
import {
|
||||
BrowserMediaPlayer,
|
||||
ERR_UNSUPPORTED_MEDIA,
|
||||
} from "./browser-media-player";
|
||||
import { BrowserMediaPlayer } from "./browser-media-player";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -158,25 +155,21 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
throw Error("Only browser supported");
|
||||
}
|
||||
this._tearDownBrowserPlayer();
|
||||
try {
|
||||
this._browserPlayer = new BrowserMediaPlayer(
|
||||
this.hass,
|
||||
item,
|
||||
resolved,
|
||||
this._browserPlayerVolume,
|
||||
() => this.requestUpdate("_browserPlayer")
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (err.message === ERR_UNSUPPORTED_MEDIA) {
|
||||
this._browserPlayer = new BrowserMediaPlayer(
|
||||
this.hass,
|
||||
item,
|
||||
resolved,
|
||||
this._browserPlayerVolume,
|
||||
() => this.requestUpdate("_browserPlayer"),
|
||||
() => {
|
||||
this._tearDownBrowserPlayer();
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.components.media-browser.media_not_supported"
|
||||
),
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
this._newMediaExpected = false;
|
||||
}
|
||||
|
||||
@@ -313,13 +306,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.card.media_player.track_position"
|
||||
)}
|
||||
?disabled=${
|
||||
isBrowser ||
|
||||
!supportsFeature(
|
||||
stateObj,
|
||||
MediaPlayerEntityFeature.SEEK
|
||||
)
|
||||
}
|
||||
?disabled=${!supportsFeature(
|
||||
stateObj,
|
||||
MediaPlayerEntityFeature.SEEK
|
||||
)}
|
||||
@change=${this._handleMediaSeekChanged}
|
||||
></ha-slider>`
|
||||
: html`
|
||||
@@ -336,13 +326,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.card.media_player.track_position"
|
||||
)}
|
||||
?disabled=${
|
||||
isBrowser ||
|
||||
!supportsFeature(
|
||||
stateObj,
|
||||
MediaPlayerEntityFeature.SEEK
|
||||
)
|
||||
}
|
||||
?disabled=${!supportsFeature(
|
||||
stateObj,
|
||||
MediaPlayerEntityFeature.SEEK
|
||||
)}
|
||||
@change=${this._handleMediaSeekChanged}
|
||||
></ha-slider>
|
||||
<div>${mediaDuration}</div>
|
||||
@@ -607,11 +594,17 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleMediaSeekChanged(e: HASSDomTargetEvent<HaSlider>): void {
|
||||
if (this.entityId === BROWSER_PLAYER || !this._stateObj) {
|
||||
if (!this._stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = e.target.value;
|
||||
|
||||
if (this.entityId === BROWSER_PLAYER) {
|
||||
this._browserPlayer?.seek(newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("media_player", "media_seek", {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
seek_position: newValue,
|
||||
|
||||
@@ -1180,12 +1180,12 @@
|
||||
"integration": "Integration",
|
||||
"device": "Device",
|
||||
"area": "Area",
|
||||
"entity": "Related entities",
|
||||
"group": "Part of the following groups",
|
||||
"scene": "Part of the following scenes",
|
||||
"script": "Part of the following scripts",
|
||||
"automation": "Part of the following automations",
|
||||
"blueprint": "Using blueprint"
|
||||
"entity": "Entities",
|
||||
"group": "Groups",
|
||||
"scene": "Scenes",
|
||||
"script": "Scripts",
|
||||
"automation": "Automations",
|
||||
"blueprint": "Blueprint"
|
||||
},
|
||||
"data-table": {
|
||||
"search": "Search",
|
||||
@@ -1687,13 +1687,15 @@
|
||||
"person": "Edit person"
|
||||
},
|
||||
"details": "Details",
|
||||
"entity": "Entity",
|
||||
"state": "State",
|
||||
"context": "Context",
|
||||
"floor": "Floor",
|
||||
"entity_id": "Entity ID",
|
||||
"entity_id": "ID",
|
||||
"copy_value": "Copy {label}: {value}",
|
||||
"labels": "Labels",
|
||||
"no_labels": "None",
|
||||
"toggle_yaml_mode": "Toggle YAML mode",
|
||||
"translated": "Translated",
|
||||
"raw": "Raw",
|
||||
"back_to_info": "Back to info",
|
||||
"info": "Information",
|
||||
"related": "Related",
|
||||
|
||||
@@ -67,10 +67,10 @@ export const moreInfoViewElements: ViewElementSmokeCase<MoreInfoView>[] = [
|
||||
},
|
||||
{
|
||||
view: "related",
|
||||
element: "ha-related-items",
|
||||
// search/related is mocked to return no relations, so the empty list
|
||||
// renders.
|
||||
content: [{ selector: "ha-related-items >> ha-list" }],
|
||||
element: "ha-more-info-related",
|
||||
// search/related is mocked to return no relations, so only the context
|
||||
// group renders.
|
||||
content: [{ selector: "ha-more-info-related >> ha-grouped-list" }],
|
||||
},
|
||||
{
|
||||
view: "add_to",
|
||||
|
||||
Reference in New Issue
Block a user