20240402.2 (#20348)

This commit is contained in:
Bram Kragten 2024-04-02 23:34:17 +02:00 committed by GitHub
commit 29eb73176a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 364 additions and 109 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240402.1" version = "20240402.2"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -1,12 +1,13 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { findRelated, RelatedResult } from "../data/search";
import type { HomeAssistant } from "../types";
import { haStyleScrollbar } from "../resources/styles";
import { Blueprints, fetchBlueprints } from "../data/blueprint"; import { Blueprints, fetchBlueprints } from "../data/blueprint";
import { findRelated, RelatedResult } from "../data/search";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@customElement("ha-filter-blueprints") @customElement("ha-filter-blueprints")
export class HaFilterBlueprints extends LitElement { export class HaFilterBlueprints extends LitElement {
@ -35,7 +36,11 @@ export class HaFilterBlueprints extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.blueprint.caption")} ${this.hass.localize("ui.panel.config.blueprint.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div>` ? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._blueprints && this._shouldRender ${this._blueprints && this._shouldRender
@ -128,6 +133,15 @@ export class HaFilterBlueprints extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -147,6 +161,10 @@ export class HaFilterBlueprints extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@ -2,6 +2,7 @@ import { ActionDetail, SelectedDetail } from "@material/mwc-list";
import { import {
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiFilterVariantRemove,
mdiPencil, mdiPencil,
mdiPlus, mdiPlus,
mdiTag, mdiTag,
@ -68,7 +69,11 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.category.caption")} ${this.hass.localize("ui.panel.config.category.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div>` ? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@ -254,6 +259,15 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -274,6 +288,10 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@ -1,3 +1,4 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -13,10 +14,11 @@ import { stringCompare } from "../common/string/compare";
import { computeDeviceName } from "../data/device_registry"; import { computeDeviceName } from "../data/device_registry";
import { findRelated, RelatedResult } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-check-list-item";
import { loadVirtualizer } from "../resources/virtualizer"; import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./search-input-outlined";
@customElement("ha-filter-devices") @customElement("ha-filter-devices")
export class HaFilterDevices extends LitElement { export class HaFilterDevices extends LitElement {
@ -32,6 +34,8 @@ export class HaFilterDevices extends LitElement {
@state() private _shouldRender = false; @state() private _shouldRender = false;
@state() private _filter?: string;
public willUpdate(properties: PropertyValues) { public willUpdate(properties: PropertyValues) {
super.willUpdate(properties); super.willUpdate(properties);
@ -51,19 +55,33 @@ export class HaFilterDevices extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.devices.caption")} ${this.hass.localize("ui.panel.config.devices.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div>` ? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
? html`<mwc-list class="ha-scrollbar"> ? html`<search-input-outlined
<lit-virtualizer .hass=${this.hass}
.items=${this._devices(this.hass.devices, this.value)} .filter=${this._filter}
.keyFunction=${this._keyFunction} @value-changed=${this._handleSearchChange}
.renderItem=${this._renderItem}
@click=${this._handleItemClick}
> >
</lit-virtualizer> </search-input-outlined>
</mwc-list>` <mwc-list class="ha-scrollbar">
<lit-virtualizer
.items=${this._devices(
this.hass.devices,
this._filter || "",
this.value
)}
.keyFunction=${this._keyFunction}
.renderItem=${this._renderItem}
@click=${this._handleItemClick}
>
</lit-virtualizer>
</mwc-list>`
: nothing} : nothing}
</ha-expansion-panel> </ha-expansion-panel>
`; `;
@ -72,12 +90,14 @@ export class HaFilterDevices extends LitElement {
private _keyFunction = (device) => device?.id; private _keyFunction = (device) => device?.id;
private _renderItem = (device) => private _renderItem = (device) =>
html`<ha-check-list-item !device
.value=${device.id} ? nothing
.selected=${this.value?.includes(device.id)} : html`<ha-check-list-item
> .value=${device.id}
${computeDeviceName(device, this.hass)} .selected=${this.value?.includes(device.id)}
</ha-check-list-item>`; >
${computeDeviceName(device, this.hass)}
</ha-check-list-item>`;
private _handleItemClick(ev) { private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item"); const listItem = ev.target.closest("ha-check-list-item");
@ -99,7 +119,7 @@ export class HaFilterDevices extends LitElement {
setTimeout(() => { setTimeout(() => {
if (!this.expanded) return; if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height = this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49}px`; `${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
}, 300); }, 300);
} }
} }
@ -112,16 +132,28 @@ export class HaFilterDevices extends LitElement {
this.expanded = ev.detail.expanded; this.expanded = ev.detail.expanded;
} }
private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => { private _handleSearchChange(ev: CustomEvent) {
const values = Object.values(devices); this._filter = ev.detail.value.toLowerCase();
return values.sort((a, b) => }
stringCompare(
a.name_by_user || a.name || "", private _devices = memoizeOne(
b.name_by_user || b.name || "", (devices: HomeAssistant["devices"], filter: string, _value) => {
this.hass.locale.language const values = Object.values(devices);
) return values
); .filter(
}); (device) =>
!filter ||
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
)
.sort((a, b) =>
stringCompare(
computeDeviceName(a, this.hass),
computeDeviceName(b, this.hass),
this.hass.locale.language
)
);
}
);
private async _findRelated() { private async _findRelated() {
const relatedPromises: Promise<RelatedResult>[] = []; const relatedPromises: Promise<RelatedResult>[] = [];
@ -158,6 +190,15 @@ export class HaFilterDevices extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -178,6 +219,10 @@ export class HaFilterDevices extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
@ -197,6 +242,10 @@ export class HaFilterDevices extends LitElement {
ha-check-list-item { ha-check-list-item {
width: 100%; width: 100%;
} }
search-input-outlined {
display: block;
padding: 0 8px;
}
`, `,
]; ];
} }

View File

@ -1,3 +1,4 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -14,10 +15,11 @@ import { computeStateName } from "../common/entity/compute_state_name";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import { findRelated, RelatedResult } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-state-icon";
import "./ha-check-list-item";
import { loadVirtualizer } from "../resources/virtualizer"; import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-state-icon";
import "./search-input-outlined";
@customElement("ha-filter-entities") @customElement("ha-filter-entities")
export class HaFilterEntities extends LitElement { export class HaFilterEntities extends LitElement {
@ -33,6 +35,8 @@ export class HaFilterEntities extends LitElement {
@state() private _shouldRender = false; @state() private _shouldRender = false;
@state() private _filter?: string;
public willUpdate(properties: PropertyValues) { public willUpdate(properties: PropertyValues) {
super.willUpdate(properties); super.willUpdate(properties);
@ -52,16 +56,27 @@ export class HaFilterEntities extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.entities.caption")} ${this.hass.localize("ui.panel.config.entities.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div>` ? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
? html` ? html`
<search-input-outlined
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar"> <mwc-list class="ha-scrollbar">
<lit-virtualizer <lit-virtualizer
.items=${this._entities( .items=${this._entities(
this.hass.states, this.hass.states,
this.type, this.type,
this._filter || "",
this.value this.value
)} )}
.keyFunction=${this._keyFunction} .keyFunction=${this._keyFunction}
@ -81,7 +96,7 @@ export class HaFilterEntities extends LitElement {
setTimeout(() => { setTimeout(() => {
if (!this.expanded) return; if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height = this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49}px`; `${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
}, 300); }, 300);
} }
} }
@ -89,18 +104,20 @@ export class HaFilterEntities extends LitElement {
private _keyFunction = (entity) => entity?.entity_id; private _keyFunction = (entity) => entity?.entity_id;
private _renderItem = (entity) => private _renderItem = (entity) =>
html`<ha-check-list-item !entity
.value=${entity.entity_id} ? nothing
.selected=${this.value?.includes(entity.entity_id)} : html`<ha-check-list-item
graphic="icon" .value=${entity.entity_id}
> .selected=${this.value?.includes(entity.entity_id)}
<ha-state-icon graphic="icon"
slot="graphic" >
.hass=${this.hass} <ha-state-icon
.stateObj=${entity} slot="graphic"
></ha-state-icon> .hass=${this.hass}
${computeStateName(entity)} .stateObj=${entity}
</ha-check-list-item>`; ></ha-state-icon>
${computeStateName(entity)}
</ha-check-list-item>`;
private _handleItemClick(ev) { private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item"); const listItem = ev.target.closest("ha-check-list-item");
@ -125,12 +142,27 @@ export class HaFilterEntities extends LitElement {
this.expanded = ev.detail.expanded; this.expanded = ev.detail.expanded;
} }
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value.toLowerCase();
}
private _entities = memoizeOne( private _entities = memoizeOne(
(states: HomeAssistant["states"], type: this["type"], _value) => { (
states: HomeAssistant["states"],
type: this["type"],
filter: string,
_value
) => {
const values = Object.values(states); const values = Object.values(states);
return values return values
.filter( .filter(
(entityState) => !type || computeStateDomain(entityState) !== type (entityState) =>
(!type || computeStateDomain(entityState) !== type) &&
(!filter ||
entityState.entity_id.toLowerCase().includes(filter) ||
entityState.attributes.friendly_name
?.toLowerCase()
.includes(filter))
) )
.sort((a, b) => .sort((a, b) =>
stringCompare( stringCompare(
@ -177,6 +209,15 @@ export class HaFilterEntities extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -196,6 +237,10 @@ export class HaFilterEntities extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
@ -216,6 +261,10 @@ export class HaFilterEntities extends LitElement {
--mdc-list-item-graphic-margin: 16px; --mdc-list-item-graphic-margin: 16px;
width: 100%; width: 100%;
} }
search-input-outlined {
display: block;
padding: 0 8px;
}
`, `,
]; ];
} }

View File

@ -1,5 +1,5 @@
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiTextureBox } from "@mdi/js"; import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -53,9 +53,13 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.areas.caption")} ${this.hass.localize("ui.panel.config.areas.caption")}
${this.value?.areas?.length || this.value?.floors?.length ${this.value?.areas?.length || this.value?.floors?.length
? html`<div class="badge"> ? html`<div class="badge">
${(this.value?.areas?.length || 0) + ${(this.value?.areas?.length || 0) +
(this.value?.floors?.length || 0)} (this.value?.floors?.length || 0)}
</div>` </div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@ -238,6 +242,15 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -257,6 +270,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@ -1,4 +1,5 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@ -38,7 +39,11 @@ export class HaFilterIntegrations extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.integrations.caption")} ${this.hass.localize("ui.panel.config.integrations.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div>` ? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._manifests && this._shouldRender ${this._manifests && this._shouldRender
@ -142,6 +147,15 @@ export class HaFilterIntegrations extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -161,6 +175,10 @@ export class HaFilterIntegrations extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@ -1,19 +1,18 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiPlus } from "@mdi/js"; import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { computeCssColor } from "../common/color/compute-color"; import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { import {
LabelRegistryEntry, LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry, subscribeLabelRegistry,
} from "../data/label_registry"; } from "../data/label_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-check-list-item"; import "./ha-check-list-item";
@ -54,7 +53,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.labels.caption")} ${this.hass.localize("ui.panel.config.labels.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div>` ? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@ -95,11 +98,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
${this.expanded ${this.expanded
? html`<ha-list-item ? html`<ha-list-item
graphic="icon" graphic="icon"
@click=${this._addLabel} @click=${this._manageLabels}
class="add" class="add"
> >
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.manage_labels")}
</ha-list-item>` </ha-list-item>`
: nothing} : nothing}
`; `;
@ -115,10 +118,8 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
} }
} }
private _addLabel() { private _manageLabels() {
showLabelDetailDialog(this, { navigate("/config/labels");
createEntry: (values) => createLabelRegistryEntry(this.hass, values),
});
} }
private _expandedWillChange(ev) { private _expandedWillChange(ev) {
@ -153,6 +154,15 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -173,6 +183,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@ -1,11 +1,12 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-check-list-item"; import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-icon"; import "./ha-icon";
@customElement("ha-filter-states") @customElement("ha-filter-states")
@ -43,7 +44,11 @@ export class HaFilterStates extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.label} ${this.label}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div>` ? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@ -118,6 +123,15 @@ export class HaFilterStates extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@ -137,6 +151,10 @@ export class HaFilterStates extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@ -1,5 +1,12 @@
import { mdiMagnify } from "@mdi/js"; import { mdiClose, mdiMagnify } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import {
CSSResultGroup,
LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@ -54,6 +61,15 @@ class SearchInputOutlined extends LitElement {
.path=${mdiMagnify} .path=${mdiMagnify}
></ha-svg-icon> ></ha-svg-icon>
</slot> </slot>
${this.filter
? html`<ha-icon-button
aria-label="Clear input"
slot="trailing-icon"
@click=${this._clearSearch}
.path=${mdiClose}
>
</ha-icon-button>`
: nothing}
</ha-outlined-text-field> </ha-outlined-text-field>
`; `;
} }
@ -66,12 +82,17 @@ class SearchInputOutlined extends LitElement {
this._filterChanged(e.target.value); this._filterChanged(e.target.value);
} }
private async _clearSearch() {
this._filterChanged("");
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
display: inline-flex; display: inline-flex;
/* For iOS */ /* For iOS */
z-index: 0; z-index: 0;
--mdc-icon-button-size: 24px;
} }
ha-outlined-text-field { ha-outlined-text-field {
display: block; display: block;

View File

@ -368,14 +368,16 @@ export class HaTabsSubpageDataTable extends LitElement {
"ui.components.subpage-data-table.filters" "ui.components.subpage-data-table.filters"
)}</span )}</span
> >
<ha-icon-button ${this.filters
slot="actionItems" ? html`<ha-icon-button
@click=${this._clearFilters} slot="actionItems"
.path=${mdiFilterVariantRemove} @click=${this._clearFilters}
.label=${localize( .path=${mdiFilterVariantRemove}
"ui.components.subpage-data-table.clear_filter" .label=${localize(
)} "ui.components.subpage-data-table.clear_filter"
></ha-icon-button> )}
></ha-icon-button>`
: nothing}
</ha-dialog-header> </ha-dialog-header>
<div class="filter-dialog-content"> <div class="filter-dialog-content">
<slot name="filter-pane"></slot></div <slot name="filter-pane"></slot></div
@ -394,13 +396,15 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiFilterVariant} .path=${mdiFilterVariant}
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
<ha-icon-button ${this.filters
.path=${mdiFilterVariantRemove} ? html`<ha-icon-button
@click=${this._clearFilters} .path=${mdiFilterVariantRemove}
.label=${localize( @click=${this._clearFilters}
"ui.components.subpage-data-table.clear_filter" .label=${localize(
)} "ui.components.subpage-data-table.clear_filter"
></ha-icon-button> )}
></ha-icon-button>`
: nothing}
</div> </div>
<div class="pane-content"> <div class="pane-content">
<slot name="filter-pane"></slot> <slot name="filter-pane"></slot>

View File

@ -271,7 +271,14 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
? html`<ha-icon .icon=${area.icon}></ha-icon>` ? html`<ha-icon .icon=${area.icon}></ha-icon>`
: ""} : ""}
</div> </div>
<h1 class="card-header">${area.name}</h1> <div class="card-header">
${area.name}
<ha-icon-button
.area=${area}
.path=${mdiPencil}
@click=${this._openAreaDetails}
></ha-icon-button>
</div>
<div class="card-content"> <div class="card-content">
<div> <div>
${formatListWithAnds( ${formatListWithAnds(
@ -305,6 +312,16 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
loadAreaRegistryDetailDialog(); loadAreaRegistryDetailDialog();
} }
private _openAreaDetails(ev) {
ev.preventDefault();
const area = ev.currentTarget.area;
showAreaRegistryDetailDialog(this, {
entry: area,
updateEntry: async (values) =>
updateAreaRegistryEntry(this.hass!, area.area_id, values),
});
}
private async _areaMoved(ev) { private async _areaMoved(ev) {
const areasAndFloors = this._processAreas( const areasAndFloors = this._processAreas(
this.hass.areas, this.hass.areas,
@ -469,8 +486,10 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
min-height: 16px; min-height: 16px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.floor { .card-header {
--primary-color: var(--secondary-text-color); display: flex;
justify-content: space-between;
align-items: center;
} }
.warning { .warning {
color: var(--error-color); color: var(--error-color);

View File

@ -35,6 +35,7 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types";
const HIDE_DOMAIN = new Set([ const HIDE_DOMAIN = new Set([
"automation", "automation",
"configurator", "configurator",
"conversation",
"device_tracker", "device_tracker",
"geo_location", "geo_location",
"persistent_notification", "persistent_notification",

View File

@ -58,18 +58,12 @@ export interface AndCondition extends BaseCondition {
function getValueFromEntityId( function getValueFromEntityId(
hass: HomeAssistant, hass: HomeAssistant,
value: string | string[] value: string
): string | string[] { ): string | undefined {
if ( if (isValidEntityId(value) && hass.states[value]) {
typeof value === "string" && return hass.states[value]?.state;
isValidEntityId(value) &&
hass.states[value]
) {
value = hass.states[value]?.state;
} else if (Array.isArray(value)) {
value = value.map((v) => getValueFromEntityId(hass, v) as string);
} }
return value; return undefined;
} }
function checkStateCondition( function checkStateCondition(
@ -83,8 +77,17 @@ function checkStateCondition(
let value = condition.state ?? condition.state_not; let value = condition.state ?? condition.state_not;
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
if (Array.isArray(value) || typeof value === "string") { if (Array.isArray(value)) {
value = getValueFromEntityId(hass, value); const entityValues = value
.map((v) => getValueFromEntityId(hass, v))
.filter((v): v is string => v !== undefined);
value = [...value, ...entityValues];
} else if (typeof value === "string") {
const entityValue = getValueFromEntityId(hass, value);
value = [value];
if (entityValue) {
value.push(entityValue);
}
} }
return condition.state != null return condition.state != null
@ -103,10 +106,10 @@ function checkStateNumericCondition(
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
if (typeof above === "string") { if (typeof above === "string") {
above = getValueFromEntityId(hass, above) as string; above = getValueFromEntityId(hass, above) ?? above;
} }
if (typeof below === "string") { if (typeof below === "string") {
below = getValueFromEntityId(hass, below) as string; below = getValueFromEntityId(hass, below) ?? below;
} }
const numericState = Number(state); const numericState = Number(state);

View File

@ -172,12 +172,14 @@ class DialogDashboardStrategyEditor extends LitElement {
`; `;
} }
private _takeControl() { private _takeControl(ev) {
ev.stopPropagation();
this._params!.takeControl(); this._params!.takeControl();
this.closeDialog(); this.closeDialog();
} }
private _showRawConfigEditor() { private _showRawConfigEditor(ev) {
ev.stopPropagation();
this._params!.showRawConfigEditor(); this._params!.showRawConfigEditor();
this.closeDialog(); this.closeDialog();
} }

View File

@ -116,6 +116,9 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
entities: { entities: {
redirect: "/config/entities", redirect: "/config/entities",
}, },
labels: {
redirect: "/config/labels",
},
energy: { energy: {
component: "energy", component: "energy",
redirect: "/energy", redirect: "/energy",

View File

@ -1962,6 +1962,7 @@
"color": "Color" "color": "Color"
}, },
"add_label": "Add label", "add_label": "Add label",
"manage_labels": "Manage labels",
"no_labels": "You don't have any labels", "no_labels": "You don't have any labels",
"introduction": "Labels can help you organize your areas, devices and entities. They can be used to filter in the UI, or use them as a target in automations.", "introduction": "Labels can help you organize your areas, devices and entities. They can be used to filter in the UI, or use them as a target in automations.",
"introduction2": "Go to the area, device or entity you want to add a label to, and click on the edit button to assign labels to them.", "introduction2": "Go to the area, device or entity you want to add a label to, and click on the edit button to assign labels to them.",