Merge branch 'dev' into break-out-assist-chat

This commit is contained in:
Paulus Schoutsen 2024-09-21 21:16:35 -04:00 committed by GitHub
commit adf77e1e80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
111 changed files with 3830 additions and 2229 deletions

View File

@ -1,16 +1,7 @@
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
--- a/modular/sortable.core.esm.js
+++ b/modular/sortable.core.esm.js
@@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
target = parent; // store last element
}
- /* jshint boss:true */ while (parent = parent.parentNode);
+ /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
}
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
@ -33,7 +24,7 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
}
parentEl = el; // actualization
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
targetRect = getRect(target);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
capture();
@ -44,11 +35,10 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
+ catch(err) {
+ return completed(false);
+ }
+
parentEl = el; // actualization
changed();
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
_silent = true;
setTimeout(_unsilent, 30);
capture();
@ -56,8 +46,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
- el.appendChild(dragEl);
- } else {
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
- }
+ try {
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
@ -67,7 +55,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
+ }
+ catch(err) {
+ return completed(false);
+ }
}
// Undo chrome's scroll adjustment (has no effect on other browsers)
if (scrolledPastTop) {
scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.4.1.cjs
yarnPath: .yarn/releases/yarn-4.5.0.cjs

View File

@ -15,23 +15,29 @@ const brotliOptions = {
};
const zopfliOptions = { threshold: 150 };
const compressDistBrotli = (rootDir, modernDir) =>
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
gulp
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
base: rootDir,
})
.src(
[
`${modernDir}/**/${filesGlob}`,
compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined,
].filter(Boolean),
{
base: rootDir,
}
)
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressDistZopfli = (rootDir, modernDir) =>
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
gulp
.src(
[
`${rootDir}/**/${filesGlob}`,
`!${modernDir}/**/${filesGlob}`,
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
],
].filter(Boolean),
{ base: rootDir }
)
.pipe(zopfli(zopfliOptions))
@ -40,12 +46,20 @@ const compressDistZopfli = (rootDir, modernDir) =>
const compressAppBrotli = () =>
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
const compressHassioBrotli = () =>
compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest);
compressDistBrotli(
paths.hassio_output_root,
paths.hassio_output_latest,
false
);
const compressAppZopfli = () =>
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
const compressHassioZopfli = () =>
compressDistZopfli(paths.hassio_output_root, paths.hassio_output_latest);
compressDistZopfli(
paths.hassio_output_root,
paths.hassio_output_latest,
true
);
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
gulp.task(

View File

@ -60,6 +60,12 @@ function copyPolyfills(staticDir) {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/")
);
// dialog-polyfill css
copyFileDir(
npmPath("dialog-polyfill/dialog-polyfill.css"),
staticPath("polyfills/")
);
}
function copyLoaderJS(staticDir) {

View File

@ -111,9 +111,37 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature",
},
},
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Outdoor temperature",
},
},
"sensor.outdoor_humidity": {
entity_id: "sensor.outdoor_humidity",
state: "70.4",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Outdoor humidity",
},
},
"device_tracker.car": {
entity_id: "sensor.outdoor_humidity",
state: "not_home",
attributes: {
friendly_name: "Car",
icon: "mdi:car",
},
},
"media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini",
state: "on",
state: "playing",
attributes: {
device_class: "speaker",
volume_level: 0.18,

View File

@ -9,6 +9,22 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
path: "home",
icon: "mdi:home-assistant",
badges: [
{
type: "entity",
entity: "sensor.outdoor_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.outdoor_humidity",
color: "indigo",
},
{
type: "entity",
entity: "device_tracker.car",
},
],
sections: [
...(isFrontpageEmbed
? []

View File

@ -232,7 +232,7 @@ export const basicTrace: DemoTrace = {
],
action: [
{
service: "input_boolean.toggle",
action: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
],
default: [
{
service: "input_boolean.toggle",
action: "input_boolean.toggle",
alias: "Toggle 2",
target: {
entity_id: "input_boolean.toggle_2",
@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
],
},
{
service: "input_boolean.toggle",
action: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},

View File

@ -143,7 +143,7 @@ export const motionLightTrace: DemoTrace = {
],
action: [
{
service: "light.turn_on",
action: "light.turn_on",
target: {
entity_id: "light.elgato_key_light_air",
},
@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
delay: 0,
},
{
service: "light.turn_off",
action: "light.turn_off",
target: {
entity_id: "light.elgato_key_light_air",
},

View File

@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: "backyard",
@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: null,
@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];

View File

@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: "backyard",
@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: null,
@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];

View File

@ -232,6 +232,7 @@ const createDeviceRegistryEntries = (
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];

View File

@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-list-new";
import "../../../../src/components/ha-list-item-new";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-list-new>
<ha-md-list>
${repositories.length
? repositories.map(
(repo) => html`
<ha-list-item-new class="option">
<ha-md-list-item class="option">
${repo.name}
<div slot="supporting-text">
<div>${repo.maintainer}</div>
@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
)}
</simple-tooltip>
</div>
</ha-list-item-new>
</ha-md-list-item>
`
)
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
</ha-list-new>
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
</ha-md-list>
<div class="layout horizontal bottom">
<ha-textfield
class="flex-auto"
@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
div.delete ha-icon-button {
color: var(--error-color);
}
ha-list-item-new {
ha-md-list-item {
position: relative;
}
`,

View File

@ -27,9 +27,9 @@
"dependencies": {
"@babel/runtime": "7.25.6",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.0",
"@codemirror/commands": "6.6.0",
"@codemirror/language": "6.10.2",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.6.2",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
@ -80,7 +80,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "2.1.0",
"@material/web": "2.2.0",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
@ -88,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.7",
"@vaadin/vaadin-themable-mixin": "24.4.7",
"@vaadin/combo-box": "24.4.9",
"@vaadin/vaadin-themable-mixin": "24.4.9",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -102,10 +102,11 @@
"comlink": "4.4.1",
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "3.6.0",
"date-fns": "4.1.0",
"date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.11",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
@ -118,7 +119,7 @@
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.5.0",
"marked": "14.1.0",
"marked": "14.1.2",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@ -127,13 +128,13 @@
"qrcode": "1.5.4",
"roboto-fontface": "0.10.0",
"rrule": "2.8.1",
"sortablejs": "1.15.2",
"sortablejs": "1.15.3",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.38",
"ua-parser-js": "1.0.39",
"unfetch": "5.0.0",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
@ -155,7 +156,7 @@
"@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.15.0",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
"@octokit/auth-oauth-device": "7.1.1",
@ -189,7 +190,7 @@
"@typescript-eslint/parser": "7.18.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
@ -198,11 +199,11 @@
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.14.0",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.3",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.1.1",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
@ -213,10 +214,10 @@
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.1.5",
"husky": "9.1.6",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.9",
"lint-staged": "15.2.10",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@ -232,16 +233,16 @@
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"sinon": "18.0.0",
"sinon": "19.0.2",
"systemjs": "6.15.1",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.5.4",
"typescript": "5.6.2",
"webpack": "5.94.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4",
"webpack-dev-server": "5.1.0",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1",
@ -255,8 +256,8 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"sortablejs@1.15.3": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@4.4.1"
"packageManager": "yarn@4.5.0"
}

View File

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

View File

@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [
"weather",
];
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
export const ASSIST_ENTITIES = [
"assist_satellite",
"conversation",
"stt",
"tts",
];
/** Domains that render an input element instead of a text value when displayed in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally

View File

@ -25,7 +25,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import { HomeAssistant } from "../../types";
@ -35,6 +34,7 @@ import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
import { LocalizeFunc } from "../../common/translations/localize";
import { nextRender } from "../../common/util/render-status";
export interface RowClickedEvent {
id: string;
@ -169,8 +169,6 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement;
@state() private _items: DataTableRowData[] = [];
@state() private _collapsedGroups: string[] = [];
private _checkableRowsCount?: number;
@ -179,7 +177,9 @@ export class HaDataTable extends LitElement {
private _sortColumns: SortableColumnContainer = {};
private curRequest = 0;
private _curRequest = 0;
private _lastUpdate = 0;
// @ts-ignore
@restoreScroll(".scroller") private _savedScrollPos?: number;
@ -206,9 +206,9 @@ export class HaDataTable extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this._items.length) {
if (this._filteredData.length) {
// Force update of location of rows
this._items = [...this._items];
this._filteredData = [...this._filteredData];
}
}
@ -291,16 +291,13 @@ export class HaDataTable extends LitElement {
properties.has("columns") ||
properties.has("_filter") ||
properties.has("sortColumn") ||
properties.has("sortDirection") ||
properties.has("groupColumn") ||
properties.has("groupOrder") ||
properties.has("_collapsedGroups")
properties.has("sortDirection")
) {
this._sortFilterData();
}
if (properties.has("selectable") || properties.has("hiddenColumns")) {
this._items = [...this._items];
this._filteredData = [...this._filteredData];
}
}
@ -467,7 +464,15 @@ export class HaDataTable extends LitElement {
scroller
class="mdc-data-table__content scroller ha-scrollbar"
@scroll=${this._saveScrollPos}
.items=${this._items}
.items=${this._groupData(
this._filteredData,
localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups
)}
.keyFunction=${this._keyFunction}
.renderItem=${renderRow}
></lit-virtualizer>
@ -602,8 +607,13 @@ export class HaDataTable extends LitElement {
private async _sortFilterData() {
const startTime = new Date().getTime();
this.curRequest++;
const curRequest = this.curRequest;
const timeBetweenUpdate = startTime - this._lastUpdate;
const timeBetweenRequest = startTime - this._curRequest;
this._curRequest = startTime;
const forceUpdate =
!this._lastUpdate ||
(timeBetweenUpdate > 500 && timeBetweenRequest < 500);
let filteredData = this.data;
if (this._filter) {
@ -614,6 +624,10 @@ export class HaDataTable extends LitElement {
);
}
if (!forceUpdate && this._curRequest !== startTime) {
return;
}
const prom = this.sortColumn
? sortData(
filteredData,
@ -634,91 +648,103 @@ export class HaDataTable extends LitElement {
setTimeout(resolve, 100 - elapsed);
});
}
if (this.curRequest !== curRequest) {
if (!forceUpdate && this._curRequest !== startTime) {
return;
}
const localize = this.localizeFunc || this.hass.localize;
if (this.appendRow || this.hasFab || this.groupColumn) {
let items = [...data];
if (this.groupColumn) {
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
if (grouped.undefined) {
// make sure ungrouped items are at the bottom
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
delete grouped.undefined;
}
const sorted: {
[key: string]: DataTableRowData[];
} = Object.keys(grouped)
.sort((a, b) => {
const orderA = this.groupOrder?.indexOf(a) ?? -1;
const orderB = this.groupOrder?.indexOf(b) ?? -1;
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
return orderA - orderB;
}
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
this.hass.locale.language
);
})
.reduce((obj, key) => {
obj[key] = grouped[key];
return obj;
}, {});
const groupedItems: DataTableRowData[] = [];
Object.entries(sorted).forEach(([groupName, rows]) => {
groupedItems.push({
append: true,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
class=${this._collapsedGroups.includes(groupName)
? "collapsed"
: ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
if (!this._collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
}
});
items = groupedItems;
}
if (this.appendRow) {
items.push({ append: true, content: this.appendRow });
}
if (this.hasFab) {
items.push({ empty: true });
}
this._items = items;
} else {
this._items = data;
}
this._lastUpdate = startTime;
this._filteredData = data;
}
private _groupData = memoizeOne(
(
data: DataTableRowData[],
localize: LocalizeFunc,
appendRow,
hasFab: boolean,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
collapsedGroups: string[]
) => {
if (appendRow || hasFab || groupColumn) {
let items = [...data];
if (groupColumn) {
const grouped = groupBy(items, (item) => item[groupColumn]);
if (grouped.undefined) {
// make sure ungrouped items are at the bottom
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
delete grouped.undefined;
}
const sorted: {
[key: string]: DataTableRowData[];
} = Object.keys(grouped)
.sort((a, b) => {
const orderA = groupOrder?.indexOf(a) ?? -1;
const orderB = groupOrder?.indexOf(b) ?? -1;
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
return orderA - orderB;
}
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
this.hass.locale.language
);
})
.reduce((obj, key) => {
obj[key] = grouped[key];
return obj;
}, {});
const groupedItems: DataTableRowData[] = [];
Object.entries(sorted).forEach(([groupName, rows]) => {
groupedItems.push({
append: true,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
class=${collapsedGroups.includes(groupName)
? "collapsed"
: ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
if (!collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
}
});
items = groupedItems;
}
if (appendRow) {
items.push({ append: true, content: appendRow });
}
if (hasFab) {
items.push({ empty: true });
}
return items;
}
return data;
}
);
private _memFilterData = memoizeOne(
(
data: DataTableRowData[],
@ -802,8 +828,8 @@ export class HaDataTable extends LitElement {
private _checkedRowsChanged() {
// force scroller to update, change it's items
if (this._items.length) {
this._items = [...this._items];
if (this._filteredData.length) {
this._filteredData = [...this._filteredData];
}
fireEvent(this, "selection-changed", {
value: this._checkedRows,

View File

@ -1,6 +1,6 @@
import { mdiTextureBox } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
@ -20,12 +20,7 @@ import {
getDeviceEntityDisplayLookup,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
@ -50,7 +45,7 @@ interface FloorAreaEntry {
}
@customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
export class HaAreaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public required = false;
@state() private _floors?: FloorRegistryEntry[];
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
public async open() {
await this.updateComplete;
await this.comboBox?.open();
@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.hass && this._floors) ||
(!this._init && this.hass) ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
const areas = this._getAreas(
this._floors!,
Object.values(this.hass.floors),
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),

155
src/components/ha-badge.ts Normal file
View File

@ -0,0 +1,155 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
type BadgeType = "badge" | "button";
@customElement("ha-badge")
export class HaBadge extends LitElement {
@property() public type: BadgeType = "badge";
@property() public label?: string;
@property({ type: Boolean, attribute: "icon-only" }) iconOnly = false;
protected render() {
const label = this.label;
return html`
<div
class="badge ${classMap({
"icon-only": this.iconOnly,
})}"
role=${ifDefined(this.type === "button" ? "button" : undefined)}
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
>
<ha-ripple .disabled=${this.type !== "button"}></ha-ripple>
<slot name="icon"></slot>
${this.iconOnly
? nothing
: html`<span class="info">
${label ? html`<span class="label">${label}</span>` : nothing}
<span class="content"><slot></slot></span>
</span>`}
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
--badge-color: var(--secondary-text-color);
-webkit-tap-highlight-color: transparent;
}
.badge {
position: relative;
--ha-ripple-color: var(--badge-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
height: var(--ha-badge-size, 36px);
min-width: var(--ha-badge-size, 36px);
padding: 0px 12px;
box-sizing: border-box;
width: auto;
border-radius: var(
--ha-badge-border-radius,
calc(var(--ha-badge-size, 36px) / 2)
);
background: var(
--ha-card-background,
var(--card-background-color, white)
);
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
backdrop-filter: var(--ha-card-backdrop-filter, none);
border-width: var(--ha-card-border-width, 1px);
box-shadow: var(--ha-card-box-shadow, none);
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
}
.badge:focus-visible {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--badge-color);
border-color: var(--badge-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
[role="button"] {
cursor: pointer;
}
[role="button"]:focus {
outline: none;
}
.info {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-inline-start: initial;
text-align: center;
font-family: Roboto;
}
.label {
font-size: 10px;
font-style: normal;
font-weight: 500;
line-height: 10px;
letter-spacing: 0.1px;
color: var(--secondary-text-color);
}
.content {
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.1px;
color: var(--primary-text-color);
}
::slotted([slot="icon"]) {
--mdc-icon-size: 18px;
color: var(--badge-color);
line-height: 0;
margin-left: -4px;
margin-right: 0;
margin-inline-start: -4px;
margin-inline-end: 0;
}
::slotted(img[slot="icon"]) {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
overflow: hidden;
margin-left: -10px;
margin-right: 0;
margin-inline-start: -10px;
margin-inline-end: 0;
}
.badge.icon-only {
padding: 0;
}
.badge.icon-only ::slotted([slot="icon"]) {
margin-left: 0;
margin-right: 0;
margin-inline-start: 0;
margin-inline-end: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-badge": HaBadge;
}
}

View File

@ -1,6 +1,5 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl";
import {
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { getFloorAreaLookup } from "../data/floor_registry";
import { RelatedResult, findRelated } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
@ -31,7 +25,7 @@ import "./ha-svg-icon";
import "./ha-tree-indicator";
@customElement("ha-filter-floor-areas")
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
export class HaFilterFloorAreas extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: {
@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
@state() private _shouldRender = false;
@state() private _floors?: FloorRegistryEntry[];
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
}
protected render() {
const areas = this._areas(this.hass.areas, this._floors);
const areas = this._areas(this.hass.areas, this.hass.floors);
return html`
<ha-expansion-panel
@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
this._findRelated();
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
protected updated(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
}
private _areas = memoizeOne(
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
const areas = Object.values(areaReg);
const floors = Object.values(floorReg);
const floorAreaLookup = getFloorAreaLookup(areas);
const unassisgnedAreas = areas.filter(

View File

@ -1,5 +1,5 @@
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@ -24,10 +24,8 @@ import {
FloorRegistryEntry,
createFloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@ -53,7 +51,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
</ha-list-item>`;
@customElement("ha-floor-picker")
export class HaFloorPicker extends SubscribeMixin(LitElement) {
export class HaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@ -111,8 +109,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
@state() private _opened?: boolean;
@state() private _floors?: FloorRegistryEntry[];
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _suggestion?: string;
@ -129,14 +125,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
await this.comboBox?.focus();
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
private _getFloors = memoizeOne(
(
floors: FloorRegistryEntry[],
@ -320,12 +308,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.hass && this._floors) ||
(!this._init && this.hass) ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
const floors = this._getFloors(
this._floors!,
Object.values(this.hass.floors),
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),
@ -360,8 +348,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
? this.hass.localize("ui.components.floor-picker.floor")
: this.label}
.placeholder=${this.placeholder
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
?.name
? this.hass.floors[this.placeholder]?.name
: undefined}
.renderer=${rowRenderer}
@filter-changed=${this._filterChanged}
@ -460,7 +447,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
floor_id: floor.floor_id,
});
});
const floors = [...this._floors!, floor];
const floors = [...Object.values(this.hass.floors), floor];
this.comboBox.filteredItems = this._getFloors(
floors,
Object.values(this.hass.areas)!,

View File

@ -95,10 +95,10 @@ export const computeInitialHaFormData = (
} else if (
"action" in selector ||
"trigger" in selector ||
"condition" in selector ||
"media" in selector ||
"target" in selector
"condition" in selector
) {
data[field.name] = [];
} else if ("media" in selector || "target" in selector) {
data[field.name] = {};
} else {
throw new Error(

View File

@ -73,6 +73,10 @@ export class HaForm extends LitElement implements HaFormElement {
schema: any
) => string | undefined;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
protected getFormProperties(): Record<string, any> {
return {};
}
@ -145,6 +149,7 @@ export class HaForm extends LitElement implements HaFormElement {
.disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? "" : item.default}
.helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue}
.required=${item.required || false}
.context=${this._generateContext(item)}
></ha-selector>`

View File

@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
import "./ha-menu";
import type { HaMenu } from "./ha-menu";
@customElement("ha-button-menu-new")
export class HaButtonMenuNew extends LitElement {
@customElement("ha-md-button-menu")
export class HaMdButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET];
@property({ type: Boolean }) public disabled = false;
@ -84,6 +84,6 @@ export class HaButtonMenuNew extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-button-menu-new": HaButtonMenuNew;
"ha-md-button-menu": HaMdButtonMenu;
}
}

View File

@ -0,0 +1,210 @@
import { MdDialog } from "@material/web/dialog/dialog";
import {
type DialogAnimation,
DIALOG_DEFAULT_CLOSE_ANIMATION,
DIALOG_DEFAULT_OPEN_ANIMATION,
} from "@material/web/dialog/internal/animations";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
/**
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
*
*/
@customElement("ha-md-dialog")
export class HaMdDialog extends MdDialog {
/**
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
*/
@property({ attribute: "disable-cancel-action", type: Boolean })
public disableCancelAction = false;
private _polyfillDialogRegistered = false;
constructor() {
super();
this.addEventListener("cancel", this._handleCancel);
if (typeof HTMLDialogElement !== "function") {
this.addEventListener("open", this._handleOpen);
if (!DIALOG_POLYFILL) {
DIALOG_POLYFILL = import("dialog-polyfill");
}
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
}
// prevent open in older browsers and wait for polyfill to load
private async _handleOpen(openEvent: Event) {
openEvent.preventDefault();
if (this._polyfillDialogRegistered) {
return;
}
this._polyfillDialogRegistered = true;
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
const dialog = this.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement;
const dialogPolyfill = await DIALOG_POLYFILL;
dialogPolyfill.default.registerDialog(dialog);
this.removeEventListener("open", this._handleOpen);
this.show();
}
private async _loadPolyfillStylesheet(href) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
return new Promise<void>((resolve, reject) => {
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Stylesheet failed to load: ${href}`));
this.shadowRoot?.appendChild(link);
});
}
_handleCancel(closeEvent: Event) {
if (this.disableCancelAction) {
closeEvent.preventDefault();
const dialogElement = this.shadowRoot?.querySelector("dialog");
if (this.animate !== undefined) {
dialogElement?.animate(
[
{
transform: "rotate(-1deg)",
"animation-timing-function": "ease-in",
},
{
transform: "rotate(1.5deg)",
"animation-timing-function": "ease-out",
},
{
transform: "rotate(0deg)",
"animation-timing-function": "ease-in",
},
],
{
duration: 200,
iterations: 2,
}
);
}
}
}
static override styles = [
...super.styles,
css`
:host {
--md-dialog-container-color: var(--card-background-color);
--md-dialog-headline-color: var(--primary-text-color);
--md-dialog-supporting-text-color: var(--primary-text-color);
--md-sys-color-scrim: #000000;
--md-dialog-headline-weight: 400;
--md-dialog-headline-size: 1.574rem;
--md-dialog-supporting-text-size: 1rem;
--md-dialog-supporting-text-line-height: 1.5rem;
}
:host([type="alert"]) {
max-width: 320px;
min-width: 320px;
}
:host(:not([type="alert"])) {
@media all and (max-width: 450px), all and (max-height: 500px) {
min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
max-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
min-height: 100%;
max-height: 100%;
--md-dialog-container-shape: 0;
}
}
:host ::slotted(ha-dialog-header) {
display: contents;
}
.scrim {
z-index: 10; // overlay navigation
}
`,
];
}
// by default the dialog open/close animation will be from/to the top
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_OPEN_ANIMATION,
dialog: [
[
// Dialog slide up
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade in
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_CLOSE_ANIMATION,
dialog: [
[
// Dialog slide down
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade out
[{ opacity: "1" }, { opacity: "0" }],
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
export const getMobileOpenFromBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
};
export const getMobileCloseToBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
};
declare global {
interface HTMLElementTagNameMap {
"ha-md-dialog": HaMdDialog;
}
}

View File

@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-list-item-new")
export class HaListItemNew extends MdListItem {
@customElement("ha-md-list-item")
export class HaMdListItem extends MdListItem {
static override styles = [
...super.styles,
css`
@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem {
declare global {
interface HTMLElementTagNameMap {
"ha-list-item-new": HaListItemNew;
"ha-md-list-item": HaMdListItem;
}
}

View File

@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-list-new")
export class HaListNew extends MdList {
@customElement("ha-md-list")
export class HaMdList extends MdList {
static override styles = [
...super.styles,
css`
@ -16,6 +16,6 @@ export class HaListNew extends MdList {
declare global {
interface HTMLElementTagNameMap {
"ha-list-new": HaListNew;
"ha-md-list": HaMdList;
}
}

View File

@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-menu-item")
export class HaMenuItem extends MdMenuItem {
@customElement("ha-md-menu-item")
export class HaMdMenuItem extends MdMenuItem {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [
@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem {
declare global {
interface HTMLElementTagNameMap {
"ha-menu-item": HaMenuItem;
"ha-md-menu-item": HaMdMenuItem;
}
}

View File

@ -6,7 +6,7 @@ import {
} from "@material/web/menu/internal/controllers/shared";
import { css } from "lit";
import { customElement } from "lit/decorators";
import type { HaMenuItem } from "./ha-menu-item";
import type { HaMdMenuItem } from "./ha-md-menu-item";
@customElement("ha-menu")
export class HaMenu extends MdMenu {
@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
) {
return;
}
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
}
static override styles = [

View File

@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
.label=${this.label || ""}
.required=${this.required}
.helper=${this.helper}
.disalbled=${this.disabled}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>
`;

View File

@ -67,7 +67,9 @@ export class HaNumberSelector extends LitElement {
}
return html`
${this.label ? html`${this.label}${this.required ? "*" : ""}` : nothing}
${this.label && !isBox
? html`${this.label}${this.required ? "*" : ""}`
: nothing}
<div class="input">
${!isBox
? html`

View File

@ -240,12 +240,24 @@ export class HaServiceControl extends LitElement {
...value,
selector: value.selector as Selector | undefined,
}));
const hasSelector: string[] = [];
fields.forEach((field) => {
if ((field as any).fields) {
Object.entries((field as any).fields).forEach(([key, subField]) => {
if ((subField as any).selector) {
hasSelector.push(key);
}
});
} else if (field.selector) {
hasSelector.push(field.key);
}
});
return {
...serviceDomains[domain][serviceName],
fields,
hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
hasSelector,
};
}
);

View File

@ -35,10 +35,6 @@ import {
computeDeviceName,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
FloorRegistryEntry,
subscribeFloorRegistry,
} from "../data/floor_registry";
import {
LabelRegistryEntry,
subscribeLabelRegistry,
@ -103,17 +99,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@state() private _floors?: FloorRegistryEntry[];
@state() private _labels?: LabelRegistryEntry[];
private _opened = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labels = labels;
}),
@ -132,9 +123,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
<div class="mdc-chip-set items">
${this.value?.floor_id
? ensureArray(this.value.floor_id).map((floor_id) => {
const floor = this._floors?.find(
(flr) => flr.floor_id === floor_id
);
const floor = this.hass.floors[floor_id];
return this._renderChip(
"floor_id",
floor_id,

View File

@ -109,7 +109,7 @@ export class HaTextField extends TextFieldBase {
color: var(--secondary-text-color);
}
.mdc-text-field__icon {
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
color: var(--secondary-text-color);
}

View File

@ -109,7 +109,7 @@ class HaWebRtcPlayer extends LitElement {
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", async (event) => {
if (!event.candidate) {
if (!event.candidate?.candidate) {
resolve(); // Gathering complete
return;
}

View File

@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { Condition, Trigger } from "../../data/automation";
import { Condition, Trigger, flattenTriggers } from "../../data/automation";
import {
Action,
ChooseAction,
@ -572,8 +572,8 @@ export class HatScriptGraph extends LitElement {
const paths = Object.keys(this.trackedNodes);
const trigger_nodes =
"trigger" in this.trace.config
? ensureArray(this.trace.config.trigger).map((trigger, i) =>
this.render_trigger(trigger, i)
? flattenTriggers(ensureArray(this.trace.config.trigger)).map(
(trigger, i) => this.render_trigger(trigger, i)
)
: undefined;
try {

View File

@ -3,6 +3,7 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { ensureArray } from "../common/array/ensure-array";
import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
@ -62,6 +63,10 @@ export interface ContextConstraint {
user_id?: string | string[];
}
export interface TriggerList {
triggers: Trigger | Trigger[] | undefined;
}
export interface BaseTrigger {
alias?: string;
platform: string;
@ -373,6 +378,27 @@ export const normalizeAutomationConfig = <
return config;
};
export const flattenTriggers = (
triggers: undefined | (Trigger | TriggerList)[]
): Trigger[] => {
if (!triggers) {
return [];
}
const flatTriggers: Trigger[] = [];
triggers.forEach((t) => {
if ("triggers" in t) {
if (t.triggers) {
flatTriggers.push(...ensureArray(t.triggers));
}
} else {
flatTriggers.push(t);
}
});
return flatTriggers;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
initialAutomationEditorData = data;
navigate("/config/automation/edit/new");

View File

@ -68,9 +68,18 @@ export const describeTrigger = (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
) => {
): string => {
try {
return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
const description = tryDescribeTrigger(
trigger,
hass,
entityRegistry,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);
@ -700,9 +709,18 @@ export const describeCondition = (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
) => {
): string => {
try {
return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
const description = tryDescribeCondition(
condition,
hass,
entityRegistry,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);

View File

@ -1,6 +1,6 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { IntegrationManifest, IntegrationType } from "./integration";
import type { IntegrationType } from "./integration";
export interface ConfigEntry {
entry_id: string;
@ -149,20 +149,19 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
export const sortConfigEntries = (
configEntries: ConfigEntry[],
manifestLookup: { [domain: string]: IntegrationManifest }
primaryConfigEntry: string | null
): ConfigEntry[] => {
const sortedConfigEntries = [...configEntries];
const getScore = (entry: ConfigEntry) => {
const manifest = manifestLookup[entry.domain] as
| IntegrationManifest
| undefined;
const isHelper = manifest?.integration_type === "helper";
return isHelper ? -1 : 1;
};
const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) =>
getScore(b) - getScore(a);
return sortedConfigEntries.sort(configEntriesCompare);
if (!primaryConfigEntry) {
return configEntries;
}
const primaryEntry = configEntries.find(
(e) => e.entry_id === primaryConfigEntry
);
if (!primaryEntry) {
return configEntries;
}
const otherEntries = configEntries.filter(
(e) => e.entry_id !== primaryConfigEntry
);
return [primaryEntry, ...otherEntries];
};

View File

@ -1,10 +1,20 @@
export interface DataTableFilters {
[key: string]: {
value: string[] | { key: string[] } | undefined;
value: DataTableFiltersValue;
items: Set<string> | undefined;
};
}
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
export interface DataTableFiltersValues {
[key: string]: DataTableFiltersValue;
}
export interface DataTableFiltersItems {
[key: string]: Set<string> | undefined;
}
export const serializeFilters = (value: DataTableFilters) => {
const serializedValue = {};
Object.entries(value).forEach(([key, val]) => {

View File

@ -33,6 +33,7 @@ export interface DeviceRegistryEntry extends RegistryEntry {
entry_type: "service" | null;
disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null;
primary_config_entry: string | null;
}
export interface DeviceEntityDisplayLookup {

View File

@ -1,7 +1,4 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
import { AreaRegistryEntry } from "./area_registry";
import { RegistryEntry } from "./registry";
@ -27,48 +24,6 @@ export interface FloorRegistryEntryMutableParams {
aliases?: string[];
}
const fetchFloorRegistry = (conn: Connection) =>
conn
.sendMessagePromise({
type: "config/floor_registry/list",
})
.then((floors) =>
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
if (ent1.level !== ent2.level) {
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
}
return stringCompare(ent1.name, ent2.name);
})
);
const subscribeFloorRegistryUpdates = (
conn: Connection,
store: Store<FloorRegistryEntry[]>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
store.setState(areas, true)
),
500,
true
),
"floor_registry_updated"
);
export const subscribeFloorRegistry = (
conn: Connection,
onChange: (floors: FloorRegistryEntry[]) => void
) =>
createCollection<FloorRegistryEntry[]>(
"_floorRegistry",
fetchFloorRegistry,
subscribeFloorRegistryUpdates,
conn,
onChange
);
export const createFloorRegistryEntry = (
hass: HomeAssistant,
values: FloorRegistryEntryMutableParams

View File

@ -2,6 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types";
import { subscribeDeviceRegistry } from "./device_registry";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
export enum NetworkType {
THREAD = "thread",
@ -51,10 +53,30 @@ export interface MatterCommissioningParameters {
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
hass.auth.external?.config.canCommissionMatter;
export const startExternalCommissioning = (hass: HomeAssistant) =>
hass.auth.external!.fireMessage({
export const startExternalCommissioning = async (hass: HomeAssistant) => {
if (isComponentLoaded(hass, "thread")) {
const datasets = await listThreadDataSets(hass);
const preferredDataset = datasets.datasets.find(
(dataset) => dataset.preferred
);
if (preferredDataset) {
return hass.auth.external!.fireMessage({
type: "matter/commission",
payload: {
active_operational_dataset: (
await getThreadDataSetTLV(hass, preferredDataset.dataset_id)
).tlv,
border_agent_id: preferredDataset.preferred_border_agent_id,
mac_extended_address: preferredDataset.preferred_extended_address,
},
});
}
}
return hass.auth.external!.fireMessage({
type: "matter/commission",
});
};
export const redirectOnNewMatterDevice = (
hass: HomeAssistant,

View File

@ -50,7 +50,7 @@ export const describeAction = <T extends ActionType>(
ignoreAlias = false
): string => {
try {
return tryDescribeAction(
const description = tryDescribeAction(
hass,
entityRegistry,
labelRegistry,
@ -59,6 +59,10 @@ export const describeAction = <T extends ActionType>(
actionType,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);

View File

@ -3,6 +3,7 @@ import { Context, HomeAssistant } from "../types";
import {
BlueprintAutomationConfig,
ManualAutomationConfig,
flattenTriggers,
} from "./automation";
import { BlueprintScriptConfig, ScriptConfig } from "./script";
@ -190,7 +191,11 @@ export const getDataFromPath = (
if (!tempResult && raw === "sequence") {
continue;
}
result = tempResult;
if (raw === "trigger") {
result = flattenTriggers(tempResult);
} else {
result = tempResult;
}
continue;
}

View File

@ -0,0 +1,47 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { FloorRegistryEntry } from "./floor_registry";
const fetchFloorRegistry = (conn: Connection) =>
conn
.sendMessagePromise({
type: "config/floor_registry/list",
})
.then((floors) =>
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
if (ent1.level !== ent2.level) {
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
}
return stringCompare(ent1.name, ent2.name);
})
);
const subscribeFloorRegistryUpdates = (
conn: Connection,
store: Store<FloorRegistryEntry[]>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
store.setState(areas, true)
),
500,
true
),
"floor_registry_updated"
);
export const subscribeFloorRegistry = (
conn: Connection,
onChange: (floors: FloorRegistryEntry[]) => void
) =>
createCollection<FloorRegistryEntry[]>(
"_floorRegistry",
fetchFloorRegistry,
subscribeFloorRegistryUpdates,
conn,
onChange
);

View File

@ -252,6 +252,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
type: string;
unit: string;
states: { [key: number]: string };
default: any;
}
export interface ZWaveJSSetConfigParamData {

View File

@ -83,7 +83,7 @@ export const showConfigFlowDialog = (
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}` : "";
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
hass.localize(

View File

@ -1,13 +1,14 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertOutline } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-svg-icon";
import "../../components/ha-switch";
import "../../components/ha-button";
import { HaTextField } from "../../components/ha-textfield";
import { HomeAssistant } from "../../types";
import { DialogBoxParams } from "./show-dialog-box";
@ -18,8 +19,12 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _closeState?: "canceled" | "confirmed";
@query("ha-textfield") private _textField?: HaTextField;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(params: DialogBoxParams): Promise<void> {
this._params = params;
}
@ -42,33 +47,33 @@ class DialogBox extends LitElement {
const confirmPrompt = this._params.confirmation || this._params.prompt;
const dialogTitle =
this._params.title ||
(this._params.confirmation &&
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
return html`
<ha-dialog
<ha-md-dialog
open
?scrimClickAction=${confirmPrompt}
?escapeKeyAction=${confirmPrompt}
.disableCancelAction=${confirmPrompt || false}
@closed=${this._dialogClosed}
defaultAction="ignore"
.heading=${html`${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
style="color: var(--warning-color)"
></ha-svg-icon> `
: ""}${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize(
"ui.dialogs.generic.default_confirmation_title"
)}`}
type="alert"
aria-labelledby="dialog-box-title"
aria-describedby="dialog-box-description"
>
<div>
${this._params.text
? html`
<p class=${this._params.prompt ? "no-bottom-padding" : ""}>
${this._params.text}
</p>
`
: ""}
<div slot="headline">
<span .title=${dialogTitle} id="dialog-box-title">
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
style="color: var(--warning-color)"
></ha-svg-icon> `
: nothing}
${dialogTitle}
</span>
</div>
<div slot="content" id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt
? html`
<ha-textfield
@ -87,58 +92,64 @@ class DialogBox extends LitElement {
`
: ""}
</div>
${confirmPrompt &&
html`
<mwc-button
@click=${this._dismiss}
slot="secondaryAction"
<div slot="actions">
${confirmPrompt &&
html`
<ha-button
@click=${this._dismiss}
?dialogInitialFocus=${!this._params.prompt &&
this._params.destructive}
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</ha-button>
`}
<ha-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt &&
this._params.destructive}
!this._params.destructive}
class=${classMap({
destructive: this._params.destructive || false,
})}
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
`}
<mwc-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt &&
!this._params.destructive}
slot="primaryAction"
class=${classMap({
destructive: this._params.destructive || false,
})}
>
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
</ha-dialog>
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _dismiss(): void {
private _cancel(): void {
if (this._params?.cancel) {
this._params.cancel();
}
this._close();
}
private _dismiss(): void {
this._cancel();
this._closeState = "canceled";
this._closeDialog();
}
private _confirm(): void {
if (this._params!.confirm) {
this._params!.confirm(this._textField?.value);
}
this._close();
this._closeState = "confirmed";
this._closeDialog();
}
private _dialogClosed(ev) {
if (ev.detail.action === "ignore") {
return;
private _closeDialog() {
this._dialog?.close();
}
private _dialogClosed() {
if (!this._closeState) {
this._cancel();
}
this._dismiss();
}
private _close(): void {
if (!this._params) {
return;
}
@ -168,10 +179,6 @@ class DialogBox extends LitElement {
.destructive {
--mdc-theme-primary: var(--error-color);
}
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
}
@media all and (min-width: 600px) {
ha-dialog {
--mdc-dialog-min-width: 400px;

View File

@ -1,15 +1,18 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import {
getMobileOpenFromBottomAnimation,
getMobileCloseToBottomAnimation,
} from "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button-toggle";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import {
formatTempColor,
LightColor,
LightColorMode,
LightEntity,
@ -38,15 +41,7 @@ class DialogLightColorFavorite extends LitElement {
@state() private _modes: LightPickerMode[] = [];
@state() private _currentValue?: string;
private _colorHovered(ev: CustomEvent<HASSDomEvents["color-hovered"]>) {
if (ev.detail && "color_temp_kelvin" in ev.detail) {
this._currentValue = formatTempColor(ev.detail.color_temp_kelvin);
} else {
this._currentValue = undefined;
}
}
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
dialogParams: LightColorFavoriteDialogParams
@ -58,10 +53,7 @@ class DialogLightColorFavorite extends LitElement {
}
public closeDialog(): void {
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
}
private _updateModes() {
@ -130,9 +122,20 @@ class DialogLightColorFavorite extends LitElement {
private async _cancel() {
this._dialogParams?.cancel?.();
}
private _cancelDialog() {
this._cancel();
this.closeDialog();
}
private _dialogClosed(): void {
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _save() {
if (!this._color) {
this._cancel();
@ -156,82 +159,83 @@ class DialogLightColorFavorite extends LitElement {
}
return html`
<ha-dialog
<ha-md-dialog
open
@closed=${this._cancel}
.heading=${this._dialogParams?.title ?? ""}
flexContent
@cancel=${this._cancel}
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<ha-dialog-header slot="heading">
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">${this._dialogParams?.title}</span>
<span slot="title" id="dialog-light-color-favorite-title"
>${this._dialogParams?.title}</span
>
</ha-dialog-header>
<div class="header">
<span class="value">${this._currentValue}</span>
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
<div slot="content">
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
@color-hovered=${this._colorHovered}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
@color-hovered=${this._colorHovered}
>
</light-color-rgb-picker>
`
: nothing}
<div slot="actions">
<ha-button @click=${this._cancelDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save} .disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
>
</div>
<ha-button slot="secondaryAction" dialogAction="cancel">
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
>
</ha-dialog>
</ha-md-dialog>
`;
}
@ -239,19 +243,23 @@ class DialogLightColorFavorite extends LitElement {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
ha-md-dialog {
min-width: 420px; /* prevent width jumps when switching modes */
max-height: min(
600px,
100% - 48px
); /* prevent scrolling on desktop */
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog {
--dialog-surface-margin-top: 100px;
--mdc-dialog-min-height: auto;
--mdc-dialog-max-height: calc(100% - 100px);
--ha-dialog-border-radius: var(
--ha-dialog-bottom-sheet-border-radius,
28px 28px 0 0
);
ha-md-dialog {
min-width: 100%;
min-height: auto;
max-height: calc(100% - 100px);
margin-bottom: 0;
--md-dialog-container-shape-start-start: 28px;
--md-dialog-container-shape-start-end: 28px;
}
}
@ -287,21 +295,6 @@ class DialogLightColorFavorite extends LitElement {
rgb(255, 160, 0) 100%
);
}
.value {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
font-style: normal;
font-weight: 500;
font-size: 16px;
height: 48px;
line-height: 48px;
letter-spacing: 0.1px;
text-align: center;
}
`,
];
}

View File

@ -21,6 +21,7 @@ import { isUnavailableState } from "../../../data/entity";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { listenMediaQuery } from "../../../common/dom/media_query";
import "../components/ha-more-info-state-header";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
@customElement("more-info-script")
class MoreInfoScript extends LitElement {
@ -28,6 +29,8 @@ class MoreInfoScript extends LitElement {
@property({ attribute: false }) public stateObj?: ScriptEntity;
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
@state() private _scriptData: Record<string, any> = {};
@state() private narrow = false;
@ -59,8 +62,9 @@ class MoreInfoScript extends LitElement {
const stateObj = this.stateObj;
const fields =
this.hass.services.script[computeObjectId(this.stateObj.entity_id)]
?.fields;
this.hass.services.script[
this.entry?.unique_id || computeObjectId(this.stateObj.entity_id)
]?.fields;
const hasFields = fields && Object.keys(fields).length > 0;
@ -138,17 +142,30 @@ class MoreInfoScript extends LitElement {
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (!changedProperties.has("stateObj")) {
return;
if (changedProperties.has("stateObj")) {
const oldState = changedProperties.get("stateObj") as
| HassEntity
| undefined;
const newState = this.stateObj;
if (
newState &&
(!oldState || oldState.entity_id !== newState.entity_id)
) {
this._scriptData = {
action:
this.entry?.entity_id === newState.entity_id
? `script.${this.entry.unique_id}`
: newState.entity_id,
};
}
}
const oldState = changedProperties.get("stateObj") as
| HassEntity
| undefined;
const newState = this.stateObj;
if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
this._scriptData = { action: newState.entity_id, data: {} };
if (this.entry?.unique_id && changedProperties.has("entry")) {
const action = `script.${this.entry?.unique_id}`;
if (this._scriptData?.action !== action) {
this._scriptData = { ...this._scriptData, action };
}
}
}
@ -161,7 +178,7 @@ class MoreInfoScript extends LitElement {
ev.stopPropagation();
this.hass.callService(
"script",
computeObjectId(this.stateObj!.entity_id),
this.entry?.unique_id || computeObjectId(this.stateObj!.entity_id),
this._scriptData.data
);
}

View File

@ -21,6 +21,7 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { subscribeAreaRegistry } from "../data/ws-area_registry";
import { subscribeDeviceRegistry } from "../data/ws-device_registry";
import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display";
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user";
@ -117,6 +118,7 @@ window.hassConnection.then(({ conn }) => {
subscribeEntityRegistryDisplay(conn, noop);
subscribeDeviceRegistry(conn, noop);
subscribeAreaRegistry(conn, noop);
subscribeFloorRegistry(conn, noop);
subscribeConfig(conn, noop);
subscribeServices(conn, noop);
subscribePanels(conn, noop);

View File

@ -57,6 +57,11 @@ interface EMOutgoingMessageBarCodeNotify extends EMMessage {
interface EMOutgoingMessageMatterCommission extends EMMessage {
type: "matter/commission";
payload?: {
mac_extended_address: string | null;
border_agent_id: string | null;
active_operational_dataset: string | null;
};
}
interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
@ -136,7 +141,7 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
type: "thread/store_in_platform_keychain";
payload: {
mac_extended_address: string;
border_agent_id: string | null;
border_agent_id: string;
active_operational_dataset: string;
};
}

View File

@ -35,10 +35,10 @@ import type {
HaDataTable,
SortingDirection,
} from "../components/data-table/ha-data-table";
import "../components/ha-button-menu-new";
import "../components/ha-md-button-menu";
import "../components/ha-dialog";
import { HaMenu } from "../components/ha-menu";
import "../components/ha-menu-item";
import "../components/ha-md-menu-item";
import "../components/search-input-outlined";
import type { HomeAssistant, Route } from "../types";
import "./hass-tabs-subpage";
@ -330,7 +330,7 @@ export class HaTabsSubpageDataTable extends LitElement {
"ui.components.subpage-data-table.exit_selection_mode"
)}
></ha-icon-button>
<ha-button-menu-new positioning="absolute">
<ha-md-button-menu positioning="absolute">
<ha-assist-chip
.label=${localize(
"ui.components.subpage-data-table.select"
@ -346,20 +346,26 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
<ha-menu-item .value=${undefined} @click=${this._selectAll}>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectAll}
>
<div slot="headline">
${localize("ui.components.subpage-data-table.select_all")}
</div>
</ha-menu-item>
<ha-menu-item .value=${undefined} @click=${this._selectNone}>
</ha-md-menu-item>
<ha-md-menu-item
.value=${undefined}
@click=${this._selectNone}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.select_none"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item
<ha-md-menu-item
.value=${undefined}
@click=${this._disableSelectMode}
>
@ -368,8 +374,8 @@ export class HaTabsSubpageDataTable extends LitElement {
"ui.components.subpage-data-table.close_select_mode"
)}
</div>
</ha-menu-item>
</ha-button-menu-new>
</ha-md-menu-item>
</ha-md-button-menu>
<p>
${localize("ui.components.subpage-data-table.selected", {
selected: this.selected || "0",
@ -476,27 +482,27 @@ export class HaTabsSubpageDataTable extends LitElement {
${Object.entries(this.columns).map(([id, column]) =>
column.groupable
? html`
<ha-menu-item
<ha-md-menu-item
.value=${id}
@click=${this._handleGroupBy}
.selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })}
>
${column.title || column.label}
</ha-menu-item>
</ha-md-menu-item>
`
: nothing
)}
<ha-menu-item
<ha-md-menu-item
.value=${undefined}
@click=${this._handleGroupBy}
.selected=${this._groupColumn === undefined}
class=${classMap({ selected: this._groupColumn === undefined })}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item
<ha-md-menu-item
@click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
@ -505,8 +511,8 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.collapse_all_groups")}
</ha-menu-item>
<ha-menu-item
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
@ -515,13 +521,13 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-menu-item>
</ha-md-menu-item>
</ha-menu>
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
${Object.entries(this.columns).map(([id, column]) =>
column.sortable
? html`
<ha-menu-item
<ha-md-menu-item
.value=${id}
@click=${this._handleSortBy}
keep-open
@ -539,7 +545,7 @@ export class HaTabsSubpageDataTable extends LitElement {
`
: nothing}
${column.title || column.label}
</ha-menu-item>
</ha-md-menu-item>
`
: nothing
)}
@ -893,7 +899,7 @@ export class HaTabsSubpageDataTable extends LitElement {
#sort-by-anchor,
#group-by-anchor,
ha-button-menu-new ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
`;

View File

@ -1,4 +1,3 @@
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
CSSResultGroup,
@ -13,6 +12,7 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-button";
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
import { subscribeConfigFlowInProgress } from "../data/config_flow";
import { domainToName } from "../data/integration";
@ -117,6 +117,30 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
const foundIntegrations = domains.length;
// there is a possibility that the user has no integrations
if (foundIntegrations === 0) {
return html`
<div class="all-set-icon">🎉</div>
<h1>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.all_set"
)}
</h1>
<p>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.lets_start"
)}
</p>
<div class="footer">
<ha-button unelevated @click=${this._finish}>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.finish"
)}
</ha-button>
</div>
`;
}
if (domains.length > 12) {
domains = domains.slice(0, 11);
}
@ -149,11 +173,11 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
: nothing}
</div>
<div class="footer">
<mwc-button unelevated @click=${this._finish}>
<ha-button unelevated @click=${this._finish}>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.finish"
)}
</mwc-button>
</ha-button>
</div>
`;
}
@ -193,6 +217,10 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
align-items: center;
height: 100%;
}
.all-set-icon {
font-size: 64px;
text-align: center;
}
`,
];
}

View File

@ -6,7 +6,6 @@ import {
mdiPencil,
mdiPlus,
} from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
@ -15,15 +14,15 @@ import {
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { formatListWithAnds } from "../../../common/string/format-list";
import "../../../components/ha-fab";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import {
AreaRegistryEntry,
createAreaRegistryEntry,
@ -34,7 +33,6 @@ import {
createFloorRegistryEntry,
deleteFloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
updateFloorRegistryEntry,
} from "../../../data/floor_registry";
import {
@ -42,7 +40,6 @@ import {
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
@ -57,7 +54,7 @@ const UNASSIGNED_PATH = ["__unassigned__"];
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
@customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@ -66,14 +63,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public route!: Route;
@state() private _floors?: FloorRegistryEntry[];
private _processAreas = memoizeOne(
(
areas: HomeAssistant["areas"],
devices: HomeAssistant["devices"],
entities: HomeAssistant["entities"],
floors: FloorRegistryEntry[]
floors: HomeAssistant["floors"]
) => {
const processArea = (area: AreaRegistryEntry) => {
let noDevicesInArea = 0;
@ -109,7 +104,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
);
return {
floors: floors.map((floor) => ({
floors: Object.values(floors).map((floor) => ({
...floor,
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
})),
@ -118,26 +113,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
}
);
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
protected render(): TemplateResult {
const areasAndFloors =
!this.hass.areas ||
!this.hass.devices ||
!this.hass.entities ||
!this._floors
!this.hass.floors
? undefined
: this._processAreas(
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._floors
this.hass.floors
);
return html`
@ -327,7 +314,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._floors!
this.hass.floors
);
let area: AreaRegistryEntry;
if (ev.detail.oldPath === UNASSIGNED_PATH) {

View File

@ -28,8 +28,8 @@ import "../../../components/ha-domain-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item-new";
import "../../../components/ha-list-new";
import "../../../components/ha-md-list-item";
import "../../../components/ha-md-list";
import "../../../components/ha-service-icon";
import "../../../components/search-input";
import {
@ -434,7 +434,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
protected _opened(): void {
// Store the width and height so that when we search, box doesn't jump
const boundingRect =
this.shadowRoot!.querySelector("ha-list-new")?.getBoundingClientRect();
this.shadowRoot!.querySelector("ha-md-list")?.getBoundingClientRect();
this._width = boundingRect?.width;
this._height = boundingRect?.height;
}
@ -526,7 +526,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
)}
></search-input>
</div>
<ha-list-new
<ha-md-list
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
style=${styleMap({
width: this._width ? `${this._width}px` : "auto",
@ -537,7 +537,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
!this._filter &&
(!this._group ||
items.find((item) => item.key === this._params!.clipboardItem))
? html`<ha-list-item-new
? html`<ha-md-list-item
interactive
type="button"
class="paste"
@ -558,14 +558,14 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
.path=${mdiContentPaste}
></ha-svg-icon
><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
</ha-list-item-new>
</ha-md-list-item>
<md-divider role="separator" tabindex="-1"></md-divider>`
: ""}
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-list-item-new
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@ -588,10 +588,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
slot="end"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-list-item-new>
</ha-md-list-item>
`
)}
</ha-list-new>
</ha-md-list>
</ha-dialog>
`;
}
@ -643,13 +643,13 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
ha-icon-next {
width: 24px;
}
ha-list-new {
ha-md-list {
max-height: 468px;
max-width: 100vw;
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
}
ha-list-item-new img {
ha-md-list-item img {
width: 24px;
}
search-input {

View File

@ -6,8 +6,8 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item-new";
import "../../../../components/ha-list-new";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-md-list";
import "../../../../components/ha-radio";
import "../../../../components/ha-textfield";
@ -90,7 +90,7 @@ class DialogAutomationMode extends LitElement implements HassDialog {
></ha-icon-button>
</a>
</ha-dialog-header>
<ha-list-new
<ha-md-list
role="listbox"
tabindex="0"
aria-activedescendant="option-${this._newMode}"
@ -103,7 +103,7 @@ class DialogAutomationMode extends LitElement implements HassDialog {
`ui.panel.config.automation.editor.modes.${mode}`
);
return html`
<ha-list-item-new
<ha-md-list-item
class="option"
type="button"
@click=${this._modeChanged}
@ -132,10 +132,10 @@ class DialogAutomationMode extends LitElement implements HassDialog {
`ui.panel.config.automation.editor.modes.${mode}_description`
)}
</div>
</ha-list-item-new>
</ha-md-list-item>
`;
})}
</ha-list-new>
</ha-md-list>
${isMaxMode(this._newMode)
? html`

