mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-14 11:49:26 +00:00
Compare commits
61 Commits
fix-menu-o
...
20240404.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e96aca90fe | ||
![]() |
0580a31961 | ||
![]() |
5c42c5130c | ||
![]() |
72d1e37a23 | ||
![]() |
61c9072a08 | ||
![]() |
08b25f9c2a | ||
![]() |
1a03b49700 | ||
![]() |
2d4a8e2e45 | ||
![]() |
8486377604 | ||
![]() |
3a4e9b6856 | ||
![]() |
5f5ac5419b | ||
![]() |
92b7a3b477 | ||
![]() |
4326519a3f | ||
![]() |
00837acdfc | ||
![]() |
7704be12b1 | ||
![]() |
712ddb531b | ||
![]() |
d52afc3f71 | ||
![]() |
92f6083e0b | ||
![]() |
5751fdbe56 | ||
![]() |
962b30adb9 | ||
![]() |
3b5b3f3bb6 | ||
![]() |
1a6d96cf3a | ||
![]() |
034fd9b4df | ||
![]() |
eb79a1e7d7 | ||
![]() |
e25d4f17aa | ||
![]() |
ccde9cceee | ||
![]() |
578d3c4260 | ||
![]() |
bfdc9a3d86 | ||
![]() |
5315545a4d | ||
![]() |
82a3b9d80f | ||
![]() |
3de985a3b8 | ||
![]() |
567ee8000d | ||
![]() |
03939001b2 | ||
![]() |
30d18050d1 | ||
![]() |
95caf8c7df | ||
![]() |
6c1f328d71 | ||
![]() |
bb20ab8c2c | ||
![]() |
29eb73176a | ||
![]() |
17ad3a87f3 | ||
![]() |
ed7c9c33b9 | ||
![]() |
59b66219cb | ||
![]() |
1e2c1d1464 | ||
![]() |
5b86b1277f | ||
![]() |
41fdf31e34 | ||
![]() |
9bef5c2af9 | ||
![]() |
ed1a69071b | ||
![]() |
56d328b4db | ||
![]() |
33c7e0fa2d | ||
![]() |
4f1cf1110f | ||
![]() |
a434bfd944 | ||
![]() |
21ed8e4206 | ||
![]() |
169d782580 | ||
![]() |
8a015f4e38 | ||
![]() |
cbb08c6202 | ||
![]() |
6301bc713c | ||
![]() |
a5d7043ce4 | ||
![]() |
d3bf0da289 | ||
![]() |
fd06d434f2 | ||
![]() |
d24d29e42f | ||
![]() |
e02a47a16a | ||
![]() |
795c16a941 |
@@ -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",
|
||||
|
@@ -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>
|
||||
`
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -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) => {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
.
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -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 : "",
|
||||
|
@@ -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;
|
||||
|
@@ -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) => {
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
40
src/components/ha-outlined-field.ts
Normal file
40
src/components/ha-outlined-field.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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}
|
||||
|
36
src/components/ha-tree-indicator.ts
Normal file
36
src/components/ha-tree-indicator.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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");
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ export type ItemType =
|
||||
| "entity"
|
||||
| "floor"
|
||||
| "group"
|
||||
| "label"
|
||||
| "scene"
|
||||
| "script"
|
||||
| "automation_blueprint"
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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>;
|
||||
}
|
||||
|
||||
|
@@ -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"),
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 : "",
|
||||
|
@@ -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,
|
||||
];
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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(",");
|
||||
}
|
||||
|
@@ -35,6 +35,7 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"automation",
|
||||
"configurator",
|
||||
"conversation",
|
||||
"device_tracker",
|
||||
"geo_location",
|
||||
"persistent_notification",
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -116,6 +116,9 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
||||
entities: {
|
||||
redirect: "/config/entities",
|
||||
},
|
||||
labels: {
|
||||
redirect: "/config/labels",
|
||||
},
|
||||
energy: {
|
||||
component: "energy",
|
||||
redirect: "/energy",
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user