mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-15 20:29: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: {
|
resolve: {
|
||||||
extensions: [".ts", ".js", ".json"],
|
extensions: [".ts", ".js", ".json"],
|
||||||
alias: {
|
alias: {
|
||||||
|
"lit/static-html$": "lit/static-html.js",
|
||||||
"lit/decorators$": "lit/decorators.js",
|
"lit/decorators$": "lit/decorators.js",
|
||||||
"lit/directive$": "lit/directive.js",
|
"lit/directive$": "lit/directive.js",
|
||||||
"lit/directives/until$": "lit/directives/until.js",
|
"lit/directives/until$": "lit/directives/until.js",
|
||||||
|
@@ -136,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
<div class="action">
|
<div class="action">
|
||||||
<span>
|
<span>
|
||||||
${this._action
|
${this._action
|
||||||
? describeAction(this.hass, [], this._action)
|
? describeAction(this.hass, [], [], [], this._action)
|
||||||
: "<invalid YAML>"}
|
: "<invalid YAML>"}
|
||||||
</span>
|
</span>
|
||||||
<ha-yaml-editor
|
<ha-yaml-editor
|
||||||
@@ -149,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
${ACTIONS.map(
|
${ACTIONS.map(
|
||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<span>${describeAction(this.hass, [], conf as any)}</span>
|
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${dump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
|
|||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
--control-select-color: var(--state-fan-active-color);
|
--control-select-color: var(--state-fan-active-color);
|
||||||
--control-select-thickness: 130px;
|
--control-select-thickness: 130px;
|
||||||
--control-select-border-radius: 48px;
|
--control-select-border-radius: 36px;
|
||||||
}
|
}
|
||||||
.vertical-selects {
|
.vertical-selects {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
|
|||||||
--control-slider-background: #ffcf4c;
|
--control-slider-background: #ffcf4c;
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 48px;
|
--control-slider-border-radius: 36px;
|
||||||
}
|
}
|
||||||
.vertical-sliders {
|
.vertical-sliders {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
|
|||||||
--control-switch-on-color: var(--green-color);
|
--control-switch-on-color: var(--green-color);
|
||||||
--control-switch-off-color: var(--red-color);
|
--control-switch-off-color: var(--red-color);
|
||||||
--control-switch-thickness: 130px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 48px;
|
--control-switch-border-radius: 36px;
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20240402.0"
|
version = "20240404.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@@ -45,8 +45,8 @@ export class HaAssistChip extends MdAssistChip {
|
|||||||
margin-inline-start: var(--_icon-label-space);
|
margin-inline-start: var(--_icon-label-space);
|
||||||
}
|
}
|
||||||
::before {
|
::before {
|
||||||
background: var(--ha-assist-chip-container-color);
|
background: var(--ha-assist-chip-container-color, transparent);
|
||||||
opacity: var(--ha-assist-chip-container-opacity);
|
opacity: var(--ha-assist-chip-container-opacity, 1);
|
||||||
}
|
}
|
||||||
:where(.active)::before {
|
:where(.active)::before {
|
||||||
background: var(--ha-assist-chip-active-container-color);
|
background: var(--ha-assist-chip-active-container-color);
|
||||||
|
@@ -33,6 +33,7 @@ import "../ha-svg-icon";
|
|||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
import { groupBy } from "../../common/util/group-by";
|
import { groupBy } from "../../common/util/group-by";
|
||||||
|
import { stringCompare } from "../../common/string/compare";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@@ -529,7 +530,13 @@ export class HaDataTable extends LitElement {
|
|||||||
const sorted: {
|
const sorted: {
|
||||||
[key: string]: DataTableRowData[];
|
[key: string]: DataTableRowData[];
|
||||||
} = Object.keys(grouped)
|
} = Object.keys(grouped)
|
||||||
.sort()
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||||
|
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
)
|
||||||
.reduce((obj, key) => {
|
.reduce((obj, key) => {
|
||||||
obj[key] = grouped[key];
|
obj[key] = grouped[key];
|
||||||
return obj;
|
return obj;
|
||||||
|
@@ -11,10 +11,10 @@ import {
|
|||||||
} from "../common/datetime/localize_date";
|
} from "../common/datetime/localize_date";
|
||||||
import { mainWindow } from "../common/dom/get_main_window";
|
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({
|
const CustomDateRangePicker = Vue.extend({
|
||||||
mixins: [DateRangePicker],
|
mixins: [DateRangePicker],
|
||||||
methods: {
|
methods: {
|
||||||
|
// Set the current date to the left picker instead of the right picker because the right is hidden
|
||||||
selectMonthDate() {
|
selectMonthDate() {
|
||||||
const dt: Date = this.end || new Date();
|
const dt: Date = this.end || new Date();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -23,6 +23,33 @@ const CustomDateRangePicker = Vue.extend({
|
|||||||
month: dt.getMonth() + 1,
|
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 { mdiTextureBox } from "@mdi/js";
|
||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
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 { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
ScorableTextItem,
|
ScorableTextItem,
|
||||||
fuzzyFilterSort,
|
fuzzyFilterSort,
|
||||||
} from "../common/string/filter/sequence-matching";
|
} from "../common/string/filter/sequence-matching";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import { AreaRegistryEntry } from "../data/area_registry";
|
import { AreaRegistryEntry } from "../data/area_registry";
|
||||||
import {
|
import {
|
||||||
DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
@@ -32,6 +34,7 @@ import "./ha-floor-icon";
|
|||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
|
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
|
||||||
|
|
||||||
@@ -41,28 +44,11 @@ interface FloorAreaEntry {
|
|||||||
icon: string | null;
|
icon: string | null;
|
||||||
strings: string[];
|
strings: string[];
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
hasFloor?: boolean;
|
|
||||||
level: number | null;
|
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")
|
@customElement("ha-area-floor-picker")
|
||||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -151,6 +137,44 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
await this.comboBox?.focus();
|
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(
|
private _getAreas = memoizeOne(
|
||||||
(
|
(
|
||||||
floors: FloorRegistryEntry[],
|
floors: FloorRegistryEntry[],
|
||||||
@@ -364,7 +388,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
output.push(
|
output.push(
|
||||||
...floorAreas.map((area) => ({
|
...floorAreas.map((area, index, array) => ({
|
||||||
id: area.area_id,
|
id: area.area_id,
|
||||||
type: "area" as const,
|
type: "area" as const,
|
||||||
name: area.name,
|
name: area.name,
|
||||||
@@ -372,6 +396,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
strings: [area.area_id, ...area.aliases, area.name],
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
hasFloor: true,
|
hasFloor: true,
|
||||||
level: null,
|
level: null,
|
||||||
|
lastArea: index === array.length - 1,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -445,7 +470,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
.placeholder=${this.placeholder
|
.placeholder=${this.placeholder
|
||||||
? this.hass.areas[this.placeholder]?.name
|
? this.hass.areas[this.placeholder]?.name
|
||||||
: undefined}
|
: undefined}
|
||||||
.renderer=${rowRenderer}
|
.renderer=${this._rowRenderer}
|
||||||
@filter-changed=${this._filterChanged}
|
@filter-changed=${this._filterChanged}
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._areaChanged}
|
@value-changed=${this._areaChanged}
|
||||||
|
@@ -428,6 +428,8 @@ export class HaAreaPicker extends LitElement {
|
|||||||
|
|
||||||
(ev.target as any).value = this._value;
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
|
||||||
showAreaRegistryDetailDialog(this, {
|
showAreaRegistryDetailDialog(this, {
|
||||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
import { SelectedDetail } from "@material/mwc-list";
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
import "@material/mwc-menu/mwc-menu-surface";
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
|
||||||
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@customElement("ha-filter-blueprints")
|
@customElement("ha-filter-blueprints")
|
||||||
export class HaFilterBlueprints extends LitElement {
|
export class HaFilterBlueprints extends LitElement {
|
||||||
@@ -35,7 +36,11 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>`
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._blueprints && this._shouldRender
|
${this._blueprints && this._shouldRender
|
||||||
@@ -128,6 +133,15 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -147,6 +161,10 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@@ -2,6 +2,7 @@ import { ActionDetail, SelectedDetail } from "@material/mwc-list";
|
|||||||
import {
|
import {
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
|
mdiFilterVariantRemove,
|
||||||
mdiPencil,
|
mdiPencil,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiTag,
|
mdiTag,
|
||||||
@@ -68,7 +69,11 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.category.caption")}
|
${this.hass.localize("ui.panel.config.category.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>`
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
@@ -254,6 +259,15 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -274,6 +288,10 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -13,10 +14,11 @@ import { stringCompare } from "../common/string/compare";
|
|||||||
import { computeDeviceName } from "../data/device_registry";
|
import { computeDeviceName } from "../data/device_registry";
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-expansion-panel";
|
|
||||||
import "./ha-check-list-item";
|
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./search-input-outlined";
|
||||||
|
|
||||||
@customElement("ha-filter-devices")
|
@customElement("ha-filter-devices")
|
||||||
export class HaFilterDevices extends LitElement {
|
export class HaFilterDevices extends LitElement {
|
||||||
@@ -32,6 +34,8 @@ export class HaFilterDevices extends LitElement {
|
|||||||
|
|
||||||
@state() private _shouldRender = false;
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
public willUpdate(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.willUpdate(properties);
|
||||||
|
|
||||||
@@ -51,19 +55,33 @@ export class HaFilterDevices extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>`
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
? html`<mwc-list class="ha-scrollbar">
|
? html`<search-input-outlined
|
||||||
<lit-virtualizer
|
.hass=${this.hass}
|
||||||
.items=${this._devices(this.hass.devices, this.value)}
|
.filter=${this._filter}
|
||||||
.keyFunction=${this._keyFunction}
|
@value-changed=${this._handleSearchChange}
|
||||||
.renderItem=${this._renderItem}
|
|
||||||
@click=${this._handleItemClick}
|
|
||||||
>
|
>
|
||||||
</lit-virtualizer>
|
</search-input-outlined>
|
||||||
</mwc-list>`
|
<mwc-list class="ha-scrollbar">
|
||||||
|
<lit-virtualizer
|
||||||
|
.items=${this._devices(
|
||||||
|
this.hass.devices,
|
||||||
|
this._filter || "",
|
||||||
|
this.value
|
||||||
|
)}
|
||||||
|
.keyFunction=${this._keyFunction}
|
||||||
|
.renderItem=${this._renderItem}
|
||||||
|
@click=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
</lit-virtualizer>
|
||||||
|
</mwc-list>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
@@ -72,12 +90,14 @@ export class HaFilterDevices extends LitElement {
|
|||||||
private _keyFunction = (device) => device?.id;
|
private _keyFunction = (device) => device?.id;
|
||||||
|
|
||||||
private _renderItem = (device) =>
|
private _renderItem = (device) =>
|
||||||
html`<ha-check-list-item
|
!device
|
||||||
.value=${device.id}
|
? nothing
|
||||||
.selected=${this.value?.includes(device.id)}
|
: html`<ha-check-list-item
|
||||||
>
|
.value=${device.id}
|
||||||
${computeDeviceName(device, this.hass)}
|
.selected=${this.value?.includes(device.id)}
|
||||||
</ha-check-list-item>`;
|
>
|
||||||
|
${computeDeviceName(device, this.hass)}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
private _handleItemClick(ev) {
|
||||||
const listItem = ev.target.closest("ha-check-list-item");
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
@@ -99,7 +119,7 @@ export class HaFilterDevices extends LitElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.expanded) return;
|
if (!this.expanded) return;
|
||||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
`${this.clientHeight - 49}px`;
|
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,16 +132,28 @@ export class HaFilterDevices extends LitElement {
|
|||||||
this.expanded = ev.detail.expanded;
|
this.expanded = ev.detail.expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => {
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
const values = Object.values(devices);
|
this._filter = ev.detail.value.toLowerCase();
|
||||||
return values.sort((a, b) =>
|
}
|
||||||
stringCompare(
|
|
||||||
a.name_by_user || a.name || "",
|
private _devices = memoizeOne(
|
||||||
b.name_by_user || b.name || "",
|
(devices: HomeAssistant["devices"], filter: string, _value) => {
|
||||||
this.hass.locale.language
|
const values = Object.values(devices);
|
||||||
)
|
return values
|
||||||
);
|
.filter(
|
||||||
});
|
(device) =>
|
||||||
|
!filter ||
|
||||||
|
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
computeDeviceName(a, this.hass),
|
||||||
|
computeDeviceName(b, this.hass),
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private async _findRelated() {
|
private async _findRelated() {
|
||||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
@@ -158,6 +190,15 @@ export class HaFilterDevices extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -178,6 +219,10 @@ export class HaFilterDevices extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -197,6 +242,10 @@ export class HaFilterDevices extends LitElement {
|
|||||||
ha-check-list-item {
|
ha-check-list-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
search-input-outlined {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -14,10 +15,11 @@ import { computeStateName } from "../common/entity/compute_state_name";
|
|||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-state-icon";
|
|
||||||
import "./ha-check-list-item";
|
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-state-icon";
|
||||||
|
import "./search-input-outlined";
|
||||||
|
|
||||||
@customElement("ha-filter-entities")
|
@customElement("ha-filter-entities")
|
||||||
export class HaFilterEntities extends LitElement {
|
export class HaFilterEntities extends LitElement {
|
||||||
@@ -33,6 +35,8 @@ export class HaFilterEntities extends LitElement {
|
|||||||
|
|
||||||
@state() private _shouldRender = false;
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
public willUpdate(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.willUpdate(properties);
|
||||||
|
|
||||||
@@ -52,16 +56,27 @@ export class HaFilterEntities extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>`
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
? html`
|
? html`
|
||||||
|
<search-input-outlined
|
||||||
|
.hass=${this.hass}
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
>
|
||||||
|
</search-input-outlined>
|
||||||
<mwc-list class="ha-scrollbar">
|
<mwc-list class="ha-scrollbar">
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
.items=${this._entities(
|
.items=${this._entities(
|
||||||
this.hass.states,
|
this.hass.states,
|
||||||
this.type,
|
this.type,
|
||||||
|
this._filter || "",
|
||||||
this.value
|
this.value
|
||||||
)}
|
)}
|
||||||
.keyFunction=${this._keyFunction}
|
.keyFunction=${this._keyFunction}
|
||||||
@@ -81,7 +96,7 @@ export class HaFilterEntities extends LitElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.expanded) return;
|
if (!this.expanded) return;
|
||||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
`${this.clientHeight - 49}px`;
|
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,18 +104,20 @@ export class HaFilterEntities extends LitElement {
|
|||||||
private _keyFunction = (entity) => entity?.entity_id;
|
private _keyFunction = (entity) => entity?.entity_id;
|
||||||
|
|
||||||
private _renderItem = (entity) =>
|
private _renderItem = (entity) =>
|
||||||
html`<ha-check-list-item
|
!entity
|
||||||
.value=${entity.entity_id}
|
? nothing
|
||||||
.selected=${this.value?.includes(entity.entity_id)}
|
: html`<ha-check-list-item
|
||||||
graphic="icon"
|
.value=${entity.entity_id}
|
||||||
>
|
.selected=${this.value?.includes(entity.entity_id)}
|
||||||
<ha-state-icon
|
graphic="icon"
|
||||||
slot="graphic"
|
>
|
||||||
.hass=${this.hass}
|
<ha-state-icon
|
||||||
.stateObj=${entity}
|
slot="graphic"
|
||||||
></ha-state-icon>
|
.hass=${this.hass}
|
||||||
${computeStateName(entity)}
|
.stateObj=${entity}
|
||||||
</ha-check-list-item>`;
|
></ha-state-icon>
|
||||||
|
${computeStateName(entity)}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
private _handleItemClick(ev) {
|
||||||
const listItem = ev.target.closest("ha-check-list-item");
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
@@ -125,12 +142,27 @@ export class HaFilterEntities extends LitElement {
|
|||||||
this.expanded = ev.detail.expanded;
|
this.expanded = ev.detail.expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
|
this._filter = ev.detail.value.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
private _entities = memoizeOne(
|
private _entities = memoizeOne(
|
||||||
(states: HomeAssistant["states"], type: this["type"], _value) => {
|
(
|
||||||
|
states: HomeAssistant["states"],
|
||||||
|
type: this["type"],
|
||||||
|
filter: string,
|
||||||
|
_value
|
||||||
|
) => {
|
||||||
const values = Object.values(states);
|
const values = Object.values(states);
|
||||||
return values
|
return values
|
||||||
.filter(
|
.filter(
|
||||||
(entityState) => !type || computeStateDomain(entityState) !== type
|
(entityState) =>
|
||||||
|
(!type || computeStateDomain(entityState) !== type) &&
|
||||||
|
(!filter ||
|
||||||
|
entityState.entity_id.toLowerCase().includes(filter) ||
|
||||||
|
entityState.attributes.friendly_name
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(filter))
|
||||||
)
|
)
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
stringCompare(
|
stringCompare(
|
||||||
@@ -177,6 +209,15 @@ export class HaFilterEntities extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -196,6 +237,10 @@ export class HaFilterEntities extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -216,6 +261,10 @@ export class HaFilterEntities extends LitElement {
|
|||||||
--mdc-list-item-graphic-margin: 16px;
|
--mdc-list-item-graphic-margin: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
search-input-outlined {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,19 @@
|
|||||||
import "@material/mwc-menu/mwc-menu-surface";
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiTextureBox } from "@mdi/js";
|
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import {
|
import {
|
||||||
FloorRegistryEntry,
|
FloorRegistryEntry,
|
||||||
getFloorAreaLookup,
|
getFloorAreaLookup,
|
||||||
subscribeFloorRegistry,
|
subscribeFloorRegistry,
|
||||||
} from "../data/floor_registry";
|
} from "../data/floor_registry";
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
import { RelatedResult, findRelated } from "../data/search";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -19,6 +21,7 @@ import "./ha-check-list-item";
|
|||||||
import "./ha-floor-icon";
|
import "./ha-floor-icon";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
@customElement("ha-filter-floor-areas")
|
@customElement("ha-filter-floor-areas")
|
||||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
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.hass.localize("ui.panel.config.areas.caption")}
|
||||||
${this.value?.areas?.length || this.value?.floors?.length
|
${this.value?.areas?.length || this.value?.floors?.length
|
||||||
? html`<div class="badge">
|
? html`<div class="badge">
|
||||||
${(this.value?.areas?.length || 0) +
|
${(this.value?.areas?.length || 0) +
|
||||||
(this.value?.floors?.length || 0)}
|
(this.value?.floors?.length || 0)}
|
||||||
</div>`
|
</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
@@ -82,8 +89,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
</ha-check-list-item>
|
</ha-check-list-item>
|
||||||
${repeat(
|
${repeat(
|
||||||
floor.areas,
|
floor.areas,
|
||||||
(area) => area.area_id,
|
(area, index) =>
|
||||||
(area) => this._renderArea(area)
|
`${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) {
|
private _renderArea(area, last: boolean = false) {
|
||||||
return html`<ha-check-list-item
|
const hasFloor = !!area.floor_id;
|
||||||
.value=${area.area_id}
|
return html`
|
||||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
<ha-check-list-item
|
||||||
.type=${"areas"}
|
.value=${area.area_id}
|
||||||
graphic="icon"
|
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||||
class=${area.floor_id ? "floor" : ""}
|
.type=${"areas"}
|
||||||
@request-selected=${this._handleItemClick}
|
graphic="icon"
|
||||||
>
|
@request-selected=${this._handleItemClick}
|
||||||
${area.icon
|
class=${classMap({
|
||||||
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
rtl: computeRTL(this.hass),
|
||||||
: html`<ha-svg-icon
|
floor: hasFloor,
|
||||||
slot="graphic"
|
})}
|
||||||
.path=${mdiTextureBox}
|
>
|
||||||
></ha-svg-icon>`}
|
${hasFloor
|
||||||
${area.name}
|
? html`
|
||||||
</ha-check-list-item>`;
|
<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) {
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -257,6 +289,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -277,9 +313,26 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
--mdc-list-item-graphic-margin: 16px;
|
--mdc-list-item-graphic-margin: 16px;
|
||||||
}
|
}
|
||||||
.floor {
|
.floor {
|
||||||
padding-left: 32px;
|
padding-left: 48px;
|
||||||
padding-inline-start: 32px;
|
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 { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-domain-icon";
|
import "./ha-domain-icon";
|
||||||
|
import "./search-input-outlined";
|
||||||
|
|
||||||
@customElement("ha-filter-integrations")
|
@customElement("ha-filter-integrations")
|
||||||
export class HaFilterIntegrations extends LitElement {
|
export class HaFilterIntegrations extends LitElement {
|
||||||
@@ -27,6 +28,8 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
|
|
||||||
@state() private _shouldRender = false;
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
@@ -38,18 +41,23 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.integrations.caption")}
|
${this.hass.localize("ui.panel.config.integrations.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>`
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._manifests && this._shouldRender
|
${this._manifests && this._shouldRender
|
||||||
? html`
|
? html`<search-input-outlined
|
||||||
<mwc-list
|
.hass=${this.hass}
|
||||||
@selected=${this._integrationsSelected}
|
.filter=${this._filter}
|
||||||
multi
|
@value-changed=${this._handleSearchChange}
|
||||||
class="ha-scrollbar"
|
|
||||||
>
|
>
|
||||||
|
</search-input-outlined>
|
||||||
|
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
|
||||||
${repeat(
|
${repeat(
|
||||||
this._integrations(this._manifests, this.value),
|
this._integrations(this._manifests, this._filter, this.value),
|
||||||
(i) => i.domain,
|
(i) => i.domain,
|
||||||
(integration) =>
|
(integration) =>
|
||||||
html`<ha-check-list-item
|
html`<ha-check-list-item
|
||||||
@@ -68,8 +76,7 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
${integration.name || integration.domain}
|
${integration.name || integration.domain}
|
||||||
</ha-check-list-item>`
|
</ha-check-list-item>`
|
||||||
)}
|
)}
|
||||||
</mwc-list>
|
</mwc-list> `
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
@@ -80,7 +87,7 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.expanded) return;
|
if (!this.expanded) return;
|
||||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
`${this.clientHeight - 49}px`;
|
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,12 +105,17 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _integrations = memoizeOne(
|
private _integrations = memoizeOne(
|
||||||
(manifest: IntegrationManifest[], _value) =>
|
(manifest: IntegrationManifest[], filter: string | undefined, _value) =>
|
||||||
manifest
|
manifest
|
||||||
.filter(
|
.filter(
|
||||||
(mnfst) =>
|
(mnfst) =>
|
||||||
!mnfst.integration_type ||
|
(!mnfst.integration_type ||
|
||||||
!["entity", "system", "hardware"].includes(mnfst.integration_type)
|
!["entity", "system", "hardware"].includes(
|
||||||
|
mnfst.integration_type
|
||||||
|
)) &&
|
||||||
|
(!filter ||
|
||||||
|
mnfst.name.toLowerCase().includes(filter) ||
|
||||||
|
mnfst.domain.toLowerCase().includes(filter))
|
||||||
)
|
)
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
stringCompare(
|
stringCompare(
|
||||||
@@ -114,34 +126,38 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
private async _integrationsSelected(
|
private _handleItemClick(ev) {
|
||||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
) {
|
const value = listItem?.value;
|
||||||
const integrations = this._integrations(this._manifests!, this.value);
|
if (!value) {
|
||||||
|
|
||||||
if (!ev.detail.index.size) {
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: [],
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
this.value = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.value?.includes(value)) {
|
||||||
const value: string[] = [];
|
this.value = this.value?.filter((val) => val !== value);
|
||||||
|
} else {
|
||||||
for (const index of ev.detail.index) {
|
this.value = [...(this.value || []), value];
|
||||||
const domain = integrations[index].domain;
|
|
||||||
value.push(domain);
|
|
||||||
}
|
}
|
||||||
this.value = value;
|
listItem.selected = this.value?.includes(value);
|
||||||
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value,
|
value: this.value,
|
||||||
items: undefined,
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -161,6 +177,10 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -177,6 +197,10 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
}
|
}
|
||||||
|
search-input-outlined {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,19 +1,18 @@
|
|||||||
import { SelectedDetail } from "@material/mwc-list";
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
import "@material/mwc-menu/mwc-menu-surface";
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiPlus } from "@mdi/js";
|
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import { computeCssColor } from "../common/color/compute-color";
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { navigate } from "../common/navigate";
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
createLabelRegistryEntry,
|
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../data/label_registry";
|
} from "../data/label_registry";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-check-list-item";
|
import "./ha-check-list-item";
|
||||||
@@ -54,7 +53,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.labels.caption")}
|
${this.hass.localize("ui.panel.config.labels.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>`
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
@@ -95,11 +98,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
${this.expanded
|
${this.expanded
|
||||||
? html`<ha-list-item
|
? html`<ha-list-item
|
||||||
graphic="icon"
|
graphic="icon"
|
||||||
@click=${this._addLabel}
|
@click=${this._manageLabels}
|
||||||
class="add"
|
class="add"
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
${this.hass.localize("ui.panel.config.labels.manage_labels")}
|
||||||
</ha-list-item>`
|
</ha-list-item>`
|
||||||
: nothing}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
@@ -115,10 +118,8 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addLabel() {
|
private _manageLabels() {
|
||||||
showLabelDetailDialog(this, {
|
navigate("/config/labels");
|
||||||
createEntry: (values) => createLabelRegistryEntry(this.hass, values),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _expandedWillChange(ev) {
|
private _expandedWillChange(ev) {
|
||||||
@@ -153,6 +154,15 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -173,6 +183,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { SelectedDetail } from "@material/mwc-list";
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-expansion-panel";
|
|
||||||
import "./ha-check-list-item";
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
|
|
||||||
@customElement("ha-filter-states")
|
@customElement("ha-filter-states")
|
||||||
@@ -43,7 +44,11 @@ export class HaFilterStates extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.label}
|
${this.label}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>`
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
@@ -118,6 +123,15 @@ export class HaFilterStates extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -137,6 +151,10 @@ export class HaFilterStates extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@@ -10,7 +10,10 @@ import {
|
|||||||
ScorableTextItem,
|
ScorableTextItem,
|
||||||
fuzzyFilterSort,
|
fuzzyFilterSort,
|
||||||
} from "../common/string/filter/sequence-matching";
|
} from "../common/string/filter/sequence-matching";
|
||||||
import { AreaRegistryEntry } from "../data/area_registry";
|
import {
|
||||||
|
AreaRegistryEntry,
|
||||||
|
updateAreaRegistryEntry,
|
||||||
|
} from "../data/area_registry";
|
||||||
import {
|
import {
|
||||||
DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
@@ -437,11 +440,18 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
(ev.target as any).value = this._value;
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
|
||||||
showFloorRegistryDetailDialog(this, {
|
showFloorRegistryDetailDialog(this, {
|
||||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
createEntry: async (values) => {
|
createEntry: async (values, addedAreas) => {
|
||||||
try {
|
try {
|
||||||
const floor = await createFloorRegistryEntry(this.hass, values);
|
const floor = await createFloorRegistryEntry(this.hass, values);
|
||||||
|
addedAreas.forEach((areaId) => {
|
||||||
|
updateAreaRegistryEntry(this.hass, areaId, {
|
||||||
|
floor_id: floor.floor_id,
|
||||||
|
});
|
||||||
|
});
|
||||||
const floors = [...this._floors!, floor];
|
const floors = [...this._floors!, floor];
|
||||||
this.comboBox.filteredItems = this._getFloors(
|
this.comboBox.filteredItems = this._getFloors(
|
||||||
floors,
|
floors,
|
||||||
|
@@ -445,6 +445,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
(ev.target as any).value = this._value;
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
|
||||||
showLabelDetailDialog(this, {
|
showLabelDetailDialog(this, {
|
||||||
entry: undefined,
|
entry: undefined,
|
||||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
|
@@ -43,6 +43,7 @@ class HaLabel extends LitElement {
|
|||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
color: var(--ha-label-text-color);
|
color: var(--ha-label-text-color);
|
||||||
--mdc-icon-size: 12px;
|
--mdc-icon-size: 12px;
|
||||||
|
text-wrap: nowrap;
|
||||||
}
|
}
|
||||||
.content > * {
|
.content > * {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@@ -2,8 +2,10 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
|||||||
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { computeCssColor } from "../common/color/compute-color";
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
@@ -17,7 +19,6 @@ import "./chips/ha-input-chip";
|
|||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./ha-label-picker";
|
import "./ha-label-picker";
|
||||||
import type { HaLabelPicker } from "./ha-label-picker";
|
import type { HaLabelPicker } from "./ha-label-picker";
|
||||||
import { stringCompare } from "../common/string/compare";
|
|
||||||
|
|
||||||
@customElement("ha-labels-picker")
|
@customElement("ha-labels-picker")
|
||||||
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
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 {
|
protected render(): TemplateResult {
|
||||||
const labels = this.value
|
const labels = this._sortedLabels(
|
||||||
?.map((id) => this._labels?.[id])
|
this.value,
|
||||||
.sort((a, b) =>
|
this._labels,
|
||||||
stringCompare(a?.name || "", b?.name || "", this.hass.locale.language)
|
this.hass.locale.language
|
||||||
);
|
);
|
||||||
return html`
|
return html`
|
||||||
${labels?.length
|
${labels?.length
|
||||||
? html`<ha-chip-set>
|
? html`<ha-chip-set>
|
||||||
${repeat(
|
${repeat(
|
||||||
labels,
|
labels,
|
||||||
(label) => label?.label_id,
|
(label) => label?.label_id,
|
||||||
(label, idx) => {
|
(label) => {
|
||||||
const color = label?.color
|
const color = label?.color
|
||||||
? computeCssColor(label.color)
|
? computeCssColor(label.color)
|
||||||
: undefined;
|
: undefined;
|
||||||
return html`
|
return html`
|
||||||
<ha-input-chip
|
<ha-input-chip
|
||||||
.idx=${idx}
|
|
||||||
.item=${label}
|
.item=${label}
|
||||||
@remove=${this._removeItem}
|
@remove=${this._removeItem}
|
||||||
@click=${this._openDetail}
|
@click=${this._openDetail}
|
||||||
@@ -161,12 +172,12 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _removeItem(ev) {
|
private _removeItem(ev) {
|
||||||
this._value.splice(ev.target.idx, 1);
|
const label = ev.currentTarget.item;
|
||||||
this._setValue([...this._value]);
|
this._setValue(this._value.filter((id) => id !== label.label_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openDetail(ev) {
|
private _openDetail(ev) {
|
||||||
const label = ev.target.item;
|
const label = ev.currentTarget.item;
|
||||||
showLabelDetailDialog(this, {
|
showLabelDetailDialog(this, {
|
||||||
entry: label,
|
entry: label,
|
||||||
updateEntry: async (values) => {
|
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 "element-internals-polyfill";
|
||||||
import { CSSResult, css } from "lit";
|
import { CSSResult, css } from "lit";
|
||||||
import { MdMenuItem } from "@material/web/menu/menu-item";
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-menu-item")
|
@customElement("ha-menu-item")
|
||||||
export class HaMenuItem extends MdMenuItem {
|
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-label-text-color: var(--error-color);
|
||||||
--md-menu-item-leading-icon-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 "element-internals-polyfill";
|
||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
import { literal } from "lit/static-html";
|
||||||
|
import "./ha-outlined-field";
|
||||||
|
|
||||||
@customElement("ha-outlined-text-field")
|
@customElement("ha-outlined-text-field")
|
||||||
export class HaOutlinedTextField extends MdOutlinedTextField {
|
export class HaOutlinedTextField extends MdOutlinedTextField {
|
||||||
|
protected readonly fieldTag = literal`ha-outlined-field`;
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
...super.styles,
|
...super.styles,
|
||||||
css`
|
css`
|
||||||
@@ -25,6 +29,8 @@ export class HaOutlinedTextField extends MdOutlinedTextField {
|
|||||||
--md-outlined-field-container-shape-end-end: 10px;
|
--md-outlined-field-container-shape-end-end: 10px;
|
||||||
--md-outlined-field-container-shape-end-start: 10px;
|
--md-outlined-field-container-shape-end-start: 10px;
|
||||||
--md-outlined-field-focus-outline-width: 1px;
|
--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);
|
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||||
}
|
}
|
||||||
.input {
|
.input {
|
||||||
|
@@ -30,6 +30,7 @@ export class HaLabelSelector extends LitElement {
|
|||||||
if (this.selector.label.multiple) {
|
if (this.selector.label.multiple) {
|
||||||
return html`
|
return html`
|
||||||
<ha-labels-picker
|
<ha-labels-picker
|
||||||
|
no-add
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${ensureArray(this.value ?? [])}
|
.value=${ensureArray(this.value ?? [])}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@@ -41,6 +42,7 @@ export class HaLabelSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-label-picker
|
<ha-label-picker
|
||||||
|
no-add
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.disabled=${this.disabled}
|
.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 { mdiClose, mdiMagnify } from "@mdi/js";
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
@@ -54,6 +61,15 @@ class SearchInputOutlined extends LitElement {
|
|||||||
.path=${mdiMagnify}
|
.path=${mdiMagnify}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
</slot>
|
</slot>
|
||||||
|
${this.filter
|
||||||
|
? html`<ha-icon-button
|
||||||
|
aria-label="Clear input"
|
||||||
|
slot="trailing-icon"
|
||||||
|
@click=${this._clearSearch}
|
||||||
|
.path=${mdiClose}
|
||||||
|
>
|
||||||
|
</ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
</ha-outlined-text-field>
|
</ha-outlined-text-field>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -66,16 +82,22 @@ class SearchInputOutlined extends LitElement {
|
|||||||
this._filterChanged(e.target.value);
|
this._filterChanged(e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _clearSearch() {
|
||||||
|
this._filterChanged("");
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
/* For iOS */
|
/* For iOS */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
}
|
}
|
||||||
ha-outlined-text-field {
|
ha-outlined-text-field {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
--ha-outlined-field-container-color: var(--card-background-color);
|
||||||
}
|
}
|
||||||
ha-svg-icon,
|
ha-svg-icon,
|
||||||
ha-icon-button {
|
ha-icon-button {
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { consume } from "@lit-labs/context";
|
||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
mdiCircle,
|
mdiCircle,
|
||||||
@@ -6,14 +7,13 @@ import {
|
|||||||
mdiProgressWrench,
|
mdiProgressWrench,
|
||||||
mdiRecordCircleOutline,
|
mdiRecordCircleOutline,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
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 { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
floorsContext,
|
||||||
subscribeEntityRegistry,
|
fullEntitiesContext,
|
||||||
} from "../../data/entity_registry";
|
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 { LogbookEntry } from "../../data/logbook";
|
||||||
import {
|
import {
|
||||||
ChooseAction,
|
ChooseAction,
|
||||||
ChooseActionChoice,
|
ChooseActionChoice,
|
||||||
getActionType,
|
|
||||||
IfAction,
|
IfAction,
|
||||||
ParallelAction,
|
ParallelAction,
|
||||||
RepeatAction,
|
RepeatAction,
|
||||||
|
getActionType,
|
||||||
} from "../../data/script";
|
} from "../../data/script";
|
||||||
import { describeAction } from "../../data/script_i18n";
|
import { describeAction } from "../../data/script_i18n";
|
||||||
import {
|
import {
|
||||||
ActionTraceStep,
|
ActionTraceStep,
|
||||||
AutomationTraceExtended,
|
AutomationTraceExtended,
|
||||||
ChooseActionTraceStep,
|
ChooseActionTraceStep,
|
||||||
getDataFromPath,
|
|
||||||
IfActionTraceStep,
|
IfActionTraceStep,
|
||||||
isTriggerPath,
|
|
||||||
TriggerTraceStep,
|
TriggerTraceStep,
|
||||||
|
getDataFromPath,
|
||||||
|
isTriggerPath,
|
||||||
} from "../../data/trace";
|
} from "../../data/trace";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "./ha-timeline";
|
import "./ha-timeline";
|
||||||
@@ -200,6 +204,8 @@ class ActionRenderer {
|
|||||||
constructor(
|
constructor(
|
||||||
private hass: HomeAssistant,
|
private hass: HomeAssistant,
|
||||||
private entityReg: EntityRegistryEntry[],
|
private entityReg: EntityRegistryEntry[],
|
||||||
|
private labelReg: LabelRegistryEntry[],
|
||||||
|
private floorReg: FloorRegistryEntry[],
|
||||||
private entries: TemplateResult[],
|
private entries: TemplateResult[],
|
||||||
private trace: AutomationTraceExtended,
|
private trace: AutomationTraceExtended,
|
||||||
private logbookRenderer: LogbookRenderer,
|
private logbookRenderer: LogbookRenderer,
|
||||||
@@ -310,7 +316,14 @@ class ActionRenderer {
|
|||||||
|
|
||||||
this._renderEntry(
|
this._renderEntry(
|
||||||
path,
|
path,
|
||||||
describeAction(this.hass, this.entityReg, data, actionType),
|
describeAction(
|
||||||
|
this.hass,
|
||||||
|
this.entityReg,
|
||||||
|
this.labelReg,
|
||||||
|
this.floorReg,
|
||||||
|
data,
|
||||||
|
actionType
|
||||||
|
),
|
||||||
undefined,
|
undefined,
|
||||||
data.enabled === false
|
data.enabled === false
|
||||||
);
|
);
|
||||||
@@ -475,7 +488,13 @@ class ActionRenderer {
|
|||||||
|
|
||||||
const name =
|
const name =
|
||||||
repeatConfig.alias ||
|
repeatConfig.alias ||
|
||||||
describeAction(this.hass, this.entityReg, repeatConfig);
|
describeAction(
|
||||||
|
this.hass,
|
||||||
|
this.entityReg,
|
||||||
|
this.labelReg,
|
||||||
|
this.floorReg,
|
||||||
|
repeatConfig
|
||||||
|
);
|
||||||
|
|
||||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||||
|
|
||||||
@@ -631,15 +650,17 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public allowPick = false;
|
@property({ type: Boolean }) public allowPick = false;
|
||||||
|
|
||||||
@state() private _entityReg: EntityRegistryEntry[] = [];
|
@state()
|
||||||
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
|
_entityReg!: EntityRegistryEntry[];
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
@state()
|
||||||
return [
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
_labelReg!: LabelRegistryEntry[];
|
||||||
this._entityReg = entities;
|
|
||||||
}),
|
@state()
|
||||||
];
|
@consume({ context: floorsContext, subscribe: true })
|
||||||
}
|
_floorReg!: FloorRegistryEntry[];
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.trace) {
|
if (!this.trace) {
|
||||||
@@ -657,6 +678,8 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
const actionRenderer = new ActionRenderer(
|
const actionRenderer = new ActionRenderer(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._entityReg,
|
this._entityReg,
|
||||||
|
this._labelReg,
|
||||||
|
this._floorReg,
|
||||||
entries,
|
entries,
|
||||||
this.trace,
|
this.trace,
|
||||||
logbookRenderer,
|
logbookRenderer,
|
||||||
|
@@ -2,6 +2,8 @@ import { createContext } from "@lit-labs/context";
|
|||||||
import { HassConfig } from "home-assistant-js-websocket";
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { EntityRegistryEntry } from "./entity_registry";
|
import { EntityRegistryEntry } from "./entity_registry";
|
||||||
|
import { FloorRegistryEntry } from "./floor_registry";
|
||||||
|
import { LabelRegistryEntry } from "./label_registry";
|
||||||
|
|
||||||
export const connectionContext =
|
export const connectionContext =
|
||||||
createContext<HomeAssistant["connection"]>("connection");
|
createContext<HomeAssistant["connection"]>("connection");
|
||||||
@@ -25,3 +27,7 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
|||||||
|
|
||||||
export const fullEntitiesContext =
|
export const fullEntitiesContext =
|
||||||
createContext<EntityRegistryEntry[]>("extendedEntities");
|
createContext<EntityRegistryEntry[]>("extendedEntities");
|
||||||
|
|
||||||
|
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
|
||||||
|
|
||||||
|
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||||
|
@@ -14,7 +14,9 @@ import {
|
|||||||
computeEntityRegistryName,
|
computeEntityRegistryName,
|
||||||
entityRegistryById,
|
entityRegistryById,
|
||||||
} from "./entity_registry";
|
} from "./entity_registry";
|
||||||
|
import { FloorRegistryEntry } from "./floor_registry";
|
||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
|
import { LabelRegistryEntry } from "./label_registry";
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
ActionTypes,
|
ActionTypes,
|
||||||
@@ -40,6 +42,8 @@ const actionTranslationBaseKey =
|
|||||||
export const describeAction = <T extends ActionType>(
|
export const describeAction = <T extends ActionType>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryEntry[],
|
entityRegistry: EntityRegistryEntry[],
|
||||||
|
labelRegistry: LabelRegistryEntry[],
|
||||||
|
floorRegistry: FloorRegistryEntry[],
|
||||||
action: ActionTypes[T],
|
action: ActionTypes[T],
|
||||||
actionType?: T,
|
actionType?: T,
|
||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
@@ -48,6 +52,8 @@ export const describeAction = <T extends ActionType>(
|
|||||||
return tryDescribeAction(
|
return tryDescribeAction(
|
||||||
hass,
|
hass,
|
||||||
entityRegistry,
|
entityRegistry,
|
||||||
|
labelRegistry,
|
||||||
|
floorRegistry,
|
||||||
action,
|
action,
|
||||||
actionType,
|
actionType,
|
||||||
ignoreAlias
|
ignoreAlias
|
||||||
@@ -66,6 +72,8 @@ export const describeAction = <T extends ActionType>(
|
|||||||
const tryDescribeAction = <T extends ActionType>(
|
const tryDescribeAction = <T extends ActionType>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryEntry[],
|
entityRegistry: EntityRegistryEntry[],
|
||||||
|
labelRegistry: LabelRegistryEntry[],
|
||||||
|
floorRegistry: FloorRegistryEntry[],
|
||||||
action: ActionTypes[T],
|
action: ActionTypes[T],
|
||||||
actionType?: T,
|
actionType?: T,
|
||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
@@ -82,10 +90,12 @@ const tryDescribeAction = <T extends ActionType>(
|
|||||||
|
|
||||||
const targets: string[] = [];
|
const targets: string[] = [];
|
||||||
if (config.target) {
|
if (config.target) {
|
||||||
for (const [key, label] of Object.entries({
|
for (const [key, name] of Object.entries({
|
||||||
area_id: "areas",
|
area_id: "areas",
|
||||||
device_id: "devices",
|
device_id: "devices",
|
||||||
entity_id: "entities",
|
entity_id: "entities",
|
||||||
|
floor_id: "floors",
|
||||||
|
label_id: "labels",
|
||||||
})) {
|
})) {
|
||||||
if (!(key in config.target)) {
|
if (!(key in config.target)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -99,7 +109,7 @@ const tryDescribeAction = <T extends ActionType>(
|
|||||||
targets.push(
|
targets.push(
|
||||||
hass.localize(
|
hass.localize(
|
||||||
`${actionTranslationBaseKey}.service.description.target_template`,
|
`${actionTranslationBaseKey}.service.description.target_template`,
|
||||||
{ name: label }
|
{ name }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
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 {
|
} else {
|
||||||
targets.push(targetThing);
|
targets.push(targetThing);
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,7 @@ export type ItemType =
|
|||||||
| "entity"
|
| "entity"
|
||||||
| "floor"
|
| "floor"
|
||||||
| "group"
|
| "group"
|
||||||
|
| "label"
|
||||||
| "scene"
|
| "scene"
|
||||||
| "script"
|
| "script"
|
||||||
| "automation_blueprint"
|
| "automation_blueprint"
|
||||||
|
@@ -190,7 +190,7 @@ class LightColorTempPicker extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 48px;
|
--control-slider-border-radius: 36px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
--control-slider-background: -webkit-linear-gradient(
|
--control-slider-background: -webkit-linear-gradient(
|
||||||
top,
|
top,
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { mdiShieldOff } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { stateColorCss } from "../../../common/entity/state_color";
|
import { stateColorCss } from "../../../common/entity/state_color";
|
||||||
import "../../../components/ha-outlined-button";
|
import "../../../components/ha-control-button";
|
||||||
import "../../../components/ha-state-icon";
|
import "../../../components/ha-state-icon";
|
||||||
import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel";
|
import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel";
|
||||||
import "../../../state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes";
|
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)
|
${["triggered", "arming", "pending"].includes(this.stateObj.state)
|
||||||
? html`
|
? html`
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<span></span>
|
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<ha-state-icon .hass=${this.hass} .stateObj=${this.stateObj}>
|
<ha-state-icon .hass=${this.hass} .stateObj=${this.stateObj}>
|
||||||
</ha-state-icon>
|
</ha-state-icon>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@@ -76,7 +70,15 @@ class MoreInfoAlarmControlPanel extends LitElement {
|
|||||||
</ha-state-control-alarm_control_panel-modes>
|
</ha-state-control-alarm_control_panel-modes>
|
||||||
`}
|
`}
|
||||||
</div>
|
</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;
|
transition: background-color 180ms ease-in-out;
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
.status ha-outlined-button {
|
ha-control-button.disarm {
|
||||||
margin-top: 32px;
|
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;
|
--control-button-border-radius: 24px;
|
||||||
}
|
}
|
||||||
.open-button {
|
.open-button {
|
||||||
width: 100px;
|
width: 130px;
|
||||||
--control-button-background-color: var(--state-color);
|
--control-button-background-color: var(--state-color);
|
||||||
}
|
}
|
||||||
.open-button.confirm {
|
.open-button.confirm {
|
||||||
|
@@ -321,19 +321,28 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.path=${mdiMenuDown}
|
.path=${mdiMenuDown}
|
||||||
></ha-svg-icon
|
></ha-svg-icon
|
||||||
></ha-assist-chip>
|
></ha-assist-chip>
|
||||||
<ha-menu-item .value=${undefined} @click=${this._selectAll}
|
<ha-menu-item .value=${undefined} @click=${this._selectAll}>
|
||||||
>${localize("ui.components.subpage-data-table.select_all")}
|
<div slot="headline">
|
||||||
|
${localize("ui.components.subpage-data-table.select_all")}
|
||||||
|
</div>
|
||||||
</ha-menu-item>
|
</ha-menu-item>
|
||||||
<ha-menu-item .value=${undefined} @click=${this._selectNone}
|
<ha-menu-item .value=${undefined} @click=${this._selectNone}>
|
||||||
>${localize("ui.components.subpage-data-table.select_none")}
|
<div slot="headline">
|
||||||
|
${localize(
|
||||||
|
"ui.components.subpage-data-table.select_none"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</ha-menu-item>
|
</ha-menu-item>
|
||||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||||
<ha-menu-item
|
<ha-menu-item
|
||||||
.value=${undefined}
|
.value=${undefined}
|
||||||
@click=${this._disableSelectMode}
|
@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-menu-item>
|
||||||
</ha-button-menu-new>
|
</ha-button-menu-new>
|
||||||
<p>
|
<p>
|
||||||
@@ -349,37 +358,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
: nothing}
|
: nothing}
|
||||||
${this.showFilters
|
${this.showFilters
|
||||||
? !showPane
|
? !showPane
|
||||||
? html`<ha-dialog
|
? nothing
|
||||||
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>`
|
|
||||||
: html`<div class="pane" slot="pane">
|
: html`<div class="pane" slot="pane">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<ha-assist-chip
|
<ha-assist-chip
|
||||||
@@ -394,13 +373,15 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.path=${mdiFilterVariant}
|
.path=${mdiFilterVariant}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
</ha-assist-chip>
|
</ha-assist-chip>
|
||||||
<ha-icon-button
|
${this.filters
|
||||||
.path=${mdiFilterVariantRemove}
|
? html`<ha-icon-button
|
||||||
@click=${this._clearFilters}
|
.path=${mdiFilterVariantRemove}
|
||||||
.label=${localize(
|
@click=${this._clearFilters}
|
||||||
"ui.components.subpage-data-table.clear_filter"
|
.label=${localize(
|
||||||
)}
|
"ui.components.subpage-data-table.clear_filter"
|
||||||
></ha-icon-button>
|
)}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-content">
|
<div class="pane-content">
|
||||||
<slot name="filter-pane"></slot>
|
<slot name="filter-pane"></slot>
|
||||||
@@ -512,6 +493,51 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
: nothing
|
: nothing
|
||||||
)}
|
)}
|
||||||
</ha-menu>
|
</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`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-data-table {
|
ha-data-table {
|
||||||
@@ -728,7 +755,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
--ha-assist-chip-container-color: var(--primary-background-color);
|
--ha-assist-chip-container-color: var(--card-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selection-controls {
|
.selection-controls {
|
||||||
@@ -755,6 +782,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
|
|
||||||
ha-assist-chip {
|
ha-assist-chip {
|
||||||
--ha-assist-chip-container-shape: 10px;
|
--ha-assist-chip-container-shape: 10px;
|
||||||
|
--ha-assist-chip-container-color: var(--card-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-mode-chip {
|
.select-mode-chip {
|
||||||
@@ -777,7 +805,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-dialog-content {
|
.filter-dialog-content {
|
||||||
height: calc(100vh - 1px - var(--header-height));
|
height: calc(100vh - 1px - 61px - var(--header-height));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,13 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
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 { 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-alert";
|
||||||
import "../../../components/ha-aliases-editor";
|
import "../../../components/ha-aliases-editor";
|
||||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
@@ -11,10 +16,15 @@ import "../../../components/ha-picture-upload";
|
|||||||
import "../../../components/ha-settings-row";
|
import "../../../components/ha-settings-row";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import { FloorRegistryEntryMutableParams } from "../../../data/floor_registry";
|
import {
|
||||||
import { haStyleDialog } from "../../../resources/styles";
|
FloorRegistryEntry,
|
||||||
|
FloorRegistryEntryMutableParams,
|
||||||
|
} from "../../../data/floor_registry";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
|
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 {
|
class DialogFloorDetail extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -33,9 +43,11 @@ class DialogFloorDetail extends LitElement {
|
|||||||
|
|
||||||
@state() private _submitting?: boolean;
|
@state() private _submitting?: boolean;
|
||||||
|
|
||||||
public async showDialog(
|
@state() private _addedAreas = new Set<string>();
|
||||||
params: FloorRegistryDetailDialogParams
|
|
||||||
): Promise<void> {
|
@state() private _removedAreas = new Set<string>();
|
||||||
|
|
||||||
|
public showDialog(params: FloorRegistryDetailDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._name = this._params.entry
|
this._name = this._params.entry
|
||||||
@@ -44,16 +56,40 @@ class DialogFloorDetail extends LitElement {
|
|||||||
this._aliases = this._params.entry?.aliases || [];
|
this._aliases = this._params.entry?.aliases || [];
|
||||||
this._icon = this._params.entry?.icon || null;
|
this._icon = this._params.entry?.icon || null;
|
||||||
this._level = this._params.entry?.level ?? null;
|
this._level = this._params.entry?.level ?? null;
|
||||||
await this.updateComplete;
|
this._addedAreas.clear();
|
||||||
|
this._removedAreas.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._error = "";
|
this._error = "";
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
|
this._addedAreas.clear();
|
||||||
|
this._removedAreas.clear();
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
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() {
|
protected render() {
|
||||||
|
const areas = this._floorAreas(
|
||||||
|
this._params?.entry,
|
||||||
|
this.hass.areas,
|
||||||
|
this._addedAreas,
|
||||||
|
this._removedAreas
|
||||||
|
);
|
||||||
|
|
||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
@@ -125,6 +161,52 @@ class DialogFloorDetail extends LitElement {
|
|||||||
: nothing}
|
: nothing}
|
||||||
</ha-icon-picker>
|
</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">
|
<h3 class="header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.floors.editor.aliases_section"
|
"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() {
|
private _isNameValid() {
|
||||||
return this._name.trim() !== "";
|
return this._name.trim() !== "";
|
||||||
}
|
}
|
||||||
@@ -189,9 +306,13 @@ class DialogFloorDetail extends LitElement {
|
|||||||
aliases: this._aliases,
|
aliases: this._aliases,
|
||||||
};
|
};
|
||||||
if (create) {
|
if (create) {
|
||||||
await this._params!.createEntry!(values);
|
await this._params!.createEntry!(values, this._addedAreas);
|
||||||
} else {
|
} else {
|
||||||
await this._params!.updateEntry!(values);
|
await this._params!.updateEntry!(
|
||||||
|
values,
|
||||||
|
this._addedAreas,
|
||||||
|
this._removedAreas
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -209,6 +330,7 @@ class DialogFloorDetail extends LitElement {
|
|||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
|
haStyle,
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
@@ -218,6 +340,9 @@ class DialogFloorDetail extends LitElement {
|
|||||||
ha-floor-icon {
|
ha-floor-icon {
|
||||||
color: var(--secondary-text-color);
|
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>`
|
? html`<ha-icon .icon=${area.icon}></ha-icon>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<h1 class="card-header">${area.name}</h1>
|
<div class="card-header">
|
||||||
|
${area.name}
|
||||||
|
<ha-icon-button
|
||||||
|
.area=${area}
|
||||||
|
.path=${mdiPencil}
|
||||||
|
@click=${this._openAreaDetails}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div>
|
<div>
|
||||||
${formatListWithAnds(
|
${formatListWithAnds(
|
||||||
@@ -305,6 +312,16 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
|||||||
loadAreaRegistryDetailDialog();
|
loadAreaRegistryDetailDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _openAreaDetails(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const area = ev.currentTarget.area;
|
||||||
|
showAreaRegistryDetailDialog(this, {
|
||||||
|
entry: area,
|
||||||
|
updateEntry: async (values) =>
|
||||||
|
updateAreaRegistryEntry(this.hass!, area.area_id, values),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async _areaMoved(ev) {
|
private async _areaMoved(ev) {
|
||||||
const areasAndFloors = this._processAreas(
|
const areasAndFloors = this._processAreas(
|
||||||
this.hass.areas,
|
this.hass.areas,
|
||||||
@@ -397,10 +414,31 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
|||||||
private _openFloorDialog(entry?: FloorRegistryEntry) {
|
private _openFloorDialog(entry?: FloorRegistryEntry) {
|
||||||
showFloorRegistryDetailDialog(this, {
|
showFloorRegistryDetailDialog(this, {
|
||||||
entry,
|
entry,
|
||||||
createEntry: async (values) =>
|
createEntry: async (values, addedAreas) => {
|
||||||
createFloorRegistryEntry(this.hass!, values),
|
const floor = await createFloorRegistryEntry(this.hass!, values);
|
||||||
updateEntry: async (values) =>
|
addedAreas.forEach((areaId) => {
|
||||||
updateFloorRegistryEntry(this.hass!, entry!.floor_id, values),
|
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;
|
min-height: 16px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
.floor {
|
.card-header {
|
||||||
--primary-color: var(--secondary-text-color);
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
|
@@ -7,9 +7,14 @@ import {
|
|||||||
export interface FloorRegistryDetailDialogParams {
|
export interface FloorRegistryDetailDialogParams {
|
||||||
entry?: FloorRegistryEntry;
|
entry?: FloorRegistryEntry;
|
||||||
suggestedName?: string;
|
suggestedName?: string;
|
||||||
createEntry?: (values: FloorRegistryEntryMutableParams) => Promise<unknown>;
|
createEntry?: (
|
||||||
|
values: FloorRegistryEntryMutableParams,
|
||||||
|
addedAreas: Set<string>
|
||||||
|
) => Promise<unknown>;
|
||||||
updateEntry?: (
|
updateEntry?: (
|
||||||
updates: Partial<FloorRegistryEntryMutableParams>
|
updates: Partial<FloorRegistryEntryMutableParams>,
|
||||||
|
addedAreas: Set<string>,
|
||||||
|
removedAreas: Set<string>
|
||||||
) => Promise<unknown>;
|
) => 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 { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||||
import { AutomationClipboard } from "../../../../data/automation";
|
import { AutomationClipboard } from "../../../../data/automation";
|
||||||
import { validateConfig } from "../../../../data/config";
|
import { validateConfig } from "../../../../data/config";
|
||||||
import { fullEntitiesContext } from "../../../../data/context";
|
import {
|
||||||
|
floorsContext,
|
||||||
|
fullEntitiesContext,
|
||||||
|
labelsContext,
|
||||||
|
} from "../../../../data/context";
|
||||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
|
import { FloorRegistryEntry } from "../../../../data/floor_registry";
|
||||||
|
import { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
NonConditionAction,
|
NonConditionAction,
|
||||||
@@ -146,6 +152,14 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
_entityReg!: EntityRegistryEntry[];
|
_entityReg!: EntityRegistryEntry[];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: labelsContext, subscribe: true })
|
||||||
|
_labelReg!: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: floorsContext, subscribe: true })
|
||||||
|
_floorReg!: FloorRegistryEntry[];
|
||||||
|
|
||||||
@state() private _warnings?: string[];
|
@state() private _warnings?: string[];
|
||||||
|
|
||||||
@state() private _uiModeAvailable = true;
|
@state() private _uiModeAvailable = true;
|
||||||
@@ -210,7 +224,13 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
.path=${ACTION_ICONS[type!]}
|
.path=${ACTION_ICONS[type!]}
|
||||||
></ha-svg-icon>`}
|
></ha-svg-icon>`}
|
||||||
${capitalizeFirstLetter(
|
${capitalizeFirstLetter(
|
||||||
describeAction(this.hass, this._entityReg, this.action)
|
describeAction(
|
||||||
|
this.hass,
|
||||||
|
this._entityReg,
|
||||||
|
this._labelReg,
|
||||||
|
this._floorReg,
|
||||||
|
this.action
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@@ -573,7 +593,15 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
),
|
),
|
||||||
inputType: "string",
|
inputType: "string",
|
||||||
placeholder: capitalizeFirstLetter(
|
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,
|
defaultValue: this.action.alias,
|
||||||
confirmText: this.hass.localize("ui.common.submit"),
|
confirmText: this.hass.localize("ui.common.submit"),
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { consume } from "@lit-labs/context";
|
import { consume } from "@lit-labs/context";
|
||||||
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
import "@material/web/divider/divider";
|
import "@material/web/divider/divider";
|
||||||
import {
|
import {
|
||||||
@@ -73,6 +74,7 @@ import {
|
|||||||
} from "../../../data/automation";
|
} from "../../../data/automation";
|
||||||
import {
|
import {
|
||||||
CategoryRegistryEntry,
|
CategoryRegistryEntry,
|
||||||
|
createCategoryRegistryEntry,
|
||||||
subscribeCategoryRegistry,
|
subscribeCategoryRegistry,
|
||||||
} from "../../../data/category_registry";
|
} from "../../../data/category_registry";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
@@ -84,6 +86,7 @@ import {
|
|||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../../../data/label_registry";
|
} from "../../../data/label_registry";
|
||||||
import { findRelated } from "../../../data/search";
|
import { findRelated } from "../../../data/search";
|
||||||
@@ -98,11 +101,14 @@ import { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
|
|||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
|
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
|
||||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||||
|
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
|
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||||
|
|
||||||
type AutomationItem = AutomationEntity & {
|
type AutomationItem = AutomationEntity & {
|
||||||
name: string;
|
name: string;
|
||||||
|
area: string | undefined;
|
||||||
last_triggered?: string | undefined;
|
last_triggered?: string | undefined;
|
||||||
formatted_state: string;
|
formatted_state: string;
|
||||||
category: string | undefined;
|
category: string | undefined;
|
||||||
@@ -148,10 +154,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||||
|
|
||||||
|
private _sizeController = new ResizeController(this, {
|
||||||
|
callback: (entries) => entries[0]?.contentRect.width,
|
||||||
|
});
|
||||||
|
|
||||||
private _automations = memoizeOne(
|
private _automations = memoizeOne(
|
||||||
(
|
(
|
||||||
automations: AutomationEntity[],
|
automations: AutomationEntity[],
|
||||||
entityReg: EntityRegistryEntry[],
|
entityReg: EntityRegistryEntry[],
|
||||||
|
areas: HomeAssistant["areas"],
|
||||||
categoryReg?: CategoryRegistryEntry[],
|
categoryReg?: CategoryRegistryEntry[],
|
||||||
labelReg?: LabelRegistryEntry[],
|
labelReg?: LabelRegistryEntry[],
|
||||||
filteredAutomations?: string[] | null
|
filteredAutomations?: string[] | null
|
||||||
@@ -174,6 +185,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
return {
|
return {
|
||||||
...automation,
|
...automation,
|
||||||
name: computeStateName(automation),
|
name: computeStateName(automation),
|
||||||
|
area: entityRegEntry?.area_id
|
||||||
|
? areas[entityRegEntry?.area_id]?.name
|
||||||
|
: undefined,
|
||||||
last_triggered: automation.attributes.last_triggered || undefined,
|
last_triggered: automation.attributes.last_triggered || undefined,
|
||||||
formatted_state: this.hass.formatEntityState(automation),
|
formatted_state: this.hass.formatEntityState(automation),
|
||||||
category: category
|
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: {
|
category: {
|
||||||
title: localize("ui.panel.config.automation.picker.headers.category"),
|
title: localize("ui.panel.config.automation.picker.headers.category"),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
@@ -256,33 +277,32 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
template: (automation) =>
|
template: (automation) =>
|
||||||
automation.labels.map((lbl) => lbl.name).join(" "),
|
automation.labels.map((lbl) => lbl.name).join(" "),
|
||||||
},
|
},
|
||||||
};
|
last_triggered: {
|
||||||
columns.last_triggered = {
|
sortable: true,
|
||||||
sortable: true,
|
width: "130px",
|
||||||
width: "130px",
|
title: localize("ui.card.automation.last_triggered"),
|
||||||
title: localize("ui.card.automation.last_triggered"),
|
hidden: narrow,
|
||||||
hidden: narrow,
|
template: (automation) => {
|
||||||
template: (automation) => {
|
if (!automation.last_triggered) {
|
||||||
if (!automation.last_triggered) {
|
return this.hass.localize("ui.components.relative_time.never");
|
||||||
return this.hass.localize("ui.components.relative_time.never");
|
}
|
||||||
}
|
const date = new Date(automation.last_triggered);
|
||||||
const date = new Date(automation.last_triggered);
|
const now = new Date();
|
||||||
const now = new Date();
|
const dayDifference = differenceInDays(now, date);
|
||||||
const dayDifference = differenceInDays(now, date);
|
return html`
|
||||||
return html`
|
${dayDifference > 3
|
||||||
${dayDifference > 3
|
? formatShortDateTime(date, locale, this.hass.config)
|
||||||
? formatShortDateTime(date, locale, this.hass.config)
|
: relativeTime(date, locale)}
|
||||||
: relativeTime(date, locale)}
|
`;
|
||||||
`;
|
},
|
||||||
},
|
},
|
||||||
};
|
formatted_state: {
|
||||||
|
|
||||||
if (!this.narrow) {
|
|
||||||
columns.formatted_state = {
|
|
||||||
width: "82px",
|
width: "82px",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
groupable: true,
|
groupable: true,
|
||||||
title: "",
|
title: "",
|
||||||
|
type: "overflow",
|
||||||
|
hidden: narrow,
|
||||||
label: this.hass.localize("ui.panel.config.automation.picker.state"),
|
label: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||||
template: (automation) => html`
|
template: (automation) => html`
|
||||||
<ha-entity-toggle
|
<ha-entity-toggle
|
||||||
@@ -290,21 +310,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></ha-entity-toggle>
|
></ha-entity-toggle>
|
||||||
`,
|
`,
|
||||||
};
|
},
|
||||||
}
|
actions: {
|
||||||
|
title: "",
|
||||||
columns.actions = {
|
width: "64px",
|
||||||
title: "",
|
type: "icon-button",
|
||||||
width: "64px",
|
template: (automation) => html`
|
||||||
type: "icon-button",
|
<ha-icon-button
|
||||||
template: (automation) => html`
|
.automation=${automation}
|
||||||
<ha-icon-button
|
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||||
.automation=${automation}
|
.path=${mdiDotsVertical}
|
||||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
@click=${this._showOverflowMenu}
|
||||||
.path=${mdiDotsVertical}
|
></ha-icon-button>
|
||||||
@click=${this._showOverflowMenu}
|
`,
|
||||||
></ha-icon-button>
|
},
|
||||||
`,
|
|
||||||
};
|
};
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
@@ -357,22 +376,60 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>`;
|
</ha-menu-item>`;
|
||||||
const labelItems = html` ${this._labels?.map((label) => {
|
const labelItems = html`${this._labels?.map((label) => {
|
||||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||||
return html`<ha-menu-item
|
const selected = this._selected.every((entityId) =>
|
||||||
.value=${label.label_id}
|
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||||
@click=${this._handleBulkLabel}
|
);
|
||||||
>
|
const partial =
|
||||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
!selected &&
|
||||||
${label.icon
|
this._selected.some((entityId) =>
|
||||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
this.hass.entities[entityId]?.labels.includes(label.label_id)
|
||||||
: nothing}
|
);
|
||||||
${label.name}
|
return html`<ha-menu-item
|
||||||
</ha-label>
|
.value=${label.label_id}
|
||||||
</ha-menu-item>`;
|
.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`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -383,13 +440,23 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
id="entity_id"
|
id="entity_id"
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configSections.automations}
|
.tabs=${configSections.automations}
|
||||||
|
.searchLabel=${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.picker.search",
|
||||||
|
{ number: automations.length }
|
||||||
|
)}
|
||||||
selectable
|
selectable
|
||||||
.selected=${this._selected.length}
|
.selected=${this._selected.length}
|
||||||
@selection-changed=${this._handleSelectionChanged}
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
hasFilters
|
hasFilters
|
||||||
.filters=${
|
.filters=${
|
||||||
Object.values(this._filters).filter((filter) => filter.value?.length)
|
Object.values(this._filters).filter((filter) =>
|
||||||
.length
|
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(
|
.columns=${this._columns(
|
||||||
this.narrow,
|
this.narrow,
|
||||||
@@ -397,13 +464,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
this.hass.locale
|
this.hass.locale
|
||||||
)}
|
)}
|
||||||
initialGroupColumn="category"
|
initialGroupColumn="category"
|
||||||
.data=${this._automations(
|
.data=${automations}
|
||||||
this.automations,
|
|
||||||
this._entityReg,
|
|
||||||
this._categories,
|
|
||||||
this._labels,
|
|
||||||
this._filteredAutomations
|
|
||||||
)}
|
|
||||||
.empty=${!this.automations.length}
|
.empty=${!this.automations.length}
|
||||||
@row-click=${this._handleRowClicked}
|
@row-click=${this._handleRowClicked}
|
||||||
.noDataText=${this.hass.localize(
|
.noDataText=${this.hass.localize(
|
||||||
@@ -495,7 +556,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
</ha-assist-chip>
|
</ha-assist-chip>
|
||||||
${categoryItems}
|
${categoryItems}
|
||||||
</ha-button-menu-new>
|
</ha-button-menu-new>
|
||||||
${this.hass.dockedSidebar === "docked"
|
${labelsInOverflow
|
||||||
? nothing
|
? nothing
|
||||||
: html`<ha-button-menu-new slot="selection-bar">
|
: html`<ha-button-menu-new slot="selection-bar">
|
||||||
<ha-assist-chip
|
<ha-assist-chip
|
||||||
@@ -557,8 +618,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
: nothing
|
: nothing
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
this.narrow || this.hass.dockedSidebar === "docked"
|
this.narrow || labelsInOverflow
|
||||||
? html` <ha-sub-menu>
|
? html`<ha-sub-menu>
|
||||||
<ha-menu-item slot="item">
|
<ha-menu-item slot="item">
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@@ -1039,6 +1100,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private async _handleBulkCategory(ev) {
|
private async _handleBulkCategory(ev) {
|
||||||
const category = ev.currentTarget.value;
|
const category = ev.currentTarget.value;
|
||||||
|
this._bulkAddCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _bulkAddCategory(category: string) {
|
||||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||||
this._selected.forEach((entityId) => {
|
this._selected.forEach((entityId) => {
|
||||||
promises.push(
|
promises.push(
|
||||||
@@ -1052,11 +1117,21 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private async _handleBulkLabel(ev) {
|
private async _handleBulkLabel(ev) {
|
||||||
const label = ev.currentTarget.value;
|
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>[] = [];
|
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||||
this._selected.forEach((entityId) => {
|
this._selected.forEach((entityId) => {
|
||||||
promises.push(
|
promises.push(
|
||||||
updateEntityRegistryEntry(this.hass, entityId, {
|
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);
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
hass-tabs-subpage-data-table {
|
hass-tabs-subpage-data-table {
|
||||||
--data-table-row-height: 60px;
|
--data-table-row-height: 60px;
|
||||||
}
|
}
|
||||||
|
@@ -237,6 +237,8 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
(ev.target as any).value = this._value;
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
|
||||||
showCategoryRegistryDetailDialog(this, {
|
showCategoryRegistryDetailDialog(this, {
|
||||||
scope: this.scope!,
|
scope: this.scope!,
|
||||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { consume } from "@lit-labs/context";
|
import { consume } from "@lit-labs/context";
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
import { mdiPlus } from "@mdi/js";
|
import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
LitElement,
|
LitElement,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +25,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
|
|||||||
import {
|
import {
|
||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
|
SelectionChangedEvent,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/data-table/ha-data-table-labels";
|
import "../../../components/data-table/ha-data-table-labels";
|
||||||
import "../../../components/entity/ha-battery-icon";
|
import "../../../components/entity/ha-battery-icon";
|
||||||
@@ -37,12 +39,15 @@ import "../../../components/ha-filter-integrations";
|
|||||||
import "../../../components/ha-filter-labels";
|
import "../../../components/ha-filter-labels";
|
||||||
import "../../../components/ha-filter-states";
|
import "../../../components/ha-filter-states";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-menu-item";
|
||||||
|
import "../../../components/ha-sub-menu";
|
||||||
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
|
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import {
|
import {
|
||||||
DeviceEntityLookup,
|
DeviceEntityLookup,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
computeDeviceName,
|
computeDeviceName,
|
||||||
|
updateDeviceRegistryEntry,
|
||||||
} from "../../../data/device_registry";
|
} from "../../../data/device_registry";
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
@@ -52,6 +57,7 @@ import {
|
|||||||
import { IntegrationManifest } from "../../../data/integration";
|
import { IntegrationManifest } from "../../../data/integration";
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../../../data/label_registry";
|
} from "../../../data/label_registry";
|
||||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
@@ -62,6 +68,7 @@ import { brandsUrl } from "../../../util/brands-url";
|
|||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
import "../integrations/ha-integration-overflow-menu";
|
import "../integrations/ha-integration-overflow-menu";
|
||||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||||
|
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||||
|
|
||||||
interface DeviceRowData extends DeviceRegistryEntry {
|
interface DeviceRowData extends DeviceRegistryEntry {
|
||||||
device?: DeviceRowData;
|
device?: DeviceRowData;
|
||||||
@@ -91,6 +98,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
@state() private _filter: string = history.state?.filter || "";
|
@state() private _filter: string = history.state?.filter || "";
|
||||||
|
|
||||||
@state() private _filters: Record<
|
@state() private _filters: Record<
|
||||||
@@ -535,6 +544,43 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
this._labels
|
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`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -545,14 +591,23 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
.tabs=${configSections.devices}
|
.tabs=${configSections.devices}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.searchLabel=${this.hass.localize(
|
.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)}
|
.columns=${this._columns(this.hass.localize, this.narrow)}
|
||||||
.data=${devicesOutput}
|
.data=${devicesOutput}
|
||||||
|
selectable
|
||||||
|
.selected=${this._selected.length}
|
||||||
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
.filter=${this._filter}
|
.filter=${this._filter}
|
||||||
hasFilters
|
hasFilters
|
||||||
.filters=${Object.values(this._filters).filter(
|
.filters=${Object.values(this._filters).filter((filter) =>
|
||||||
(filter) => filter.value?.length
|
Array.isArray(filter.value)
|
||||||
|
? filter.value.length
|
||||||
|
: filter.value &&
|
||||||
|
Object.values(filter.value).some((val) =>
|
||||||
|
Array.isArray(val) ? val.length : val
|
||||||
|
)
|
||||||
).length}
|
).length}
|
||||||
@clear-filter=${this._clearFilter}
|
@clear-filter=${this._clearFilter}
|
||||||
@search-changed=${this._handleSearchChange}
|
@search-changed=${this._handleSearchChange}
|
||||||
@@ -621,6 +676,49 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@expanded-changed=${this._filterExpanded}
|
@expanded-changed=${this._filterExpanded}
|
||||||
></ha-filter-labels>
|
></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>
|
</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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
@@ -721,6 +858,16 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
direction: var(--direction);
|
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,
|
haStyle,
|
||||||
];
|
];
|
||||||
|
@@ -3,12 +3,17 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
|||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
mdiCancel,
|
mdiCancel,
|
||||||
|
mdiChevronRight,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
|
mdiDotsVertical,
|
||||||
|
mdiEye,
|
||||||
mdiEyeOff,
|
mdiEyeOff,
|
||||||
|
mdiMenuDown,
|
||||||
mdiPencilOff,
|
mdiPencilOff,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiRestoreAlert,
|
mdiRestoreAlert,
|
||||||
mdiUndo,
|
mdiToggleSwitch,
|
||||||
|
mdiToggleSwitchOffOutline,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
@@ -22,8 +27,8 @@ import {
|
|||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { until } from "lit/directives/until";
|
|
||||||
import memoize from "memoize-one";
|
import memoize from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
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-devices";
|
||||||
import "../../../components/ha-filter-floor-areas";
|
import "../../../components/ha-filter-floor-areas";
|
||||||
import "../../../components/ha-filter-integrations";
|
import "../../../components/ha-filter-integrations";
|
||||||
import "../../../components/ha-filter-states";
|
|
||||||
import "../../../components/ha-filter-labels";
|
import "../../../components/ha-filter-labels";
|
||||||
|
import "../../../components/ha-filter-states";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-menu-item";
|
||||||
|
import "../../../components/ha-sub-menu";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import { UNAVAILABLE } from "../../../data/entity";
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
|
UpdateEntityRegistryEntryResult,
|
||||||
computeEntityRegistryName,
|
computeEntityRegistryName,
|
||||||
removeEntityRegistryEntry,
|
removeEntityRegistryEntry,
|
||||||
updateEntityRegistryEntry,
|
updateEntityRegistryEntry,
|
||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import { entryIcon } from "../../../data/icons";
|
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../../../data/label_registry";
|
} from "../../../data/label_registry";
|
||||||
import {
|
import {
|
||||||
@@ -77,6 +85,11 @@ import type { HomeAssistant, Route } from "../../../types";
|
|||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
import "../integrations/ha-integration-overflow-menu";
|
import "../integrations/ha-integration-overflow-menu";
|
||||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
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
|
export interface StateEntity
|
||||||
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
|
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
|
||||||
@@ -123,13 +136,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
{ value: string[] | undefined; items: Set<string> | undefined }
|
{ value: string[] | undefined; items: Set<string> | undefined }
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
@state() private _selectedEntities: string[] = [];
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
@state() private _expandedFilter?: string;
|
@state() private _expandedFilter?: string;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
_labels!: LabelRegistryEntry[];
|
_labels!: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
@state() private _entitySources?: EntitySources;
|
||||||
|
|
||||||
@query("hass-tabs-subpage-data-table", true)
|
@query("hass-tabs-subpage-data-table", true)
|
||||||
private _dataTable!: HaTabsSubpageDataTable;
|
private _dataTable!: HaTabsSubpageDataTable;
|
||||||
|
|
||||||
@@ -190,21 +205,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
type: "icon",
|
type: "icon",
|
||||||
template: (entry) =>
|
template: (entry) =>
|
||||||
entry.icon
|
entry.icon
|
||||||
? html`
|
? html`<ha-icon .icon=${entry.icon}></ha-icon>`
|
||||||
<ha-state-icon
|
: entry.entity
|
||||||
title=${ifDefined(entry.entity?.state)}
|
? html`
|
||||||
slot="item-icon"
|
<ha-state-icon
|
||||||
.hass=${this.hass}
|
title=${ifDefined(entry.entity?.state)}
|
||||||
.stateObj=${entry.entity}
|
slot="item-icon"
|
||||||
></ha-state-icon>
|
.hass=${this.hass}
|
||||||
`
|
.stateObj=${entry.entity}
|
||||||
: html`
|
></ha-state-icon>
|
||||||
<ha-icon
|
`
|
||||||
icon=${until(
|
: html`<ha-domain-icon
|
||||||
entryIcon(this.hass, entry as EntityRegistryEntry)
|
.domain=${computeDomain(entry.entity_id)}
|
||||||
)}
|
></ha-domain-icon>`,
|
||||||
></ha-icon>
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
main: true,
|
main: true,
|
||||||
@@ -394,10 +407,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
const entryIds = entries
|
const entryIds = entries
|
||||||
.filter((entry) => filter.value!.includes(entry.domain))
|
.filter((entry) => filter.value!.includes(entry.domain))
|
||||||
.map((entry) => entry.entry_id);
|
.map((entry) => entry.entry_id);
|
||||||
|
|
||||||
filteredEntities = filteredEntities.filter(
|
filteredEntities = filteredEntities.filter(
|
||||||
(entity) =>
|
(entity) =>
|
||||||
entity.config_entry_id &&
|
filter.value?.includes(entity.platform) ||
|
||||||
entryIds.includes(entity.config_entry_id)
|
(entity.config_entry_id &&
|
||||||
|
entryIds.includes(entity.config_entry_id))
|
||||||
);
|
);
|
||||||
filter.value!.forEach((domain) => filteredDomains.add(domain));
|
filter.value!.forEach((domain) => filteredDomains.add(domain));
|
||||||
} else if (key === "ha-filter-labels" && filter.value?.length) {
|
} else if (key === "ha-filter-labels" && filter.value?.length) {
|
||||||
@@ -505,13 +520,50 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
[...filteredDomains][0]
|
[...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`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.backPath=${this._searchParms.has("historyBack")
|
.backPath=${
|
||||||
? undefined
|
this._searchParms.has("historyBack") ? undefined : "/config"
|
||||||
: "/config"}
|
}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configSections.devices}
|
.tabs=${configSections.devices}
|
||||||
.columns=${this._columns(
|
.columns=${this._columns(
|
||||||
@@ -521,15 +573,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
)}
|
)}
|
||||||
.data=${filteredEntities}
|
.data=${filteredEntities}
|
||||||
.searchLabel=${this.hass.localize(
|
.searchLabel=${this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.search"
|
"ui.panel.config.entities.picker.search",
|
||||||
|
{ number: filteredEntities.length }
|
||||||
)}
|
)}
|
||||||
hasFilters
|
hasFilters
|
||||||
.filters=${Object.values(this._filters).filter(
|
.filters=${
|
||||||
(filter) => filter.value?.length
|
Object.values(this._filters).filter((filter) =>
|
||||||
).length}
|
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}
|
.filter=${this._filter}
|
||||||
selectable
|
selectable
|
||||||
.selected=${this._selectedEntities.length}
|
.selected=${this._selected.length}
|
||||||
@selection-changed=${this._handleSelectionChanged}
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
clickable
|
clickable
|
||||||
@clear-filter=${this._clearFilter}
|
@clear-filter=${this._clearFilter}
|
||||||
@@ -543,100 +603,131 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
slot="toolbar-icon"
|
slot="toolbar-icon"
|
||||||
></ha-integration-overflow-menu>
|
></ha-integration-overflow-menu>
|
||||||
<div class="header-btns" slot="selection-bar">
|
|
||||||
${!this.narrow
|
|
||||||
? html`
|
${
|
||||||
<mwc-button
|
!this.narrow
|
||||||
@click=${this._enableSelected}
|
? html`<ha-button-menu-new slot="selection-bar">
|
||||||
.disabled=${!this._selectedEntities.length}
|
<ha-assist-chip
|
||||||
>${this.hass.localize(
|
slot="trigger"
|
||||||
"ui.panel.config.entities.picker.enable_selected.button"
|
.label=${this.hass.localize(
|
||||||
)}</mwc-button
|
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||||
>
|
)}
|
||||||
<mwc-button
|
>
|
||||||
@click=${this._disableSelected}
|
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
.disabled=${!this._selectedEntities.length}
|
</ha-assist-chip>
|
||||||
>${this.hass.localize(
|
${labelItems}
|
||||||
"ui.panel.config.entities.picker.disable_selected.button"
|
</ha-button-menu-new>`
|
||||||
)}</mwc-button
|
: nothing
|
||||||
>
|
}
|
||||||
<mwc-button
|
<ha-button-menu-new has-overflow slot="selection-bar">
|
||||||
@click=${this._hideSelected}
|
${
|
||||||
.disabled=${!this._selectedEntities.length}
|
this.narrow
|
||||||
>${this.hass.localize(
|
? html`<ha-assist-chip
|
||||||
"ui.panel.config.entities.picker.hide_selected.button"
|
.label=${this.hass.localize(
|
||||||
)}</mwc-button
|
"ui.panel.config.automation.picker.bulk_action"
|
||||||
>
|
)}
|
||||||
<mwc-button
|
slot="trigger"
|
||||||
@click=${this._removeSelected}
|
>
|
||||||
.disabled=${!this._selectedEntities.length}
|
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
class="warning"
|
</ha-assist-chip>`
|
||||||
>${this.hass.localize(
|
: html`<ha-icon-button
|
||||||
"ui.panel.config.entities.picker.remove_selected.button"
|
.path=${mdiDotsVertical}
|
||||||
)}</mwc-button
|
.label=${"ui.panel.config.automation.picker.bulk_action"}
|
||||||
>
|
slot="trigger"
|
||||||
`
|
></ha-icon-button>`
|
||||||
: html`
|
}
|
||||||
<ha-icon-button
|
<ha-svg-icon
|
||||||
id="enable-btn"
|
slot="trailing-icon"
|
||||||
.disabled=${!this._selectedEntities.length}
|
.path=${mdiMenuDown}
|
||||||
@click=${this._enableSelected}
|
></ha-svg-icon
|
||||||
.path=${mdiUndo}
|
></ha-assist-chip>
|
||||||
.label=${this.hass.localize("ui.common.enable")}
|
${
|
||||||
></ha-icon-button>
|
this.narrow
|
||||||
<simple-tooltip animation-delay="0" for="enable-btn">
|
? html`<ha-sub-menu>
|
||||||
${this.hass.localize(
|
<ha-menu-item slot="item">
|
||||||
"ui.panel.config.entities.picker.enable_selected.button"
|
<div slot="headline">
|
||||||
)}
|
${this.hass.localize(
|
||||||
</simple-tooltip>
|
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||||
<ha-icon-button
|
)}
|
||||||
id="disable-btn"
|
</div>
|
||||||
.disabled=${!this._selectedEntities.length}
|
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||||
@click=${this._disableSelected}
|
</ha-menu-item>
|
||||||
.path=${mdiCancel}
|
<ha-menu slot="menu">${labelItems}</ha-menu>
|
||||||
.label=${this.hass.localize("ui.common.disable")}
|
</ha-sub-menu>
|
||||||
></ha-icon-button>
|
<md-divider role="separator" tabindex="-1"></md-divider>`
|
||||||
<simple-tooltip animation-delay="0" for="disable-btn">
|
: nothing
|
||||||
${this.hass.localize(
|
}
|
||||||
"ui.panel.config.entities.picker.disable_selected.button"
|
|
||||||
)}
|
<ha-menu-item @click=${this._enableSelected}>
|
||||||
</simple-tooltip>
|
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||||
<ha-icon-button
|
<div slot="headline">
|
||||||
id="hide-btn"
|
${this.hass.localize(
|
||||||
.disabled=${!this._selectedEntities.length}
|
"ui.panel.config.entities.picker.enable_selected.button"
|
||||||
@click=${this._hideSelected}
|
)}
|
||||||
.path=${mdiEyeOff}
|
</div>
|
||||||
.label=${this.hass.localize("ui.common.hide")}
|
</ha-menu-item>
|
||||||
></ha-icon-button>
|
<ha-menu-item @click=${this._disableSelected}>
|
||||||
<simple-tooltip animation-delay="0" for="hide-btn">
|
<ha-svg-icon
|
||||||
${this.hass.localize(
|
slot="start"
|
||||||
"ui.panel.config.entities.picker.hide_selected.button"
|
.path=${mdiToggleSwitchOffOutline}
|
||||||
)}
|
></ha-svg-icon>
|
||||||
</simple-tooltip>
|
<div slot="headline">
|
||||||
<ha-icon-button
|
${this.hass.localize(
|
||||||
class="warning"
|
"ui.panel.config.entities.picker.disable_selected.button"
|
||||||
id="remove-btn"
|
)}
|
||||||
.disabled=${!this._selectedEntities.length}
|
</div>
|
||||||
@click=${this._removeSelected}
|
</ha-menu-item>
|
||||||
.path=${mdiDelete}
|
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||||
.label=${this.hass.localize("ui.common.remove")}
|
|
||||||
></ha-icon-button>
|
<ha-menu-item @click=${this._unhideSelected}>
|
||||||
<simple-tooltip animation-delay="0" for="remove-btn">
|
<ha-svg-icon
|
||||||
${this.hass.localize(
|
slot="start"
|
||||||
"ui.panel.config.entities.picker.remove_selected.button"
|
.path=${mdiEye}
|
||||||
)}
|
></ha-svg-icon>
|
||||||
</simple-tooltip>
|
<div slot="headline">
|
||||||
`}
|
${this.hass.localize(
|
||||||
</div>
|
"ui.panel.config.entities.picker.unhide_selected.button"
|
||||||
${this._filters.config_entry?.value?.length
|
)}
|
||||||
? html`<ha-alert slot="filter-pane">
|
</div>
|
||||||
Filtering by config entry
|
</ha-menu-item>
|
||||||
${this._entries?.find(
|
<ha-menu-item @click=${this._hideSelected}>
|
||||||
(entry) =>
|
<ha-svg-icon
|
||||||
entry.entry_id === this._filters.config_entry!.value![0]
|
slot="start"
|
||||||
)?.title || this._filters.config_entry.value[0]}
|
.path=${mdiEyeOff}
|
||||||
</ha-alert>`
|
></ha-svg-icon>
|
||||||
: nothing}
|
<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
|
<ha-filter-floor-areas
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
type="entity"
|
type="entity"
|
||||||
@@ -688,16 +779,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@expanded-changed=${this._filterExpanded}
|
@expanded-changed=${this._filterExpanded}
|
||||||
></ha-filter-labels>
|
></ha-filter-labels>
|
||||||
${includeAddDeviceFab
|
${
|
||||||
? html`<ha-fab
|
includeAddDeviceFab
|
||||||
.label=${this.hass.localize("ui.panel.config.devices.add_device")}
|
? html`<ha-fab
|
||||||
extended
|
.label=${this.hass.localize(
|
||||||
@click=${this._addDevice}
|
"ui.panel.config.devices.add_device"
|
||||||
slot="fab"
|
)}
|
||||||
>
|
extended
|
||||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
@click=${this._addDevice}
|
||||||
</ha-fab>`
|
slot="fab"
|
||||||
: nothing}
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</ha-fab>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
</hass-tabs-subpage-data-table>
|
</hass-tabs-subpage-data-table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -723,6 +818,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
this._setFiltersFromUrl();
|
this._setFiltersFromUrl();
|
||||||
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
|
this._entitySources = sources;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setFiltersFromUrl() {
|
private _setFiltersFromUrl() {
|
||||||
@@ -781,14 +879,18 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
this._filters = {};
|
this._filters = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues<this>): void {
|
public willUpdate(changedProps: PropertyValues): void {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
const oldHass = changedProps.get("hass");
|
const oldHass = changedProps.get("hass");
|
||||||
let changed = false;
|
let changed = false;
|
||||||
if (!this.hass || !this._entities) {
|
if (!this.hass || !this._entities) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (changedProps.has("hass") || changedProps.has("_entities")) {
|
if (
|
||||||
|
changedProps.has("hass") ||
|
||||||
|
changedProps.has("_entities") ||
|
||||||
|
changedProps.has("_entitySources")
|
||||||
|
) {
|
||||||
const stateEntities: StateEntity[] = [];
|
const stateEntities: StateEntity[] = [];
|
||||||
const regEntityIds = new Set(
|
const regEntityIds = new Set(
|
||||||
this._entities.map((entity) => entity.entity_id)
|
this._entities.map((entity) => entity.entity_id)
|
||||||
@@ -799,6 +901,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!oldHass ||
|
!oldHass ||
|
||||||
|
changedProps.has("_entitySources") ||
|
||||||
this.hass.states[entityId] !== oldHass.states[entityId]
|
this.hass.states[entityId] !== oldHass.states[entityId]
|
||||||
) {
|
) {
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -806,7 +909,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
stateEntities.push({
|
stateEntities.push({
|
||||||
name: computeStateName(this.hass.states[entityId]),
|
name: computeStateName(this.hass.states[entityId]),
|
||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
platform: computeDomain(entityId),
|
platform:
|
||||||
|
this._entitySources?.[entityId]?.domain || computeDomain(entityId),
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
area_id: null,
|
area_id: null,
|
||||||
@@ -836,14 +940,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
private _handleSelectionChanged(
|
private _handleSelectionChanged(
|
||||||
ev: HASSDomEvent<SelectionChangedEvent>
|
ev: HASSDomEvent<SelectionChangedEvent>
|
||||||
): void {
|
): void {
|
||||||
this._selectedEntities = ev.detail.value;
|
this._selected = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _enableSelected() {
|
private async _enableSelected() {
|
||||||
showConfirmationDialog(this, {
|
showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.enable_selected.confirm_title",
|
"ui.panel.config.entities.picker.enable_selected.confirm_title",
|
||||||
{ number: this._selectedEntities.length }
|
{ number: this._selected.length }
|
||||||
),
|
),
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.enable_selected.confirm_text"
|
"ui.panel.config.entities.picker.enable_selected.confirm_text"
|
||||||
@@ -854,7 +958,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
let require_restart = false;
|
let require_restart = false;
|
||||||
let reload_delay = 0;
|
let reload_delay = 0;
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this._selectedEntities.map(async (entity) => {
|
this._selected.map(async (entity) => {
|
||||||
const result = await updateEntityRegistryEntry(this.hass, entity, {
|
const result = await updateEntityRegistryEntry(this.hass, entity, {
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
});
|
});
|
||||||
@@ -891,7 +995,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
showConfirmationDialog(this, {
|
showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.disable_selected.confirm_title",
|
"ui.panel.config.entities.picker.disable_selected.confirm_title",
|
||||||
{ number: this._selectedEntities.length }
|
{ number: this._selected.length }
|
||||||
),
|
),
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.disable_selected.confirm_text"
|
"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"),
|
confirmText: this.hass.localize("ui.common.disable"),
|
||||||
dismissText: this.hass.localize("ui.common.cancel"),
|
dismissText: this.hass.localize("ui.common.cancel"),
|
||||||
confirm: () => {
|
confirm: () => {
|
||||||
this._selectedEntities.forEach((entity) =>
|
this._selected.forEach((entity) =>
|
||||||
updateEntityRegistryEntry(this.hass, entity, {
|
updateEntityRegistryEntry(this.hass, entity, {
|
||||||
disabled_by: "user",
|
disabled_by: "user",
|
||||||
})
|
})
|
||||||
@@ -913,7 +1017,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
showConfirmationDialog(this, {
|
showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.hide_selected.confirm_title",
|
"ui.panel.config.entities.picker.hide_selected.confirm_title",
|
||||||
{ number: this._selectedEntities.length }
|
{ number: this._selected.length }
|
||||||
),
|
),
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.hide_selected.confirm_text"
|
"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"),
|
confirmText: this.hass.localize("ui.common.hide"),
|
||||||
dismissText: this.hass.localize("ui.common.cancel"),
|
dismissText: this.hass.localize("ui.common.cancel"),
|
||||||
confirm: () => {
|
confirm: () => {
|
||||||
this._selectedEntities.forEach((entity) =>
|
this._selected.forEach((entity) =>
|
||||||
updateEntityRegistryEntry(this.hass, entity, {
|
updateEntityRegistryEntry(this.hass, entity, {
|
||||||
hidden_by: "user",
|
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() {
|
private _removeSelected() {
|
||||||
const removeableEntities = this._selectedEntities.filter((entity) => {
|
const removeableEntities = this._selected.filter((entity) => {
|
||||||
const stateObj = this.hass.states[entity];
|
const stateObj = this.hass.states[entity];
|
||||||
return stateObj?.attributes.restored;
|
return stateObj?.attributes.restored;
|
||||||
});
|
});
|
||||||
showConfirmationDialog(this, {
|
showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
`ui.panel.config.entities.picker.remove_selected.confirm_${
|
`ui.panel.config.entities.picker.remove_selected.confirm_${
|
||||||
removeableEntities.length !== this._selectedEntities.length
|
removeableEntities.length !== this._selected.length ? "partly_" : ""
|
||||||
? "partly_"
|
|
||||||
: ""
|
|
||||||
}title`,
|
}title`,
|
||||||
{ number: removeableEntities.length }
|
{ number: removeableEntities.length }
|
||||||
),
|
),
|
||||||
text:
|
text:
|
||||||
removeableEntities.length === this._selectedEntities.length
|
removeableEntities.length === this._selected.length
|
||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.remove_selected.confirm_text"
|
"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",
|
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
|
||||||
{
|
{
|
||||||
removable: removeableEntities.length,
|
removable: removeableEntities.length,
|
||||||
selected: this._selectedEntities.length,
|
selected: this._selected.length,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
confirmText: this.hass.localize("ui.common.remove"),
|
confirmText: this.hass.localize("ui.common.remove"),
|
||||||
@@ -1080,6 +1228,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
direction: var(--direction);
|
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 { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||||
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
|
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
|
||||||
import { fullEntitiesContext } from "../../data/context";
|
import {
|
||||||
|
floorsContext,
|
||||||
|
fullEntitiesContext,
|
||||||
|
labelsContext,
|
||||||
|
} from "../../data/context";
|
||||||
import {
|
import {
|
||||||
entityRegistryByEntityId,
|
entityRegistryByEntityId,
|
||||||
entityRegistryById,
|
entityRegistryById,
|
||||||
@@ -45,6 +49,8 @@ import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
|
|||||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant, Route } from "../../types";
|
import { HomeAssistant, Route } from "../../types";
|
||||||
|
import { subscribeLabelRegistry } from "../../data/label_registry";
|
||||||
|
import { subscribeFloorRegistry } from "../../data/floor_registry";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@@ -379,11 +385,27 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
|||||||
initialValue: [],
|
initialValue: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private _labelsContext = new ContextProvider(this, {
|
||||||
|
context: labelsContext,
|
||||||
|
initialValue: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
private _floorsContext = new ContextProvider(this, {
|
||||||
|
context: floorsContext,
|
||||||
|
initialValue: [],
|
||||||
|
});
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
return [
|
return [
|
||||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||||
this._entitiesContext.setValue(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 "@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 { HassEntity } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -11,8 +22,9 @@ import {
|
|||||||
nothing,
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { consume } from "@lit-labs/context";
|
|
||||||
import memoizeOne from "memoize-one";
|
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 { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import {
|
import {
|
||||||
@@ -23,22 +35,42 @@ import { extractSearchParam } from "../../../common/url/search-params";
|
|||||||
import {
|
import {
|
||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
|
SelectionChangedEvent,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/data-table/ha-data-table-labels";
|
import "../../../components/data-table/ha-data-table-labels";
|
||||||
import "../../../components/ha-fab";
|
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";
|
||||||
|
import "../../../components/ha-icon-overflow-menu";
|
||||||
import "../../../components/ha-state-icon";
|
import "../../../components/ha-state-icon";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
|
import {
|
||||||
|
CategoryRegistryEntry,
|
||||||
|
createCategoryRegistryEntry,
|
||||||
|
subscribeCategoryRegistry,
|
||||||
|
} from "../../../data/category_registry";
|
||||||
import {
|
import {
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
subscribeConfigEntries,
|
subscribeConfigEntries,
|
||||||
} from "../../../data/config_entries";
|
} from "../../../data/config_entries";
|
||||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||||
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import {
|
import {
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
|
UpdateEntityRegistryEntryResult,
|
||||||
subscribeEntityRegistry,
|
subscribeEntityRegistry,
|
||||||
|
updateEntityRegistryEntry,
|
||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import { domainToName } from "../../../data/integration";
|
import { domainToName } from "../../../data/integration";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../../../data/label_registry";
|
||||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||||
import {
|
import {
|
||||||
@@ -49,18 +81,15 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info
|
|||||||
import "../../../layouts/hass-loading-screen";
|
import "../../../layouts/hass-loading-screen";
|
||||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
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 { configSections } from "../ha-panel-config";
|
||||||
import "../integrations/ha-integration-overflow-menu";
|
import "../integrations/ha-integration-overflow-menu";
|
||||||
|
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||||
import { isHelperDomain } from "./const";
|
import { isHelperDomain } from "./const";
|
||||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
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 = {
|
type HelperItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -71,6 +100,7 @@ type HelperItem = {
|
|||||||
type: string;
|
type: string;
|
||||||
configEntry?: ConfigEntry;
|
configEntry?: ConfigEntry;
|
||||||
entity?: HassEntity;
|
entity?: HassEntity;
|
||||||
|
category: string | undefined;
|
||||||
label_entries: LabelRegistryEntry[];
|
label_entries: LabelRegistryEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,6 +141,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||||
|
|
||||||
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
@state() private _activeFilters?: string[];
|
@state() private _activeFilters?: string[];
|
||||||
|
|
||||||
@state() private _filters: Record<
|
@state() private _filters: Record<
|
||||||
@@ -120,6 +152,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _expandedFilter?: string;
|
@state() private _expandedFilter?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
_categories!: CategoryRegistryEntry[];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
_labels!: LabelRegistryEntry[];
|
_labels!: LabelRegistryEntry[];
|
||||||
|
|
||||||
@@ -129,6 +164,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _filteredStateItems?: string[] | null;
|
@state() private _filteredStateItems?: string[] | null;
|
||||||
|
|
||||||
|
private _sizeController = new ResizeController(this, {
|
||||||
|
callback: (entries) => entries[0]?.contentRect.width,
|
||||||
|
});
|
||||||
|
|
||||||
public hassSubscribe() {
|
public hassSubscribe() {
|
||||||
return [
|
return [
|
||||||
subscribeConfigEntries(
|
subscribeConfigEntries(
|
||||||
@@ -156,65 +195,86 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
this._labels = labels;
|
this._labels = labels;
|
||||||
}),
|
}),
|
||||||
|
subscribeCategoryRegistry(
|
||||||
|
this.hass.connection,
|
||||||
|
"helpers",
|
||||||
|
(categories) => {
|
||||||
|
this._categories = categories;
|
||||||
|
}
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _columns = memoizeOne(
|
private _columns = memoizeOne(
|
||||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
(
|
||||||
const columns: DataTableColumnContainer<HelperItem> = {
|
narrow: boolean,
|
||||||
icon: {
|
localize: LocalizeFunc
|
||||||
title: "",
|
): DataTableColumnContainer<HelperItem> => ({
|
||||||
label: localize("ui.panel.config.helpers.picker.headers.icon"),
|
icon: {
|
||||||
type: "icon",
|
title: "",
|
||||||
template: (helper) =>
|
label: localize("ui.panel.config.helpers.picker.headers.icon"),
|
||||||
helper.entity
|
type: "icon",
|
||||||
? html`<ha-state-icon
|
template: (helper) =>
|
||||||
.hass=${this.hass}
|
helper.entity
|
||||||
.stateObj=${helper.entity}
|
? html`<ha-state-icon
|
||||||
></ha-state-icon>`
|
.hass=${this.hass}
|
||||||
: html`<ha-svg-icon
|
.stateObj=${helper.entity}
|
||||||
.path=${helper.icon}
|
></ha-state-icon>`
|
||||||
style="color: var(--error-color)"
|
: html`<ha-svg-icon
|
||||||
></ha-svg-icon>`,
|
.path=${helper.icon}
|
||||||
},
|
style="color: var(--error-color)"
|
||||||
name: {
|
></ha-svg-icon>`,
|
||||||
title: localize("ui.panel.config.helpers.picker.headers.name"),
|
},
|
||||||
main: true,
|
name: {
|
||||||
sortable: true,
|
title: localize("ui.panel.config.helpers.picker.headers.name"),
|
||||||
filterable: true,
|
main: true,
|
||||||
grows: true,
|
sortable: true,
|
||||||
direction: "asc",
|
filterable: true,
|
||||||
template: (helper) => html`
|
grows: true,
|
||||||
<div style="font-size: 14px;">${helper.name}</div>
|
direction: "asc",
|
||||||
${narrow
|
template: (helper) => html`
|
||||||
? html`<div class="secondary">${helper.entity_id}</div> `
|
<div style="font-size: 14px;">${helper.name}</div>
|
||||||
: nothing}
|
${narrow
|
||||||
${helper.label_entries.length
|
? html`<div class="secondary">${helper.entity_id}</div> `
|
||||||
? html`
|
: nothing}
|
||||||
<ha-data-table-labels
|
${helper.label_entries.length
|
||||||
.labels=${helper.label_entries}
|
? html`
|
||||||
></ha-data-table-labels>
|
<ha-data-table-labels
|
||||||
`
|
.labels=${helper.label_entries}
|
||||||
: nothing}
|
></ha-data-table-labels>
|
||||||
`,
|
`
|
||||||
},
|
: nothing}
|
||||||
};
|
`,
|
||||||
if (!narrow) {
|
},
|
||||||
columns.entity_id = {
|
entity_id: {
|
||||||
title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
|
title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
|
||||||
sortable: true,
|
hidden: this.narrow,
|
||||||
filterable: true,
|
sortable: true,
|
||||||
width: "25%",
|
filterable: true,
|
||||||
};
|
width: "25%",
|
||||||
}
|
},
|
||||||
columns.localized_type = {
|
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"),
|
title: localize("ui.panel.config.helpers.picker.headers.type"),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
width: "25%",
|
width: "25%",
|
||||||
filterable: true,
|
filterable: true,
|
||||||
groupable: true,
|
groupable: true,
|
||||||
};
|
},
|
||||||
columns.editable = {
|
editable: {
|
||||||
title: "",
|
title: "",
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
"ui.panel.config.helpers.picker.headers.editable"
|
"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(
|
private _getItems = memoizeOne(
|
||||||
@@ -249,6 +336,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
entityEntries: Record<string, EntityRegistryEntry>,
|
entityEntries: Record<string, EntityRegistryEntry>,
|
||||||
configEntries: Record<string, ConfigEntry>,
|
configEntries: Record<string, ConfigEntry>,
|
||||||
entityReg: EntityRegistryEntry[],
|
entityReg: EntityRegistryEntry[],
|
||||||
|
categoryReg?: CategoryRegistryEntry[],
|
||||||
labelReg?: LabelRegistryEntry[],
|
labelReg?: LabelRegistryEntry[],
|
||||||
filteredStateItems?: string[] | null
|
filteredStateItems?: string[] | null
|
||||||
): HelperItem[] => {
|
): HelperItem[] => {
|
||||||
@@ -292,6 +380,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
type: configEntry.domain,
|
type: configEntry.domain,
|
||||||
configEntry,
|
configEntry,
|
||||||
entity: undefined,
|
entity: undefined,
|
||||||
|
selectable: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...states, ...entries]
|
return [...states, ...entries]
|
||||||
@@ -305,6 +394,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
(reg) => reg.entity_id === item.entity_id
|
(reg) => reg.entity_id === item.entity_id
|
||||||
);
|
);
|
||||||
const labels = labelReg && entityRegEntry?.labels;
|
const labels = labelReg && entityRegEntry?.labels;
|
||||||
|
const category = entityRegEntry?.categories.helpers;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
localized_type: item.configEntry
|
localized_type: item.configEntry
|
||||||
@@ -315,6 +405,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
label_entries: (labels || []).map(
|
label_entries: (labels || []).map(
|
||||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
(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> `;
|
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`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -337,20 +503,25 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
back-path="/config"
|
back-path="/config"
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configSections.devices}
|
.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
|
hasFilters
|
||||||
.filters=${Object.values(this._filters).filter(
|
.filters=${Object.values(this._filters).filter((filter) =>
|
||||||
(filter) => filter.value?.length
|
Array.isArray(filter.value)
|
||||||
|
? filter.value.length
|
||||||
|
: filter.value &&
|
||||||
|
Object.values(filter.value).some((val) =>
|
||||||
|
Array.isArray(val) ? val.length : val
|
||||||
|
)
|
||||||
).length}
|
).length}
|
||||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||||
.data=${this._getItems(
|
.data=${helpers}
|
||||||
this.hass.localize,
|
initialGroupColumn="category"
|
||||||
this._stateItems,
|
|
||||||
this._entityEntries,
|
|
||||||
this._configEntries,
|
|
||||||
this._entityReg,
|
|
||||||
this._labels,
|
|
||||||
this._filteredStateItems
|
|
||||||
)}
|
|
||||||
.activeFilters=${this._activeFilters}
|
.activeFilters=${this._activeFilters}
|
||||||
@clear-filter=${this._clearFilter}
|
@clear-filter=${this._clearFilter}
|
||||||
@row-click=${this._openEditDialog}
|
@row-click=${this._openEditDialog}
|
||||||
@@ -361,6 +532,26 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
)}
|
)}
|
||||||
class=${this.narrow ? "narrow" : ""}
|
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
|
<ha-filter-labels
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._filters["ha-filter-labels"]?.value}
|
.value=${this._filters["ha-filter-labels"]?.value}
|
||||||
@@ -370,6 +561,114 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@expanded-changed=${this._filterExpanded}
|
@expanded-changed=${this._filterExpanded}
|
||||||
></ha-filter-labels>
|
></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
|
<ha-integration-overflow-menu
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -437,6 +736,27 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
items.intersection(labelItems)
|
items.intersection(labelItems)
|
||||||
: new Set([...items].filter((x) => labelItems!.has(x)));
|
: 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;
|
this._filteredStateItems = items ? [...items] : undefined;
|
||||||
}
|
}
|
||||||
@@ -446,6 +766,73 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
this._applyFilters();
|
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) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
if (this.route.path === "/add") {
|
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() {
|
private _createHelper() {
|
||||||
showHelperDetailDialog(this, {});
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
hass-tabs-subpage-data-table {
|
hass-tabs-subpage-data-table {
|
||||||
--data-table-row-height: 60px;
|
--data-table-row-height: 60px;
|
||||||
}
|
}
|
||||||
hass-tabs-subpage-data-table.narrow {
|
hass-tabs-subpage-data-table.narrow {
|
||||||
--data-table-row-height: 72px;
|
--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 { consume } from "@lit-labs/context";
|
||||||
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
import {
|
import {
|
||||||
|
mdiChevronRight,
|
||||||
|
mdiCog,
|
||||||
mdiContentDuplicate,
|
mdiContentDuplicate,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
|
mdiDotsVertical,
|
||||||
mdiHelpCircle,
|
mdiHelpCircle,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
|
mdiMenuDown,
|
||||||
mdiPalette,
|
mdiPalette,
|
||||||
mdiPencilOff,
|
mdiPencilOff,
|
||||||
mdiPlay,
|
mdiPlay,
|
||||||
@@ -24,6 +29,7 @@ import {
|
|||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||||
@@ -33,6 +39,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
|
|||||||
import {
|
import {
|
||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
|
SelectionChangedEvent,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/data-table/ha-data-table-labels";
|
import "../../../components/data-table/ha-data-table-labels";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
@@ -44,18 +51,26 @@ import "../../../components/ha-filter-floor-areas";
|
|||||||
import "../../../components/ha-filter-labels";
|
import "../../../components/ha-filter-labels";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-icon-overflow-menu";
|
import "../../../components/ha-icon-overflow-menu";
|
||||||
|
import "../../../components/ha-menu-item";
|
||||||
import "../../../components/ha-state-icon";
|
import "../../../components/ha-state-icon";
|
||||||
|
import "../../../components/ha-sub-menu";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
CategoryRegistryEntry,
|
CategoryRegistryEntry,
|
||||||
|
createCategoryRegistryEntry,
|
||||||
subscribeCategoryRegistry,
|
subscribeCategoryRegistry,
|
||||||
} from "../../../data/category_registry";
|
} from "../../../data/category_registry";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import { isUnavailableState } from "../../../data/entity";
|
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 { forwardHaptic } from "../../../data/haptics";
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../../../data/label_registry";
|
} from "../../../data/label_registry";
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +84,7 @@ import {
|
|||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../../../dialogs/generic/show-dialog-box";
|
} 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 "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
@@ -76,10 +92,13 @@ import { HomeAssistant, Route } from "../../../types";
|
|||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
import { showToast } from "../../../util/toast";
|
import { showToast } from "../../../util/toast";
|
||||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||||
|
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
|
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||||
|
|
||||||
type SceneItem = SceneEntity & {
|
type SceneItem = SceneEntity & {
|
||||||
name: string;
|
name: string;
|
||||||
|
area: string | undefined;
|
||||||
category: string | undefined;
|
category: string | undefined;
|
||||||
labels: LabelRegistryEntry[];
|
labels: LabelRegistryEntry[];
|
||||||
};
|
};
|
||||||
@@ -98,6 +117,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
@state() private _activeFilters?: string[];
|
@state() private _activeFilters?: string[];
|
||||||
|
|
||||||
@state() private _filteredScenes?: string[] | null;
|
@state() private _filteredScenes?: string[] | null;
|
||||||
@@ -119,10 +140,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
_entityReg!: EntityRegistryEntry[];
|
_entityReg!: EntityRegistryEntry[];
|
||||||
|
|
||||||
|
private _sizeController = new ResizeController(this, {
|
||||||
|
callback: (entries) => entries[0]?.contentRect.width,
|
||||||
|
});
|
||||||
|
|
||||||
private _scenes = memoizeOne(
|
private _scenes = memoizeOne(
|
||||||
(
|
(
|
||||||
scenes: SceneEntity[],
|
scenes: SceneEntity[],
|
||||||
entityReg: EntityRegistryEntry[],
|
entityReg: EntityRegistryEntry[],
|
||||||
|
areas: HomeAssistant["areas"],
|
||||||
categoryReg?: CategoryRegistryEntry[],
|
categoryReg?: CategoryRegistryEntry[],
|
||||||
labelReg?: LabelRegistryEntry[],
|
labelReg?: LabelRegistryEntry[],
|
||||||
filteredScenes?: string[] | null
|
filteredScenes?: string[] | null
|
||||||
@@ -143,6 +169,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
return {
|
return {
|
||||||
...scene,
|
...scene,
|
||||||
name: computeStateName(scene),
|
name: computeStateName(scene),
|
||||||
|
area: entityRegEntry?.area_id
|
||||||
|
? areas[entityRegEntry?.area_id]?.name
|
||||||
|
: undefined,
|
||||||
category: category
|
category: category
|
||||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -185,6 +214,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
: nothing}
|
: nothing}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
area: {
|
||||||
|
title: localize("ui.panel.config.scene.picker.headers.area"),
|
||||||
|
hidden: true,
|
||||||
|
groupable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
category: {
|
category: {
|
||||||
title: localize("ui.panel.config.scene.picker.headers.category"),
|
title: localize("ui.panel.config.scene.picker.headers.category"),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
@@ -198,14 +234,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
filterable: true,
|
filterable: true,
|
||||||
template: (scene) => scene.labels.map((lbl) => lbl.name).join(" "),
|
template: (scene) => scene.labels.map((lbl) => lbl.name).join(" "),
|
||||||
},
|
},
|
||||||
};
|
state: {
|
||||||
if (!narrow) {
|
|
||||||
columns.state = {
|
|
||||||
title: localize(
|
title: localize(
|
||||||
"ui.panel.config.scene.picker.headers.last_activated"
|
"ui.panel.config.scene.picker.headers.last_activated"
|
||||||
),
|
),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
width: "30%",
|
width: "30%",
|
||||||
|
hidden: narrow,
|
||||||
template: (scene) => {
|
template: (scene) => {
|
||||||
const lastActivated = scene.state;
|
const lastActivated = scene.state;
|
||||||
if (!lastActivated || isUnavailableState(lastActivated)) {
|
if (!lastActivated || isUnavailableState(lastActivated)) {
|
||||||
@@ -220,80 +255,87 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
: relativeTime(date, this.hass.locale)}
|
: relativeTime(date, this.hass.locale)}
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
}
|
only_editable: {
|
||||||
columns.only_editable = {
|
title: "",
|
||||||
title: "",
|
width: "56px",
|
||||||
width: "56px",
|
template: (scene) =>
|
||||||
template: (scene) =>
|
!scene.attributes.id
|
||||||
!scene.attributes.id
|
? html`
|
||||||
? html`
|
<simple-tooltip animation-delay="0" position="left">
|
||||||
<simple-tooltip animation-delay="0" position="left">
|
${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.panel.config.scene.picker.only_editable"
|
||||||
"ui.panel.config.scene.picker.only_editable"
|
)}
|
||||||
)}
|
</simple-tooltip>
|
||||||
</simple-tooltip>
|
<ha-svg-icon
|
||||||
<ha-svg-icon
|
.path=${mdiPencilOff}
|
||||||
.path=${mdiPencilOff}
|
style="color: var(--secondary-text-color)"
|
||||||
style="color: var(--secondary-text-color)"
|
></ha-svg-icon>
|
||||||
></ha-svg-icon>
|
`
|
||||||
`
|
: "",
|
||||||
: "",
|
},
|
||||||
};
|
actions: {
|
||||||
columns.actions = {
|
title: "",
|
||||||
title: "",
|
width: "64px",
|
||||||
width: "64px",
|
type: "overflow-menu",
|
||||||
type: "overflow-menu",
|
template: (scene) => html`
|
||||||
template: (scene) => html`
|
<ha-icon-overflow-menu
|
||||||
<ha-icon-overflow-menu
|
.hass=${this.hass}
|
||||||
.hass=${this.hass}
|
narrow
|
||||||
narrow
|
.items=${[
|
||||||
.items=${[
|
{
|
||||||
{
|
path: mdiInformationOutline,
|
||||||
path: mdiInformationOutline,
|
label: this.hass.localize(
|
||||||
label: this.hass.localize(
|
"ui.panel.config.scene.picker.show_info"
|
||||||
"ui.panel.config.scene.picker.show_info"
|
),
|
||||||
),
|
action: () => this._showInfo(scene),
|
||||||
action: () => this._showInfo(scene),
|
},
|
||||||
},
|
{
|
||||||
{
|
path: mdiCog,
|
||||||
path: mdiPlay,
|
label: this.hass.localize(
|
||||||
label: this.hass.localize(
|
"ui.panel.config.automation.picker.show_settings"
|
||||||
"ui.panel.config.scene.picker.activate"
|
),
|
||||||
),
|
action: () => this._openSettings(scene),
|
||||||
action: () => this._activateScene(scene),
|
},
|
||||||
},
|
{
|
||||||
{
|
path: mdiPlay,
|
||||||
path: mdiTag,
|
label: this.hass.localize(
|
||||||
label: this.hass.localize(
|
"ui.panel.config.scene.picker.activate"
|
||||||
`ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}`
|
),
|
||||||
),
|
action: () => this._activateScene(scene),
|
||||||
action: () => this._editCategory(scene),
|
},
|
||||||
},
|
{
|
||||||
{
|
path: mdiTag,
|
||||||
divider: true,
|
label: this.hass.localize(
|
||||||
},
|
`ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}`
|
||||||
{
|
),
|
||||||
path: mdiContentDuplicate,
|
action: () => this._editCategory(scene),
|
||||||
label: this.hass.localize(
|
},
|
||||||
"ui.panel.config.scene.picker.duplicate"
|
{
|
||||||
),
|
divider: true,
|
||||||
action: () => this._duplicate(scene),
|
},
|
||||||
disabled: !scene.attributes.id,
|
{
|
||||||
},
|
path: mdiContentDuplicate,
|
||||||
{
|
label: this.hass.localize(
|
||||||
label: this.hass.localize(
|
"ui.panel.config.scene.picker.duplicate"
|
||||||
"ui.panel.config.scene.picker.delete"
|
),
|
||||||
),
|
action: () => this._duplicate(scene),
|
||||||
path: mdiDelete,
|
disabled: !scene.attributes.id,
|
||||||
action: () => this._deleteConfirm(scene),
|
},
|
||||||
warning: scene.attributes.id,
|
{
|
||||||
disabled: !scene.attributes.id,
|
label: this.hass.localize(
|
||||||
},
|
"ui.panel.config.scene.picker.delete"
|
||||||
]}
|
),
|
||||||
>
|
path: mdiDelete,
|
||||||
</ha-icon-overflow-menu>
|
action: () => this._deleteConfirm(scene),
|
||||||
`,
|
warning: scene.attributes.id,
|
||||||
|
disabled: !scene.attributes.id,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
</ha-icon-overflow-menu>
|
||||||
|
`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return columns;
|
return columns;
|
||||||
@@ -319,6 +361,78 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
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`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -326,20 +440,26 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
back-path="/config"
|
back-path="/config"
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configSections.automations}
|
.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
|
hasFilters
|
||||||
.filters=${Object.values(this._filters).filter(
|
.filters=${Object.values(this._filters).filter((filter) =>
|
||||||
(filter) => filter.value?.length
|
Array.isArray(filter.value)
|
||||||
|
? filter.value.length
|
||||||
|
: filter.value &&
|
||||||
|
Object.values(filter.value).some((val) =>
|
||||||
|
Array.isArray(val) ? val.length : val
|
||||||
|
)
|
||||||
).length}
|
).length}
|
||||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||||
id="entity_id"
|
id="entity_id"
|
||||||
initialGroupColumn="category"
|
initialGroupColumn="category"
|
||||||
.data=${this._scenes(
|
.data=${scenes}
|
||||||
this.scenes,
|
|
||||||
this._entityReg,
|
|
||||||
this._categories,
|
|
||||||
this._labels,
|
|
||||||
this._filteredScenes
|
|
||||||
)}
|
|
||||||
.empty=${!this.scenes.length}
|
.empty=${!this.scenes.length}
|
||||||
.activeFilters=${this._activeFilters}
|
.activeFilters=${this._activeFilters}
|
||||||
.noDataText=${this.hass.localize(
|
.noDataText=${this.hass.localize(
|
||||||
@@ -407,6 +527,103 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
@expanded-changed=${this._filterExpanded}
|
@expanded-changed=${this._filterExpanded}
|
||||||
></ha-filter-categories>
|
></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
|
${!this.scenes.length
|
||||||
? html`<div class="empty" slot="empty">
|
? html`<div class="empty" slot="empty">
|
||||||
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
|
||||||
@@ -553,6 +770,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
this._applyFilters();
|
this._applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleSelectionChanged(
|
||||||
|
ev: HASSDomEvent<SelectionChangedEvent>
|
||||||
|
): void {
|
||||||
|
this._selected = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||||
const scene = this.scenes.find((a) => a.entity_id === ev.detail.id);
|
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) {
|
private _editCategory(scene: any) {
|
||||||
const entityReg = this._entityReg.find(
|
const entityReg = this._entityReg.find(
|
||||||
(reg) => reg.entity_id === scene.entity_id
|
(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 });
|
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) => {
|
private _activateScene = async (scene: SceneEntity) => {
|
||||||
await activateScene(this.hass, scene.entity_id);
|
await activateScene(this.hass, scene.entity_id);
|
||||||
showToast(this, {
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
hass-tabs-subpage-data-table {
|
hass-tabs-subpage-data-table {
|
||||||
--data-table-row-height: 60px;
|
--data-table-row-height: 60px;
|
||||||
}
|
}
|
||||||
@@ -664,6 +962,16 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||||||
--mdc-icon-size: 80px;
|
--mdc-icon-size: 80px;
|
||||||
max-width: 500px;
|
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 { consume } from "@lit-labs/context";
|
||||||
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
import {
|
import {
|
||||||
|
mdiChevronRight,
|
||||||
|
mdiCog,
|
||||||
mdiContentDuplicate,
|
mdiContentDuplicate,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
|
mdiDotsVertical,
|
||||||
mdiHelpCircle,
|
mdiHelpCircle,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
|
mdiMenuDown,
|
||||||
mdiPlay,
|
mdiPlay,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiScriptText,
|
mdiScriptText,
|
||||||
@@ -24,6 +29,7 @@ import {
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||||
@@ -34,6 +40,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
|
|||||||
import {
|
import {
|
||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
|
SelectionChangedEvent,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/data-table/ha-data-table-labels";
|
import "../../../components/data-table/ha-data-table-labels";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
@@ -45,16 +52,24 @@ import "../../../components/ha-filter-floor-areas";
|
|||||||
import "../../../components/ha-filter-labels";
|
import "../../../components/ha-filter-labels";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-icon-overflow-menu";
|
import "../../../components/ha-icon-overflow-menu";
|
||||||
|
import "../../../components/ha-menu-item";
|
||||||
|
import "../../../components/ha-sub-menu";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
CategoryRegistryEntry,
|
CategoryRegistryEntry,
|
||||||
|
createCategoryRegistryEntry,
|
||||||
subscribeCategoryRegistry,
|
subscribeCategoryRegistry,
|
||||||
} from "../../../data/category_registry";
|
} from "../../../data/category_registry";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import { UNAVAILABLE } from "../../../data/entity";
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
import {
|
||||||
|
EntityRegistryEntry,
|
||||||
|
UpdateEntityRegistryEntryResult,
|
||||||
|
updateEntityRegistryEntry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../../../data/label_registry";
|
} from "../../../data/label_registry";
|
||||||
import {
|
import {
|
||||||
@@ -70,6 +85,7 @@ import {
|
|||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../../../dialogs/generic/show-dialog-box";
|
} 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 "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
@@ -78,10 +94,13 @@ import { documentationUrl } from "../../../util/documentation-url";
|
|||||||
import { showToast } from "../../../util/toast";
|
import { showToast } from "../../../util/toast";
|
||||||
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
|
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
|
||||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||||
|
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
|
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||||
|
|
||||||
type ScriptItem = ScriptEntity & {
|
type ScriptItem = ScriptEntity & {
|
||||||
name: string;
|
name: string;
|
||||||
|
area: string | undefined;
|
||||||
category: string | undefined;
|
category: string | undefined;
|
||||||
labels: LabelRegistryEntry[];
|
labels: LabelRegistryEntry[];
|
||||||
};
|
};
|
||||||
@@ -102,6 +121,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
@state() private _activeFilters?: string[];
|
@state() private _activeFilters?: string[];
|
||||||
|
|
||||||
@state() private _filteredScripts?: string[] | null;
|
@state() private _filteredScripts?: string[] | null;
|
||||||
@@ -123,10 +144,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
_entityReg!: EntityRegistryEntry[];
|
_entityReg!: EntityRegistryEntry[];
|
||||||
|
|
||||||
|
private _sizeController = new ResizeController(this, {
|
||||||
|
callback: (entries) => entries[0]?.contentRect.width,
|
||||||
|
});
|
||||||
|
|
||||||
private _scripts = memoizeOne(
|
private _scripts = memoizeOne(
|
||||||
(
|
(
|
||||||
scripts: ScriptEntity[],
|
scripts: ScriptEntity[],
|
||||||
entityReg: EntityRegistryEntry[],
|
entityReg: EntityRegistryEntry[],
|
||||||
|
areas: HomeAssistant["areas"],
|
||||||
categoryReg?: CategoryRegistryEntry[],
|
categoryReg?: CategoryRegistryEntry[],
|
||||||
labelReg?: LabelRegistryEntry[],
|
labelReg?: LabelRegistryEntry[],
|
||||||
filteredScripts?: string[] | null
|
filteredScripts?: string[] | null
|
||||||
@@ -149,6 +175,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
return {
|
return {
|
||||||
...script,
|
...script,
|
||||||
name: computeStateName(script),
|
name: computeStateName(script),
|
||||||
|
area: entityRegEntry?.area_id
|
||||||
|
? areas[entityRegEntry?.area_id]?.name
|
||||||
|
: undefined,
|
||||||
last_triggered: script.attributes.last_triggered || undefined,
|
last_triggered: script.attributes.last_triggered || undefined,
|
||||||
category: category
|
category: category
|
||||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
? 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: {
|
category: {
|
||||||
title: localize("ui.panel.config.script.picker.headers.category"),
|
title: localize("ui.panel.config.script.picker.headers.category"),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
@@ -227,9 +263,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
filterable: true,
|
filterable: true,
|
||||||
template: (script) => script.labels.map((lbl) => lbl.name).join(" "),
|
template: (script) => script.labels.map((lbl) => lbl.name).join(" "),
|
||||||
},
|
},
|
||||||
};
|
last_triggered: {
|
||||||
if (!narrow) {
|
hidden: narrow,
|
||||||
columns.last_triggered = {
|
|
||||||
sortable: true,
|
sortable: true,
|
||||||
width: "40%",
|
width: "40%",
|
||||||
title: localize("ui.card.automation.last_triggered"),
|
title: localize("ui.card.automation.last_triggered"),
|
||||||
@@ -249,66 +284,74 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
: this.hass.localize("ui.components.relative_time.never")}
|
: this.hass.localize("ui.components.relative_time.never")}
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
}
|
actions: {
|
||||||
|
title: "",
|
||||||
columns.actions = {
|
width: "64px",
|
||||||
title: "",
|
type: "overflow-menu",
|
||||||
width: "64px",
|
template: (script) => html`
|
||||||
type: "overflow-menu",
|
<ha-icon-overflow-menu
|
||||||
template: (script) => html`
|
.hass=${this.hass}
|
||||||
<ha-icon-overflow-menu
|
narrow
|
||||||
.hass=${this.hass}
|
.items=${[
|
||||||
narrow
|
{
|
||||||
.items=${[
|
path: mdiInformationOutline,
|
||||||
{
|
label: this.hass.localize(
|
||||||
path: mdiInformationOutline,
|
"ui.panel.config.script.picker.show_info"
|
||||||
label: this.hass.localize(
|
),
|
||||||
"ui.panel.config.script.picker.show_info"
|
action: () => this._showInfo(script),
|
||||||
),
|
},
|
||||||
action: () => this._showInfo(script),
|
{
|
||||||
},
|
path: mdiCog,
|
||||||
{
|
label: this.hass.localize(
|
||||||
path: mdiTag,
|
"ui.panel.config.automation.picker.show_settings"
|
||||||
label: this.hass.localize(
|
),
|
||||||
`ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}`
|
action: () => this._openSettings(script),
|
||||||
),
|
},
|
||||||
action: () => this._editCategory(script),
|
{
|
||||||
},
|
path: mdiTag,
|
||||||
{
|
label: this.hass.localize(
|
||||||
path: mdiPlay,
|
`ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}`
|
||||||
label: this.hass.localize("ui.panel.config.script.picker.run"),
|
),
|
||||||
action: () => this._runScript(script),
|
action: () => this._editCategory(script),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: mdiTransitConnection,
|
path: mdiPlay,
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
"ui.panel.config.script.picker.show_trace"
|
"ui.panel.config.script.picker.run"
|
||||||
),
|
),
|
||||||
action: () => this._showTrace(script),
|
action: () => this._runScript(script),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
divider: true,
|
path: mdiTransitConnection,
|
||||||
},
|
label: this.hass.localize(
|
||||||
{
|
"ui.panel.config.script.picker.show_trace"
|
||||||
path: mdiContentDuplicate,
|
),
|
||||||
label: this.hass.localize(
|
action: () => this._showTrace(script),
|
||||||
"ui.panel.config.script.picker.duplicate"
|
},
|
||||||
),
|
{
|
||||||
action: () => this._duplicate(script),
|
divider: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.hass.localize(
|
path: mdiContentDuplicate,
|
||||||
"ui.panel.config.script.picker.delete"
|
label: this.hass.localize(
|
||||||
),
|
"ui.panel.config.script.picker.duplicate"
|
||||||
path: mdiDelete,
|
),
|
||||||
action: () => this._deleteConfirm(script),
|
action: () => this._duplicate(script),
|
||||||
warning: true,
|
},
|
||||||
},
|
{
|
||||||
]}
|
label: this.hass.localize(
|
||||||
>
|
"ui.panel.config.script.picker.delete"
|
||||||
</ha-icon-overflow-menu>
|
),
|
||||||
`,
|
path: mdiDelete,
|
||||||
|
action: () => this._deleteConfirm(script),
|
||||||
|
warning: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
</ha-icon-overflow-menu>
|
||||||
|
`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return columns;
|
return columns;
|
||||||
@@ -331,6 +374,77 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
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`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -338,23 +452,29 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
back-path="/config"
|
back-path="/config"
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configSections.automations}
|
.tabs=${configSections.automations}
|
||||||
|
.searchLabel=${this.hass.localize(
|
||||||
|
"ui.panel.config.script.picker.search",
|
||||||
|
{ number: scripts.length }
|
||||||
|
)}
|
||||||
hasFilters
|
hasFilters
|
||||||
initialGroupColumn="category"
|
initialGroupColumn="category"
|
||||||
.filters=${Object.values(this._filters).filter(
|
selectable
|
||||||
(filter) => filter.value?.length
|
.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}
|
).length}
|
||||||
.columns=${this._columns(
|
.columns=${this._columns(
|
||||||
this.narrow,
|
this.narrow,
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.hass.locale
|
this.hass.locale
|
||||||
)}
|
)}
|
||||||
.data=${this._scripts(
|
.data=${scripts}
|
||||||
this.scripts,
|
|
||||||
this._entityReg,
|
|
||||||
this._categories,
|
|
||||||
this._labels,
|
|
||||||
this._filteredScripts
|
|
||||||
)}
|
|
||||||
.empty=${!this.scripts.length}
|
.empty=${!this.scripts.length}
|
||||||
.activeFilters=${this._activeFilters}
|
.activeFilters=${this._activeFilters}
|
||||||
id="entity_id"
|
id="entity_id"
|
||||||
@@ -432,6 +552,104 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@expanded-changed=${this._filterExpanded}
|
@expanded-changed=${this._filterExpanded}
|
||||||
></ha-filter-blueprints>
|
></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
|
${!this.scripts.length
|
||||||
? html` <div class="empty" slot="empty">
|
? html` <div class="empty" slot="empty">
|
||||||
<ha-svg-icon .path=${mdiScriptText}></ha-svg-icon>
|
<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>) {
|
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||||
const entry = this.entityRegistry.find((e) => e.entity_id === ev.detail.id);
|
const entry = this.entityRegistry.find((e) => e.entity_id === ev.detail.id);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
@@ -665,6 +929,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
fireEvent(this, "hass-more-info", { entityId: script.entity_id });
|
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) {
|
private _showTrace(script: any) {
|
||||||
const entry = this.entityRegistry.find(
|
const entry = this.entityRegistry.find(
|
||||||
(e) => e.entity_id === script.entity_id
|
(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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
hass-tabs-subpage-data-table {
|
hass-tabs-subpage-data-table {
|
||||||
--data-table-row-height: 60px;
|
--data-table-row-height: 60px;
|
||||||
}
|
}
|
||||||
@@ -782,6 +1081,16 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||||||
--mdc-icon-size: 80px;
|
--mdc-icon-size: 80px;
|
||||||
max-width: 500px;
|
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 memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { storage } from "../../common/decorators/storage";
|
import { storage } from "../../common/decorators/storage";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||||
import {
|
import {
|
||||||
@@ -27,37 +28,29 @@ import "../../components/ha-menu-button";
|
|||||||
import "../../components/ha-target-picker";
|
import "../../components/ha-target-picker";
|
||||||
import "../../components/ha-top-app-bar-fixed";
|
import "../../components/ha-top-app-bar-fixed";
|
||||||
import {
|
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,
|
EntityHistoryState,
|
||||||
|
HistoryResult,
|
||||||
|
HistoryStates,
|
||||||
|
LineChartState,
|
||||||
LineChartUnit,
|
LineChartUnit,
|
||||||
computeGroupKey,
|
computeGroupKey,
|
||||||
LineChartState,
|
computeHistory,
|
||||||
|
subscribeHistory,
|
||||||
} from "../../data/history";
|
} 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 { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { fileDownload } from "../../util/file_download";
|
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({ attribute: false }) hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) public narrow = false;
|
@property({ reflect: true, type: Boolean }) public narrow = false;
|
||||||
@@ -83,12 +76,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _statisticsHistory?: HistoryResult;
|
@state() private _statisticsHistory?: HistoryResult;
|
||||||
|
|
||||||
@state() private _deviceEntityLookup?: DeviceEntityLookup;
|
|
||||||
|
|
||||||
@state() private _areaEntityLookup?: AreaEntityLookup;
|
|
||||||
|
|
||||||
@state() private _areaDeviceLookup?: AreaDeviceLookup;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _showBack?: boolean;
|
private _showBack?: boolean;
|
||||||
|
|
||||||
@@ -123,18 +110,6 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
|||||||
this._unsubscribeHistory();
|
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 {
|
private _goBack(): void {
|
||||||
history.back();
|
history.back();
|
||||||
}
|
}
|
||||||
@@ -332,7 +307,9 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
|||||||
const entityIds = searchParams.entity_id;
|
const entityIds = searchParams.entity_id;
|
||||||
const deviceIds = searchParams.device_id;
|
const deviceIds = searchParams.device_id;
|
||||||
const areaIds = searchParams.area_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 = {};
|
this._targetPickerValue = {};
|
||||||
}
|
}
|
||||||
if (entityIds) {
|
if (entityIds) {
|
||||||
@@ -347,6 +324,14 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
|||||||
const splitIds = areaIds.split(",");
|
const splitIds = areaIds.split(",");
|
||||||
this._targetPickerValue!.area_id = splitIds;
|
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;
|
const startDate = searchParams.start_date;
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
@@ -522,95 +507,77 @@ class HaPanelHistory extends SubscribeMixin(LitElement) {
|
|||||||
private _getEntityIds(): string[] {
|
private _getEntityIds(): string[] {
|
||||||
return this.__getEntityIds(
|
return this.__getEntityIds(
|
||||||
this._targetPickerValue,
|
this._targetPickerValue,
|
||||||
this._deviceEntityLookup,
|
this.hass.entities,
|
||||||
this._areaEntityLookup,
|
this.hass.devices,
|
||||||
this._areaDeviceLookup
|
this.hass.areas
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private __getEntityIds = memoizeOne(
|
private __getEntityIds = memoizeOne(
|
||||||
(
|
(
|
||||||
targetPickerValue: HassServiceTarget,
|
targetPickerValue: HassServiceTarget,
|
||||||
deviceEntityLookup: DeviceEntityLookup | undefined,
|
entities: HomeAssistant["entities"],
|
||||||
areaEntityLookup: AreaEntityLookup | undefined,
|
devices: HomeAssistant["devices"],
|
||||||
areaDeviceLookup: AreaDeviceLookup | undefined
|
areas: HomeAssistant["areas"]
|
||||||
): string[] => {
|
): string[] => {
|
||||||
if (
|
if (!targetPickerValue) {
|
||||||
!targetPickerValue ||
|
|
||||||
deviceEntityLookup === undefined ||
|
|
||||||
areaEntityLookup === undefined ||
|
|
||||||
areaDeviceLookup === undefined
|
|
||||||
) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityIds = new Set<string>();
|
const targetSelector = { target: {} };
|
||||||
let {
|
const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
|
||||||
area_id: searchingAreaId,
|
const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
|
||||||
device_id: searchingDeviceId,
|
const targetAreas = new Set(ensureArray(targetPickerValue.area_id));
|
||||||
entity_id: searchingEntityId,
|
const targetFloors = new Set(ensureArray(targetPickerValue.floor_id));
|
||||||
} = targetPickerValue;
|
const targetLabels = new Set(ensureArray(targetPickerValue.label_id));
|
||||||
|
|
||||||
if (searchingAreaId) {
|
targetLabels.forEach((labelId) => {
|
||||||
searchingAreaId = ensureArray(searchingAreaId);
|
const expanded = expandLabelTarget(
|
||||||
for (const singleSearchingAreaId of searchingAreaId) {
|
this.hass,
|
||||||
const foundEntities = areaEntityLookup[singleSearchingAreaId];
|
labelId,
|
||||||
if (foundEntities?.length) {
|
areas,
|
||||||
for (const foundEntity of foundEntities) {
|
devices,
|
||||||
if (foundEntity.entity_category === null) {
|
entities,
|
||||||
entityIds.add(foundEntity.entity_id);
|
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];
|
targetFloors.forEach((floorId) => {
|
||||||
if (!foundDevices?.length) {
|
const expanded = expandFloorTarget(
|
||||||
continue;
|
this.hass,
|
||||||
}
|
floorId,
|
||||||
|
areas,
|
||||||
|
targetSelector
|
||||||
|
);
|
||||||
|
expanded.areas.forEach((id) => targetAreas.add(id));
|
||||||
|
});
|
||||||
|
|
||||||
for (const foundDevice of foundDevices) {
|
targetAreas.forEach((areaId) => {
|
||||||
const foundDeviceEntities = deviceEntityLookup[foundDevice.id];
|
const expanded = expandAreaTarget(
|
||||||
if (!foundDeviceEntities?.length) {
|
this.hass,
|
||||||
continue;
|
areaId,
|
||||||
}
|
devices,
|
||||||
|
entities,
|
||||||
|
targetSelector
|
||||||
|
);
|
||||||
|
expanded.devices.forEach((id) => targetDevices.add(id));
|
||||||
|
expanded.entities.forEach((id) => targetEntities.add(id));
|
||||||
|
});
|
||||||
|
|
||||||
for (const foundDeviceEntity of foundDeviceEntities) {
|
targetDevices.forEach((deviceId) => {
|
||||||
if (
|
const expanded = expandDeviceTarget(
|
||||||
(!foundDeviceEntity.area_id ||
|
this.hass,
|
||||||
foundDeviceEntity.area_id === singleSearchingAreaId) &&
|
deviceId,
|
||||||
foundDeviceEntity.entity_category === null
|
entities,
|
||||||
) {
|
targetSelector
|
||||||
entityIds.add(foundDeviceEntity.entity_id);
|
);
|
||||||
}
|
expanded.entities.forEach((id) => targetEntities.add(id));
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchingDeviceId) {
|
return Array.from(targetEntities);
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -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) {
|
if (this._targetPickerValue.area_id) {
|
||||||
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
|
params.area_id = ensureArray(this._targetPickerValue.area_id).join(",");
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,7 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
|||||||
const HIDE_DOMAIN = new Set([
|
const HIDE_DOMAIN = new Set([
|
||||||
"automation",
|
"automation",
|
||||||
"configurator",
|
"configurator",
|
||||||
|
"conversation",
|
||||||
"device_tracker",
|
"device_tracker",
|
||||||
"geo_location",
|
"geo_location",
|
||||||
"persistent_notification",
|
"persistent_notification",
|
||||||
|
@@ -58,18 +58,12 @@ export interface AndCondition extends BaseCondition {
|
|||||||
|
|
||||||
function getValueFromEntityId(
|
function getValueFromEntityId(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
value: string | string[]
|
value: string
|
||||||
): string | string[] {
|
): string | undefined {
|
||||||
if (
|
if (isValidEntityId(value) && hass.states[value]) {
|
||||||
typeof value === "string" &&
|
return hass.states[value]?.state;
|
||||||
isValidEntityId(value) &&
|
|
||||||
hass.states[value]
|
|
||||||
) {
|
|
||||||
value = hass.states[value]?.state;
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
value = value.map((v) => getValueFromEntityId(hass, v) as string);
|
|
||||||
}
|
}
|
||||||
return value;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkStateCondition(
|
function checkStateCondition(
|
||||||
@@ -83,8 +77,17 @@ function checkStateCondition(
|
|||||||
let value = condition.state ?? condition.state_not;
|
let value = condition.state ?? condition.state_not;
|
||||||
|
|
||||||
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
|
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
|
||||||
if (Array.isArray(value) || typeof value === "string") {
|
if (Array.isArray(value)) {
|
||||||
value = getValueFromEntityId(hass, value);
|
const entityValues = value
|
||||||
|
.map((v) => getValueFromEntityId(hass, v))
|
||||||
|
.filter((v): v is string => v !== undefined);
|
||||||
|
value = [...value, ...entityValues];
|
||||||
|
} else if (typeof value === "string") {
|
||||||
|
const entityValue = getValueFromEntityId(hass, value);
|
||||||
|
value = [value];
|
||||||
|
if (entityValue) {
|
||||||
|
value.push(entityValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return condition.state != null
|
return condition.state != null
|
||||||
@@ -103,10 +106,10 @@ function checkStateNumericCondition(
|
|||||||
|
|
||||||
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
|
// Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now)
|
||||||
if (typeof above === "string") {
|
if (typeof above === "string") {
|
||||||
above = getValueFromEntityId(hass, above) as string;
|
above = getValueFromEntityId(hass, above) ?? above;
|
||||||
}
|
}
|
||||||
if (typeof below === "string") {
|
if (typeof below === "string") {
|
||||||
below = getValueFromEntityId(hass, below) as string;
|
below = getValueFromEntityId(hass, below) ?? below;
|
||||||
}
|
}
|
||||||
|
|
||||||
const numericState = Number(state);
|
const numericState = Number(state);
|
||||||
|
@@ -172,12 +172,14 @@ class DialogDashboardStrategyEditor extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _takeControl() {
|
private _takeControl(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
this._params!.takeControl();
|
this._params!.takeControl();
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showRawConfigEditor() {
|
private _showRawConfigEditor(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
this._params!.showRawConfigEditor();
|
this._params!.showRawConfigEditor();
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
@@ -116,6 +116,9 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
|||||||
entities: {
|
entities: {
|
||||||
redirect: "/config/entities",
|
redirect: "/config/entities",
|
||||||
},
|
},
|
||||||
|
labels: {
|
||||||
|
redirect: "/config/labels",
|
||||||
|
},
|
||||||
energy: {
|
energy: {
|
||||||
component: "energy",
|
component: "energy",
|
||||||
redirect: "/energy",
|
redirect: "/energy",
|
||||||
|
@@ -129,7 +129,7 @@ export class HaStateControlAlarmControlPanelModes extends LitElement {
|
|||||||
max-height: max(320px, var(--modes-count, 1) * 80px);
|
max-height: max(320px, var(--modes-count, 1) * 80px);
|
||||||
min-height: max(200px, var(--modes-count, 1) * 80px);
|
min-height: max(200px, var(--modes-count, 1) * 80px);
|
||||||
--control-select-thickness: 130px;
|
--control-select-thickness: 130px;
|
||||||
--control-select-border-radius: 48px;
|
--control-select-border-radius: 36px;
|
||||||
--control-select-color: var(--primary-color);
|
--control-select-color: var(--primary-color);
|
||||||
--control-select-background: var(--disabled-color);
|
--control-select-background: var(--disabled-color);
|
||||||
--control-select-background-opacity: 0.2;
|
--control-select-background-opacity: 0.2;
|
||||||
|
@@ -75,7 +75,7 @@ export class HaStateControlCoverPosition extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 48px;
|
--control-slider-border-radius: 36px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
--control-slider-background: var(--disabled-color);
|
--control-slider-background: var(--disabled-color);
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
|
@@ -112,7 +112,7 @@ export class HaStateControlInfoCoverTiltPosition extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 48px;
|
--control-slider-border-radius: 36px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
--control-slider-background: var(--disabled-color);
|
--control-slider-background: var(--disabled-color);
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
|
@@ -142,7 +142,7 @@ export class HaStateControlCoverToggle extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-switch-thickness: 130px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 48px;
|
--control-switch-border-radius: 36px;
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ export class HaStateControlCoverToggle extends LitElement {
|
|||||||
ha-control-button {
|
ha-control-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
--control-button-border-radius: 48px;
|
--control-button-border-radius: 36px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
ha-control-button.active {
|
ha-control-button.active {
|
||||||
|
@@ -142,7 +142,7 @@ export class HaStateControlFanSpeed extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 48px;
|
--control-slider-border-radius: 36px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
--control-slider-background: var(--disabled-color);
|
--control-slider-background: var(--disabled-color);
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
@@ -153,7 +153,7 @@ export class HaStateControlFanSpeed extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-select-thickness: 130px;
|
--control-select-thickness: 130px;
|
||||||
--control-select-border-radius: 48px;
|
--control-select-border-radius: 36px;
|
||||||
--control-select-color: var(--primary-color);
|
--control-select-color: var(--primary-color);
|
||||||
--control-select-background: var(--disabled-color);
|
--control-select-background: var(--disabled-color);
|
||||||
--control-select-background-opacity: 0.2;
|
--control-select-background-opacity: 0.2;
|
||||||
|
@@ -133,7 +133,7 @@ export class HaStateControlToggle extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-switch-thickness: 130px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 48px;
|
--control-switch-border-radius: 36px;
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ export class HaStateControlToggle extends LitElement {
|
|||||||
ha-control-button {
|
ha-control-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
--control-button-border-radius: 48px;
|
--control-button-border-radius: 36px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
ha-control-button.active {
|
ha-control-button.active {
|
||||||
|
@@ -89,7 +89,7 @@ export class HaStateControlLightBrightness extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 48px;
|
--control-slider-border-radius: 36px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
--control-slider-background: var(--disabled-color);
|
--control-slider-background: var(--disabled-color);
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
|
@@ -167,7 +167,7 @@ export class HaStateControlLockToggle extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-switch-thickness: 130px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 48px;
|
--control-switch-border-radius: 36px;
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ export class HaStateControlLockToggle extends LitElement {
|
|||||||
ha-control-button {
|
ha-control-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
--control-button-border-radius: 48px;
|
--control-button-border-radius: 36px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
ha-control-button.active {
|
ha-control-button.active {
|
||||||
|
@@ -71,7 +71,7 @@ export class HaStateControlValvePosition extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 48px;
|
--control-slider-border-radius: 36px;
|
||||||
--control-slider-color: var(--primary-color);
|
--control-slider-color: var(--primary-color);
|
||||||
--control-slider-background: var(--disabled-color);
|
--control-slider-background: var(--disabled-color);
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
|
@@ -142,7 +142,7 @@ export class HaStateControlValveToggle extends LitElement {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
--control-switch-thickness: 130px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 48px;
|
--control-switch-border-radius: 36px;
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ export class HaStateControlValveToggle extends LitElement {
|
|||||||
ha-control-button {
|
ha-control-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
--control-button-border-radius: 48px;
|
--control-button-border-radius: 36px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
ha-control-button.active {
|
ha-control-button.active {
|
||||||
|
@@ -501,6 +501,7 @@
|
|||||||
},
|
},
|
||||||
"subpage-data-table": {
|
"subpage-data-table": {
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
|
"show_results": "show {number} results",
|
||||||
"clear_filter": "Clear filter",
|
"clear_filter": "Clear filter",
|
||||||
"close_filter": "Close filters",
|
"close_filter": "Close filters",
|
||||||
"exit_selection_mode": "Exit selection mode",
|
"exit_selection_mode": "Exit selection mode",
|
||||||
@@ -1927,7 +1928,10 @@
|
|||||||
"aliases_section": "Aliases",
|
"aliases_section": "Aliases",
|
||||||
"no_aliases": "No configured aliases",
|
"no_aliases": "No configured aliases",
|
||||||
"configured_aliases": "{count} configured {count, plural,\n one {alias}\n other {aliases}\n}",
|
"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": {
|
"category": {
|
||||||
@@ -1962,6 +1966,7 @@
|
|||||||
"color": "Color"
|
"color": "Color"
|
||||||
},
|
},
|
||||||
"add_label": "Add label",
|
"add_label": "Add label",
|
||||||
|
"manage_labels": "Manage labels",
|
||||||
"no_labels": "You don't have any labels",
|
"no_labels": "You don't have any labels",
|
||||||
"introduction": "Labels can help you organize your areas, devices and entities. They can be used to filter in the UI, or use them as a target in automations.",
|
"introduction": "Labels can help you organize your areas, devices and entities. They can be used to filter in the UI, or use them as a target in automations.",
|
||||||
"introduction2": "Go to the area, device or entity you want to add a label to, and click on the edit button to assign labels to them.",
|
"introduction2": "Go to the area, device or entity you want to add a label to, and click on the edit button to assign labels to them.",
|
||||||
@@ -2262,10 +2267,12 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"entity_id": "Entity ID",
|
"entity_id": "Entity ID",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"editable": "Editable"
|
"editable": "Editable",
|
||||||
|
"category": "Category"
|
||||||
},
|
},
|
||||||
"create_helper": "Create helper",
|
"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": {
|
"dialog": {
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
@@ -2679,13 +2686,15 @@
|
|||||||
"assign_category": "Assign category",
|
"assign_category": "Assign category",
|
||||||
"no_category_support": "You can't assign an category to this automation",
|
"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.",
|
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
|
||||||
|
"search": "Search {number} automations",
|
||||||
"headers": {
|
"headers": {
|
||||||
"toggle": "Enable/disable",
|
"toggle": "Enable/disable",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"trigger": "Trigger",
|
"trigger": "Trigger",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"state": "State",
|
"state": "State",
|
||||||
"category": "Category"
|
"category": "Category",
|
||||||
|
"area": "Area"
|
||||||
},
|
},
|
||||||
"bulk_action": "Action",
|
"bulk_action": "Action",
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
@@ -3235,7 +3244,9 @@
|
|||||||
"target_template": "templated {name}",
|
"target_template": "templated {name}",
|
||||||
"target_unknown_entity": "unknown entity",
|
"target_unknown_entity": "unknown entity",
|
||||||
"target_unknown_device": "unknown device",
|
"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": {
|
"play_media": {
|
||||||
@@ -3559,7 +3570,8 @@
|
|||||||
"headers": {
|
"headers": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"state": "State",
|
"state": "State",
|
||||||
"category": "Category"
|
"category": "Category",
|
||||||
|
"area": "Area"
|
||||||
},
|
},
|
||||||
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
|
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
|
||||||
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
|
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
|
||||||
@@ -3568,7 +3580,8 @@
|
|||||||
"delete": "[%key:ui::common::delete%]",
|
"delete": "[%key:ui::common::delete%]",
|
||||||
"duplicate": "[%key:ui::common::duplicate%]",
|
"duplicate": "[%key:ui::common::duplicate%]",
|
||||||
"empty_header": "Create your first script",
|
"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": {
|
"dialog_new": {
|
||||||
"header": "Create script",
|
"header": "Create script",
|
||||||
@@ -3668,14 +3681,16 @@
|
|||||||
"state": "State",
|
"state": "State",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"last_activated": "Last activated",
|
"last_activated": "Last activated",
|
||||||
"category": "Category"
|
"category": "Category",
|
||||||
|
"area": "Area"
|
||||||
},
|
},
|
||||||
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
|
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
|
||||||
"assign_category": "[%key:ui::panel::config::automation::picker::assign_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_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.",
|
"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_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": {
|
"editor": {
|
||||||
"default_name": "New scene",
|
"default_name": "New scene",
|
||||||
@@ -4000,7 +4015,7 @@
|
|||||||
"confirm_delete": "Are you sure you want to delete this device?",
|
"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}?",
|
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
|
||||||
"picker": {
|
"picker": {
|
||||||
"search": "Search devices",
|
"search": "Search {number} devices",
|
||||||
"state": "State"
|
"state": "State"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4011,7 +4026,7 @@
|
|||||||
"header": "Entities",
|
"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.",
|
"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.",
|
"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",
|
"unnamed_entity": "Unnamed entity",
|
||||||
"status": {
|
"status": {
|
||||||
"restored": "Restored",
|
"restored": "Restored",
|
||||||
@@ -4052,6 +4067,9 @@
|
|||||||
"button": "Hide selected",
|
"button": "Hide selected",
|
||||||
"confirm_title": "Do you want to hide {number} {number, plural,\n one {entity}\n other {entities}\n}?",
|
"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."
|
"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