View File

@ -67,8 +67,8 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu";
import type { HaMenu } from "../../../components/ha-menu";
import "../../../components/ha-menu-item";
import type { HaMenuItem } from "../../../components/ha-menu-item";
import "../../../components/ha-md-menu-item";
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
@ -403,7 +403,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
@ -411,21 +411,21 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
? 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-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<ha-md-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>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<ha-md-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
@ -437,7 +437,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
@ -455,18 +455,18 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
</ha-md-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
</div></ha-md-menu-item
>`;
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
@ -477,23 +477,23 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
</ha-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<ha-md-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<ha-md-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
@ -633,7 +633,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-filter-blueprints>
${
!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -646,10 +646,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -662,10 +662,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}
</ha-md-button-menu>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -678,10 +678,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
</ha-md-button-menu>`}`
: nothing
}
<ha-button-menu-new has-overflow slot="selection-bar">
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
@ -709,7 +709,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
@ -719,7 +719,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
@ -727,7 +727,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
@ -737,7 +737,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
@ -745,7 +745,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
@ -755,20 +755,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
<ha-menu-item @click=${this._handleBulkEnable}>
<ha-md-menu-item @click=${this._handleBulkEnable}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.enable"
)}
</div>
</ha-menu-item>
<ha-menu-item @click=${this._handleBulkDisable}>
</ha-md-menu-item>
<ha-md-menu-item @click=${this._handleBulkDisable}>
<ha-svg-icon
slot="start"
.path=${mdiToggleSwitchOffOutline}
@ -778,8 +778,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_actions.disable"
)}
</div>
</ha-menu-item>
</ha-button-menu-new>
</ha-md-menu-item>
</ha-md-button-menu>
${
!this.automations.length
? html`<div class="empty" slot="empty">
@ -827,7 +827,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-menu id="overflow-menu" positioning="fixed">
<ha-menu-item .clickAction=${this._showInfo}>
<ha-md-menu-item .clickAction=${this._showInfo}>
<ha-svg-icon
.path=${mdiInformationOutline}
slot="start"
@ -835,46 +835,46 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
</div>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu-item .clickAction=${this._showSettings}>
<ha-md-menu-item .clickAction=${this._showSettings}>
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</div>
</ha-menu-item>
<ha-menu-item .clickAction=${this._editCategory}>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._editCategory}>
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
)}
</div>
</ha-menu-item>
<ha-menu-item .clickAction=${this._runActions}>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._runActions}>
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.run")}
</div>
</ha-menu-item>
<ha-menu-item .clickAction=${this._showTrace}>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._showTrace}>
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item .clickAction=${this._duplicate}>
<ha-md-menu-item .clickAction=${this._duplicate}>
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
</div>
</ha-menu-item>
<ha-menu-item .clickAction=${this._toggle}>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._toggle}>
<ha-svg-icon
.path=${
this._overflowAutomation?.state === "off"
@ -892,13 +892,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
)
}
</div>
</ha-menu-item>
<ha-menu-item .clickAction=${this._deleteConfirm} class="warning">
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.delete")}
</div>
</ha-menu-item>
</ha-md-menu-item>
</ha-menu>
`;
}
@ -1056,13 +1056,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _showInfo = (item: HaMenuItem) => {
private _showInfo = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
};
private _showSettings = (item: HaMenuItem) => {
private _showSettings = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
@ -1072,14 +1072,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _runActions = (item: HaMenuItem) => {
private _runActions = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
triggerAutomationActions(this.hass, automation.entity_id);
};
private _editCategory = (item: HaMenuItem) => {
private _editCategory = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
@ -1103,7 +1103,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _showTrace = (item: HaMenuItem) => {
private _showTrace = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
@ -1120,7 +1120,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
};
private _toggle = async (item: HaMenuItem): Promise<void> => {
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
@ -1130,7 +1130,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _deleteConfirm = async (item: HaMenuItem) => {
private _deleteConfirm = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
@ -1167,7 +1167,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _duplicate = async (item: HaMenuItem) => {
private _duplicate = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMenu)!.anchorElement as any)!
.automation;
@ -1455,7 +1455,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@ -153,7 +153,7 @@ export class HaConfigDevicePage extends LitElement {
.filter((entId) => entId in entryLookup)
.map((entry) => entryLookup[entry]);
return sortConfigEntries(deviceEntries, manifestLookup);
return sortConfigEntries(deviceEntries, device.primary_config_entry);
}
);
@ -189,20 +189,20 @@ export class HaConfigDevicePage extends LitElement {
const result = groupBy(entities, (entry) => {
const domain = computeDomain(entry.entity_id);
if (entry.entity_category) {
return entry.entity_category;
if (ASSIST_ENTITIES.includes(domain)) {
return "assist";
}
if (domain === "event" || domain === "notify") {
return domain;
}
if (SENSOR_ENTITIES.includes(domain)) {
return "sensor";
if (entry.entity_category) {
return entry.entity_category;
}
if (ASSIST_ENTITIES.includes(domain)) {
return "assist";
if (SENSOR_ENTITIES.includes(domain)) {
return "sensor";
}
return "control";
@ -1536,6 +1536,10 @@ export class HaConfigDevicePage extends LitElement {
padding-bottom: 16px;
}
ha-card:has(ha-logbook) {
padding-bottom: var(--ha-card-border-radius, 12px);
}
ha-logbook {
height: 400px;
}

View File

@ -54,7 +54,7 @@ import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-item";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
@ -388,7 +388,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
device.config_entries
.filter((entId) => entId in entryLookup)
.map((entId) => entryLookup[entId]),
manifestLookup
device.primary_config_entry
);
const labels = labelReg && device?.labels;
@ -629,7 +629,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
@ -640,23 +640,23 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
</ha-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<ha-md-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<ha-md-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
@ -668,7 +668,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
@ -686,13 +686,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
</ha-md-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
</div></ha-md-menu-item
>`;
return html`
@ -802,7 +802,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-filter-labels>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -815,11 +815,11 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>
</ha-md-button-menu>
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -832,10 +832,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
</ha-md-button-menu>`}`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-button-menu-new has-overflow slot="selection-bar">
? html`<ha-md-button-menu has-overflow slot="selection-bar">
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
@ -855,7 +855,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-icon-button>`}
${this.narrow
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
@ -865,12 +865,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing}
<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
@ -880,10 +880,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>
</ha-button-menu-new>`
</ha-md-button-menu>`
: nothing}
</hass-tabs-subpage-data-table>
`;
@ -1100,7 +1100,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@ -60,7 +60,7 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-item";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
@ -99,9 +99,8 @@ import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
serializeFilters,
deserializeFilters,
DataTableFilters,
DataTableFiltersValues,
DataTableFiltersItems,
} from "../../../data/data_table_filters";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
@ -157,13 +156,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@storage({
storage: "sessionStorage",
key: "entities-table-filters-full",
key: "entities-table-filters",
state: true,
subscribe: false,
serializer: serializeFilters,
deserializer: deserializeFilters,
})
private _filters: DataTableFilters = {};
private _filters: DataTableFiltersValues = {};
@state() private _filteredItems: DataTableFiltersItems = {};
@state() private _selected: string[] = [];
@ -460,13 +459,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
stateEntities: StateEntity[],
filters: DataTableFilters,
filters: DataTableFiltersValues,
filteredItems: DataTableFiltersItems,
entries?: ConfigEntry[],
labelReg?: LabelRegistryEntry[]
) => {
const result: EntityRow[] = [];
const stateFilters = filters["ha-filter-states"]?.value as string[];
const stateFilters = filters["ha-filter-states"] as string[];
const showEnabled =
!stateFilters?.length || stateFilters.includes("enabled");
@ -491,15 +491,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
const filteredDomains = new Set<string>();
Object.entries(filters).forEach(([key, filter]) => {
if (
key === "config_entry" &&
Array.isArray(filter.value) &&
filter.value.length
) {
if (key === "config_entry" && Array.isArray(filter) && filter.length) {
filteredEntities = filteredEntities.filter(
(entity) =>
entity.config_entry_id &&
(filter.value as string[]).includes(entity.config_entry_id)
(filter as string[]).includes(entity.config_entry_id)
);
if (!entries) {
@ -509,8 +505,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
const configEntries = entries.filter(
(entry) =>
entry.entry_id &&
(filter.value as string[]).includes(entry.entry_id)
entry.entry_id && (filter as string[]).includes(entry.entry_id)
);
configEntries.forEach((configEntry) => {
@ -521,17 +516,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}
} else if (
key === "ha-filter-integrations" &&
Array.isArray(filter.value) &&
filter.value.length
Array.isArray(filter) &&
filter.length
) {
if (!entries) {
this._loadConfigEntries();
return;
}
const entryIds = entries
.filter((entry) =>
(filter.value as string[]).includes(entry.domain)
)
.filter((entry) => (filter as string[]).includes(entry.domain))
.map((entry) => entry.entry_id);
const filteredEntitiesByDomain = new Set<string>();
@ -547,7 +540,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entitiesByDomain[source.domain].push(entity);
}
for (const val of filter.value) {
for (const val of filter) {
if (val in entitiesByDomain) {
entitiesByDomain[val].forEach((item) =>
filteredEntitiesByDomain.add(item)
@ -558,32 +551,34 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filteredEntities = filteredEntities.filter(
(entity) =>
filteredEntitiesByDomain.has(entity.entity_id) ||
(filter.value as string[]).includes(entity.platform) ||
(filter as string[]).includes(entity.platform) ||
(entity.config_entry_id &&
entryIds.includes(entity.config_entry_id))
);
filter.value!.forEach((domain) => filteredDomains.add(domain));
filter!.forEach((domain) => filteredDomains.add(domain));
} else if (
key === "ha-filter-domains" &&
Array.isArray(filter.value) &&
filter.value.length
Array.isArray(filter) &&
filter.length
) {
filteredEntities = filteredEntities.filter((entity) =>
(filter.value as string[]).includes(computeDomain(entity.entity_id))
(filter as string[]).includes(computeDomain(entity.entity_id))
);
} else if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
Array.isArray(filter) &&
filter.length
) {
filteredEntities = filteredEntities.filter((entity) =>
entity.labels.some((lbl) =>
(filter.value as string[]).includes(lbl)
)
entity.labels.some((lbl) => (filter as string[]).includes(lbl))
);
} else if (filter.items) {
}
});
Object.values(filteredItems).forEach((items) => {
if (items) {
filteredEntities = filteredEntities.filter((entity) =>
filter.items!.has(entity.entity_id)
items.has(entity.entity_id)
);
}
});
@ -684,6 +679,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this.hass.areas,
this._stateEntities,
this._filters,
this._filteredItems,
this._entries,
this._labels
);
@ -704,7 +700,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
@ -722,13 +718,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
</ha-md-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
</div></ha-md-menu-item
>`;
return html`
@ -749,10 +745,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
hasFilters
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
@ -786,7 +782,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
${
!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -796,10 +792,10 @@ ${
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`
</ha-md-button-menu>`
: nothing
}
<ha-button-menu-new has-overflow slot="selection-bar">
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
@ -824,29 +820,29 @@ ${
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-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-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>
<md-divider role="separator" tabindex="-1"></md-divider>`
: nothing
}
<ha-menu-item @click=${this._enableSelected}>
<ha-md-menu-item @click=${this._enableSelected}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</div>
</ha-menu-item>
<ha-menu-item @click=${this._disableSelected}>
</ha-md-menu-item>
<ha-md-menu-item @click=${this._disableSelected}>
<ha-svg-icon
slot="start"
.path=${mdiToggleSwitchOffOutline}
@ -856,10 +852,10 @@ ${
"ui.panel.config.entities.picker.disable_selected.button"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._unhideSelected}>
<ha-md-menu-item @click=${this._unhideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEye}
@ -869,8 +865,8 @@ ${
"ui.panel.config.entities.picker.unhide_selected.button"
)}
</div>
</ha-menu-item>
<ha-menu-item @click=${this._hideSelected}>
</ha-md-menu-item>
<ha-md-menu-item @click=${this._hideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEyeOff}
@ -880,10 +876,10 @@ ${
"ui.panel.config.entities.picker.hide_selected.button"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._removeSelected} class="warning">
<ha-md-menu-item @click=${this._removeSelected} class="warning">
<ha-svg-icon
slot="start"
.path=${mdiDelete}
@ -893,25 +889,24 @@ ${
"ui.panel.config.entities.picker.delete_selected.button"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
</ha-button-menu-new>
</ha-md-button-menu>
${
Array.isArray(this._filters.config_entry?.value) &&
this._filters.config_entry?.value.length
Array.isArray(this._filters.config_entry) &&
this._filters.config_entry?.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]}
(entry) => entry.entry_id === this._filters.config_entry![0]
)?.title || this._filters.config_entry[0]}
</ha-alert>`
: nothing
}
<ha-filter-floor-areas
.hass=${this.hass}
type="entity"
.value=${this._filters["ha-filter-floor-areas"]?.value}
.value=${this._filters["ha-filter-floor-areas"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-floor-areas"}
@ -921,7 +916,7 @@ ${
<ha-filter-devices
.hass=${this.hass}
.type=${"entity"}
.value=${this._filters["ha-filter-devices"]?.value}
.value=${this._filters["ha-filter-devices"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-devices"}
@ -930,7 +925,7 @@ ${
></ha-filter-devices>
<ha-filter-domains
.hass=${this.hass}
.value=${this._filters["ha-filter-domains"]?.value}
.value=${this._filters["ha-filter-domains"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-domains"}
@ -939,7 +934,7 @@ ${
></ha-filter-domains>
<ha-filter-integrations
.hass=${this.hass}
.value=${this._filters["ha-filter-integrations"]?.value}
.value=${this._filters["ha-filter-integrations"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-integrations"}
@ -951,7 +946,7 @@ ${
.label=${this.hass.localize(
"ui.panel.config.entities.picker.headers.status"
)}
.value=${this._filters["ha-filter-states"]?.value}
.value=${this._filters["ha-filter-states"]}
.states=${this._states(this.hass.localize)}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@ -961,7 +956,7 @@ ${
></ha-filter-states>
<ha-filter-labels
.hass=${this.hass}
.value=${this._filters["ha-filter-labels"]?.value}
.value=${this._filters["ha-filter-labels"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-labels"}
@ -996,7 +991,9 @@ ${
private _filterChanged(ev) {
const type = ev.target.localName;
this._filters = { ...this._filters, [type]: ev.detail };
this._filters = { ...this._filters, [type]: ev.detail.value };
this._filteredItems = { ...this._filteredItems, [type]: ev.detail.items };
}
protected firstUpdated() {
@ -1008,10 +1005,7 @@ ${
return;
}
this._filters = {
"ha-filter-states": {
value: ["enabled"],
items: undefined,
},
"ha-filter-states": ["enabled"],
};
}
@ -1026,18 +1020,9 @@ ${
this._filter = history.state?.filter || "";
this._filters = {
"ha-filter-states": {
value: [],
items: undefined,
},
"ha-filter-integrations": {
value: domain ? [domain] : [],
items: undefined,
},
config_entry: {
value: configEntry ? [configEntry] : [],
items: undefined,
},
"ha-filter-states": [],
"ha-filter-integrations": domain ? [domain] : [],
config_entry: configEntry ? [configEntry] : [],
};
if (this._searchParms.has("label")) {
@ -1052,15 +1037,13 @@ ${
}
this._filters = {
...this._filters,
"ha-filter-labels": {
value: [label],
items: undefined,
},
"ha-filter-labels": [label],
};
}
private _clearFilter() {
this._filters = {};
this._filteredItems = {};
}
public willUpdate(changedProps: PropertyValues): void {
@ -1070,8 +1053,10 @@ ${
if (!this.hass || !this._entities) {
return;
}
if (
changedProps.has("hass") ||
(changedProps.has("hass") &&
(!oldHass || oldHass.states !== this.hass.states)) ||
changedProps.has("_entities") ||
changedProps.has("_entitySources")
) {
@ -1084,9 +1069,9 @@ ${
continue;
}
if (
!oldHass ||
changedProps.has("_entitySources") ||
this.hass.states[entityId] !== oldHass.states[entityId]
(changedProps.has("hass") && !oldHass) ||
!oldHass.states[entityId]
) {
changed = true;
}
@ -1357,6 +1342,7 @@ ${rejected
this.hass.areas,
this._stateEntities,
this._filters,
this._filteredItems,
this._entries,
this._labels
);
@ -1468,7 +1454,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

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

View File

@ -479,7 +479,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
@ -487,21 +487,21 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
? 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-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<ha-md-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>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<ha-md-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@ -512,7 +512,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
@ -530,13 +530,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-menu-item> `;
</ha-md-menu-item> `;
})}<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@ -637,7 +637,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-filter-categories>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -650,10 +650,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -666,11 +666,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}`
</ha-md-button-menu>`}`
: nothing}
${this.narrow || labelsInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
@ -698,7 +698,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
@ -708,7 +708,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
@ -716,7 +716,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
@ -726,12 +726,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
</ha-md-button-menu>`
: nothing}
<ha-integration-overflow-menu
@ -1155,7 +1155,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@ -45,12 +45,12 @@ import { isDevVersion } from "../../../common/config/version";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-button-menu-new";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-list-item";
import "../../../components/ha-list-item-new";
import "../../../components/ha-list-new";
import "../../../components/ha-menu-item";
import "../../../components/ha-md-list-item";
import "../../../components/ha-md-list";
import "../../../components/ha-md-menu-item";
import {
deleteApplicationCredential,
fetchApplicationCredentialsConfigEntry,
@ -474,10 +474,10 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
"ui.panel.config.integrations.discovered"
)}
</h1>
<ha-list-new>
<ha-md-list>
${discoveryFlows.map(
(flow) =>
html`<ha-list-item-new class="discovered">
html`<ha-md-list-item class="discovered">
${flow.localized_title}
<ha-button
slot="end"
@ -488,9 +488,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
"ui.panel.config.integrations.configure"
)}
></ha-button>
</ha-list-item-new>`
</ha-md-list-item>`
)}
</ha-list-new>
</ha-md-list>
</ha-card>`
: ""}
${attentionFlows.length || attentionEntries.length
@ -500,12 +500,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
`ui.panel.config.integrations.integration_page.attention_entries`
)}
</h1>
<ha-list-new>
<ha-md-list>
${attentionFlows.map((flow) => {
const attention = ATTENTION_SOURCES.includes(
flow.context.source
);
return html` <ha-list-item-new
return html` <ha-md-list-item
class="config_entry ${attention ? "attention" : ""}"
>
${flow.localized_title}
@ -527,7 +527,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}`
)}
></ha-button>
</ha-list-item-new>`;
</ha-md-list-item>`;
})}
${attentionEntries.map(
(item, index) =>
@ -539,7 +539,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
></md-divider>`
: ""} `
)}
</ha-list-new>
</ha-md-list>
</ha-card>`
: ""}
@ -568,7 +568,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
)}
</div>`
: nothing}
<ha-list-new>
<ha-md-list>
${normalEntries.map(
(item, index) =>
html`${this._renderConfigEntry(item)}
@ -579,7 +579,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
></md-divider>`
: ""} `
)}
</ha-list-new>
</ha-md-list>
<div class="card-actions">
<ha-button @click=${this._addIntegration}>
${this._manifest?.integration_type
@ -743,7 +743,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
const configPanel = this._configPanel(item.domain, this.hass.panels);
return html`<ha-list-item-new
return html`<ha-md-list-item
class=${classMap({
config_entry: true,
"state-not-loaded": item!.state === "not_loaded",
@ -797,23 +797,23 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
</mwc-button>
`
: ""}
<ha-button-menu-new positioning="popover" slot="end">
<ha-md-button-menu positioning="popover" slot="end">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${item.supports_options && stateText
? html`<ha-menu-item @click=${this._showOptions}>
? html`<ha-md-menu-item @click=${this._showOptions}>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.configure"
)}
</ha-menu-item>`
</ha-md-menu-item>`
: ""}
${item.disabled_by && devices.length
? html`
<ha-menu-item
<ha-md-menu-item
href=${devices.length === 1
? `/config/devices/device/${devices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
@ -824,11 +824,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
{ count: devices.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-menu-item>
</ha-md-menu-item>
`
: ""}
${item.disabled_by && services.length
? html`<ha-menu-item
? html`<ha-md-menu-item
href=${services.length === 1
? `/config/devices/device/${services[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
@ -842,11 +842,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
{ count: services.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-menu-item> `
</ha-md-menu-item> `
: ""}
${item.disabled_by && entities.length
? html`
<ha-menu-item
<ha-md-menu-item
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon
@ -858,7 +858,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
{ count: entities.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-menu-item>
</ha-md-menu-item>
`
: ""}
${!item.disabled_by &&
@ -866,27 +866,27 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
item.supports_unload &&
item.source !== "system"
? html`
<ha-menu-item @click=${this._handleReload}>
<ha-md-menu-item @click=${this._handleReload}>
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</ha-menu-item>
</ha-md-menu-item>
`
: nothing}
<ha-menu-item @click=${this._handleRename} graphic="icon">
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
${this._diagnosticHandler && item.state === "loaded"
? html`
<ha-menu-item
<ha-md-menu-item
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
target="_blank"
@click=${this._signUrl}
@ -895,41 +895,41 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
${this.hass.localize(
"ui.panel.config.integrations.config_entry.download_diagnostics"
)}
</ha-menu-item>
</ha-md-menu-item>
`
: ""}
${!item.disabled_by &&
item.supports_reconfigure &&
item.source !== "system"
? html`
<ha-menu-item @click=${this._handleReconfigure}>
<ha-md-menu-item @click=${this._handleReconfigure}>
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reconfigure"
)}
</ha-menu-item>
</ha-md-menu-item>
`
: nothing}
<ha-menu-item @click=${this._handleSystemOptions} graphic="icon">
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}
</ha-menu-item>
</ha-md-menu-item>
${item.disabled_by === "user"
? html`
<ha-menu-item @click=${this._handleEnable}>
<ha-md-menu-item @click=${this._handleEnable}>
<ha-svg-icon
slot="start"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.enable")}
</ha-menu-item>
</ha-md-menu-item>
`
: item.source !== "system"
? html`
<ha-menu-item
<ha-md-menu-item
class="warning"
@click=${this._handleDisable}
graphic="icon"
@ -940,12 +940,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
.path=${mdiStopCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.disable")}
</ha-menu-item>
</ha-md-menu-item>
`
: nothing}
${item.source !== "system"
? html`
<ha-menu-item class="warning" @click=${this._handleDelete}>
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
<ha-svg-icon
slot="start"
class="warning"
@ -954,11 +954,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}
</ha-menu-item>
</ha-md-menu-item>
`
: nothing}
</ha-button-menu-new>
</ha-list-item-new>`;
</ha-md-button-menu>
</ha-md-list-item>`;
}
private async _highlightEntry() {
@ -1485,13 +1485,13 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
ha-alert:first-of-type {
margin-top: 16px;
}
ha-list-item-new {
ha-md-list-item {
position: relative;
}
ha-list-item-new.discovered {
ha-md-list-item.discovered {
height: 72px;
}
ha-list-item-new.config_entry::after {
ha-md-list-item.config_entry::after {
position: absolute;
top: 0;
right: 0;
@ -1561,7 +1561,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
.state-disabled [slot="supporting-text"] {
opacity: var(--md-list-item-disabled-opacity, 0.3);
}
ha-list-new {
ha-md-list {
margin-top: 8px;
margin-bottom: 8px;
}

View File

@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-list-item-new";
import "../../../../../../components/ha-list-new";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";

View File

@ -3,8 +3,8 @@ import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-list-item-new";
import "../../../../../../components/ha-list-new";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import { HomeAssistant } from "../../../../../../types";
import { MatterAddDeviceStep } from "../dialog-matter-add-device";
import { sharedStyles } from "./matter-add-device-shared-styles";
@ -23,8 +23,8 @@ class MatterAddDeviceExisting extends LitElement {
</p>
</div>
<ha-list-new>
<ha-list-item-new
<ha-md-list>
<ha-md-list-item
interactive
type="button"
.step=${"google_home"}
@ -43,8 +43,8 @@ class MatterAddDeviceExisting extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-new>
<ha-list-item-new
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
.step=${"apple_home"}
@ -63,8 +63,8 @@ class MatterAddDeviceExisting extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-new>
<ha-list-item-new
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
.step=${"generic"}
@ -80,8 +80,8 @@ class MatterAddDeviceExisting extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-new>
</ha-list-new>
</ha-md-list-item>
</ha-md-list>
`;
}

View File

@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-list-item-new";
import "../../../../../../components/ha-list-new";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";

View File

@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-list-item-new";
import "../../../../../../components/ha-list-new";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";

View File

@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-list-item-new";
import "../../../../../../components/ha-list-new";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";

View File

@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-icon-next";
import "../../../../../../components/ha-list-item-new";
import "../../../../../../components/ha-list-new";
import "../../../../../../components/ha-md-list-item";
import "../../../../../../components/ha-md-list";
import { HomeAssistant } from "../../../../../../types";
import { sharedStyles } from "./matter-add-device-shared-styles";
@ -18,8 +18,8 @@ class MatterAddDeviceMain extends LitElement {
${this.hass.localize(`ui.dialogs.matter-add-device.main.question`)}
</p>
</div>
<ha-list-new>
<ha-list-item-new
<ha-md-list>
<ha-md-list-item
interactive
type="button"
.step=${"new"}
@ -37,8 +37,8 @@ class MatterAddDeviceMain extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-new>
<ha-list-item-new
</ha-md-list-item>
<ha-md-list-item
interactive
type="button"
.step=${"existing"}
@ -56,8 +56,8 @@ class MatterAddDeviceMain extends LitElement {
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-list-item-new>
</ha-list-new>
</ha-md-list-item>
</ha-md-list>
`;
}

View File

@ -23,7 +23,7 @@ export const sharedStyles = css`
cursor: pointer;
text-decoration: underline;
}
ha-list-new {
ha-md-list {
padding: 0;
--md-list-item-leading-space: var(--horizontal-padding, 16px);
--md-list-item-trailing-space: var(--horizontal-padding, 16px);

View File

@ -106,13 +106,11 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
"ui.panel.config.thread.add_dataset_from_tlv"
)}</mwc-list-item
>
${!this._otbrInfo
? html`<mwc-list-item @click=${this._addOTBR}
>${this.hass.localize(
"ui.panel.config.thread.add_open_thread_border_router"
)}</mwc-list-item
>`
: ""}
<mwc-list-item @click=${this._addOTBR}
>${this.hass.localize(
"ui.panel.config.thread.add_open_thread_border_router"
)}</mwc-list-item
>
</ha-button-menu>
<div class="content">
<h1>${this.hass.localize("ui.panel.config.thread.my_network")}</h1>
@ -342,8 +340,8 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address: otbr.extended_address,
border_agent_id: otbr.border_agent_id ?? "",
active_operational_dataset: otbr.active_dataset_tlvs ?? "",
border_agent_id: otbr.border_agent_id,
active_operational_dataset: otbr.active_dataset_tlvs,
},
});
}

View File

@ -153,8 +153,9 @@ class DialogZWaveJSAddNode extends LitElement {
.label=${html`<b>Secure if possible</b>
<div class="secondary">
Requires user interaction during inclusion. Fast and
secure with S2 when supported. Fallback to legacy S0
or no encryption when necessary.
secure with S2 when supported. Allows manually
selecting which security keys to grant. Fallback to
legacy S0 or no encryption when necessary.
</div>`}
>
<ha-radio

View File

@ -6,46 +6,39 @@ import {
mdiCloseCircle,
mdiProgressClock,
} from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { groupBy } from "../../../../../common/util/group-by";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-select";
import "../../../../../components/ha-settings-row";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-textfield";
import { groupBy } from "../../../../../common/util/group-by";
import {
computeDeviceName,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../../../data/device_registry";
import "../../../../../components/ha-selector/ha-selector-boolean";
import { computeDeviceName } from "../../../../../data/device_registry";
import {
ZWaveJSNodeConfigParam,
ZWaveJSNodeConfigParams,
ZWaveJSSetConfigParamResult,
ZwaveJSNodeMetadata,
fetchZwaveNodeConfigParameters,
fetchZwaveNodeMetadata,
setZwaveNodeConfigParameter,
ZWaveJSNodeConfigParam,
ZWaveJSNodeConfigParams,
ZwaveJSNodeMetadata,
ZWaveJSSetConfigParamResult,
} from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-loading-screen";
import "../../../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
@ -57,16 +50,8 @@ const icons = {
error: mdiCloseCircle,
};
const getDevice = memoizeOne(
(
deviceId: string,
entries?: DeviceRegistryEntry[]
): DeviceRegistryEntry | undefined =>
entries?.find((device) => device.id === deviceId)
);
@customElement("zwave_js-node-config")
class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
class ZWaveJSNodeConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public route!: Route;
@ -79,8 +64,6 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
@property() public deviceId!: string;
@state() private _deviceRegistryEntries?: DeviceRegistryEntry[];
@state() private _nodeMetadata?: ZwaveJSNodeMetadata;
@state() private _config?: ZWaveJSNodeConfigParams;
@ -94,19 +77,8 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
this.deviceId = this.route.path.substr(1);
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._deviceRegistryEntries = entries;
}),
];
}
protected updated(changedProps: PropertyValues): void {
if (
(!this._config || changedProps.has("deviceId")) &&
changedProps.has("_deviceRegistryEntries")
) {
if (!this._config || changedProps.has("deviceId")) {
this._fetchData();
}
}
@ -125,7 +97,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
const device = this._device!;
const device = this.hass.devices[this.deviceId];
return html`
<hass-tabs-subpage
@ -217,6 +189,11 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
item: ZWaveJSNodeConfigParam
): TemplateResult {
const result = this._results[id];
const isTypeBoolean =
item.configuration_value_type === "boolean" ||
this._isEnumeratedBool(item);
const labelAndDescription = html`
<span slot="prefix" class="prefix">
${this.hass.localize("ui.panel.config.zwave_js.node_config.parameter")}
@ -268,23 +245,36 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
</span>
`;
const defaultLabel =
item.metadata.writeable && item.metadata.default !== undefined
? `${this.hass.localize("ui.panel.config.zwave_js.node_config.default")}:
${
isTypeBoolean
? this.hass.localize(
item.metadata.default === 1 ? "ui.common.yes" : "ui.common.no"
)
: item.configuration_value_type === "enumerated"
? item.metadata.states[item.metadata.default] ||
item.metadata.default
: item.metadata.default
}`
: "";
// Numeric entries with a min value of 0 and max of 1 are considered boolean
if (
item.configuration_value_type === "boolean" ||
this._isEnumeratedBool(item)
) {
if (isTypeBoolean) {
return html`
${labelAndDescription}
<div class="switch">
<ha-switch
<ha-selector-boolean
.property=${item.property}
.endpoint=${item.endpoint}
.propertyKey=${item.property_key}
.checked=${item.value === 1}
.value=${item.value === 1}
.key=${id}
@change=${this._switchToggled}
.disabled=${!item.metadata.writeable}
></ha-switch>
.helper=${defaultLabel}
></ha-selector-boolean>
</div>
`;
}
@ -303,6 +293,8 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
.disabled=${!item.metadata.writeable}
@change=${this._numericInputChanged}
.suffix=${item.metadata.unit}
.helper=${`${this.hass.localize("ui.panel.config.zwave_js.node_config.between_min_max", { min: item.metadata.min, max: item.metadata.max })}${defaultLabel ? `, ${defaultLabel}` : ""}`}
helperPersistent
>
</ha-textfield>`;
}
@ -318,6 +310,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
.endpoint=${item.endpoint}
.propertyKey=${item.property_key}
@selected=${this._dropdownSelected}
.helper=${defaultLabel}
>
${Object.entries(item.metadata.states).map(
([key, entityState]) => html`
@ -384,6 +377,19 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
if (Number(this._config![ev.target.key].value) === value) {
return;
}
if (
(ev.target.min !== undefined && value < ev.target.min) ||
(ev.target.max !== undefined && value > ev.target.max)
) {
this.setError(
ev.target.key,
this.hass.localize(
"ui.panel.config.zwave_js.node_config.error_not_in_range",
{ min: ev.target.min, max: ev.target.max }
)
);
return;
}
this.setResult(ev.target.key, undefined);
this._updateConfigParameter(ev.target, value);
}
@ -392,7 +398,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
try {
const result = await setZwaveNodeConfigParameter(
this.hass,
this._device!.id,
this.deviceId,
target.property,
target.endpoint,
value,
@ -420,16 +426,12 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
this._results = { ...this._results, [key]: errorParam };
}
private get _device(): DeviceRegistryEntry | undefined {
return getDevice(this.deviceId, this._deviceRegistryEntries);
}
private async _fetchData() {
if (!this.configEntryId || !this._deviceRegistryEntries) {
if (!this.configEntryId) {
return;
}
const device = this._device;
const device = this.hass.devices[this.deviceId];
if (!device) {
this._error = "device_not_found";
return;

View File

@ -250,7 +250,7 @@ export class HaConfigLogs extends LitElement {
--mdc-theme-primary: var(--primary-text-color);
--mdc-icon-size: 36px;
}
ha-button-menu > mwc-button > ha-svg-icon {
ha-button-menu > ha-button > ha-svg-icon {
margin-inline-end: 0px;
margin-inline-start: 8px;
}

View File

@ -322,22 +322,16 @@ export class HaConfigLovelaceDashboards extends LitElement {
hasFab
clickable
>
${this.hass.userData?.showAdvanced
? html`
<ha-button-menu slot="toolbar-icon" activatable>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-clickable-list-item href="/config/lovelace/resources">
${this.hass.localize(
"ui.panel.config.lovelace.resources.caption"
)}
</ha-clickable-list-item>
</ha-button-menu>
`
: ""}
<ha-button-menu slot="toolbar-icon" activatable>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-clickable-list-item href="/config/lovelace/resources">
${this.hass.localize("ui.panel.config.lovelace.resources.caption")}
</ha-clickable-list-item>
</ha-button-menu>
<ha-fab
slot="fab"
.label=${this.hass.localize(

View File

@ -1,13 +1,16 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { mdiClose } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon-button";
import { SchemaUnion } from "../../../../components/ha-form/types";
import { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail";
@ -40,6 +43,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
@state() private _submitting = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog(params: LovelaceResourceDetailsDialogParams): void {
this._params = params;
this._error = undefined;
@ -55,32 +60,52 @@ export class DialogLovelaceResourceDetail extends LitElement {
}
}
public closeDialog(): void {
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeDialog(): void {
this._dialog?.close();
}
protected render() {
if (!this._params) {
return nothing;
}
const urlInvalid = !this._data?.url || this._data.url.trim() === "";
const dialogTitle =
this._params.resource?.url ||
this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
);
const ariaLabel = this._params.resource?.url
? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.edit_resource"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
);
return html`
<ha-dialog
<ha-md-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._params.resource
? this._params.resource.url
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.new_resource"
)
)}
disable-cancel-action
@closed=${this._dialogClosed}
.ariaLabel=${ariaLabel}
>
<div>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
</ha-dialog-header>
<div slot="content">
<ha-alert
alert-type="warning"
.title=${this.hass!.localize(
@ -101,34 +126,24 @@ export class DialogLovelaceResourceDetail extends LitElement {
@value-changed=${this._valueChanged}
></ha-form>
</div>
${this._params.resource
? html`
<mwc-button
slot="secondaryAction"
class="warning"
@click=${this._deleteResource}
.disabled=${this._submitting}
>
${this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.delete"
<div slot="actions">
<mwc-button @click=${this.closeDialog}>
${this.hass!.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
@click=${this._updateResource}
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
>
${this._params.resource
? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.update"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.create"
)}
</mwc-button>
`
: nothing}
<mwc-button
slot="primaryAction"
@click=${this._updateResource}
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
>
${this._params.resource
? this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.update"
)
: this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.create"
)}
</mwc-button>
</ha-dialog>
</mwc-button>
</div>
</ha-md-dialog>
`;
}
@ -231,21 +246,6 @@ export class DialogLovelaceResourceDetail extends LitElement {
this._submitting = false;
}
}
private async _deleteResource() {
this._submitting = true;
try {
if (await this._params!.removeResource()) {
this.closeDialog();
}
} finally {
this._submitting = false;
}
}
static get styles(): CSSResultGroup {
return haStyleDialog;
}
}
declare global {

View File

@ -1,4 +1,4 @@
import { mdiPlus } from "@mdi/js";
import { mdiDelete, mdiPlus } from "@mdi/js";
import {
css,
CSSResultGroup,
@ -109,6 +109,20 @@ export class HaConfigLovelaceRescources extends LitElement {
) || resource.type}
`,
},
delete: {
title: "",
type: "icon-button",
minWidth: "48px",
maxWidth: "48px",
showNarrow: true,
template: (resource) =>
html`<ha-icon-button
@click=${this._removeResource}
.label=${this.hass.localize("ui.common.delete")}
.path=${mdiDelete}
.resource=${resource}
></ha-icon-button>`,
},
})
);
@ -235,46 +249,49 @@ export class HaConfigLovelaceRescources extends LitElement {
);
loadLovelaceResources([updated], this.hass!);
},
removeResource: async () => {
if (
!(await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_title"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_text",
{ url: resource!.url }
),
dismissText: this.hass!.localize("ui.common.cancel"),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
}))
) {
return false;
}
try {
await deleteResource(this.hass!, resource!.id);
this._resources = this._resources!.filter((res) => res !== resource);
showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_header"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_body"
),
confirmText: this.hass.localize("ui.common.refresh"),
dismissText: this.hass.localize("ui.common.not_now"),
confirm: () => location.reload(),
});
return true;
} catch (err: any) {
return false;
}
},
});
}
private _removeResource = async (event: any) => {
const resource = event.currentTarget.resource as LovelaceResource;
if (
!(await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_title"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.confirm_delete_text",
{ url: resource.url }
),
dismissText: this.hass!.localize("ui.common.cancel"),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
}))
) {
return false;
}
try {
await deleteResource(this.hass!, resource.id);
this._resources = this._resources!.filter(({ id }) => id !== resource.id);
showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_header"
),
text: this.hass!.localize(
"ui.panel.config.lovelace.resources.refresh_body"
),
confirmText: this.hass.localize("ui.common.refresh"),
dismissText: this.hass.localize("ui.common.not_now"),
confirm: () => location.reload(),
});
return true;
} catch (err: any) {
return false;
}
};
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}

View File

@ -10,7 +10,6 @@ export interface LovelaceResourceDetailsDialogParams {
updateResource: (
updates: Partial<LovelaceResourcesMutableParams>
) => Promise<unknown>;
removeResource: () => Promise<boolean>;
}
export const loadResourceDetailDialog = () =>

View File

@ -1,16 +1,34 @@
import { html } from "lit";
import { DataEntryFlowStep } from "../../../data/data_entry_flow";
import { domainToName } from "../../../data/integration";
import {
RepairsIssue,
createRepairsFlow,
deleteRepairsFlow,
fetchRepairsFlow,
handleRepairsFlowStep,
RepairsIssue,
} from "../../../data/repairs";
import {
loadDataEntryFlowDialog,
showFlowDialog,
} from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
import { HomeAssistant } from "../../../types";
const mergePlaceholders = (issue: RepairsIssue, step: DataEntryFlowStep) =>
step.description_placeholders && issue.translation_placeholders
? { ...issue.translation_placeholders, ...step.description_placeholders }
: step.description_placeholders || issue.translation_placeholders;
const renderIssueDescription = (hass: HomeAssistant, issue: RepairsIssue) =>
issue.breaks_in_ha_version
? html`
<ha-alert alert-type="warning">
${hass.localize("ui.panel.config.repairs.dialog.breaks_in_version", {
version: issue.breaks_in_ha_version,
})} </ha-alert
><br />
`
: "";
export const loadRepairFlowDialog = loadDataEntryFlowDialog;
@ -53,10 +71,11 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.abort.${step.reason}`,
step.description_placeholders
mergePlaceholders(issue, step)
);
return description
return html`${renderIssueDescription(hass, issue)}
${description
? html`
<ha-markdown
breaks
@ -64,7 +83,7 @@ export const showRepairsFlowDialog = (
.content=${description}
></ha-markdown>
`
: step.reason;
: step.reason}`;
},
renderShowFormStepHeader(hass, step) {
@ -73,7 +92,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.title`,
step.description_placeholders
mergePlaceholders(issue, step)
) || hass.localize("ui.dialogs.repair_flow.form.header")
);
},
@ -83,9 +102,10 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.description`,
step.description_placeholders
mergePlaceholders(issue, step)
);
return description
return html`${renderIssueDescription(hass, issue)}
${description
? html`
<ha-markdown
allowsvg
@ -93,7 +113,7 @@ export const showRepairsFlowDialog = (
.content=${description}
></ha-markdown>
`
: "";
: ""}`;
},
renderShowFormStepFieldLabel(hass, step, field, options) {
@ -101,7 +121,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data.${field.name}`,
step.description_placeholders
mergePlaceholders(issue, step)
);
},
@ -110,11 +130,12 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data_description.${field.name}`,
step.description_placeholders
mergePlaceholders(issue, step)
);
return description
return html`${renderIssueDescription(hass, issue)}
${description
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
: "";
: ""}`;
},
renderShowFormStepFieldError(hass, step, error) {
@ -122,7 +143,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.error.${error}`,
step.description_placeholders
mergePlaceholders(issue, step)
);
},
@ -165,7 +186,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.step.${
issue.translation_key || issue.issue_id
}.fix_flow.${step.step_id}.title`,
step.description_placeholders
mergePlaceholders(issue, step)
) || hass.localize(`component.${issue.domain}.title`)
);
},
@ -175,9 +196,9 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.progress.${step.progress_action}`,
step.description_placeholders
mergePlaceholders(issue, step)
);
return description
return html`${renderIssueDescription(hass, issue)}${description
? html`
<ha-markdown
allowsvg
@ -185,7 +206,7 @@ export const showRepairsFlowDialog = (
.content=${description}
></ha-markdown>
`
: "";
: ""}`;
},
renderMenuHeader(hass, step) {
@ -194,7 +215,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.title`,
step.description_placeholders
mergePlaceholders(issue, step)
) || hass.localize(`component.${issue.domain}.title`)
);
},
@ -204,9 +225,10 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.description`,
step.description_placeholders
mergePlaceholders(issue, step)
);
return description
return html`${renderIssueDescription(hass, issue)}
${description
? html`
<ha-markdown
allowsvg
@ -214,7 +236,7 @@ export const showRepairsFlowDialog = (
.content=${description}
></ha-markdown>
`
: "";
: ""}`;
},
renderMenuOption(hass, step, option) {
@ -222,7 +244,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
mergePlaceholders(issue, step)
);
},

View File

@ -58,7 +58,7 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
@ -423,7 +423,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
@ -431,21 +431,21 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
? 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-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<ha-md-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>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<ha-md-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
@ -457,7 +457,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
@ -475,18 +475,18 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
</ha-md-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
</div></ha-md-menu-item
>`;
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
@ -497,23 +497,23 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
</ha-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<ha-md-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<ha-md-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
@ -637,7 +637,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-filter-categories>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -650,10 +650,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -666,10 +666,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}
</ha-md-button-menu>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -682,11 +682,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
</ha-md-button-menu>`}`
: nothing}
${this.narrow || areasInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
@ -714,7 +714,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
@ -724,7 +724,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
@ -732,7 +732,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
@ -742,7 +742,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
@ -750,7 +750,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
@ -760,12 +760,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
</ha-md-button-menu>`
: nothing}
${!this.scenes.length
? html`<div class="empty" slot="empty">
@ -1204,7 +1204,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@ -59,7 +59,7 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
@ -413,7 +413,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
@ -421,20 +421,20 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
? 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-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<ha-md-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
</div> </ha-md-menu-item
><md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}>
<ha-md-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
@ -446,7 +446,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
@ -464,18 +464,18 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
</ha-md-menu-item>`;
})}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}>
<ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item
</div></ha-md-menu-item
>`;
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
html`<ha-md-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
@ -486,23 +486,23 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
</ha-md-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<ha-md-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
</ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<ha-md-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
</ha-md-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
@ -635,7 +635,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-filter-blueprints>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -648,10 +648,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -664,10 +664,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}
</ha-md-button-menu>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@ -680,11 +680,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
</ha-md-button-menu>`}`
: nothing}
${this.narrow || areasInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
@ -712,7 +712,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
@ -722,7 +722,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
@ -730,7 +730,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
@ -740,7 +740,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
@ -748,7 +748,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
@ -758,12 +758,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
</ha-md-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
</ha-md-button-menu>`
: nothing}
${!this.scripts.length
? html` <div class="empty" slot="empty">
@ -1295,7 +1295,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@ -3,7 +3,6 @@ import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
@ -12,6 +11,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-badge";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
@ -160,15 +160,14 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
if (!stateObj) {
return html`
<div class="badge error">
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon>
<span class="info">
<span class="label">${entityId}</span>
<span class="content">
${this.hass.localize("ui.badge.entity.not_found")}
</span>
</span>
</div>
<ha-badge .label=${entityId} class="error">
<ha-svg-icon
slot="icon"
.hass=${this.hass}
.path=${mdiAlertCircle}
></ha-svg-icon>
${this.hass.localize("ui.badge.entity.not_found")}
</ha-badge>
`;
}
@ -204,42 +203,32 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
const content = showState ? stateDisplay : showName ? name : undefined;
return html`
<div
style=${styleMap(style)}
class="badge ${classMap({
active,
"no-info": !showState && !showName,
"no-icon": !showIcon,
})}"
<ha-badge
.type=${this.hasAction ? "button" : "badge"}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
role=${ifDefined(this.hasAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasAction ? "0" : undefined)}
.label=${label}
.iconOnly=${!content}
style=${styleMap(style)}
class=${classMap({ active })}
>
<ha-ripple .disabled=${!this.hasAction}></ha-ripple>
${showIcon
? imageUrl
? html`<img src=${imageUrl} aria-hidden />`
? html`<img slot="icon" src=${imageUrl} aria-hidden />`
: html`
<ha-state-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
.icon=${this._config.icon}
></ha-state-icon>
`
: nothing}
${content
? html`
<span class="info">
${label ? html`<span class="label">${name}</span>` : nothing}
<span class="content">${content}</span>
</span>
`
: nothing}
</div>
${content}
</ha-badge>
`;
}
@ -249,119 +238,15 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
static get styles(): CSSResultGroup {
return css`
:host {
ha-badge {
--badge-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
.badge.error {
ha-badge.error {
--badge-color: var(--red-color);
}
.badge {
position: relative;
--ha-ripple-color: var(--badge-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
height: var(--ha-badge-size, 36px);
min-width: var(--ha-badge-size, 36px);
padding: 0px 8px;
box-sizing: border-box;
width: auto;
border-radius: var(
--ha-badge-border-radius,
calc(var(--ha-badge-size, 36px) / 2)
);
background: var(
--ha-card-background,
var(--card-background-color, white)
);
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
backdrop-filter: var(--ha-card-backdrop-filter, none);
border-width: var(--ha-card-border-width, 1px);
box-shadow: var(--ha-card-box-shadow, none);
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
--mdc-icon-size: 18px;
text-align: center;
font-family: Roboto;
}
.badge:focus-visible {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--badge-color);
border-color: var(--badge-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
button,
[role="button"] {
cursor: pointer;
}
button:focus,
[role="button"]:focus {
outline: none;
}
.badge.active {
ha-badge.active {
--badge-color: var(--primary-color);
}
.info {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-right: 4px;
padding-inline-end: 4px;
padding-inline-start: initial;
}
.label {
font-size: 10px;
font-style: normal;
font-weight: 500;
line-height: 10px;
letter-spacing: 0.1px;
color: var(--secondary-text-color);
}
.content {
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.1px;
color: var(--primary-text-color);
}
ha-state-icon,
ha-svg-icon {
color: var(--badge-color);
line-height: 0;
}
img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
overflow: hidden;
}
.badge.no-info {
padding: 0;
}
.badge:not(.no-icon):not(.no-info) img {
margin-left: -6px;
margin-inline-start: -6px;
margin-inline-end: initial;
}
.badge.no-icon .info {
padding-right: 4px;
padding-left: 4px;
padding-inline-end: 4px;
padding-inline-start: 4px;
}
`;
}
}

View File

@ -2,12 +2,11 @@ import { mdiAlertCircle } from "@mdi/js";
import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../components/ha-label-badge";
import "../../../components/ha-badge";
import "../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../custom-card-helpers";
import { LovelaceBadge } from "../types";
import { HuiEntityBadge } from "./hui-entity-badge";
import { ErrorBadgeConfig } from "./types";
export const createErrorBadgeElement = (config) => {
@ -55,41 +54,36 @@ export class HuiErrorBadge extends LitElement implements LovelaceBadge {
}
return html`
<button class="badge error" @click=${this._viewDetail}>
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon>
<ha-ripple></ha-ripple>
<span class="content">
<span class="name">Error</span>
<span class="state">${this._config.error}</span>
</span>
</button>
<ha-badge
class="error"
@click=${this._viewDetail}
type="button"
label="Error"
>
<ha-svg-icon slot="icon" .path=${mdiAlertCircle}></ha-svg-icon>
<div class="content">${this._config.error}</div>
</ha-badge>
`;
}
static get styles(): CSSResultGroup {
return [
HuiEntityBadge.styles,
css`
.badge.error {
--badge-color: var(--error-color);
border-color: var(--badge-color);
}
ha-svg-icon {
color: var(--badge-color);
}
.state {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
pre {
font-family: var(--code-font-family, monospace);
white-space: break-spaces;
user-select: text;
}
`,
];
return css`
ha-badge {
--badge-color: var(--error-color);
--ha-card-border-color: var(--error-color);
}
.content {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
pre {
font-family: var(--code-font-family, monospace);
white-space: break-spaces;
user-select: text;
}
`;
}
}

View File

@ -0,0 +1,260 @@
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-state-icon";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import type { HeadingCardConfig, HeadingCardEntityConfig } from "./types";
@customElement("hui-heading-card")
export class HuiHeadingCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-heading-card-editor");
return document.createElement("hui-heading-card-editor");
}
public static getStubConfig(hass: HomeAssistant): HeadingCardConfig {
return {
type: "heading",
icon: "mdi:fridge",
heading: hass.localize("ui.panel.lovelace.cards.heading.default_heading"),
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: HeadingCardConfig;
public setConfig(config: HeadingCardConfig): void {
this._config = {
tap_action: {
action: "none",
},
...config,
};
}
public getCardSize(): number {
return 1;
}
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: "full",
grid_rows: this._config?.heading_style === "subtitle" ? "auto" : 1,
};
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
protected render() {
if (!this._config || !this.hass) {
return nothing;
}
const actionable = hasAction(this._config.tap_action);
const style = this._config.heading_style || "title";
return html`
<ha-card>
<div class="container">
<div
class="content ${style}"
@action=${this._handleAction}
.actionHandler=${actionHandler()}
role=${ifDefined(actionable ? "button" : undefined)}
tabindex=${ifDefined(actionable ? "0" : undefined)}
>
${this._config.icon
? html`<ha-icon .icon=${this._config.icon}></ha-icon>`
: nothing}
${this._config.heading
? html`<p>${this._config.heading}</p>`
: nothing}
${actionable ? html`<ha-icon-next></ha-icon-next>` : nothing}
</div>
${this._config.entities?.length
? html`
<div class="entities">
${this._config.entities.map((config) =>
this._renderEntity(config)
)}
</div>
`
: nothing}
</div>
</ha-card>
`;
}
private _handleEntityAction(ev: ActionHandlerEvent) {
const config = {
tap_action: {
action: "none",
},
...(ev.currentTarget as any).config,
};
handleAction(this, this.hass!, config, ev.detail.action!);
}
_renderEntity(entityConfig: string | HeadingCardEntityConfig) {
const config =
typeof entityConfig === "string"
? { entity: entityConfig }
: entityConfig;
const stateObj = this.hass!.states[config.entity];
if (!stateObj) {
return nothing;
}
const actionable = hasAction(config.tap_action || { action: "none" });
return html`
<div
.config=${config}
class="entity"
@action=${this._handleEntityAction}
.actionHandler=${actionHandler()}
role=${ifDefined(actionable ? "button" : undefined)}
tabindex=${ifDefined(actionable ? "0" : undefined)}
>
<ha-state-icon
.hass=${this.hass}
.icon=${config.icon}
.stateObj=${stateObj}
></ha-state-icon>
<state-display
.hass=${this.hass}
.stateObj=${stateObj}
.content=${config.content || "state"}
></state-display>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-card {
background: none;
border: none;
box-shadow: none;
padding: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
height: 100%;
}
[role="button"] {
cursor: pointer;
}
ha-icon-next {
display: inline-block;
transition: transform 180ms ease-in-out;
}
.container {
padding: 2px 4px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
overflow: hidden;
gap: 8px;
}
.content:hover ha-icon-next {
transform: translateX(calc(4px * var(--scale-direction)));
}
.container .content {
flex: 1 0 fill;
min-width: 100px;
}
.container .content:not(:has(p)) {
min-width: fit-content;
}
.container .entities {
flex: 0 0;
}
.content {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
color: var(--primary-text-color);
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.1px;
--mdc-icon-size: 16px;
}
.content ha-icon,
.content ha-icon-next {
display: flex;
flex: none;
}
.content p {
margin: 0;
font-family: Roboto;
font-style: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
min-width: 0;
}
.content.subtitle {
color: var(--secondary-text-color);
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.entities {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 4px 10px;
}
.entities .entity {
display: flex;
flex-direction: row;
white-space: nowrap;
align-items: center;
gap: 3px;
color: var(--secondary-text-color);
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: 0.1px;
--mdc-icon-size: 14px;
}
.entities .entity ha-state-icon {
--ha-icon-display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-heading-card": HuiHeadingCard;
}
}

View File

@ -6,14 +6,13 @@ import {
html,
nothing,
} from "lit";
import { mdiChevronRight } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/chart/state-history-charts";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import {
HistoryResult,
computeHistory,
@ -209,9 +208,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
? html`
<h1 class="card-header">
${this._config.title}
<a href=${configUrl}
><ha-icon-button .path=${mdiChevronRight}></ha-icon-button
></a>
<a href=${configUrl}><ha-icon-next></ha-icon-next></a>
</h1>
`
: nothing}
@ -258,7 +255,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
justify-content: space-between;
display: flex;
}
.card-header ha-icon-button {
.card-header ha-icon-next {
--mdc-icon-button-size: 24px;
line-height: 24px;
color: var(--primary-text-color);

View File

@ -136,8 +136,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
const config = {
entity: this._config!.entity,
tap_action: this._config!.icon_tap_action,
hold_action: this._config!.icon_hold_action,
double_tap_action: this._config!.icon_double_tap_action,
};
handleAction(this, this.hass!, config, "tap");
handleAction(this, this.hass!, config, ev.detail.action!);
}
private _getImageUrl(entity: HassEntity): string | undefined {
@ -286,7 +288,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
role=${ifDefined(this.hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasIconAction ? "0" : undefined)}
@action=${this._handleIconAction}
.actionHandler=${actionHandler()}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.icon_hold_action),
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
})}
>
${imageUrl
? html`

View File

@ -498,5 +498,22 @@ export interface TileCardConfig extends LovelaceCardConfig {
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
icon_tap_action?: ActionConfig;
icon_hold_action?: ActionConfig;
icon_double_tap_action?: ActionConfig;
features?: LovelaceCardFeatureConfig[];
}
export interface HeadingCardEntityConfig {
entity: string;
content?: string | string[];
icon?: string;
tap_action?: ActionConfig;
}
export interface HeadingCardConfig extends LovelaceCardConfig {
heading_style?: "title" | "subtitle";
heading?: string;
icon?: string;
tap_action?: ActionConfig;
entities?: (string | HeadingCardEntityConfig)[];
}

View File

@ -275,9 +275,9 @@ export class HuiCardEditMode extends LitElement {
position: relative;
color: var(--primary-text-color);
border-radius: 50%;
padding: 12px;
padding: 8px;
background: var(--secondary-background-color);
--mdc-icon-size: 24px;
--mdc-icon-size: 20px;
}
.more {
position: absolute;

View File

@ -10,6 +10,7 @@ import "../cards/hui-sensor-card";
import "../cards/hui-thermostat-card";
import "../cards/hui-weather-forecast-card";
import "../cards/hui-tile-card";
import "../cards/hui-heading-card";
import {
createLovelaceElement,
getLovelaceElementClass,
@ -29,6 +30,7 @@ const ALWAYS_LOADED_TYPES = new Set([
"thermostat",
"weather-forecast",
"tile",
"heading",
]);
const LAZY_LOAD_TYPES = {

View File

@ -34,4 +34,21 @@ export const configElementStyle = css`
margin-top: 8px;
display: block;
}
ha-expansion-panel {
display: block;
--expansion-panel-content-padding: 0;
border-radius: 6px;
--ha-card-border-radius: 6px;
}
ha-expansion-panel .content {
padding: 12px;
}
ha-expansion-panel > * {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
ha-expansion-panel ha-svg-icon {
color: var(--secondary-text-color);
}
`;

View File

@ -1,11 +1,10 @@
import { mdiDelete, mdiDrag, mdiListBox, mdiPencil, mdiPlus } from "@mdi/js";
import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
@ -236,119 +235,108 @@ export class HuiCardFeaturesEditor extends LitElement {
);
return html`
<ha-expansion-panel outlined>
<h3 slot="header">
<ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
${this.hass!.localize("ui.panel.lovelace.editor.features.name")}
</h3>
<div class="content">
${supportedFeaturesType.length === 0 && this.features.length === 0
? html`
<ha-alert type="info">
${this.hass!.localize(
"ui.panel.lovelace.editor.features.no_compatible_available"
)}
</ha-alert>
`
: nothing}
<ha-sortable
handle-selector=".handle"
@item-moved=${this._featureMoved}
>
<div class="features">
${repeat(
this.features,
(featureConf) => this._getKey(featureConf),
(featureConf, index) => {
const type = featureConf.type;
const supported = this._supportsFeatureType(type);
const editable = this._isFeatureTypeEditable(type);
return html`
<div class="feature">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
<div class="feature-content">
<div>
<span> ${this._getFeatureTypeLabel(type)} </span>
${this.stateObj && !supported
? html`
<span class="secondary">
${this.hass!.localize(
"ui.panel.lovelace.editor.features.not_compatible"
)}
</span>
`
: nothing}
</div>
</div>
${editable
${supportedFeaturesType.length === 0 && this.features.length === 0
? html`
<ha-alert type="info">
${this.hass!.localize(
"ui.panel.lovelace.editor.features.no_compatible_available"
)}
</ha-alert>
`
: nothing}
<ha-sortable handle-selector=".handle" @item-moved=${this._featureMoved}>
<div class="features">
${repeat(
this.features,
(featureConf) => this._getKey(featureConf),
(featureConf, index) => {
const type = featureConf.type;
const supported = this._supportsFeatureType(type);
const editable = this._isFeatureTypeEditable(type);
return html`
<div class="feature">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
<div class="feature-content">
<div>
<span> ${this._getFeatureTypeLabel(type)} </span>
${this.stateObj && !supported
? html`
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.features.edit`
<span class="secondary">
${this.hass!.localize(
"ui.panel.lovelace.editor.features.not_compatible"
)}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editFeature}
.disabled=${!supported}
></ha-icon-button>
</span>
`
: nothing}
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.features.remove`
)}
.path=${mdiDelete}
class="remove-icon"
.index=${index}
@click=${this._removeFeature}
></ha-icon-button>
</div>
`;
}
)}
</div>
</ha-sortable>
${supportedFeaturesType.length > 0
? html`
<ha-button-menu
fixed
@action=${this._addFeature}
@closed=${stopPropagation}
>
<ha-button
slot="trigger"
outlined
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.features.add`
)}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${types.map(
(type) => html`
<ha-list-item .value=${type}>
${this._getFeatureTypeLabel(type)}
</ha-list-item>
`
)}
${types.length > 0 && customTypes.length > 0
? html`<li divider role="separator"></li>`
</div>
${editable
? html`
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.features.edit`
)}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editFeature}
.disabled=${!supported}
></ha-icon-button>
`
: nothing}
${customTypes.map(
(type) => html`
<ha-list-item .value=${type}>
${this._getFeatureTypeLabel(type)}
</ha-list-item>
`
)}
</ha-button-menu>
`
: nothing}
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.features.remove`
)}
.path=${mdiDelete}
class="remove-icon"
.index=${index}
@click=${this._removeFeature}
></ha-icon-button>
</div>
`;
}
)}
</div>
</ha-expansion-panel>
</ha-sortable>
${supportedFeaturesType.length > 0
? html`
<ha-button-menu
fixed
@action=${this._addFeature}
@closed=${stopPropagation}
>
<ha-button
slot="trigger"
outlined
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.features.add`
)}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${types.map(
(type) => html`
<ha-list-item .value=${type}>
${this._getFeatureTypeLabel(type)}
</ha-list-item>
`
)}
${types.length > 0 && customTypes.length > 0
? html`<li divider role="separator"></li>`
: nothing}
${customTypes.map(
(type) => html`
<ha-list-item .value=${type}>
${this._getFeatureTypeLabel(type)}
</ha-list-item>
`
)}
</ha-button-menu>
`
: nothing}
`;
}
@ -409,23 +397,6 @@ export class HuiCardFeaturesEditor extends LitElement {
display: flex !important;
flex-direction: column;
}
.content {
padding: 12px;
}
ha-expansion-panel {
display: block;
--expansion-panel-content-padding: 0;
border-radius: 6px;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
ha-svg-icon,
ha-icon {
color: var(--secondary-text-color);
}
ha-button-menu {
margin-top: 8px;
}

View File

@ -0,0 +1,296 @@
import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js";
import { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../../types";
type EntityConfig = {
entity: string;
};
declare global {
interface HASSDomEvents {
"edit-entity": { index: number };
}
}
@customElement("hui-entities-editor")
export class HuiEntitiesEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public entities?: EntityConfig[];
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@query("ha-entity-picker") private _entityPicker?: HaEntityPicker;
@state() private _addMode = false;
private _opened = false;
private _entitiesKeys = new WeakMap<EntityConfig, string>();
private _getKey(entity: EntityConfig) {
if (!this._entitiesKeys.has(entity)) {
this._entitiesKeys.set(entity, Math.random().toString());
}
return this._entitiesKeys.get(entity)!;
}
protected render() {
if (!this.hass) {
return nothing;
}
return html`
${this.entities
? html`
<ha-sortable
handle-selector=".handle"
@item-moved=${this._entityMoved}
>
<div class="entities">
${repeat(
this.entities,
(entityConf) => this._getKey(entityConf),
(entityConf, index) => {
const editable = true;
const entityId = entityConf.entity;
const stateObj = this.hass.states[entityId];
const name = stateObj
? stateObj.attributes.friendly_name
: undefined;
return html`
<div class="entity">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
<div class="entity-content">
<span>${name || entityId}</span>
</div>
${editable
? html`
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.entities.edit`
)}
.path=${mdiPencil}
class="edit-icon"
.index=${index}
@click=${this._editEntity}
.disabled=${!editable}
></ha-icon-button>
`
: nothing}
<ha-icon-button
.label=${this.hass!.localize(
`ui.panel.lovelace.editor.entities.remove`
)}
.path=${mdiDelete}
class="remove-icon"
.index=${index}
@click=${this._removeEntity}
></ha-icon-button>
</div>
`;
}
)}
</div>
</ha-sortable>
`
: nothing}
<div class="add-container">
<ha-button
data-add-entity
outlined
.label=${this.hass!.localize(`ui.panel.lovelace.editor.entities.add`)}
@click=${this._addEntity}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${this._renderPicker()}
</div>
`;
}
private _renderPicker() {
if (!this._addMode) {
return nothing;
}
return html`
<mwc-menu-surface
open
.anchor=${this._addContainer}
@closed=${this._onClosed}
@opened=${this._onOpened}
@opened-changed=${this._openedChanged}
@input=${stopPropagation}
>
<ha-entity-picker
.hass=${this.hass}
id="input"
.type=${"entity_id"}
.label=${this.hass.localize(
"ui.components.target-picker.add_entity_id"
)}
@value-changed=${this._entityPicked}
@click=${preventDefault}
allow-custom-entity
></ha-entity-picker>
</mwc-menu-surface>
`;
}
private _onClosed(ev) {
ev.stopPropagation();
ev.target.open = true;
}
private async _onOpened() {
if (!this._addMode) {
return;
}
await this._entityPicker?.focus();
await this._entityPicker?.open();
this._opened = true;
}
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
if (this._opened && !ev.detail.value) {
this._opened = false;
this._addMode = false;
}
}
private async _addEntity(ev): Promise<void> {
ev.stopPropagation();
this._addMode = true;
}
private _entityPicked(ev) {
ev.stopPropagation();
if (!ev.detail.value) {
return;
}
const newEntity: EntityConfig = { entity: ev.detail.value };
const newEntities = (this.entities || []).concat(newEntity);
fireEvent(this, "entities-changed", { entities: newEntities });
}
private _entityMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const newEntities = (this.entities || []).concat();
newEntities.splice(newIndex, 0, newEntities.splice(oldIndex, 1)[0]);
fireEvent(this, "entities-changed", { entities: newEntities });
}
private _removeEntity(ev: CustomEvent): void {
const index = (ev.currentTarget as any).index;
const newEntities = (this.entities || []).concat();
newEntities.splice(index, 1);
fireEvent(this, "entities-changed", { entities: newEntities });
}
private _editEntity(ev: CustomEvent): void {
const index = (ev.currentTarget as any).index;
fireEvent(this, "edit-entity", {
index,
});
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex !important;
flex-direction: column;
}
ha-button {
margin-top: 8px;
}
.entity {
display: flex;
align-items: center;
}
.entity .handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
padding-right: 8px;
padding-inline-end: 8px;
padding-inline-start: initial;
direction: var(--direction);
}
.entity .handle > * {
pointer-events: none;
}
.entity-content {
height: 60px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-grow: 1;
}
.entity-content div {
display: flex;
flex-direction: column;
}
.remove-icon,
.edit-icon {
--mdc-icon-button-size: 36px;
color: var(--secondary-text-color);
}
.secondary {
font-size: 12px;
color: var(--secondary-text-color);
}
li[divider] {
border-bottom-color: var(--divider-color);
}
.add-container {
position: relative;
width: 100%;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-entity-picker {
display: block;
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-entities-editor": HuiEntitiesEditor;
}
}

View File

@ -0,0 +1,352 @@
import { mdiGestureTap, mdiListBox } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
array,
assert,
assign,
literal,
object,
optional,
string,
union,
} from "superstruct";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
import type {
HeadingCardConfig,
HeadingCardEntityConfig,
} from "../../cards/types";
import { UiAction } from "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-form-editor";
import { processEditorEntities } from "../process-editor-entities";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { SubFormEditorData } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-entities-editor";
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
heading_style: optional(union([literal("title"), literal("subtitle")])),
heading: optional(string()),
icon: optional(string()),
tap_action: optional(actionConfigStruct),
entities: optional(array(any())),
})
);
const entityConfigStruct = object({
entity: string(),
content: optional(union([string(), array(string())])),
icon: optional(string()),
tap_action: optional(actionConfigStruct),
});
@customElement("hui-heading-card-editor")
export class HuiHeadingCardEditor
extends LitElement
implements LovelaceCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: HeadingCardConfig;
@state()
private _entityFormEditorData?: SubFormEditorData<HeadingCardEntityConfig>;
public setConfig(config: HeadingCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
public _assertEntityConfig(config: HeadingCardEntityConfig): void {
assert(config, entityConfigStruct);
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "heading_style",
selector: {
select: {
mode: "dropdown",
options: ["title", "subtitle"].map((value) => ({
label: localize(
`ui.panel.lovelace.editor.card.heading.heading_style_options.${value}`
),
value: value,
})),
},
},
},
{ name: "heading", selector: { text: {} } },
{
name: "icon",
selector: {
icon: {},
},
},
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "none",
actions,
},
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
private _entitySchema = memoizeOne(
() =>
[
{
name: "entity",
selector: { entity: {} },
},
{
name: "icon",
selector: { icon: {} },
context: { icon_entity: "entity" },
},
{
name: "content",
selector: { ui_state_content: {} },
context: { filter_entity: "entity" },
},
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return cache(
this._entityFormEditorData ? this._renderEntityForm() : this._renderForm()
);
}
private _renderEntityForm() {
const schema = this._entitySchema();
return html`
<hui-sub-form-editor
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.entities.form-label"
)}
.hass=${this.hass}
.data=${this._entityFormEditorData!.data}
@go-back=${this._goBack}
@value-changed=${this._subFormChanged}
.schema=${schema}
.assertConfig=${this._assertEntityConfig}
.computeLabel=${this._computeEntityLabelCallback}
>
</hui-sub-form-editor>
`;
}
private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
processEditorEntities(entities || [])
);
private _renderForm() {
const data = {
...this._config!,
};
if (!data.heading_style) {
data.heading_style = "title";
}
const schema = this._schema(this.hass!.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<ha-expansion-panel outlined>
<h3 slot="header">
<ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.card.heading.entities"
)}
</h3>
<div class="content">
<hui-entities-editor
.hass=${this.hass}
.entities=${this._entities(this._config!.entities)}
@entities-changed=${this._entitiesChanged}
@edit-entity=${this._editEntity}
>
</hui-entities-editor>
</div>
</ha-expansion-panel>
`;
}
private _entitiesChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const config = {
...this._config,
entities: ev.detail.entities as HeadingCardEntityConfig[],
};
fireEvent(this, "config-changed", { config });
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const config = ev.detail.value as HeadingCardConfig;
fireEvent(this, "config-changed", { config });
}
private _subFormChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const value = ev.detail.value;
const newEntities = this._config!.entities
? [...this._config!.entities]
: [];
if (!value) {
newEntities.splice(this._entityFormEditorData!.index!, 1);
this._goBack();
} else {
newEntities[this._entityFormEditorData!.index!] = value;
}
this._config = { ...this._config!, entities: newEntities };
this._entityFormEditorData = {
...this._entityFormEditorData!,
data: value,
};
fireEvent(this, "config-changed", { config: this._config });
}
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
const entities = this._entities(this._config!.entities);
this._entityFormEditorData = {
data: entities[ev.detail.index],
index: ev.detail.index,
};
}
private _goBack(): void {
this._entityFormEditorData = undefined;
}
private _computeEntityLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._entitySchema>>
) => {
switch (schema.name) {
case "content":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "heading_style":
case "heading":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.heading.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
static get styles() {
return [
configElementStyle,
css`
.container {
display: flex;
flex-direction: column;
}
ha-form {
display: block;
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-heading-card-editor": HuiHeadingCardEditor;
}
}

View File

@ -1,5 +1,7 @@
import { mdiListBox } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
@ -12,11 +14,13 @@ import {
string,
} from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
import {
LovelaceCardFeatureConfig,
@ -27,6 +31,7 @@ import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor";
@ -93,22 +98,30 @@ export class HuiHumidifierCardEditor
return nothing;
}
const stateObj = this._config.entity
? this.hass.states[this._config.entity]
: undefined;
return cache(
this._subElementEditorConfig
? this._renderFeatureForm()
: this._renderForm()
);
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(this._config.entity)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderFeatureForm() {
const entityId = this._config!.entity;
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(entityId)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
return html`
<ha-form
@ -118,14 +131,24 @@ export class HuiHumidifierCardEditor
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<hui-card-features-editor
.hass=${this.hass}
.stateObj=${stateObj}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES}
.features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement}
></hui-card-features-editor>
<ha-expansion-panel outlined>
<h3 slot="header">
<ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.features"
)}
</h3>
<div class="content">
<hui-card-features-editor
.hass=${this.hass}
.stateObj=${stateObj}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES}
.features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement}
></hui-card-features-editor>
</div>
</ha-expansion-panel>
`;
}
@ -202,12 +225,15 @@ export class HuiHumidifierCardEditor
};
static get styles() {
return css`
ha-form {
display: block;
margin-bottom: 24px;
}
`;
return [
configElementStyle,
css`
ha-form {
display: block;
margin-bottom: 24px;
}
`,
];
}
}

