Compare commits

...

61 Commits

Author SHA1 Message Date
Bram Kragten
e96aca90fe 20240404.0 (#20414) 2024-04-04 16:23:24 +02:00
Bram Kragten
0580a31961 Bumped version to 20240404.0 2024-04-04 16:16:30 +02:00
Bram Kragten
5c42c5130c Add count of items (#20410)
* Add count of items

* Adjust layout, correct filter count
2024-04-04 16:15:13 +02:00
Bram Kragten
72d1e37a23 Fix integration filter search (#20408) 2024-04-04 13:26:26 +02:00
Bram Kragten
61c9072a08 Fix icons in entity settings (#20405) 2024-04-04 13:00:14 +02:00
Bram Kragten
08b25f9c2a Add floor and label support to describe action (#20403) 2024-04-04 13:00:05 +02:00
Samuel Schultze
1a03b49700 Fix calendar range selection (#20394)
fix: calendar range selector
2024-04-04 12:59:54 +02:00
Paul Bottein
2d4a8e2e45 Fix search input outlined background color and margin (#20407) 2024-04-04 12:53:03 +02:00
Bram Kragten
8486377604 Fix z-index create category dialog (#20406) 2024-04-04 10:51:04 +00:00
Paul Bottein
3a4e9b6856 Avoid duplicate entity ids in history (#20402)
* Avoid duplicate entity ids in history

* Don't need to check for size
2024-04-04 12:12:04 +02:00
Bram Kragten
5f5ac5419b Add floor and label support to history panel (#20388) 2024-04-04 00:06:03 +02:00
Bram Kragten
92b7a3b477 Adjust integration filter height for search bar (#20382) 2024-04-03 21:54:27 +02:00
Bram Kragten
4326519a3f 20240403.1 (#20380) 2024-04-03 16:58:47 +02:00
Bram Kragten
00837acdfc Bumped version to 20240403.1 2024-04-03 16:52:23 +02:00
Bram Kragten
7704be12b1 When creating a label or category with multi select, also assign it (#20379)
* When creating a label or category with multi select, also assign it

* correct scope

* again
2024-04-03 16:51:33 +02:00
Bram Kragten
712ddb531b Make selection bar responsive (#20378) 2024-04-03 16:48:02 +02:00
Bram Kragten
d52afc3f71 Add settings shortcut to scene and script table (#20375) 2024-04-03 16:11:32 +02:00
Bram Kragten
92f6083e0b Faulty helpers should not be selectable (#20373) 2024-04-03 15:19:05 +02:00
Bram Kragten
5751fdbe56 Improve entity integration filter (#20372) 2024-04-03 15:18:40 +02:00
Bram Kragten
962b30adb9 20240403.0 (#20370) 2024-04-03 14:50:16 +02:00
Bram Kragten
3b5b3f3bb6 Handle disabled entities in multi select label (#20371) 2024-04-03 14:40:48 +02:00
Bram Kragten
1a6d96cf3a Bumped version to 20240403.0 2024-04-03 14:18:07 +02:00
Bram Kragten
034fd9b4df Manage areas from floor dialog (#20347)
* manage areas from floor dialog

* Finish

* fix exclude
2024-04-03 14:17:32 +02:00
Bram Kragten
eb79a1e7d7 Allow to remove labels in multi select (#20368)
* Allow to remove labels in multi select

* reducedTouchTarget

* fix devices

* Update ha-config-devices-dashboard.ts
2024-04-03 14:17:21 +02:00
Bram Kragten
e25d4f17aa Add create category and label to multi select (#20365)
* Add create category and label to multi select

* move out of map
2024-04-03 13:23:00 +02:00
Bram Kragten
ccde9cceee Add category and filters to helpers (#20346)
* Add category and filters to helpers

* Add support for adding label and category in multi select

* remove labels multi
2024-04-03 13:22:40 +02:00
Paul Bottein
578d3c4260 Set input and button background color to white for toolbar (#20369) 2024-04-03 11:10:51 +00:00
Bram Kragten
bfdc9a3d86 Add area to automation, scene, script tables (#20366)
* Add area to automation, scene, script tables

* typing
2024-04-03 13:04:47 +02:00
Bram Kragten
5315545a4d Add search to integration filter (#20367) 2024-04-03 12:03:35 +02:00
Paul Bottein
82a3b9d80f Use tree for nested floor instead of icon (#20363) 2024-04-03 09:27:30 +00:00
Bram Kragten
3de985a3b8 Prevent line break in selection menu (#20361) 2024-04-03 11:04:24 +02:00
Bram Kragten
567ee8000d Fix toggles in automation datatable on firefox (#20360) 2024-04-03 08:30:08 +00:00
Bram Kragten
03939001b2 Fix elements above filter dialog (#20359) 2024-04-03 08:28:33 +00:00
Bram Kragten
30d18050d1 Add arrow to areas under floors (#20344) 2024-04-03 10:24:10 +02:00
Bram Kragten
95caf8c7df make subpage data table full height (#20358) 2024-04-03 10:12:36 +02:00
Bram Kragten
6c1f328d71 Take lang into account when sorting groups (#20355)
* Take lang into account when sorting groups

* make sure empty values are at the bottom
2024-04-03 10:12:25 +02:00
Bram Kragten
bb20ab8c2c Fix removing labels (#20354) 2024-04-03 09:54:49 +02:00
Bram Kragten
29eb73176a 20240402.2 (#20348) 2024-04-02 23:34:17 +02:00
Bram Kragten
17ad3a87f3 Bumped version to 20240402.2 2024-04-02 23:31:14 +02:00
Bram Kragten
ed7c9c33b9 Add my link support for labels (#20345) 2024-04-02 22:31:37 +02:00
Bram Kragten
59b66219cb Add clear filter button to individual filters (#20343) 2024-04-02 22:05:03 +02:00
Bram Kragten
1e2c1d1464 Add search to device and entity filters (#20341) 2024-04-02 21:46:21 +02:00
Bram Kragten
5b86b1277f Add edit button to areas in area dashboard + color add floor fab (#20339) 2024-04-02 21:41:56 +02:00
Paul Bottein
41fdf31e34 Check for entity state and entity string in conditional card (#20331)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-04-02 20:39:39 +02:00
Bram Kragten
9bef5c2af9 Add clear button to search field (#20332) 2024-04-02 19:42:54 +02:00
Paul Bottein
ed1a69071b Replace add label by manage labels in filters (#20330) 2024-04-02 19:05:01 +02:00
Paul Bottein
56d328b4db Fix error in console when taking control of the strategy (#20329) 2024-04-02 18:33:30 +02:00
Paulus Schoutsen
33c7e0fa2d Hide conversation entities from default dashboard (#20327) 2024-04-02 14:55:22 +00:00
Bram Kragten
4f1cf1110f 20240402.1 (#20326) 2024-04-02 16:41:12 +02:00
Bram Kragten
a434bfd944 Bumped version to 20240402.1 2024-04-02 16:33:05 +02:00
Bram Kragten
21ed8e4206 Load translations when adding item in pickers (#20325)
* Load translations when adding item in pickers

* Update ha-selector-label.ts
2024-04-02 15:33:10 +02:00
Paul Bottein
169d782580 Fix label wrap (#20323)
* Don't wrap headline on ha-menu-item

* Don't wrap text on ha-label
2024-04-02 13:20:22 +00:00
Bram Kragten
8a015f4e38 Update multi select of entities config (#20319)
* Update multi select of entities config

* Update ha-config-entities.ts
2024-04-02 15:16:20 +02:00
Bram Kragten
cbb08c6202 Add multi select to scripts and scenes (#20318) 2024-04-02 15:16:10 +02:00
Bram Kragten
6301bc713c Add multi select to devices (#20321) 2024-04-02 15:16:00 +02:00
Paul Bottein
a5d7043ce4 Update style of more info style (#20322)
* Set more info border radius to 36px

* Use control button for alarm more info
2024-04-02 15:05:21 +02:00
Bram Kragten
d3bf0da289 20240402.0 (#20314) 2024-04-02 11:44:05 +02:00
Paul Bottein
fd06d434f2 20240329.1 (#20280) 2024-03-29 21:25:10 +01:00
Paul Bottein
d24d29e42f 20240329.0 (#20277) 2024-03-29 19:10:34 +01:00
Paul Bottein
e02a47a16a 20240328.0 (#20250) 2024-03-28 16:49:01 +01:00
Bram Kragten
795c16a941 20240327.0 (#20210) 2024-03-27 17:52:08 +01:00
65 changed files with 3108 additions and 886 deletions

View File

@@ -161,6 +161,7 @@ const createWebpackConfig = ({
resolve: {
extensions: [".ts", ".js", ".json"],
alias: {
"lit/static-html$": "lit/static-html.js",
"lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js",

View File

@@ -136,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], this._action)
? describeAction(this.hass, [], [], [], this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -149,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], conf as any)}</span>
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
--mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px;
--control-select-border-radius: 48px;
--control-select-border-radius: 36px;
}
.vertical-selects {
height: 300px;

View File

@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
--control-slider-background: #ffcf4c;
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 130px;
--control-slider-border-radius: 48px;
--control-slider-border-radius: 36px;
}
.vertical-sliders {
height: 300px;

View File

@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
--control-switch-on-color: var(--green-color);
--control-switch-off-color: var(--red-color);
--control-switch-thickness: 130px;
--control-switch-border-radius: 48px;
--control-switch-border-radius: 36px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}

View File

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

View File

@@ -45,8 +45,8 @@ export class HaAssistChip extends MdAssistChip {
margin-inline-start: var(--_icon-label-space);
}
::before {
background: var(--ha-assist-chip-container-color);
opacity: var(--ha-assist-chip-container-opacity);
background: var(--ha-assist-chip-container-color, transparent);
opacity: var(--ha-assist-chip-container-opacity, 1);
}
:where(.active)::before {
background: var(--ha-assist-chip-active-container-color);

View File

@@ -33,6 +33,7 @@ import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
import { groupBy } from "../../common/util/group-by";
import { stringCompare } from "../../common/string/compare";
declare global {
// for fire event
@@ -529,7 +530,13 @@ export class HaDataTable extends LitElement {
const sorted: {
[key: string]: DataTableRowData[];
} = Object.keys(grouped)
.sort()
.sort((a, b) =>
stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
this.hass.locale.language
)
)
.reduce((obj, key) => {
obj[key] = grouped[key];
return obj;

View File

@@ -11,10 +11,10 @@ import {
} from "../common/datetime/localize_date";
import { mainWindow } from "../common/dom/get_main_window";
// Set the current date to the left picker instead of the right picker because the right is hidden
const CustomDateRangePicker = Vue.extend({
mixins: [DateRangePicker],
methods: {
// Set the current date to the left picker instead of the right picker because the right is hidden
selectMonthDate() {
const dt: Date = this.end || new Date();
// @ts-ignore
@@ -23,6 +23,33 @@ const CustomDateRangePicker = Vue.extend({
month: dt.getMonth() + 1,
});
},
// Fix the start/end date calculation when selecting a date range. The
// original code keeps track of the first clicked date (in_selection) but it
// never sets it to either the start or end date variables, so if the
// in_selection date is between the start and end date that were set by the
// hover the selection will enter a broken state that's counter-intuitive
// when hovering between weeks and leads to a random date when selecting a
// range across months. This bug doesn't seem to be present on v0.6.7 of the
// lib
hoverDate(value: Date) {
if (this.readonly) return;
if (this.in_selection) {
const pickA = this.in_selection as Date;
const pickB = value;
this.start = this.normalizeDatetime(
Math.min(pickA.valueOf(), pickB.valueOf()),
this.start
);
this.end = this.normalizeDatetime(
Math.max(pickA.valueOf(), pickB.valueOf()),
this.end
);
}
this.$emit("hover-date", value);
},
},
});

View File

@@ -1,8 +1,9 @@
import { mdiTextureBox } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@@ -11,6 +12,7 @@ import {
ScorableTextItem,
fuzzyFilterSort,
} from "../common/string/filter/sequence-matching";
import { computeRTL } from "../common/util/compute_rtl";
import { AreaRegistryEntry } from "../data/area_registry";
import {
DeviceEntityDisplayLookup,
@@ -32,6 +34,7 @@ import "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
import "./ha-tree-indicator";
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
@@ -41,28 +44,11 @@ interface FloorAreaEntry {
icon: string | null;
strings: string[];
type: "floor" | "area";
hasFloor?: boolean;
level: number | null;
hasFloor?: boolean;
lastArea?: boolean;
}
const rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) =>
html`<ha-list-item
graphic="icon"
style=${item.type === "area" && item.hasFloor
? "--mdc-list-side-padding-left: 48px;"
: ""}
>
${item.type === "floor"
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-list-item>`;
@customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -151,6 +137,44 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
await this.comboBox?.focus();
}
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
const rtl = computeRTL(this.hass);
return html`
<ha-list-item
graphic="icon"
style=${item.type === "area" && item.hasFloor
? rtl
? "--mdc-list-side-padding-right: 48px;"
: "--mdc-list-side-padding-left: 48px;"
: ""}
>
${item.type === "area" && item.hasFloor
? html`<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "8px",
right: rtl ? "8px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="graphic"
></ha-tree-indicator>`
: nothing}
${item.type === "floor"
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-list-item>
`;
};
private _getAreas = memoizeOne(
(
floors: FloorRegistryEntry[],
@@ -364,7 +388,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
});
}
output.push(
...floorAreas.map((area) => ({
...floorAreas.map((area, index, array) => ({
id: area.area_id,
type: "area" as const,
name: area.name,
@@ -372,6 +396,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
strings: [area.area_id, ...area.aliases, area.name],
hasFloor: true,
level: null,
lastArea: index === array.length - 1,
}))
);
});
@@ -445,7 +470,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
.placeholder=${this.placeholder
? this.hass.areas[this.placeholder]?.name
: undefined}
.renderer=${rowRenderer}
.renderer=${this._rowRenderer}
@filter-changed=${this._filterChanged}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}

View File

@@ -428,6 +428,8 @@ export class HaAreaPicker extends LitElement {
(ev.target as any).value = this._value;
this.hass.loadFragmentTranslation("config");
showAreaRegistryDetailDialog(this, {
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
createEntry: async (values) => {

View File

@@ -1,12 +1,13 @@
import { SelectedDetail } from "@material/mwc-list";
import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
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 { findRelated, RelatedResult } from "../data/search";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@customElement("ha-filter-blueprints")
export class HaFilterBlueprints extends LitElement {
@@ -35,7 +36,11 @@ export class HaFilterBlueprints extends LitElement {
<div slot="header" class="header">
${this.hass.localize("ui.panel.config.blueprint.caption")}
${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}
</div>
${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 {
return [
haStyleScrollbar,
@@ -147,6 +161,10 @@ export class HaFilterBlueprints extends LitElement {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;

View File

@@ -2,6 +2,7 @@ import { ActionDetail, SelectedDetail } from "@material/mwc-list";
import {
mdiDelete,
mdiDotsVertical,
mdiFilterVariantRemove,
mdiPencil,
mdiPlus,
mdiTag,
@@ -68,7 +69,11 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
<div slot="header" class="header">
${this.hass.localize("ui.panel.config.category.caption")}
${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}
</div>
${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 {
return [
haStyleScrollbar,
@@ -274,6 +288,10 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;

View File

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

View File

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

View File

@@ -1,17 +1,19 @@
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 { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl";
import {
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { findRelated, RelatedResult } from "../data/search";
import { RelatedResult, findRelated } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -19,6 +21,7 @@ import "./ha-check-list-item";
import "./ha-floor-icon";
import "./ha-icon";
import "./ha-svg-icon";
import "./ha-tree-indicator";
@customElement("ha-filter-floor-areas")
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
@@ -53,9 +56,13 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.areas.caption")}
${this.value?.areas?.length || this.value?.floors?.length
? html`<div class="badge">
${(this.value?.areas?.length || 0) +
(this.value?.floors?.length || 0)}
</div>`
${(this.value?.areas?.length || 0) +
(this.value?.floors?.length || 0)}
</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing}
</div>
${this._shouldRender
@@ -82,8 +89,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
</ha-check-list-item>
${repeat(
floor.areas,
(area) => area.area_id,
(area) => this._renderArea(area)
(area, index) =>
`${area.area_id}${index === floor.areas.length - 1 ? "___last" : ""}`,
(area, index) =>
this._renderArea(area, index === floor.areas.length - 1)
)}
`
)}
@@ -99,23 +108,37 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
`;
}
private _renderArea(area) {
return html`<ha-check-list-item
.value=${area.area_id}
.selected=${this.value?.areas?.includes(area.area_id) || false}
.type=${"areas"}
graphic="icon"
class=${area.floor_id ? "floor" : ""}
@request-selected=${this._handleItemClick}
>
${area.icon
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-check-list-item>`;
private _renderArea(area, last: boolean = false) {
const hasFloor = !!area.floor_id;
return html`
<ha-check-list-item
.value=${area.area_id}
.selected=${this.value?.areas?.includes(area.area_id) || false}
.type=${"areas"}
graphic="icon"
@request-selected=${this._handleItemClick}
class=${classMap({
rtl: computeRTL(this.hass),
floor: hasFloor,
})}
>
${hasFloor
? html`
<ha-tree-indicator
.end=${last}
slot="graphic"
></ha-tree-indicator>
`
: nothing}
${area.icon
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-check-list-item>
`;
}
private _handleItemClick(ev) {
@@ -238,6 +261,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 {
return [
haStyleScrollbar,
@@ -257,6 +289,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
@@ -277,9 +313,26 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
--mdc-list-item-graphic-margin: 16px;
}
.floor {
padding-left: 32px;
padding-inline-start: 32px;
padding-left: 48px;
padding-inline-start: 48px;
padding-inline-end: 16px;
}
ha-tree-indicator {
width: 56px;
position: absolute;
top: 0px;
left: 0px;
}
.rtl ha-tree-indicator {
right: 0px;
left: initial;
transform: scaleX(-1);
}
.subdir {
margin-inline-end: 8px;
opacity: .6;
}
.
`,
];
}

View File

@@ -1,4 +1,4 @@
import { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
@@ -12,6 +12,7 @@ import {
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-domain-icon";
import "./search-input-outlined";
@customElement("ha-filter-integrations")
export class HaFilterIntegrations extends LitElement {
@@ -27,6 +28,8 @@ export class HaFilterIntegrations extends LitElement {
@state() private _shouldRender = false;
@state() private _filter?: string;
protected render() {
return html`
<ha-expansion-panel
@@ -38,18 +41,23 @@ export class HaFilterIntegrations extends LitElement {
<div slot="header" class="header">
${this.hass.localize("ui.panel.config.integrations.caption")}
${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}
</div>
${this._manifests && this._shouldRender
? html`
<mwc-list
@selected=${this._integrationsSelected}
multi
class="ha-scrollbar"
? html`<search-input-outlined
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
${repeat(
this._integrations(this._manifests, this.value),
this._integrations(this._manifests, this._filter, this.value),
(i) => i.domain,
(integration) =>
html`<ha-check-list-item
@@ -68,8 +76,7 @@ export class HaFilterIntegrations extends LitElement {
${integration.name || integration.domain}
</ha-check-list-item>`
)}
</mwc-list>
`
</mwc-list> `
: nothing}
</ha-expansion-panel>
`;
@@ -80,7 +87,7 @@ export class HaFilterIntegrations extends LitElement {
setTimeout(() => {
if (!this.expanded) return;
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);
}
}
@@ -98,12 +105,17 @@ export class HaFilterIntegrations extends LitElement {
}
private _integrations = memoizeOne(
(manifest: IntegrationManifest[], _value) =>
(manifest: IntegrationManifest[], filter: string | undefined, _value) =>
manifest
.filter(
(mnfst) =>
!mnfst.integration_type ||
!["entity", "system", "hardware"].includes(mnfst.integration_type)
(!mnfst.integration_type ||
!["entity", "system", "hardware"].includes(
mnfst.integration_type
)) &&
(!filter ||
mnfst.name.toLowerCase().includes(filter) ||
mnfst.domain.toLowerCase().includes(filter))
)
.sort((a, b) =>
stringCompare(
@@ -114,34 +126,38 @@ export class HaFilterIntegrations extends LitElement {
)
);
private async _integrationsSelected(
ev: CustomEvent<SelectedDetail<Set<number>>>
) {
const integrations = this._integrations(this._manifests!, this.value);
if (!ev.detail.index.size) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item");
const value = listItem?.value;
if (!value) {
return;
}
const value: string[] = [];
for (const index of ev.detail.index) {
const domain = integrations[index].domain;
value.push(domain);
if (this.value?.includes(value)) {
this.value = this.value?.filter((val) => val !== value);
} else {
this.value = [...(this.value || []), value];
}
this.value = value;
listItem.selected = this.value?.includes(value);
fireEvent(this, "data-table-filter-changed", {
value,
value: this.value,
items: undefined,
});
}
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value.toLowerCase();
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
@@ -161,6 +177,10 @@ export class HaFilterIntegrations extends LitElement {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
@@ -177,6 +197,10 @@ export class HaFilterIntegrations extends LitElement {
padding: 0px 2px;
color: var(--text-primary-color);
}
search-input-outlined {
display: block;
padding: 0 8px;
}
`,
];
}

View File

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

View File

@@ -1,11 +1,12 @@
import { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-icon";
@customElement("ha-filter-states")
@@ -43,7 +44,11 @@ export class HaFilterStates extends LitElement {
<div slot="header" class="header">
${this.label}
${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}
</div>
${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 {
return [
haStyleScrollbar,
@@ -137,6 +151,10 @@ export class HaFilterStates extends LitElement {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;

View File

@@ -10,7 +10,10 @@ import {
ScorableTextItem,
fuzzyFilterSort,
} from "../common/string/filter/sequence-matching";
import { AreaRegistryEntry } from "../data/area_registry";
import {
AreaRegistryEntry,
updateAreaRegistryEntry,
} from "../data/area_registry";
import {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
@@ -437,11 +440,18 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
(ev.target as any).value = this._value;
this.hass.loadFragmentTranslation("config");
showFloorRegistryDetailDialog(this, {
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
createEntry: async (values) => {
createEntry: async (values, addedAreas) => {
try {
const floor = await createFloorRegistryEntry(this.hass, values);
addedAreas.forEach((areaId) => {
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: floor.floor_id,
});
});
const floors = [...this._floors!, floor];
this.comboBox.filteredItems = this._getFloors(
floors,

View File

@@ -445,6 +445,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
(ev.target as any).value = this._value;
this.hass.loadFragmentTranslation("config");
showLabelDetailDialog(this, {
entry: undefined,
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",

View File

@@ -43,6 +43,7 @@ class HaLabel extends LitElement {
border-radius: 18px;
color: var(--ha-label-text-color);
--mdc-icon-size: 12px;
text-wrap: nowrap;
}
.content > * {
position: relative;

View File

@@ -2,8 +2,10 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import {
LabelRegistryEntry,
subscribeLabelRegistry,
@@ -17,7 +19,6 @@ import "./chips/ha-input-chip";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-label-picker";
import type { HaLabelPicker } from "./ha-label-picker";
import { stringCompare } from "../common/string/compare";
@customElement("ha-labels-picker")
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
@@ -102,25 +103,35 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
];
}
private _sortedLabels = memoizeOne(
(
value: string[] | undefined,
labels: { [id: string]: LabelRegistryEntry } | undefined,
language: string
) =>
value
?.map((id) => labels?.[id])
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
);
protected render(): TemplateResult {
const labels = this.value
?.map((id) => this._labels?.[id])
.sort((a, b) =>
stringCompare(a?.name || "", b?.name || "", this.hass.locale.language)
);
const labels = this._sortedLabels(
this.value,
this._labels,
this.hass.locale.language
);
return html`
${labels?.length
? html`<ha-chip-set>
${repeat(
labels,
(label) => label?.label_id,
(label, idx) => {
(label) => {
const color = label?.color
? computeCssColor(label.color)
: undefined;
return html`
<ha-input-chip
.idx=${idx}
.item=${label}
@remove=${this._removeItem}
@click=${this._openDetail}
@@ -161,12 +172,12 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
}
private _removeItem(ev) {
this._value.splice(ev.target.idx, 1);
this._setValue([...this._value]);
const label = ev.currentTarget.item;
this._setValue(this._value.filter((id) => id !== label.label_id));
}
private _openDetail(ev) {
const label = ev.target.item;
const label = ev.currentTarget.item;
showLabelDetailDialog(this, {
entry: label,
updateEntry: async (values) => {

View File

@@ -1,7 +1,7 @@
import { customElement } from "lit/decorators";
import { MdMenuItem } from "@material/web/menu/menu-item";
import "element-internals-polyfill";
import { CSSResult, css } from "lit";
import { MdMenuItem } from "@material/web/menu/menu-item";
import { customElement } from "lit/decorators";
@customElement("ha-menu-item")
export class HaMenuItem extends MdMenuItem {
@@ -30,6 +30,9 @@ export class HaMenuItem extends MdMenuItem {
--md-menu-item-label-text-color: var(--error-color);
--md-menu-item-leading-icon-color: var(--error-color);
}
::slotted([slot="headline"]) {
text-wrap: nowrap;
}
`,
];
}

View File

@@ -0,0 +1,40 @@
import { MdOutlinedField } from "@material/web/field/outlined-field";
import "element-internals-polyfill";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { literal } from "lit/static-html";
@customElement("ha-outlined-field")
export class HaOutlinedField extends MdOutlinedField {
protected readonly fieldTag = literal`ha-outlined-field`;
static override styles = [
...super.styles,
css`
.container::before {
display: block;
content: "";
position: absolute;
inset: 0;
background-color: var(--ha-outlined-field-container-color, transparent);
opacity: var(--ha-outlined-field-container-opacity, 1);
border-start-start-radius: var(--_container-shape-start-start);
border-start-end-radius: var(--_container-shape-start-end);
border-end-start-radius: var(--_container-shape-end-start);
border-end-end-radius: var(--_container-shape-end-end);
}
.with-start .start {
margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
}
.with-end .end {
margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-outlined-field": HaOutlinedField;
}
}

View File

@@ -2,9 +2,13 @@ import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field
import "element-internals-polyfill";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { literal } from "lit/static-html";
import "./ha-outlined-field";
@customElement("ha-outlined-text-field")
export class HaOutlinedTextField extends MdOutlinedTextField {
protected readonly fieldTag = literal`ha-outlined-field`;
static override styles = [
...super.styles,
css`
@@ -25,6 +29,8 @@ export class HaOutlinedTextField extends MdOutlinedTextField {
--md-outlined-field-container-shape-end-end: 10px;
--md-outlined-field-container-shape-end-start: 10px;
--md-outlined-field-focus-outline-width: 1px;
--ha-outlined-field-start-margin: -4px;
--ha-outlined-field-end-margin: -4px;
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
}
.input {

View File

@@ -30,6 +30,7 @@ export class HaLabelSelector extends LitElement {
if (this.selector.label.multiple) {
return html`
<ha-labels-picker
no-add
.hass=${this.hass}
.value=${ensureArray(this.value ?? [])}
.disabled=${this.disabled}
@@ -41,6 +42,7 @@ export class HaLabelSelector extends LitElement {
}
return html`
<ha-label-picker
no-add
.hass=${this.hass}
.value=${this.value}
.disabled=${this.disabled}

View File

@@ -0,0 +1,36 @@
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-tree-indicator")
export class HaTreeIndicator extends LitElement {
@property({ type: Boolean, reflect: true })
public end?: boolean = false;
protected render(): TemplateResult {
return html`
<svg width="100%" height="100%" viewBox="0 0 48 48">
<line x1="24" y1="0" x2="24" y2=${this.end ? "24" : "48"}></line>
<line x1="24" y1="24" x2="36" y2="24"></line>
</svg>
`;
}
static styles = css`
:host {
display: block;
width: 48px;
height: 48px;
}
line {
stroke: var(--divider-color);
stroke-width: 2;
stroke-dasharray: 2;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-tree-indicator": HaTreeIndicator;
}
}

View File

@@ -1,5 +1,12 @@
import { mdiMagnify } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { mdiClose, mdiMagnify } from "@mdi/js";
import {
CSSResultGroup,
LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
@@ -54,6 +61,15 @@ class SearchInputOutlined extends LitElement {
.path=${mdiMagnify}
></ha-svg-icon>
</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>
`;
}
@@ -66,16 +82,22 @@ class SearchInputOutlined extends LitElement {
this._filterChanged(e.target.value);
}
private async _clearSearch() {
this._filterChanged("");
}
static get styles(): CSSResultGroup {
return css`
:host {
display: inline-flex;
/* For iOS */
z-index: 0;
--mdc-icon-button-size: 24px;
}
ha-outlined-text-field {
display: block;
width: 100%;
--ha-outlined-field-container-color: var(--card-background-color);
}
ha-svg-icon,
ha-icon-button {

View File

@@ -1,3 +1,4 @@
import { consume } from "@lit-labs/context";
import {
mdiAlertCircle,
mdiCircle,
@@ -6,14 +7,13 @@ import {
mdiProgressWrench,
mdiRecordCircleOutline,
} from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -23,27 +23,31 @@ import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook";
import {
ChooseAction,
ChooseActionChoice,
getActionType,
IfAction,
ParallelAction,
RepeatAction,
getActionType,
} from "../../data/script";
import { describeAction } from "../../data/script_i18n";
import {
ActionTraceStep,
AutomationTraceExtended,
ChooseActionTraceStep,
getDataFromPath,
IfActionTraceStep,
isTriggerPath,
TriggerTraceStep,
getDataFromPath,
isTriggerPath,
} from "../../data/trace";
import { HomeAssistant } from "../../types";
import "./ha-timeline";
@@ -200,6 +204,8 @@ class ActionRenderer {
constructor(
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: FloorRegistryEntry[],
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -310,7 +316,14 @@ class ActionRenderer {
this._renderEntry(
path,
describeAction(this.hass, this.entityReg, data, actionType),
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
undefined,
data.enabled === false
);
@@ -475,7 +488,13 @@ class ActionRenderer {
const name =
repeatConfig.alias ||
describeAction(this.hass, this.entityReg, repeatConfig);
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -631,15 +650,17 @@ export class HaAutomationTracer extends LitElement {
@property({ type: Boolean }) public allowPick = false;
@state() private _entityReg: EntityRegistryEntry[] = [];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entityReg = entities;
}),
];
}
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
protected render() {
if (!this.trace) {
@@ -657,6 +678,8 @@ export class HaAutomationTracer extends LitElement {
const actionRenderer = new ActionRenderer(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
entries,
this.trace,
logbookRenderer,

View File

@@ -2,6 +2,8 @@ import { createContext } from "@lit-labs/context";
import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { LabelRegistryEntry } from "./label_registry";
export const connectionContext =
createContext<HomeAssistant["connection"]>("connection");
@@ -25,3 +27,7 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");

View File

@@ -14,7 +14,9 @@ import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry";
import {
ActionType,
ActionTypes,
@@ -40,6 +42,8 @@ const actionTranslationBaseKey =
export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -48,6 +52,8 @@ export const describeAction = <T extends ActionType>(
return tryDescribeAction(
hass,
entityRegistry,
labelRegistry,
floorRegistry,
action,
actionType,
ignoreAlias
@@ -66,6 +72,8 @@ export const describeAction = <T extends ActionType>(
const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -82,10 +90,12 @@ const tryDescribeAction = <T extends ActionType>(
const targets: string[] = [];
if (config.target) {
for (const [key, label] of Object.entries({
for (const [key, name] of Object.entries({
area_id: "areas",
device_id: "devices",
entity_id: "entities",
floor_id: "floors",
label_id: "labels",
})) {
if (!(key in config.target)) {
continue;
@@ -99,7 +109,7 @@ const tryDescribeAction = <T extends ActionType>(
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`,
{ name: label }
{ name }
)
);
break;
@@ -147,6 +157,32 @@ const tryDescribeAction = <T extends ActionType>(
)
);
}
} else if (key === "floor_id") {
const floor = floorRegistry.find(
(flr) => flr.floor_id === targetThing
);
if (floor?.name) {
targets.push(floor.name);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_floor`
)
);
}
} else if (key === "label_id") {
const label = labelRegistry.find(
(lbl) => lbl.label_id === targetThing
);
if (label?.name) {
targets.push(label.name);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_label`
)
);
}
} else {
targets.push(targetThing);
}

View File

@@ -28,6 +28,7 @@ export type ItemType =
| "entity"
| "floor"
| "group"
| "label"
| "scene"
| "script"
| "automation_blueprint"

View File

@@ -190,7 +190,7 @@ class LightColorTempPicker extends LitElement {
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 130px;
--control-slider-border-radius: 48px;
--control-slider-border-radius: 36px;
--control-slider-color: var(--primary-color);
--control-slider-background: -webkit-linear-gradient(
top,

View File

@@ -1,9 +1,8 @@
import { mdiShieldOff } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-outlined-button";
import "../../../components/ha-control-button";
import "../../../components/ha-state-icon";
import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel";
import "../../../state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes";
@@ -57,15 +56,10 @@ class MoreInfoAlarmControlPanel extends LitElement {
${["triggered", "arming", "pending"].includes(this.stateObj.state)
? html`
<div class="status">
<span></span>
<div class="icon">
<ha-state-icon .hass=${this.hass} .stateObj=${this.stateObj}>
</ha-state-icon>
</div>
<ha-outlined-button @click=${this._disarm}>
${this.hass.localize("ui.card.alarm_control_panel.disarm")}
<ha-svg-icon slot="icon" .path=${mdiShieldOff}></ha-svg-icon>
</ha-outlined-button>
</div>
`
: html`
@@ -76,7 +70,15 @@ class MoreInfoAlarmControlPanel extends LitElement {
</ha-state-control-alarm_control_panel-modes>
`}
</div>
<span></span>
<div>
${["triggered", "arming", "pending"].includes(this.stateObj.state)
? html`
<ha-control-button @click=${this._disarm} class="disarm">
${this.hass.localize("ui.card.alarm_control_panel.disarm")}
</ha-control-button>
`
: nothing}
</div>
`;
}
@@ -127,8 +129,12 @@ class MoreInfoAlarmControlPanel extends LitElement {
transition: background-color 180ms ease-in-out;
opacity: 0.2;
}
.status ha-outlined-button {
margin-top: 32px;
ha-control-button.disarm {
height: 60px;
min-width: 130px;
max-width: 200px;
margin: 0 auto;
--control-button-border-radius: 24px;
}
`,
];

View File

@@ -170,7 +170,7 @@ class MoreInfoLock extends LitElement {
--control-button-border-radius: 24px;
}
.open-button {
width: 100px;
width: 130px;
--control-button-background-color: var(--state-color);
}
.open-button.confirm {

View File

@@ -321,19 +321,28 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
<ha-menu-item .value=${undefined} @click=${this._selectAll}
>${localize("ui.components.subpage-data-table.select_all")}
<ha-menu-item .value=${undefined} @click=${this._selectAll}>
<div slot="headline">
${localize("ui.components.subpage-data-table.select_all")}
</div>
</ha-menu-item>
<ha-menu-item .value=${undefined} @click=${this._selectNone}
>${localize("ui.components.subpage-data-table.select_none")}
<ha-menu-item .value=${undefined} @click=${this._selectNone}>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.select_none"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item
.value=${undefined}
@click=${this._disableSelectMode}
>${localize(
"ui.components.subpage-data-table.close_select_mode"
)}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.close_select_mode"
)}
</div>
</ha-menu-item>
</ha-button-menu-new>
<p>
@@ -349,37 +358,7 @@ export class HaTabsSubpageDataTable extends LitElement {
: nothing}
${this.showFilters
? !showPane
? html`<ha-dialog
open
hideActions
.heading=${localize("ui.components.subpage-data-table.filters")}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
.path=${mdiClose}
@click=${this._toggleFilters}
.label=${localize(
"ui.components.subpage-data-table.close_filter"
)}
></ha-icon-button>
<span slot="title"
>${localize(
"ui.components.subpage-data-table.filters"
)}</span
>
<ha-icon-button
slot="actionItems"
@click=${this._clearFilters}
.path=${mdiFilterVariantRemove}
.label=${localize(
"ui.components.subpage-data-table.clear_filter"
)}
></ha-icon-button>
</ha-dialog-header>
<div class="filter-dialog-content">
<slot name="filter-pane"></slot></div
></ha-dialog>`
? nothing
: html`<div class="pane" slot="pane">
<div class="table-header">
<ha-assist-chip
@@ -394,13 +373,15 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiFilterVariant}
></ha-svg-icon>
</ha-assist-chip>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilters}
.label=${localize(
"ui.components.subpage-data-table.clear_filter"
)}
></ha-icon-button>
${this.filters
? html`<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilters}
.label=${localize(
"ui.components.subpage-data-table.clear_filter"
)}
></ha-icon-button>`
: nothing}
</div>
<div class="pane-content">
<slot name="filter-pane"></slot>
@@ -512,6 +493,51 @@ export class HaTabsSubpageDataTable extends LitElement {
: nothing
)}
</ha-menu>
${this.showFilters && !showPane
? html`<ha-dialog
open
.heading=${localize("ui.components.subpage-data-table.filters", {
number: this.data.length,
})}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
.path=${mdiClose}
@click=${this._toggleFilters}
.label=${localize(
"ui.components.subpage-data-table.close_filter"
)}
></ha-icon-button>
<span slot="title"
>${localize("ui.components.subpage-data-table.filters", {
number: this.data.length,
})}</span
>
${this.filters
? html`<ha-icon-button
slot="actionItems"
@click=${this._clearFilters}
.path=${mdiFilterVariantRemove}
.label=${localize(
"ui.components.subpage-data-table.clear_filter"
)}
></ha-icon-button>`
: nothing}
</ha-dialog-header>
<div class="filter-dialog-content">
<slot name="filter-pane"></slot>
</div>
<div slot="primaryAction">
<ha-button @click=${this._toggleFilters}>
${this.hass.localize(
"ui.components.subpage-data-table.show_results",
{ number: this.data.length }
)}
</ha-button>
</div>
</ha-dialog>`
: nothing}
`;
}
@@ -573,6 +599,7 @@ export class HaTabsSubpageDataTable extends LitElement {
return css`
:host {
display: block;
height: 100%;
}
ha-data-table {
@@ -728,7 +755,7 @@ export class HaTabsSubpageDataTable extends LitElement {
padding: 8px 12px;
box-sizing: border-box;
font-size: 14px;
--ha-assist-chip-container-color: var(--primary-background-color);
--ha-assist-chip-container-color: var(--card-background-color);
}
.selection-controls {
@@ -755,6 +782,7 @@ export class HaTabsSubpageDataTable extends LitElement {
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
--ha-assist-chip-container-color: var(--card-background-color);
}
.select-mode-chip {
@@ -777,7 +805,7 @@ export class HaTabsSubpageDataTable extends LitElement {
}
.filter-dialog-content {
height: calc(100vh - 1px - var(--header-height));
height: calc(100vh - 1px - 61px - var(--header-height));
display: flex;
flex-direction: column;
}

View File

@@ -1,8 +1,13 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list";
import { mdiTextureBox } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/chips/ha-chip-set";
import "../../../components/chips/ha-input-chip";
import "../../../components/ha-alert";
import "../../../components/ha-aliases-editor";
import { createCloseHeading } from "../../../components/ha-dialog";
@@ -11,10 +16,15 @@ import "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import { FloorRegistryEntryMutableParams } from "../../../data/floor_registry";
import { haStyleDialog } from "../../../resources/styles";
import {
FloorRegistryEntry,
FloorRegistryEntryMutableParams,
} from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail";
import { updateAreaRegistryEntry } from "../../../data/area_registry";
class DialogFloorDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -33,9 +43,11 @@ class DialogFloorDetail extends LitElement {
@state() private _submitting?: boolean;
public async showDialog(
params: FloorRegistryDetailDialogParams
): Promise<void> {
@state() private _addedAreas = new Set<string>();
@state() private _removedAreas = new Set<string>();
public showDialog(params: FloorRegistryDetailDialogParams): void {
this._params = params;
this._error = undefined;
this._name = this._params.entry
@@ -44,16 +56,40 @@ class DialogFloorDetail extends LitElement {
this._aliases = this._params.entry?.aliases || [];
this._icon = this._params.entry?.icon || null;
this._level = this._params.entry?.level ?? null;
await this.updateComplete;
this._addedAreas.clear();
this._removedAreas.clear();
}
public closeDialog(): void {
this._error = "";
this._params = undefined;
this._addedAreas.clear();
this._removedAreas.clear();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _floorAreas = memoizeOne(
(
entry: FloorRegistryEntry | undefined,
areas: HomeAssistant["areas"],
added: Set<string>,
removed: Set<string>
) =>
Object.values(areas).filter(
(area) =>
(area.floor_id === entry?.floor_id || added.has(area.area_id)) &&
!removed.has(area.area_id)
)
);
protected render() {
const areas = this._floorAreas(
this._params?.entry,
this.hass.areas,
this._addedAreas,
this._removedAreas
);
if (!this._params) {
return nothing;
}
@@ -125,6 +161,52 @@ class DialogFloorDetail extends LitElement {
: nothing}
</ha-icon-picker>
<h3 class="header">
${this.hass.localize(
"ui.panel.config.floors.editor.areas_section"
)}
</h3>
<p class="description">
${this.hass.localize(
"ui.panel.config.floors.editor.areas_description"
)}
</p>
${areas.length
? html`<ha-chip-set>
${repeat(
areas,
(area) => area.area_id,
(area) =>
html`<ha-input-chip
.area=${area}
@click=${this._openArea}
@remove=${this._removeArea}
.label=${area?.name}
>
${area.icon
? html`<ha-icon
slot="icon"
.icon=${area.icon}
></ha-icon>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>`}
</ha-input-chip>`
)}
</ha-chip-set>`
: nothing}
<ha-area-picker
no-add
.hass=${this.hass}
@value-changed=${this._addArea}
.excludeAreas=${areas.map((a) => a.area_id)}
.label=${this.hass.localize(
"ui.panel.config.floors.editor.add_area"
)}
></ha-area-picker>
<h3 class="header">
${this.hass.localize(
"ui.panel.config.floors.editor.aliases_section"
@@ -159,6 +241,41 @@ class DialogFloorDetail extends LitElement {
`;
}
private _openArea(ev) {
const area = ev.target.area;
showAreaRegistryDetailDialog(this, {
entry: area,
updateEntry: (values) =>
updateAreaRegistryEntry(this.hass!, area.area_id, values),
});
}
private _removeArea(ev) {
const areaId = ev.target.area.area_id;
if (this._addedAreas.has(areaId)) {
this._addedAreas.delete(areaId);
this._addedAreas = new Set(this._addedAreas);
return;
}
this._removedAreas.add(areaId);
this._removedAreas = new Set(this._removedAreas);
}
private _addArea(ev) {
const areaId = ev.detail.value;
if (!areaId) {
return;
}
ev.target.value = "";
if (this._removedAreas.has(areaId)) {
this._removedAreas.delete(areaId);
this._removedAreas = new Set(this._removedAreas);
return;
}
this._addedAreas.add(areaId);
this._addedAreas = new Set(this._addedAreas);
}
private _isNameValid() {
return this._name.trim() !== "";
}
@@ -189,9 +306,13 @@ class DialogFloorDetail extends LitElement {
aliases: this._aliases,
};
if (create) {
await this._params!.createEntry!(values);
await this._params!.createEntry!(values, this._addedAreas);
} else {
await this._params!.updateEntry!(values);
await this._params!.updateEntry!(
values,
this._addedAreas,
this._removedAreas
);
}
this.closeDialog();
} catch (err: any) {
@@ -209,6 +330,7 @@ class DialogFloorDetail extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-textfield {
@@ -218,6 +340,9 @@ class DialogFloorDetail extends LitElement {
ha-floor-icon {
color: var(--secondary-text-color);
}
ha-chip-set {
margin-bottom: 8px;
}
`,
];
}

View File

@@ -271,7 +271,14 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
? html`<ha-icon .icon=${area.icon}></ha-icon>`
: ""}
</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>
${formatListWithAnds(
@@ -305,6 +312,16 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
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) {
const areasAndFloors = this._processAreas(
this.hass.areas,
@@ -397,10 +414,31 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
private _openFloorDialog(entry?: FloorRegistryEntry) {
showFloorRegistryDetailDialog(this, {
entry,
createEntry: async (values) =>
createFloorRegistryEntry(this.hass!, values),
updateEntry: async (values) =>
updateFloorRegistryEntry(this.hass!, entry!.floor_id, values),
createEntry: async (values, addedAreas) => {
const floor = await createFloorRegistryEntry(this.hass!, values);
addedAreas.forEach((areaId) => {
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: floor.floor_id,
});
});
},
updateEntry: async (values, addedAreas, removedAreas) => {
const floor = await updateFloorRegistryEntry(
this.hass!,
entry!.floor_id,
values
);
addedAreas.forEach((areaId) => {
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: floor.floor_id,
});
});
removedAreas.forEach((areaId) => {
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: null,
});
});
},
});
}
@@ -469,8 +507,10 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
min-height: 16px;
color: var(--secondary-text-color);
}
.floor {
--primary-color: var(--secondary-text-color);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.warning {
color: var(--error-color);

View File

@@ -7,9 +7,14 @@ import {
export interface FloorRegistryDetailDialogParams {
entry?: FloorRegistryEntry;
suggestedName?: string;
createEntry?: (values: FloorRegistryEntryMutableParams) => Promise<unknown>;
createEntry?: (
values: FloorRegistryEntryMutableParams,
addedAreas: Set<string>
) => Promise<unknown>;
updateEntry?: (
updates: Partial<FloorRegistryEntryMutableParams>
updates: Partial<FloorRegistryEntryMutableParams>,
addedAreas: Set<string>,
removedAreas: Set<string>
) => Promise<unknown>;
}

View File

@@ -42,8 +42,14 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { FloorRegistryEntry } from "../../../../data/floor_registry";
import { LabelRegistryEntry } from "../../../../data/label_registry";
import {
Action,
NonConditionAction,
@@ -146,6 +152,14 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@@ -210,7 +224,13 @@ export default class HaAutomationActionRow extends LitElement {
.path=${ACTION_ICONS[type!]}
></ha-svg-icon>`}
${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action)
describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
</h3>
@@ -573,7 +593,15 @@ export default class HaAutomationActionRow extends LitElement {
),
inputType: "string",
placeholder: capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action, undefined, true)
describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action,
undefined,
true
)
),
defaultValue: this.action.alias,
confirmText: this.hass.localize("ui.common.submit"),

View File

@@ -1,4 +1,5 @@
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import "@material/web/divider/divider";
import {
@@ -73,6 +74,7 @@ import {
} from "../../../data/automation";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
subscribeCategoryRegistry,
} from "../../../data/category_registry";
import { fullEntitiesContext } from "../../../data/context";
@@ -84,6 +86,7 @@ import {
} from "../../../data/entity_registry";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry,
} from "../../../data/label_registry";
import { findRelated } from "../../../data/search";
@@ -98,11 +101,14 @@ import { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
type AutomationItem = AutomationEntity & {
name: string;
area: string | undefined;
last_triggered?: string | undefined;
formatted_state: string;
category: string | undefined;
@@ -148,10 +154,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@query("#overflow-menu") private _overflowMenu!: HaMenu;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
private _automations = memoizeOne(
(
automations: AutomationEntity[],
entityReg: EntityRegistryEntry[],
areas: HomeAssistant["areas"],
categoryReg?: CategoryRegistryEntry[],
labelReg?: LabelRegistryEntry[],
filteredAutomations?: string[] | null
@@ -174,6 +185,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
return {
...automation,
name: computeStateName(automation),
area: entityRegEntry?.area_id
? areas[entityRegEntry?.area_id]?.name
: undefined,
last_triggered: automation.attributes.last_triggered || undefined,
formatted_state: this.hass.formatEntityState(automation),
category: category
@@ -242,6 +256,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
`;
},
},
area: {
title: localize("ui.panel.config.automation.picker.headers.area"),
hidden: true,
groupable: true,
filterable: true,
sortable: true,
},
category: {
title: localize("ui.panel.config.automation.picker.headers.category"),
hidden: true,
@@ -256,33 +277,32 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
template: (automation) =>
automation.labels.map((lbl) => lbl.name).join(" "),
},
};
columns.last_triggered = {
sortable: true,
width: "130px",
title: localize("ui.card.automation.last_triggered"),
hidden: narrow,
template: (automation) => {
if (!automation.last_triggered) {
return this.hass.localize("ui.components.relative_time.never");
}
const date = new Date(automation.last_triggered);
const now = new Date();
const dayDifference = differenceInDays(now, date);
return html`
${dayDifference > 3
? formatShortDateTime(date, locale, this.hass.config)
: relativeTime(date, locale)}
`;
last_triggered: {
sortable: true,
width: "130px",
title: localize("ui.card.automation.last_triggered"),
hidden: narrow,
template: (automation) => {
if (!automation.last_triggered) {
return this.hass.localize("ui.components.relative_time.never");
}
const date = new Date(automation.last_triggered);
const now = new Date();
const dayDifference = differenceInDays(now, date);
return html`
${dayDifference > 3
? formatShortDateTime(date, locale, this.hass.config)
: relativeTime(date, locale)}
`;
},
},
};
if (!this.narrow) {
columns.formatted_state = {
formatted_state: {
width: "82px",
sortable: true,
groupable: true,
title: "",
type: "overflow",
hidden: narrow,
label: this.hass.localize("ui.panel.config.automation.picker.state"),
template: (automation) => html`
<ha-entity-toggle
@@ -290,21 +310,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass}
></ha-entity-toggle>
`,
};
}
columns.actions = {
title: "",
width: "64px",
type: "icon-button",
template: (automation) => html`
<ha-icon-button
.automation=${automation}
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@click=${this._showOverflowMenu}
></ha-icon-button>
`,
},
actions: {
title: "",
width: "64px",
type: "icon-button",
template: (automation) => html`
<ha-icon-button
.automation=${automation}
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@click=${this._showOverflowMenu}
></ha-icon-button>
`,
},
};
return columns;
}
@@ -357,22 +376,60 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
return html`<ha-menu-item
.value=${label.label_id}
@click=${this._handleBulkLabel}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const automations = this._automations(
this.automations,
this._entityReg,
this.hass.areas,
this._categories,
this._labels,
this._filteredAutomations
);
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -383,13 +440,23 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
id="entity_id"
.route=${this.route}
.tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
"ui.panel.config.automation.picker.search",
{ number: automations.length }
)}
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
hasFilters
.filters=${
Object.values(this._filters).filter((filter) => filter.value?.length)
.length
Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
}
.columns=${this._columns(
this.narrow,
@@ -397,13 +464,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this.hass.locale
)}
initialGroupColumn="category"
.data=${this._automations(
this.automations,
this._entityReg,
this._categories,
this._labels,
this._filteredAutomations
)}
.data=${automations}
.empty=${!this.automations.length}
@row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize(
@@ -495,7 +556,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
${this.hass.dockedSidebar === "docked"
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
@@ -557,8 +618,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
@@ -1039,6 +1100,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
private async _handleBulkCategory(ev) {
const category = ev.currentTarget.value;
this._bulkAddCategory(category);
}
private async _bulkAddCategory(category: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
@@ -1052,11 +1117,21 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
labels: this.hass.entities[entityId].labels.concat(label),
labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
})
);
});
@@ -1079,10 +1154,38 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
await Promise.all(promises);
}
private async _bulkCreateCategory() {
showCategoryRegistryDetailDialog(this, {
scope: "automation",
createEntry: async (values) => {
const category = await createCategoryRegistryEntry(
this.hass,
"automation",
values
);
this._bulkAddCategory(category.category_id);
return category;
},
});
}
private _bulkCreateLabel() {
showLabelDetailDialog(this, {
createEntry: async (values) => {
const label = await createLabelRegistryEntry(this.hass, values);
this._bulkLabel(label.label_id, "add");
return label;
},
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
hass-tabs-subpage-data-table {
--data-table-row-height: 60px;
}

View File

@@ -237,6 +237,8 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
(ev.target as any).value = this._value;
this.hass.loadFragmentTranslation("config");
showCategoryRegistryDetailDialog(this, {
scope: this.scope!,
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",

View File

@@ -1,6 +1,6 @@
import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { mdiPlus } from "@mdi/js";
import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js";
import {
CSSResultGroup,
LitElement,
@@ -13,6 +13,7 @@ import {
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import {
@@ -24,6 +25,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-battery-icon";
@@ -37,12 +39,15 @@ import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
import {
DeviceEntityLookup,
DeviceRegistryEntry,
computeDeviceName,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
@@ -52,6 +57,7 @@ import {
import { IntegrationManifest } from "../../../data/integration";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry,
} from "../../../data/label_registry";
import "../../../layouts/hass-tabs-subpage-data-table";
@@ -62,6 +68,7 @@ import { brandsUrl } from "../../../util/brands-url";
import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
@@ -91,6 +98,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _selected: string[] = [];
@state() private _filter: string = history.state?.filter || "";
@state() private _filters: Record<
@@ -535,6 +544,43 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._labels
);
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -545,14 +591,23 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.tabs=${configSections.devices}
.route=${this.route}
.searchLabel=${this.hass.localize(
"ui.panel.config.devices.picker.search"
"ui.panel.config.devices.picker.search",
{ number: devicesOutput.length }
)}
.columns=${this._columns(this.hass.localize, this.narrow)}
.data=${devicesOutput}
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
.filter=${this._filter}
hasFilters
.filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
@clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange}
@@ -621,6 +676,49 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`
: html` <ha-button-menu-new has-overflow slot="selection-bar"
><ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>
</ha-button-menu-new>`}
</hass-tabs-subpage-data-table>
`;
}
@@ -700,6 +798,45 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
});
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
updateDeviceRegistryEntry(this.hass, deviceId, {
labels:
action === "add"
? this.hass.devices[deviceId].labels.concat(label)
: this.hass.devices[deviceId].labels.filter(
(lbl) => lbl !== label
),
})
);
});
await Promise.all(promises);
}
private _bulkCreateLabel() {
showLabelDetailDialog(this, {
createEntry: async (values) => {
const label = await createLabelRegistryEntry(this.hass, values);
this._bulkLabel(label.label_id, "add");
return label;
},
});
}
static get styles(): CSSResultGroup {
return [
css`
@@ -721,6 +858,16 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
text-transform: uppercase;
direction: var(--direction);
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
haStyle,
];

View File

@@ -3,12 +3,17 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiAlertCircle,
mdiCancel,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiEye,
mdiEyeOff,
mdiMenuDown,
mdiPencilOff,
mdiPlus,
mdiRestoreAlert,
mdiUndo,
mdiToggleSwitch,
mdiToggleSwitchOffOutline,
} from "@mdi/js";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
@@ -22,8 +27,8 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import memoize from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -44,23 +49,26 @@ import "../../../components/ha-check-list-item";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-states";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity";
import {
EntityRegistryEntry,
UpdateEntityRegistryEntryResult,
computeEntityRegistryName,
removeEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { entryIcon } from "../../../data/icons";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry,
} from "../../../data/label_registry";
import {
@@ -77,6 +85,11 @@ import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../../data/entity_sources";
export interface StateEntity
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
@@ -123,13 +136,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
{ value: string[] | undefined; items: Set<string> | undefined }
> = {};
@state() private _selectedEntities: string[] = [];
@state() private _selected: string[] = [];
@state() private _expandedFilter?: string;
@state()
_labels!: LabelRegistryEntry[];
@state() private _entitySources?: EntitySources;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@@ -190,21 +205,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon",
template: (entry) =>
entry.icon
? html`
<ha-state-icon
title=${ifDefined(entry.entity?.state)}
slot="item-icon"
.hass=${this.hass}
.stateObj=${entry.entity}
></ha-state-icon>
`
: html`
<ha-icon
icon=${until(
entryIcon(this.hass, entry as EntityRegistryEntry)
)}
></ha-icon>
`,
? html`<ha-icon .icon=${entry.icon}></ha-icon>`
: entry.entity
? html`
<ha-state-icon
title=${ifDefined(entry.entity?.state)}
slot="item-icon"
.hass=${this.hass}
.stateObj=${entry.entity}
></ha-state-icon>
`
: html`<ha-domain-icon
.domain=${computeDomain(entry.entity_id)}
></ha-domain-icon>`,
},
name: {
main: true,
@@ -394,10 +407,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
const entryIds = entries
.filter((entry) => filter.value!.includes(entry.domain))
.map((entry) => entry.entry_id);
filteredEntities = filteredEntities.filter(
(entity) =>
entity.config_entry_id &&
entryIds.includes(entity.config_entry_id)
filter.value?.includes(entity.platform) ||
(entity.config_entry_id &&
entryIds.includes(entity.config_entry_id))
);
filter.value!.forEach((domain) => filteredDomains.add(domain));
} else if (key === "ha-filter-labels" && filter.value?.length) {
@@ -505,13 +520,50 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
[...filteredDomains][0]
);
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.route=${this.route}
.tabs=${configSections.devices}
.columns=${this._columns(
@@ -521,15 +573,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)}
.data=${filteredEntities}
.searchLabel=${this.hass.localize(
"ui.panel.config.entities.picker.search"
"ui.panel.config.entities.picker.search",
{ number: filteredEntities.length }
)}
hasFilters
.filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length
).length}
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
}
.filter=${this._filter}
selectable
.selected=${this._selectedEntities.length}
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
clickable
@clear-filter=${this._clearFilter}
@@ -543,100 +603,131 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.hass=${this.hass}
slot="toolbar-icon"
></ha-integration-overflow-menu>
<div class="header-btns" slot="selection-bar">
${!this.narrow
? html`
<mwc-button
@click=${this._enableSelected}
.disabled=${!this._selectedEntities.length}
>${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}</mwc-button
>
<mwc-button
@click=${this._disableSelected}
.disabled=${!this._selectedEntities.length}
>${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}</mwc-button
>
<mwc-button
@click=${this._hideSelected}
.disabled=${!this._selectedEntities.length}
>${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}</mwc-button
>
<mwc-button
@click=${this._removeSelected}
.disabled=${!this._selectedEntities.length}
class="warning"
>${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}</mwc-button
>
`
: html`
<ha-icon-button
id="enable-btn"
.disabled=${!this._selectedEntities.length}
@click=${this._enableSelected}
.path=${mdiUndo}
.label=${this.hass.localize("ui.common.enable")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="enable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</simple-tooltip>
<ha-icon-button
id="disable-btn"
.disabled=${!this._selectedEntities.length}
@click=${this._disableSelected}
.path=${mdiCancel}
.label=${this.hass.localize("ui.common.disable")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="disable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</simple-tooltip>
<ha-icon-button
id="hide-btn"
.disabled=${!this._selectedEntities.length}
@click=${this._hideSelected}
.path=${mdiEyeOff}
.label=${this.hass.localize("ui.common.hide")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="hide-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</simple-tooltip>
<ha-icon-button
class="warning"
id="remove-btn"
.disabled=${!this._selectedEntities.length}
@click=${this._removeSelected}
.path=${mdiDelete}
.label=${this.hass.localize("ui.common.remove")}
></ha-icon-button>
<simple-tooltip animation-delay="0" for="remove-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}
</simple-tooltip>
`}
</div>
${this._filters.config_entry?.value?.length
? html`<ha-alert slot="filter-pane">
Filtering by config entry
${this._entries?.find(
(entry) =>
entry.entry_id === this._filters.config_entry!.value![0]
)?.title || this._filters.config_entry.value[0]}
</ha-alert>`
: nothing}
${
!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`
: nothing
}
<ha-button-menu-new has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>
<md-divider role="separator" tabindex="-1"></md-divider>`
: nothing
}
<ha-menu-item @click=${this._enableSelected}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</div>
</ha-menu-item>
<ha-menu-item @click=${this._disableSelected}>
<ha-svg-icon
slot="start"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._unhideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEye}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.unhide_selected.button"
)}
</div>
</ha-menu-item>
<ha-menu-item @click=${this._hideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEyeOff}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._removeSelected} class="warning">
<ha-svg-icon
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}
</div>
</ha-menu-item>
</ha-button-menu-new>
${
this._filters.config_entry?.value?.length
? html`<ha-alert slot="filter-pane">
Filtering by config entry
${this._entries?.find(
(entry) =>
entry.entry_id === this._filters.config_entry!.value![0]
)?.title || this._filters.config_entry.value[0]}
</ha-alert>`
: nothing
}
<ha-filter-floor-areas
.hass=${this.hass}
type="entity"
@@ -688,16 +779,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
${includeAddDeviceFab
? html`<ha-fab
.label=${this.hass.localize("ui.panel.config.devices.add_device")}
extended
@click=${this._addDevice}
slot="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing}
${
includeAddDeviceFab
? html`<ha-fab
.label=${this.hass.localize(
"ui.panel.config.devices.add_device"
)}
extended
@click=${this._addDevice}
slot="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing
}
</hass-tabs-subpage-data-table>
`;
}
@@ -723,6 +818,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
},
};
this._setFiltersFromUrl();
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
}
private _setFiltersFromUrl() {
@@ -781,14 +879,18 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._filters = {};
}
public willUpdate(changedProps: PropertyValues<this>): void {
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
const oldHass = changedProps.get("hass");
let changed = false;
if (!this.hass || !this._entities) {
return;
}
if (changedProps.has("hass") || changedProps.has("_entities")) {
if (
changedProps.has("hass") ||
changedProps.has("_entities") ||
changedProps.has("_entitySources")
) {
const stateEntities: StateEntity[] = [];
const regEntityIds = new Set(
this._entities.map((entity) => entity.entity_id)
@@ -799,6 +901,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}
if (
!oldHass ||
changedProps.has("_entitySources") ||
this.hass.states[entityId] !== oldHass.states[entityId]
) {
changed = true;
@@ -806,7 +909,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
stateEntities.push({
name: computeStateName(this.hass.states[entityId]),
entity_id: entityId,
platform: computeDomain(entityId),
platform:
this._entitySources?.[entityId]?.domain || computeDomain(entityId),
disabled_by: null,
hidden_by: null,
area_id: null,
@@ -836,14 +940,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedEntities = ev.detail.value;
this._selected = ev.detail.value;
}
private async _enableSelected() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.confirm_title",
{ number: this._selectedEntities.length }
{ number: this._selected.length }
),
text: this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.confirm_text"
@@ -854,7 +958,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
let require_restart = false;
let reload_delay = 0;
await Promise.all(
this._selectedEntities.map(async (entity) => {
this._selected.map(async (entity) => {
const result = await updateEntityRegistryEntry(this.hass, entity, {
disabled_by: null,
});
@@ -891,7 +995,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.confirm_title",
{ number: this._selectedEntities.length }
{ number: this._selected.length }
),
text: this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.confirm_text"
@@ -899,7 +1003,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
confirmText: this.hass.localize("ui.common.disable"),
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
this._selectedEntities.forEach((entity) =>
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, {
disabled_by: "user",
})
@@ -913,7 +1017,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.confirm_title",
{ number: this._selectedEntities.length }
{ number: this._selected.length }
),
text: this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.confirm_text"
@@ -921,7 +1025,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
confirmText: this.hass.localize("ui.common.hide"),
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => {
this._selectedEntities.forEach((entity) =>
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, {
hidden_by: "user",
})
@@ -931,22 +1035,66 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
});
}
private _unhideSelected() {
this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, {
hidden_by: null,
})
);
this._clearSelection();
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
await this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
const entityReg =
this.hass.entities[entityId] ||
this._entities.find((entReg) => entReg.entity_id === entityId);
if (!entityReg) {
return;
}
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
labels:
action === "add"
? entityReg.labels.concat(label)
: entityReg.labels.filter((lbl) => lbl !== label),
})
);
});
await Promise.all(promises);
}
private _bulkCreateLabel() {
showLabelDetailDialog(this, {
createEntry: async (values) => {
const label = await createLabelRegistryEntry(this.hass, values);
this._bulkLabel(label.label_id, "add");
return label;
},
});
}
private _removeSelected() {
const removeableEntities = this._selectedEntities.filter((entity) => {
const removeableEntities = this._selected.filter((entity) => {
const stateObj = this.hass.states[entity];
return stateObj?.attributes.restored;
});
showConfirmationDialog(this, {
title: this.hass.localize(
`ui.panel.config.entities.picker.remove_selected.confirm_${
removeableEntities.length !== this._selectedEntities.length
? "partly_"
: ""
removeableEntities.length !== this._selected.length ? "partly_" : ""
}title`,
{ number: removeableEntities.length }
),
text:
removeableEntities.length === this._selectedEntities.length
removeableEntities.length === this._selected.length
? this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.confirm_text"
)
@@ -954,7 +1102,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
{
removable: removeableEntities.length,
selected: this._selectedEntities.length,
selected: this._selected.length,
}
),
confirmText: this.hass.localize("ui.common.remove"),
@@ -1080,6 +1228,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
text-transform: uppercase;
direction: var(--direction);
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}

View File

@@ -35,7 +35,11 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import { fullEntitiesContext } from "../../data/context";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import {
entityRegistryByEntityId,
entityRegistryById,
@@ -45,6 +49,8 @@ import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../types";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { subscribeFloorRegistry } from "../../data/floor_registry";
declare global {
// for fire event
@@ -379,11 +385,27 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
initialValue: [],
});
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
private _floorsContext = new ContextProvider(this, {
context: floorsContext,
initialValue: [],
});
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entitiesContext.setValue(entities);
}),
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
subscribeFloorRegistry(this.hass.connection!, (floors) => {
this._floorsContext.setValue(floors);
}),
];
}

View File

@@ -1,5 +1,16 @@
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js";
import {
mdiAlertCircle,
mdiChevronRight,
mdiCog,
mdiDotsVertical,
mdiMenuDown,
mdiPencilOff,
mdiPlus,
mdiTag,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import {
CSSResultGroup,
@@ -11,8 +22,9 @@ import {
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { consume } from "@lit-labs/context";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { navigate } from "../../../common/navigate";
import {
@@ -23,22 +35,42 @@ import { extractSearchParam } from "../../../common/url/search-params";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-fab";
import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
subscribeCategoryRegistry,
} from "../../../data/category_registry";
import {
ConfigEntry,
subscribeConfigEntries,
} from "../../../data/config_entries";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { fullEntitiesContext } from "../../../data/context";
import {
EntityRegistryEntry,
UpdateEntityRegistryEntryResult,
subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry,
} from "../../../data/label_registry";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import {
@@ -49,18 +81,15 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { isHelperDomain } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import {
LabelRegistryEntry,
subscribeLabelRegistry,
} from "../../../data/label_registry";
import { fullEntitiesContext } from "../../../data/context";
import "../../../components/ha-filter-labels";
import { haStyle } from "../../../resources/styles";
type HelperItem = {
id: string;
@@ -71,6 +100,7 @@ type HelperItem = {
type: string;
configEntry?: ConfigEntry;
entity?: HassEntity;
category: string | undefined;
label_entries: LabelRegistryEntry[];
};
@@ -111,6 +141,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@state() private _configEntries?: Record<string, ConfigEntry>;
@state() private _selected: string[] = [];
@state() private _activeFilters?: string[];
@state() private _filters: Record<
@@ -120,6 +152,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@state() private _expandedFilter?: string;
@state()
_categories!: CategoryRegistryEntry[];
@state()
_labels!: LabelRegistryEntry[];
@@ -129,6 +164,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@state() private _filteredStateItems?: string[] | null;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
public hassSubscribe() {
return [
subscribeConfigEntries(
@@ -156,65 +195,86 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labels = labels;
}),
subscribeCategoryRegistry(
this.hass.connection,
"helpers",
(categories) => {
this._categories = categories;
}
),
];
}
private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<HelperItem> = {
icon: {
title: "",
label: localize("ui.panel.config.helpers.picker.headers.icon"),
type: "icon",
template: (helper) =>
helper.entity
? html`<ha-state-icon
.hass=${this.hass}
.stateObj=${helper.entity}
></ha-state-icon>`
: html`<ha-svg-icon
.path=${helper.icon}
style="color: var(--error-color)"
></ha-svg-icon>`,
},
name: {
title: localize("ui.panel.config.helpers.picker.headers.name"),
main: true,
sortable: true,
filterable: true,
grows: true,
direction: "asc",
template: (helper) => html`
<div style="font-size: 14px;">${helper.name}</div>
${narrow
? html`<div class="secondary">${helper.entity_id}</div> `
: nothing}
${helper.label_entries.length
? html`
<ha-data-table-labels
.labels=${helper.label_entries}
></ha-data-table-labels>
`
: nothing}
`,
},
};
if (!narrow) {
columns.entity_id = {
title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
sortable: true,
filterable: true,
width: "25%",
};
}
columns.localized_type = {
(
narrow: boolean,
localize: LocalizeFunc
): DataTableColumnContainer<HelperItem> => ({
icon: {
title: "",
label: localize("ui.panel.config.helpers.picker.headers.icon"),
type: "icon",
template: (helper) =>
helper.entity
? html`<ha-state-icon
.hass=${this.hass}
.stateObj=${helper.entity}
></ha-state-icon>`
: html`<ha-svg-icon
.path=${helper.icon}
style="color: var(--error-color)"
></ha-svg-icon>`,
},
name: {
title: localize("ui.panel.config.helpers.picker.headers.name"),
main: true,
sortable: true,
filterable: true,
grows: true,
direction: "asc",
template: (helper) => html`
<div style="font-size: 14px;">${helper.name}</div>
${narrow
? html`<div class="secondary">${helper.entity_id}</div> `
: nothing}
${helper.label_entries.length
? html`
<ha-data-table-labels
.labels=${helper.label_entries}
></ha-data-table-labels>
`
: nothing}
`,
},
entity_id: {
title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
hidden: this.narrow,
sortable: true,
filterable: true,
width: "25%",
},
category: {
title: localize("ui.panel.config.helpers.picker.headers.category"),
hidden: true,
groupable: true,
filterable: true,
sortable: true,
},
labels: {
title: "",
hidden: true,
filterable: true,
template: (helper) =>
helper.label_entries.map((lbl) => lbl.name).join(" "),
},
localized_type: {
title: localize("ui.panel.config.helpers.picker.headers.type"),
sortable: true,
width: "25%",
filterable: true,
groupable: true,
};
columns.editable = {
},
editable: {
title: "",
label: this.hass.localize(
"ui.panel.config.helpers.picker.headers.editable"
@@ -237,9 +297,36 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
`
: ""}
`,
};
return columns;
}
},
actions: {
title: "",
width: "64px",
type: "overflow-menu",
template: (helper) => html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
path: mdiCog,
label: this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
),
action: () => this._openSettings(helper),
},
{
path: mdiTag,
label: this.hass.localize(
`ui.panel.config.automation.picker.${helper.category ? "edit_category" : "assign_category"}`
),
action: () => this._editCategory(helper),
},
]}
>
</ha-icon-overflow-menu>
`,
},
})
);
private _getItems = memoizeOne(
@@ -249,6 +336,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
entityEntries: Record<string, EntityRegistryEntry>,
configEntries: Record<string, ConfigEntry>,
entityReg: EntityRegistryEntry[],
categoryReg?: CategoryRegistryEntry[],
labelReg?: LabelRegistryEntry[],
filteredStateItems?: string[] | null
): HelperItem[] => {
@@ -292,6 +380,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
type: configEntry.domain,
configEntry,
entity: undefined,
selectable: false,
}));
return [...states, ...entries]
@@ -305,6 +394,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
(reg) => reg.entity_id === item.entity_id
);
const labels = labelReg && entityRegEntry?.labels;
const category = entityRegEntry?.categories.helpers;
return {
...item,
localized_type: item.configEntry
@@ -315,6 +405,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
label_entries: (labels || []).map(
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
),
category: category
? categoryReg?.find((cat) => cat.category_id === category)?.name
: undefined,
};
});
}
@@ -330,6 +423,79 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
return html` <hass-loading-screen></hass-loading-screen> `;
}
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item> `;
})}<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div>
</ha-menu-item>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const helpers = this._getItems(
this.hass.localize,
this._stateItems,
this._entityEntries,
this._configEntries,
this._entityReg,
this._categories,
this._labels,
this._filteredStateItems
);
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -337,20 +503,25 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
back-path="/config"
.route=${this.route}
.tabs=${configSections.devices}
.searchLabel=${this.hass.localize(
"ui.panel.config.helpers.picker.search",
{ number: helpers.length }
)}
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
hasFilters
.filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._getItems(
this.hass.localize,
this._stateItems,
this._entityEntries,
this._configEntries,
this._entityReg,
this._labels,
this._filteredStateItems
)}
.data=${helpers}
initialGroupColumn="category"
.activeFilters=${this._activeFilters}
@clear-filter=${this._clearFilter}
@row-click=${this._openEditDialog}
@@ -361,6 +532,26 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
)}
class=${this.narrow ? "narrow" : ""}
>
<ha-filter-floor-areas
.hass=${this.hass}
.type=${"entity"}
.value=${this._filters["ha-filter-floor-areas"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-floor-areas"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-floor-areas>
<ha-filter-devices
.hass=${this.hass}
.type=${"entity"}
.value=${this._filters["ha-filter-devices"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-devices"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-devices>
<ha-filter-labels
.hass=${this.hass}
.value=${this._filters["ha-filter-labels"]?.value}
@@ -370,6 +561,114 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
<ha-filter-categories
.hass=${this.hass}
scope="helpers"
.value=${this._filters["ha-filter-categories"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-categories"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || labelsInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
: nothing}
<ha-integration-overflow-menu
.hass=${this.hass}
@@ -437,6 +736,27 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
}
if (key === "ha-filter-categories" && filter.value?.length) {
const categoryItems: Set<string> = new Set();
this._stateItems
.filter(
(stateItem) =>
filter.value![0] ===
this._entityReg.find(
(reg) => reg.entity_id === stateItem.entity_id
)?.categories.helpers
)
.forEach((stateItem) => categoryItems.add(stateItem.entity_id));
if (!items) {
items = categoryItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
}
}
this._filteredStateItems = items ? [...items] : undefined;
}
@@ -446,6 +766,73 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _editCategory(helper: any) {
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === helper.entity_id
);
if (!entityReg) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.no_category_support"
),
text: this.hass.localize(
"ui.panel.config.automation.picker.no_category_entity_reg"
),
});
return;
}
showAssignCategoryDialog(this, {
scope: "helpers",
entityReg,
});
}
private async _handleBulkCategory(ev) {
const category = ev.currentTarget.value;
this._bulkAddCategory(category);
}
private async _bulkAddCategory(category: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
categories: { helpers: category },
})
);
});
await Promise.all(promises);
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
})
);
});
await Promise.all(promises);
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (this.route.path === "/add") {
@@ -563,20 +950,69 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
}
}
private _openSettings(helper: HelperItem) {
if (helper.entity) {
showMoreInfoDialog(this, {
entityId: helper.entity_id,
view: "settings",
});
} else {
showOptionsFlowDialog(this, helper.configEntry!);
}
}
private _createHelper() {
showHelperDetailDialog(this, {});
}
private async _bulkCreateCategory() {
showCategoryRegistryDetailDialog(this, {
scope: "helpers",
createEntry: async (values) => {
const category = await createCategoryRegistryEntry(
this.hass,
"helpers",
values
);
this._bulkAddCategory(category.category_id);
return category;
},
});
}
private _bulkCreateLabel() {
showLabelDetailDialog(this, {
createEntry: async (values) => {
const label = await createLabelRegistryEntry(this.hass, values);
this._bulkLabel(label.label_id, "add");
return label;
},
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
hass-tabs-subpage-data-table {
--data-table-row-height: 60px;
}
hass-tabs-subpage-data-table.narrow {
--data-table-row-height: 72px;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}

View File

@@ -1,10 +1,15 @@
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import {
mdiChevronRight,
mdiCog,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiHelpCircle,
mdiInformationOutline,
mdiMenuDown,
mdiPalette,
mdiPencilOff,
mdiPlay,
@@ -24,6 +29,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
@@ -33,6 +39,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-button";
@@ -44,18 +51,26 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
subscribeCategoryRegistry,
} from "../../../data/category_registry";
import { fullEntitiesContext } from "../../../data/context";
import { isUnavailableState } from "../../../data/entity";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import {
EntityRegistryEntry,
UpdateEntityRegistryEntryResult,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { forwardHaptic } from "../../../data/haptics";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry,
} from "../../../data/label_registry";
import {
@@ -69,6 +84,7 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
@@ -76,10 +92,13 @@ import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
type SceneItem = SceneEntity & {
name: string;
area: string | undefined;
category: string | undefined;
labels: LabelRegistryEntry[];
};
@@ -98,6 +117,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _selected: string[] = [];
@state() private _activeFilters?: string[];
@state() private _filteredScenes?: string[] | null;
@@ -119,10 +140,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
private _scenes = memoizeOne(
(
scenes: SceneEntity[],
entityReg: EntityRegistryEntry[],
areas: HomeAssistant["areas"],
categoryReg?: CategoryRegistryEntry[],
labelReg?: LabelRegistryEntry[],
filteredScenes?: string[] | null
@@ -143,6 +169,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
return {
...scene,
name: computeStateName(scene),
area: entityRegEntry?.area_id
? areas[entityRegEntry?.area_id]?.name
: undefined,
category: category
? categoryReg?.find((cat) => cat.category_id === category)?.name
: undefined,
@@ -185,6 +214,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing}
`,
},
area: {
title: localize("ui.panel.config.scene.picker.headers.area"),
hidden: true,
groupable: true,
filterable: true,
sortable: true,
},
category: {
title: localize("ui.panel.config.scene.picker.headers.category"),
hidden: true,
@@ -198,14 +234,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
filterable: true,
template: (scene) => scene.labels.map((lbl) => lbl.name).join(" "),
},
};
if (!narrow) {
columns.state = {
state: {
title: localize(
"ui.panel.config.scene.picker.headers.last_activated"
),
sortable: true,
width: "30%",
hidden: narrow,
template: (scene) => {
const lastActivated = scene.state;
if (!lastActivated || isUnavailableState(lastActivated)) {
@@ -220,80 +255,87 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: relativeTime(date, this.hass.locale)}
`;
},
};
}
columns.only_editable = {
title: "",
width: "56px",
template: (scene) =>
!scene.attributes.id
? html`
<simple-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.scene.picker.only_editable"
)}
</simple-tooltip>
<ha-svg-icon
.path=${mdiPencilOff}
style="color: var(--secondary-text-color)"
></ha-svg-icon>
`
: "",
};
columns.actions = {
title: "",
width: "64px",
type: "overflow-menu",
template: (scene) => html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
path: mdiInformationOutline,
label: this.hass.localize(
"ui.panel.config.scene.picker.show_info"
),
action: () => this._showInfo(scene),
},
{
path: mdiPlay,
label: this.hass.localize(
"ui.panel.config.scene.picker.activate"
),
action: () => this._activateScene(scene),
},
{
path: mdiTag,
label: this.hass.localize(
`ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}`
),
action: () => this._editCategory(scene),
},
{
divider: true,
},
{
path: mdiContentDuplicate,
label: this.hass.localize(
"ui.panel.config.scene.picker.duplicate"
),
action: () => this._duplicate(scene),
disabled: !scene.attributes.id,
},
{
label: this.hass.localize(
"ui.panel.config.scene.picker.delete"
),
path: mdiDelete,
action: () => this._deleteConfirm(scene),
warning: scene.attributes.id,
disabled: !scene.attributes.id,
},
]}
>
</ha-icon-overflow-menu>
`,
},
only_editable: {
title: "",
width: "56px",
template: (scene) =>
!scene.attributes.id
? html`
<simple-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.scene.picker.only_editable"
)}
</simple-tooltip>
<ha-svg-icon
.path=${mdiPencilOff}
style="color: var(--secondary-text-color)"
></ha-svg-icon>
`
: "",
},
actions: {
title: "",
width: "64px",
type: "overflow-menu",
template: (scene) => html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
path: mdiInformationOutline,
label: this.hass.localize(
"ui.panel.config.scene.picker.show_info"
),
action: () => this._showInfo(scene),
},
{
path: mdiCog,
label: this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
),
action: () => this._openSettings(scene),
},
{
path: mdiPlay,
label: this.hass.localize(
"ui.panel.config.scene.picker.activate"
),
action: () => this._activateScene(scene),
},
{
path: mdiTag,
label: this.hass.localize(
`ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}`
),
action: () => this._editCategory(scene),
},
{
divider: true,
},
{
path: mdiContentDuplicate,
label: this.hass.localize(
"ui.panel.config.scene.picker.duplicate"
),
action: () => this._duplicate(scene),
disabled: !scene.attributes.id,
},
{
label: this.hass.localize(
"ui.panel.config.scene.picker.delete"
),
path: mdiDelete,
action: () => this._deleteConfirm(scene),
warning: scene.attributes.id,
disabled: !scene.attributes.id,
},
]}
>
</ha-icon-overflow-menu>
`,
},
};
return columns;
@@ -319,6 +361,78 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
}
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const scenes = this._scenes(
this.scenes,
this._entityReg,
this.hass.areas,
this._categories,
this._labels,
this._filteredScenes
);
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -326,20 +440,26 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
back-path="/config"
.route=${this.route}
.tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
"ui.panel.config.scene.picker.search",
{ number: scenes.length }
)}
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
hasFilters
.filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
id="entity_id"
initialGroupColumn="category"
.data=${this._scenes(
this.scenes,
this._entityReg,
this._categories,
this._labels,
this._filteredScenes
)}
.data=${scenes}
.empty=${!this.scenes.length}
.activeFilters=${this._activeFilters}
.noDataText=${this.hass.localize(
@@ -407,6 +527,103 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || labelsInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
: nothing}
${!this.scenes.length
? html`<div class="empty" slot="empty">
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
@@ -553,6 +770,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const scene = this.scenes.find((a) => a.entity_id === ev.detail.id);
@@ -561,6 +784,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
}
}
private async _handleBulkCategory(ev) {
const category = ev.currentTarget.value;
this._bulkAddCategory(category);
}
private async _bulkAddCategory(category: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
categories: { scene: category },
})
);
});
await Promise.all(promises);
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
})
);
});
await Promise.all(promises);
}
private _editCategory(scene: any) {
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === scene.entity_id
@@ -586,6 +849,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
fireEvent(this, "hass-more-info", { entityId: scene.entity_id });
}
private _openSettings(scene: SceneEntity) {
showMoreInfoDialog(this, {
entityId: scene.entity_id,
view: "settings",
});
}
private _activateScene = async (scene: SceneEntity) => {
await activateScene(this.hass, scene.entity_id);
showToast(this, {
@@ -649,10 +919,38 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
});
}
private async _bulkCreateCategory() {
showCategoryRegistryDetailDialog(this, {
scope: "scene",
createEntry: async (values) => {
const category = await createCategoryRegistryEntry(
this.hass,
"scene",
values
);
this._bulkAddCategory(category.category_id);
return category;
},
});
}
private _bulkCreateLabel() {
showLabelDetailDialog(this, {
createEntry: async (values) => {
const label = await createLabelRegistryEntry(this.hass, values);
this._bulkLabel(label.label_id, "add");
return label;
},
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
hass-tabs-subpage-data-table {
--data-table-row-height: 60px;
}
@@ -664,6 +962,16 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
--mdc-icon-size: 80px;
max-width: 500px;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}

View File

@@ -1,9 +1,14 @@
import { consume } from "@lit-labs/context";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import {
mdiChevronRight,
mdiCog,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiHelpCircle,
mdiInformationOutline,
mdiMenuDown,
mdiPlay,
mdiPlus,
mdiScriptText,
@@ -24,6 +29,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
@@ -34,6 +40,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-fab";
@@ -45,16 +52,24 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
subscribeCategoryRegistry,
} from "../../../data/category_registry";
import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import {
EntityRegistryEntry,
UpdateEntityRegistryEntryResult,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry,
} from "../../../data/label_registry";
import {
@@ -70,6 +85,7 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
@@ -78,10 +94,13 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
type ScriptItem = ScriptEntity & {
name: string;
area: string | undefined;
category: string | undefined;
labels: LabelRegistryEntry[];
};
@@ -102,6 +121,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _selected: string[] = [];
@state() private _activeFilters?: string[];
@state() private _filteredScripts?: string[] | null;
@@ -123,10 +144,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
private _scripts = memoizeOne(
(
scripts: ScriptEntity[],
entityReg: EntityRegistryEntry[],
areas: HomeAssistant["areas"],
categoryReg?: CategoryRegistryEntry[],
labelReg?: LabelRegistryEntry[],
filteredScripts?: string[] | null
@@ -149,6 +175,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
return {
...script,
name: computeStateName(script),
area: entityRegEntry?.area_id
? areas[entityRegEntry?.area_id]?.name
: undefined,
last_triggered: script.attributes.last_triggered || undefined,
category: category
? categoryReg?.find((cat) => cat.category_id === category)?.name
@@ -214,6 +243,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
`;
},
},
area: {
title: localize("ui.panel.config.script.picker.headers.area"),
hidden: true,
groupable: true,
filterable: true,
sortable: true,
},
category: {
title: localize("ui.panel.config.script.picker.headers.category"),
hidden: true,
@@ -227,9 +263,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
filterable: true,
template: (script) => script.labels.map((lbl) => lbl.name).join(" "),
},
};
if (!narrow) {
columns.last_triggered = {
last_triggered: {
hidden: narrow,
sortable: true,
width: "40%",
title: localize("ui.card.automation.last_triggered"),
@@ -249,66 +284,74 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: this.hass.localize("ui.components.relative_time.never")}
`;
},
};
}
columns.actions = {
title: "",
width: "64px",
type: "overflow-menu",
template: (script) => html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
path: mdiInformationOutline,
label: this.hass.localize(
"ui.panel.config.script.picker.show_info"
),
action: () => this._showInfo(script),
},
{
path: mdiTag,
label: this.hass.localize(
`ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}`
),
action: () => this._editCategory(script),
},
{
path: mdiPlay,
label: this.hass.localize("ui.panel.config.script.picker.run"),
action: () => this._runScript(script),
},
{
path: mdiTransitConnection,
label: this.hass.localize(
"ui.panel.config.script.picker.show_trace"
),
action: () => this._showTrace(script),
},
{
divider: true,
},
{
path: mdiContentDuplicate,
label: this.hass.localize(
"ui.panel.config.script.picker.duplicate"
),
action: () => this._duplicate(script),
},
{
label: this.hass.localize(
"ui.panel.config.script.picker.delete"
),
path: mdiDelete,
action: () => this._deleteConfirm(script),
warning: true,
},
]}
>
</ha-icon-overflow-menu>
`,
},
actions: {
title: "",
width: "64px",
type: "overflow-menu",
template: (script) => html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
path: mdiInformationOutline,
label: this.hass.localize(
"ui.panel.config.script.picker.show_info"
),
action: () => this._showInfo(script),
},
{
path: mdiCog,
label: this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
),
action: () => this._openSettings(script),
},
{
path: mdiTag,
label: this.hass.localize(
`ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}`
),
action: () => this._editCategory(script),
},
{
path: mdiPlay,
label: this.hass.localize(
"ui.panel.config.script.picker.run"
),
action: () => this._runScript(script),
},
{
path: mdiTransitConnection,
label: this.hass.localize(
"ui.panel.config.script.picker.show_trace"
),
action: () => this._showTrace(script),
},
{
divider: true,
},
{
path: mdiContentDuplicate,
label: this.hass.localize(
"ui.panel.config.script.picker.duplicate"
),
action: () => this._duplicate(script),
},
{
label: this.hass.localize(
"ui.panel.config.script.picker.delete"
),
path: mdiDelete,
action: () => this._deleteConfirm(script),
warning: true,
},
]}
>
</ha-icon-overflow-menu>
`,
},
};
return columns;
@@ -331,6 +374,77 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div> </ha-menu-item
><md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
reducedTouchTarget
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const scripts = this._scripts(
this.scripts,
this._entityReg,
this.hass.areas,
this._categories,
this._labels,
this._filteredScripts
);
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -338,23 +452,29 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
back-path="/config"
.route=${this.route}
.tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
"ui.panel.config.script.picker.search",
{ number: scripts.length }
)}
hasFilters
initialGroupColumn="category"
.filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
.columns=${this._columns(
this.narrow,
this.hass.localize,
this.hass.locale
)}
.data=${this._scripts(
this.scripts,
this._entityReg,
this._categories,
this._labels,
this._filteredScripts
)}
.data=${scripts}
.empty=${!this.scripts.length}
.activeFilters=${this._activeFilters}
id="entity_id"
@@ -432,6 +552,104 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-blueprints>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || labelsInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
: nothing}
${!this.scripts.length
? html` <div class="empty" slot="empty">
<ha-svg-icon .path=${mdiScriptText}></ha-svg-icon>
@@ -629,6 +847,52 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
});
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private async _handleBulkCategory(ev) {
const category = ev.currentTarget.value;
this._bulkAddCategory(category);
}
private async _bulkAddCategory(category: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
categories: { script: category },
})
);
});
await Promise.all(promises);
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
})
);
});
await Promise.all(promises);
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const entry = this.entityRegistry.find((e) => e.entity_id === ev.detail.id);
if (entry) {
@@ -665,6 +929,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "hass-more-info", { entityId: script.entity_id });
}
private _openSettings(script: any) {
showMoreInfoDialog(this, {
entityId: script.entity_id,
view: "settings",
});
}
private _showTrace(script: any) {
const entry = this.entityRegistry.find(
(e) => e.entity_id === script.entity_id
@@ -764,10 +1035,38 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}
}
private async _bulkCreateCategory() {
showCategoryRegistryDetailDialog(this, {
scope: "script",
createEntry: async (values) => {
const category = await createCategoryRegistryEntry(
this.hass,
"script",
values
);
this._bulkAddCategory(category.category_id);
return category;
},
});
}
private _bulkCreateLabel() {
showLabelDetailDialog(this, {
createEntry: async (values) => {
const label = await createLabelRegistryEntry(this.hass, values);
this._bulkLabel(label.label_id, "add");
return label;
},
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
hass-tabs-subpage-data-table {
--data-table-row-height: 60px;
}
@@ -782,6 +1081,16 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
--mdc-icon-size: 80px;
max-width: 500px;
}
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`,
];
}

View File

@@ -9,6 +9,7 @@ import { property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { storage } from "../../common/decorators/storage";
import { computeDomain } from "../../common/entity/compute_domain";
import { navigate } from "../../common/navigate";
import { constructUrlCurrentPath } from "../../common/url/construct-url";
import {
@@ -27,37 +28,29 @@ import "../../components/ha-menu-button";
import "../../components/ha-target-picker";
import "../../components/ha-top-app-bar-fixed";
import {
AreaDeviceLookup,
AreaEntityLookup,
getAreaDeviceLookup,
getAreaEntityLookup,
} from "../../data/area_registry";
import {
DeviceEntityLookup,
getDeviceEntityLookup,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import {
HistoryResult,
computeHistory,
subscribeHistory,
HistoryStates,
EntityHistoryState,
HistoryResult,
HistoryStates,
LineChartState,
LineChartUnit,
computeGroupKey,
LineChartState,
computeHistory,
subscribeHistory,
} from "../../data/history";
import { fetchStatistics, Statistics } from "../../data/recorder";
import { Statistics, fetchStatistics } from "../../data/recorder";
import {
expandAreaTarget,
expandDeviceTarget,
expandFloorTarget,
expandLabelTarget,
} from "../../data/selector";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { fileDownload } from "../../util/file_download";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { computeDomain } from "../../common/entity/compute_domain";
class HaPanelHistory extends SubscribeMixin(LitElement) {
class HaPanelHistory extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
@property({ reflect: true, type: Boolean }) public narrow = false;
@@ -83,12 +76,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
@state() private _statisticsHistory?: HistoryResult;
@state() private _deviceEntityLookup?: DeviceEntityLookup;
@state() private _areaEntityLookup?: AreaEntityLookup;
@state() private _areaDeviceLookup?: AreaDeviceLookup;
@state()
private _showBack?: boolean;
@@ -123,18 +110,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
this._unsubscribeHistory();
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._deviceEntityLookup = getDeviceEntityLookup(entities);
this._areaEntityLookup = getAreaEntityLookup(entities);
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._areaDeviceLookup = getAreaDeviceLookup(devices);
}),
];
}
private _goBack(): void {
history.back();
}
@@ -332,7 +307,9 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
const entityIds = searchParams.entity_id;
const deviceIds = searchParams.device_id;
const areaIds = searchParams.area_id;
if (entityIds || deviceIds || areaIds) {
const floorIds = searchParams.floor_id;
const labelsIds = searchParams.label_id;
if (entityIds || deviceIds || areaIds || floorIds || labelsIds) {
this._targetPickerValue = {};
}
if (entityIds) {
@@ -347,6 +324,14 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
const splitIds = areaIds.split(",");
this._targetPickerValue!.area_id = splitIds;
}
if (floorIds) {
const splitIds = floorIds.split(",");
this._targetPickerValue!.floor_id = splitIds;
}
if (labelsIds) {
const splitIds = labelsIds.split(",");
this._targetPickerValue!.label_id = splitIds;
}
const startDate = searchParams.start_date;
if (startDate) {
@@ -522,95 +507,77 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
private _getEntityIds(): string[] {
return this.__getEntityIds(
this._targetPickerValue,
this._deviceEntityLookup,
this._areaEntityLookup,
this._areaDeviceLookup
this.hass.entities,
this.hass.devices,
this.hass.areas
);
}
private __getEntityIds = memoizeOne(
(
targetPickerValue: HassServiceTarget,
deviceEntityLookup: DeviceEntityLookup | undefined,
areaEntityLookup: AreaEntityLookup | undefined,
areaDeviceLookup: AreaDeviceLookup | undefined
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"]
): string[] => {
if (
!targetPickerValue ||
deviceEntityLookup === undefined ||
areaEntityLookup === undefined ||
areaDeviceLookup === undefined
) {
if (!targetPickerValue) {
return [];
}
const entityIds = new Set<string>();
let {
area_id: searchingAreaId,
device_id: searchingDeviceId,
entity_id: searchingEntityId,
} = targetPickerValue;
const targetSelector = { target: {} };
const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
const targetAreas = new Set(ensureArray(targetPickerValue.area_id));
const targetFloors = new Set(ensureArray(targetPickerValue.floor_id));
const targetLabels = new Set(ensureArray(targetPickerValue.label_id));
if (searchingAreaId) {
searchingAreaId = ensureArray(searchingAreaId);
for (const singleSearchingAreaId of searchingAreaId) {
const foundEntities = areaEntityLookup[singleSearchingAreaId];
if (foundEntities?.length) {
for (const foundEntity of foundEntities) {
if (foundEntity.entity_category === null) {
entityIds.add(foundEntity.entity_id);
}
}
}
targetLabels.forEach((labelId) => {
const expanded = expandLabelTarget(
this.hass,
labelId,
areas,
devices,
entities,
targetSelector
);
expanded.devices.forEach((id) => targetDevices.add(id));
expanded.entities.forEach((id) => targetEntities.add(id));
expanded.areas.forEach((id) => targetAreas.add(id));
});
const foundDevices = areaDeviceLookup[singleSearchingAreaId];
if (!foundDevices?.length) {
continue;
}
targetFloors.forEach((floorId) => {
const expanded = expandFloorTarget(
this.hass,
floorId,
areas,
targetSelector
);
expanded.areas.forEach((id) => targetAreas.add(id));
});
for (const foundDevice of foundDevices) {
const foundDeviceEntities = deviceEntityLookup[foundDevice.id];
if (!foundDeviceEntities?.length) {
continue;
}
targetAreas.forEach((areaId) => {
const expanded = expandAreaTarget(
this.hass,
areaId,
devices,
entities,
targetSelector
);
expanded.devices.forEach((id) => targetDevices.add(id));
expanded.entities.forEach((id) => targetEntities.add(id));
});
for (const foundDeviceEntity of foundDeviceEntities) {
if (
(!foundDeviceEntity.area_id ||
foundDeviceEntity.area_id === singleSearchingAreaId) &&
foundDeviceEntity.entity_category === null
) {
entityIds.add(foundDeviceEntity.entity_id);
}
}
}
}
}
targetDevices.forEach((deviceId) => {
const expanded = expandDeviceTarget(
this.hass,
deviceId,
entities,
targetSelector
);
expanded.entities.forEach((id) => targetEntities.add(id));
});
if (searchingDeviceId) {
searchingDeviceId = ensureArray(searchingDeviceId);
for (const singleSearchingDeviceId of searchingDeviceId) {
const foundEntities = deviceEntityLookup[singleSearchingDeviceId];
if (!foundEntities?.length) {
continue;
}
for (const foundEntity of foundEntities) {
if (foundEntity.entity_category === null) {
entityIds.add(foundEntity.entity_id);
}
}
}
}
if (searchingEntityId) {
searchingEntityId = ensureArray(searchingEntityId);
for (const singleSearchingEntityId of searchingEntityId) {
entityIds.add(singleSearchingEntityId);
}
}
return [...entityIds];
return Array.from(targetEntities);
}
);
@@ -639,6 +606,12 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
","
);
}
if (this._targetPickerValue.label_id) {
params.label_id = ensureArray(this._targetPickerValue.label_id).join(",");
}
if (this._targetPickerValue.floor_id) {
params.floor_id = ensureArray(this._targetPickerValue.floor_id).join(",");
}
if (this._targetPickerValue.area_id) {
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
}

View File

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

View File

@@ -58,18 +58,12 @@ export interface AndCondition extends BaseCondition {
function getValueFromEntityId(
hass: HomeAssistant,
value: string | string[]
): string | string[] {
if (
typeof value === "string" &&
isValidEntityId(value) &&
hass.states[value]
) {
value = hass.states[value]?.state;
} else if (Array.isArray(value)) {
value = value.map((v) => getValueFromEntityId(hass, v) as string);
value: string
): string | undefined {
if (isValidEntityId(value) && hass.states[value]) {
return hass.states[value]?.state;
}
return value;
return undefined;
}
function checkStateCondition(
@@ -83,8 +77,17 @@ function checkStateCondition(
let value = condition.state ?? condition.state_not;
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
if (Array.isArray(value) || typeof value === "string") {
value = getValueFromEntityId(hass, value);
if (Array.isArray(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
@@ -103,10 +106,10 @@ function checkStateNumericCondition(
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
if (typeof above === "string") {
above = getValueFromEntityId(hass, above) as string;
above = getValueFromEntityId(hass, above) ?? above;
}
if (typeof below === "string") {
below = getValueFromEntityId(hass, below) as string;
below = getValueFromEntityId(hass, below) ?? below;
}
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.closeDialog();
}
private _showRawConfigEditor() {
private _showRawConfigEditor(ev) {
ev.stopPropagation();
this._params!.showRawConfigEditor();
this.closeDialog();
}

View File

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

View File

@@ -129,7 +129,7 @@ export class HaStateControlAlarmControlPanelModes extends LitElement {
max-height: max(320px, var(--modes-count, 1) * 80px);
min-height: max(200px, var(--modes-count, 1) * 80px);
--control-select-thickness: 130px;
--control-select-border-radius: 48px;
--control-select-border-radius: 36px;
--control-select-color: var(--primary-color);
--control-select-background: var(--disabled-color);
--control-select-background-opacity: 0.2;

View File

@@ -75,7 +75,7 @@ export class HaStateControlCoverPosition extends LitElement {
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 130px;
--control-slider-border-radius: 48px;
--control-slider-border-radius: 36px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;

View File

@@ -112,7 +112,7 @@ export class HaStateControlInfoCoverTiltPosition extends LitElement {
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 130px;
--control-slider-border-radius: 48px;
--control-slider-border-radius: 36px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;

View File

@@ -142,7 +142,7 @@ export class HaStateControlCoverToggle extends LitElement {
max-height: 320px;
min-height: 200px;
--control-switch-thickness: 130px;
--control-switch-border-radius: 48px;
--control-switch-border-radius: 36px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
@@ -159,7 +159,7 @@ export class HaStateControlCoverToggle extends LitElement {
ha-control-button {
flex: 1;
width: 100%;
--control-button-border-radius: 48px;
--control-button-border-radius: 36px;
--mdc-icon-size: 24px;
}
ha-control-button.active {

View File

@@ -142,7 +142,7 @@ export class HaStateControlFanSpeed extends LitElement {
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 130px;
--control-slider-border-radius: 48px;
--control-slider-border-radius: 36px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;
@@ -153,7 +153,7 @@ export class HaStateControlFanSpeed extends LitElement {
max-height: 320px;
min-height: 200px;
--control-select-thickness: 130px;
--control-select-border-radius: 48px;
--control-select-border-radius: 36px;
--control-select-color: var(--primary-color);
--control-select-background: var(--disabled-color);
--control-select-background-opacity: 0.2;

View File

@@ -133,7 +133,7 @@ export class HaStateControlToggle extends LitElement {
max-height: 320px;
min-height: 200px;
--control-switch-thickness: 130px;
--control-switch-border-radius: 48px;
--control-switch-border-radius: 36px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
@@ -150,7 +150,7 @@ export class HaStateControlToggle extends LitElement {
ha-control-button {
flex: 1;
width: 100%;
--control-button-border-radius: 48px;
--control-button-border-radius: 36px;
--mdc-icon-size: 24px;
}
ha-control-button.active {

View File

@@ -89,7 +89,7 @@ export class HaStateControlLightBrightness extends LitElement {
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 130px;
--control-slider-border-radius: 48px;
--control-slider-border-radius: 36px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;

View File

@@ -167,7 +167,7 @@ export class HaStateControlLockToggle extends LitElement {
max-height: 320px;
min-height: 200px;
--control-switch-thickness: 130px;
--control-switch-border-radius: 48px;
--control-switch-border-radius: 36px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
@@ -187,7 +187,7 @@ export class HaStateControlLockToggle extends LitElement {
ha-control-button {
flex: 1;
width: 100%;
--control-button-border-radius: 48px;
--control-button-border-radius: 36px;
--mdc-icon-size: 24px;
}
ha-control-button.active {

View File

@@ -71,7 +71,7 @@ export class HaStateControlValvePosition extends LitElement {
max-height: 320px;
min-height: 200px;
--control-slider-thickness: 130px;
--control-slider-border-radius: 48px;
--control-slider-border-radius: 36px;
--control-slider-color: var(--primary-color);
--control-slider-background: var(--disabled-color);
--control-slider-background-opacity: 0.2;

View File

@@ -142,7 +142,7 @@ export class HaStateControlValveToggle extends LitElement {
max-height: 320px;
min-height: 200px;
--control-switch-thickness: 130px;
--control-switch-border-radius: 48px;
--control-switch-border-radius: 36px;
--control-switch-padding: 6px;
--mdc-icon-size: 24px;
}
@@ -159,7 +159,7 @@ export class HaStateControlValveToggle extends LitElement {
ha-control-button {
flex: 1;
width: 100%;
--control-button-border-radius: 48px;
--control-button-border-radius: 36px;
--mdc-icon-size: 24px;
}
ha-control-button.active {

View File

@@ -501,6 +501,7 @@
},
"subpage-data-table": {
"filters": "Filters",
"show_results": "show {number} results",
"clear_filter": "Clear filter",
"close_filter": "Close filters",
"exit_selection_mode": "Exit selection mode",
@@ -1927,7 +1928,10 @@
"aliases_section": "Aliases",
"no_aliases": "No configured aliases",
"configured_aliases": "{count} configured {count, plural,\n one {alias}\n other {aliases}\n}",
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor."
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor.",
"areas_section": "Areas",
"areas_description": "Specify the areas that are on this floor.",
"add_area": "Add area"
}
},
"category": {
@@ -1962,6 +1966,7 @@
"color": "Color"
},
"add_label": "Add label",
"manage_labels": "Manage 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.",
"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.",
@@ -2262,10 +2267,12 @@
"name": "Name",
"entity_id": "Entity ID",
"type": "Type",
"editable": "Editable"
"editable": "Editable",
"category": "Category"
},
"create_helper": "Create helper",
"no_helpers": "Looks like you don't have any helpers yet!"
"no_helpers": "Looks like you don't have any helpers yet!",
"search": "Search {number} helpers"
},
"dialog": {
"create": "Create",
@@ -2679,13 +2686,15 @@
"assign_category": "Assign category",
"no_category_support": "You can't assign an category to this automation",
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
"search": "Search {number} automations",
"headers": {
"toggle": "Enable/disable",
"name": "Name",
"trigger": "Trigger",
"actions": "Actions",
"state": "State",
"category": "Category"
"category": "Category",
"area": "Area"
},
"bulk_action": "Action",
"bulk_actions": {
@@ -3235,7 +3244,9 @@
"target_template": "templated {name}",
"target_unknown_entity": "unknown entity",
"target_unknown_device": "unknown device",
"target_unknown_area": "unknown area"
"target_unknown_area": "unknown area",
"target_unknown_floor": "unknown floor",
"target_unknown_label": "unknown label"
}
},
"play_media": {
@@ -3559,7 +3570,8 @@
"headers": {
"name": "Name",
"state": "State",
"category": "Category"
"category": "Category",
"area": "Area"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
@@ -3568,7 +3580,8 @@
"delete": "[%key:ui::common::delete%]",
"duplicate": "[%key:ui::common::duplicate%]",
"empty_header": "Create your first script",
"empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay."
"empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay.",
"search": "Search {number} scripts"
},
"dialog_new": {
"header": "Create script",
@@ -3668,14 +3681,16 @@
"state": "State",
"name": "Name",
"last_activated": "Last activated",
"category": "Category"
"category": "Category",
"area": "Area"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
"no_category_support": "You can't assign an category to this scene",
"no_category_entity_reg": "To assign an category to an scene it needs to have a unique ID.",
"empty_header": "Create your first scene",
"empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV."
"empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV.",
"search": "Search {number} scenes"
},
"editor": {
"default_name": "New scene",
@@ -4000,7 +4015,7 @@
"confirm_delete": "Are you sure you want to delete this device?",
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": {
"search": "Search devices",
"search": "Search {number} devices",
"state": "State"
}
},
@@ -4011,7 +4026,7 @@
"header": "Entities",
"introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.",
"introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant.",
"search": "Search entities",
"search": "Search {number} entities",
"unnamed_entity": "Unnamed entity",
"status": {
"restored": "Restored",
@@ -4052,6 +4067,9 @@
"button": "Hide selected",
"confirm_title": "Do you want to hide {number} {number, plural,\n one {entity}\n other {entities}\n}?",
"confirm_text": "Hidden entities will not be shown on your dashboard. Their history is still tracked and you can still interact with them with services."
},
"unhide_selected": {
"button": "Unhide selected"
}
}
},