View File

@ -181,7 +181,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
if (ev.detail && ev.detail.entities) {
this._config = { ...this._config!, entities: ev.detail.entities };
this._configEntities = processEditorEntities(this._config.entities);
this._configEntities = processEditorEntities(this._config.entities || []);
fireEvent(this, "config-changed", { config: this._config! });
}
}

View File

@ -1,5 +1,7 @@
import { mdiListBox } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
@ -12,11 +14,13 @@ import {
string,
} from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
import {
LovelaceCardFeatureConfig,
@ -27,6 +31,7 @@ import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor";
@ -91,22 +96,29 @@ export class HuiThermostatCardEditor
return nothing;
}
const stateObj = this._config.entity
? this.hass.states[this._config.entity]
: undefined;
return cache(
this._subElementEditorConfig
? this._renderFeatureForm()
: this._renderForm()
);
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(this._config.entity)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderFeatureForm() {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(this._config!.entity)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
return html`
<ha-form
@ -116,14 +128,24 @@ export class HuiThermostatCardEditor
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<hui-card-features-editor
.hass=${this.hass}
.stateObj=${stateObj}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES}
.features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement}
></hui-card-features-editor>
<ha-expansion-panel outlined>
<h3 slot="header">
<ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.features"
)}
</h3>
<div class="content">
<hui-card-features-editor
.hass=${this.hass}
.stateObj=${stateObj}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES}
.features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement}
></hui-card-features-editor>
</div>
</ha-expansion-panel>
`;
}
@ -200,12 +222,15 @@ export class HuiThermostatCardEditor
};
static get styles() {
return css`
ha-form {
display: block;
margin-bottom: 24px;
}
`;
return [
configElementStyle,
css`
ha-form {
display: block;
margin-bottom: 24px;
}
`,
];
}
}

View File

@ -1,6 +1,7 @@
import { mdiGestureTap, mdiPalette } from "@mdi/js";
import { mdiGestureTap, mdiListBox, mdiPalette } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one";
import {
any,
@ -14,11 +15,13 @@ import {
union,
} from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
import {
LovelaceCardFeatureConfig,
@ -168,27 +171,31 @@ export class HuiTileCardEditor
return nothing;
}
const stateObj = this._config.entity
? this.hass.states[this._config.entity]
: undefined;
const schema = this._schema(
this._config.entity,
this._config.hide_state ?? false
return cache(
this._subElementEditorConfig
? this._renderFeatureForm()
: this._renderForm()
);
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(this._config.entity)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderFeatureForm() {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.context=${this._context(this._config!.entity)}
@go-back=${this._goBack}
@config-changed=${this.subElementChanged}
>
</hui-sub-element-editor>
`;
}
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
const schema = this._schema(entityId, this._config!.hide_state ?? false);
const data = this._config;
@ -200,13 +207,23 @@ export class HuiTileCardEditor
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<hui-card-features-editor
.hass=${this.hass}
.stateObj=${stateObj}
.features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement}
></hui-card-features-editor>
<ha-expansion-panel outlined>
<h3 slot="header">
<ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.features"
)}
</h3>
<div class="content">
<hui-card-features-editor
.hass=${this.hass}
.stateObj=${stateObj}
.features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement}
></hui-card-features-editor>
</div>
</ha-expansion-panel>
`;
}

View File

@ -0,0 +1,190 @@
import "@material/mwc-button";
import { mdiCodeBraces, mdiListBoxOutline } from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-form/ha-form";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-yaml-editor";
import "../../../components/ha-alert";
import type { HomeAssistant } from "../../../types";
import type { LovelaceConfigForm } from "../types";
import type { EditSubFormEvent } from "./types";
import { handleStructError } from "../../../common/structs/handle-errors";
declare global {
interface HASSDomEvents {
"go-back": undefined;
"edit-sub-form": EditSubFormEvent;
}
}
@customElement("hui-sub-form-editor")
export class HuiSubFormEditor<T = any> extends LitElement {
public hass!: HomeAssistant;
@property() public label?: string;
@property({ attribute: false }) public data!: T;
public schema!: LovelaceConfigForm["schema"];
public assertConfig?: (config: T) => void;
public computeLabel?: LovelaceConfigForm["computeLabel"];
public computeHelper?: LovelaceConfigForm["computeHelper"];
@state() public _yamlMode = false;
@state() private _errors?: string[];
@state() private _warnings?: string[];
protected render(): TemplateResult {
const uiAvailable = !this.hasWarning && !this.hasError;
return html`
<div class="header">
<div class="back-title">
<ha-icon-button-prev
.label=${this.hass!.localize("ui.common.back")}
@click=${this._goBack}
></ha-icon-button-prev>
<span slot="title">${this.label}</span>
</div>
<ha-icon-button
class="gui-mode-button"
@click=${this._toggleMode}
.disabled=${!uiAvailable}
.label=${this.hass!.localize(
this._yamlMode
? "ui.panel.lovelace.editor.edit_card.show_visual_editor"
: "ui.panel.lovelace.editor.edit_card.show_code_editor"
)}
.path=${this._yamlMode ? mdiListBoxOutline : mdiCodeBraces}
></ha-icon-button>
</div>
${this._yamlMode
? html`
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.data}
@value-changed=${this._valueChanged}
></ha-yaml-editor>
`
: html`
<ha-form
.hass=${this.hass}
.schema=${this.schema}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.data=${this.data}
@value-changed=${this._valueChanged}
>
</ha-form>
`}
${this.hasError
? html`
<ha-alert alert-type="error">
${this.hass.localize("ui.errors.config.error_detected")}:
<br />
<ul>
${this._errors!.map((error) => html`<li>${error}</li>`)}
</ul>
</ha-alert>
`
: nothing}
${this.hasWarning
? html`
<ha-alert
alert-type="warning"
.title="${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}:"
>
${this._warnings!.length > 0 && this._warnings![0] !== undefined
? html`
<ul>
${this._warnings!.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>
`
: nothing}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</ha-alert>
`
: nothing}
`;
}
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("data")) {
if (this.assertConfig) {
try {
this.assertConfig(this.data);
this._warnings = undefined;
this._errors = undefined;
} catch (err: any) {
const msgs = handleStructError(this.hass, err);
this._warnings = msgs.warnings ?? [err.message];
this._errors = msgs.errors || undefined;
this._yamlMode = true;
}
}
}
}
public get hasWarning(): boolean {
return this._warnings !== undefined && this._warnings.length > 0;
}
public get hasError(): boolean {
return this._errors !== undefined && this._errors.length > 0;
}
private _goBack(): void {
fireEvent(this, "go-back");
}
private _toggleMode(): void {
this._yamlMode = !this._yamlMode;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const value = (ev.detail.value ?? (ev.target as any).value ?? {}) as T;
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return css`
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.back-title {
display: flex;
align-items: center;
font-size: 18px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-sub-form-editor": HuiSubFormEditor;
}
}

Some files were not shown because too many files have changed in this diff Show More